Skip to content

Commit

Permalink
Add integrations support
Browse files Browse the repository at this point in the history
  • Loading branch information
yanthomasdev committed Oct 4, 2024
1 parent 4e31e85 commit 12c4040
Show file tree
Hide file tree
Showing 33 changed files with 1,606 additions and 2,016 deletions.
3 changes: 1 addition & 2 deletions examples/starlight/astro.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import starlight from '@astrojs/starlight';
import lunaria from '@lunariajs/starlight';
import { defineConfig } from 'astro/config';

const locales = {
Expand All @@ -17,7 +16,7 @@ const locales = {
export default defineConfig({
integrations: [
starlight({
plugins: [lunaria()],
plugins: [],
title: 'My Docs',
locales,
social: {
Expand Down
30 changes: 30 additions & 0 deletions examples/starlight/lunaria.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { defineConfig } from '@lunariajs/core/config';

export default defineConfig({
repository: 'yanthomasdev/lunaria',
//sourceLocale: {
// label: 'English',
// lang: 'en',
//},
//locales: [
// {
// lang: 'pt',
// label: 'Português',
// },
//],
tracking: {
ignoredKeywords: ['TEST'],
},
//files: [
// {
// include: ['src/content/docs/**/*.(md|mdx)'],
// exclude: ['src/content/docs/pt/**/*.(md|mdx)'],
// pattern: {
// source: 'src/content/docs/@path',
// locales: 'src/content/docs/@lang/@path',
// },
// type: 'universal',
// },
//],
integrations: [],
});
7 changes: 3 additions & 4 deletions examples/starlight/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.4.1",
"@astrojs/starlight": "^0.16.0",
"@astrojs/check": "^0.9.3",
"@astrojs/starlight": "^0.28.1",
"@lunariajs/core": "workspace:^",
"@lunariajs/starlight": "workspace:^",
"astro": "^4.2.1",
"astro": "^4.15.7",
"sharp": "^0.32.5",
"typescript": "^5.3.3"
}
Expand Down
7 changes: 7 additions & 0 deletions examples/starlight/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Lunaria } from '@lunariajs/core';

console.time('Lunaria benchmark');
const lunaria = new Lunaria();
const status = await lunaria.getFullStatus();
console.log(status);
console.timeEnd('Lunaria benchmark');
32 changes: 24 additions & 8 deletions packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { existsSync } from 'node:fs';
import { resolve } from 'node:path';
import { ConfigNotFound } from '../errors/errors.js';
import { parseWithFriendlyErrors } from '../errors/index.js';
import { ConfigNotFound, ConfigValidationError } from '../errors/errors.js';
import { moduleLoader } from '../files/loaders.js';
import { parseWithFriendlyErrors } from '../utils/utils.js';
import { LunariaPreSetupSchema } from '../integrations/schema.js';
import { LunariaConfigSchema } from './schema.js';
import type { LunariaConfig, LunariaUserConfig } from './types.js';
import type { LunariaUserConfig } from './types.js';
import type { CompleteLunariaUserConfig } from '../integrations/types.js';

// Paths to search for the Lunaria config file,
// sorted by chance of appearing.
/**
* Paths to search for the Lunaria config file,
* sorted by how likely they're to appear.
*/
const configPaths = Object.freeze([
'lunaria.config.mjs',
'lunaria.config.js',
Expand All @@ -17,6 +21,7 @@ const configPaths = Object.freeze([
'lunaria.config.cts',
]);

/** Finds the first `lunaria.config.*` file in the current working directoy and return its path. */
function findConfig() {
for (const path of configPaths) {
if (existsSync(resolve(path))) {
Expand All @@ -27,6 +32,7 @@ function findConfig() {
return new Error(ConfigNotFound.message);
}

/** Loads a CJS/ESM `lunaria.config.*` file from the root of the current working directory. */
export function loadConfig() {
const path = findConfig();
if (path instanceof Error) {
Expand All @@ -38,9 +44,19 @@ export function loadConfig() {
throw mod;
}

return validateConfig(mod);
return validateInitialConfig(mod);
}

export function validateConfig(config: LunariaUserConfig) {
return parseWithFriendlyErrors(LunariaConfigSchema, config) as LunariaConfig;
/** Validates the Lunaria config before the integrations' setup hook have run. */
export function validateInitialConfig(config: LunariaUserConfig) {
return parseWithFriendlyErrors(LunariaPreSetupSchema, config, (issues) =>
ConfigValidationError.message(issues),
);
}

/** Validates the Lunaria config after all the integrations' setup hook have run. */
export function validateFinalConfig(config: CompleteLunariaUserConfig) {
return parseWithFriendlyErrors(LunariaConfigSchema, config, (issues) =>
ConfigValidationError.message(issues),
);
}
146 changes: 77 additions & 69 deletions packages/core/src/config/schema.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,21 @@
import { z } from 'zod';
import { isRelative, stripTrailingSlash } from '../utils/utils.js';
import type { OptionalKeys } from './types.js';
import type { LunariaUserConfig, OptionalKeys } from './types.js';
import type { Consola, InputLogObject, LogType } from 'consola';

// TODO: Move types into separate file, Zod has a few issues with more complex types.
const RepositorySchema = z.preprocess(
(val) => {
if (typeof val === 'string') {
return { name: val };
}
return val;
},
z.union([
z.string(),
z.object({
name: z.string().transform((path) => stripTrailingSlash(path)),
branch: z.string().default('main'),
rootDir: z
.string()
.default('.')
.refine((path) => !isRelative(path), {
message:
'The root directory should not be a relative path, it should follow the example: `examples/vitepress`',
})
// TODO: See if this transform is even necessary still?
.transform((path) => stripTrailingSlash(path)),
hosting: z.union([z.literal('github'), z.literal('gitlab')]).default('github'),
}),
]),
);

const LocaleSchema = z.object({
label: z.string(),
lang: z.string(),
const RepositorySchema = z.object({
name: z.string().transform((path) => stripTrailingSlash(path)),
branch: z.string().default('main'),
rootDir: z
.string()
.default('.')
.refine((path) => !isRelative(path), {
message:
'The root directory should not be a relative path, it should follow the example: `examples/vitepress`',
})
// TODO: See if this transform is even necessary still?
.transform((path) => stripTrailingSlash(path)),
hosting: z.union([z.literal('github'), z.literal('gitlab')]).default('github'),
});

const BaseFileSchema = z.object({
Expand All @@ -50,54 +34,78 @@ const OptionalKeysSchema: z.ZodType<OptionalKeys> = z.lazy(() =>
z.record(z.string(), z.union([z.boolean(), OptionalKeysSchema])),
);

const FileSchema = z.discriminatedUnion('type', [
export const FileSchema = z.discriminatedUnion('type', [
BaseFileSchema.extend({ type: z.literal('universal') }),
BaseFileSchema.extend({
type: z.literal('dictionary'),
optionalKeys: OptionalKeysSchema.optional(),
}),
]);

export const LunariaConfigSchema = z
.object({
repository: RepositorySchema,
sourceLocale: LocaleSchema,
locales: z.array(LocaleSchema).nonempty(),
files: z.array(FileSchema).nonempty(),
tracking: z
.object({
ignoredKeywords: z.array(z.string()).default(['lunaria-ignore', 'fix typo']),
localizableProperty: z.string().optional(),
})
.default({}),
// TODO: Add validation for Lunaria integrations
/*integrations: z
.array(
z.object({
name: z.string(),
hooks: z.object({}).passthrough().default({}),
}),
)
.default([]),*/
cacheDir: z.string().default('./node_modules/.cache/lunaria'),
cloneDir: z.string().default('./node_modules/.cache/lunaria/history'),
})
.superRefine((config, ctx) => {
// Adds an validation issue if any locales share the same lang.
const allLocales = [...config.locales.map(({ lang }) => lang), config.sourceLocale.lang];
const uniqueLocales = new Set(allLocales);
export const SetupOptionsSchema = z.object({
config: z.any() as z.Schema<LunariaUserConfig>,
updateConfig: z.function(
z.tuple([z.record(z.any()) as z.Schema<Partial<LunariaUserConfig>>]),
z.void(),
),
// Importing ConsolaInstance from 'consola' directly is not possible due to missing imports for `LogFn`
logger: z.any() as z.Schema<
Consola &
Record<
LogType,
{
// biome-ignore lint/suspicious/noExplicitAny: copied from Consola
(message: InputLogObject | any, ...args: any[]): void;
// biome-ignore lint/suspicious/noExplicitAny: copied from Consola
raw: (...args: any[]) => void;
}
>
>,
fileLoader: z.function(z.tuple([z.string()]), z.any()),
});

if (allLocales.length !== uniqueLocales.size) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'All locales should have a unique `lang` value',
});
}
const LunariaIntegrationSchema = z.object({
name: z.string(),
hooks: z.object({
setup: z.function(z.tuple([SetupOptionsSchema]), z.void()).optional(),
}),
});

if (config.cacheDir === config.cloneDir) {
// We need both of these schemas so that we can extend the Lunaria config
// e.g. to validate integrations
export const BaseLunariaConfigSchema = z.object({
repository: RepositorySchema,
sourceLocale: z.string(),
locales: z.array(z.string()).nonempty(),
files: z.array(FileSchema).nonempty(),
tracking: z
.object({
ignoredKeywords: z.array(z.string()).default(['lunaria-ignore', 'fix typo']),
localizableProperty: z.string().optional(),
})
.default({}),
integrations: z.array(LunariaIntegrationSchema).default([]),
cacheDir: z.string().default('./node_modules/.cache/lunaria'),
cloneDir: z.string().default('./node_modules/.cache/lunaria/history'),
});

export const LunariaConfigSchema = BaseLunariaConfigSchema.superRefine((config, ctx) => {
// Adds an validation issue if any locales share the same value.
const locales = new Set();
for (const locale of [config.sourceLocale, ...config.locales]) {
if (locales.has(locale)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: '`cacheDir` and `cloneDir` should not be in the same directory',
message: `Repeated \`locales\` value: \`"${locale}"\``,
});
}
});
locales.add(locale);
}

if (config.cacheDir === config.cloneDir) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: '`cacheDir` and `cloneDir` should not be in the same directory',
});
}
});
31 changes: 14 additions & 17 deletions packages/core/src/config/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
type Locale = {
label: string;
lang: string;
};
import type { LunariaIntegration } from '../integrations/types.js';

export type Pattern = string | { source: string; locales: string };

Expand Down Expand Up @@ -30,33 +27,33 @@ export interface LunariaConfig {
rootDir: string;
hosting: GitHostingOptions;
};
sourceLocale: Locale;
locales: [Locale, ...Locale[]];
sourceLocale: string;
locales: [string, ...string[]];
files: [File, ...File[]];
tracking: {
ignoredKeywords: string[];
localizableProperty?: string;
};
integrations: LunariaIntegration[];
cacheDir: string;
cloneDir: string;
}

export interface LunariaUserConfig {
repository:
| string
| {
name: string;
branch?: string;
rootDir?: string;
hosting?: GitHostingOptions;
};
sourceLocale: Locale;
locales: [Locale, ...Locale[]];
files: [File, ...File[]];
repository: {
name: string;
branch?: string;
rootDir?: string;
hosting?: 'github' | 'gitlab';
};
sourceLocale?: string;
locales?: [string, ...string[]];
files?: [File, ...File[]];
tracking?: {
ignoredKeywords?: string[];
localizableProperty?: string;
};
integrations?: LunariaIntegration[];
cacheDir?: string;
cloneDir?: string;
}
9 changes: 9 additions & 0 deletions packages/core/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Logging levels available for the console.
// Used to translate consola's numeric values into human-readable strings.
export const CONSOLE_LEVELS = Object.freeze({
error: 0,
warn: 1,
info: 3,
debug: 999,
silent: -999,
});
11 changes: 9 additions & 2 deletions packages/core/src/errors/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const UnknownError = {
title: 'An unknown error occurred.',
message:
"An unknown error occurred. If restarting the development server or reinstalling `\node_modules` doesn't fix it, please open a GitHub issue.",
};
} satisfies ErrorContext;

export const ConfigNotFound = {
name: 'ConfigNotFound',
Expand Down Expand Up @@ -65,4 +65,11 @@ export const InvalidDictionaryFormat = {
title: 'A file with an invalid dictionary format was found.',
message: (path: string) =>
`The \`type: "dictionary"\` file \`${path}\` has an invalid format. Dictionaries are expected to be a recursive Record of string keys and values. Alternatively, you can track this file without key completion checking by setting it to \`type: "universal"\` instead.`,
};
} satisfies ErrorContext;

export const UnsupportedIntegrationSelfUpdate = {
name: 'UnsupportedIntegrationSelfUpdate',
title: "An integration attempted to update the configuration's `integrations` field.",
message: (name: string) =>
`The integration \`${name}\` attempted to update the \`integrations\` field, which is not supported.`,
} satisfies ErrorContext;
Loading

0 comments on commit 12c4040

Please sign in to comment.