Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Lunaria V1.0 #155

Draft
wants to merge 58 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
bc75b12
Update `packageManager` to [email protected]
yanthomasdev Aug 15, 2024
641a233
Reorganize monorepo setup
yanthomasdev Aug 15, 2024
2bffdd7
Replace Prettier with Biome
yanthomasdev Aug 16, 2024
12d67bf
v1 foundation
yanthomasdev Sep 9, 2024
faefab9
Fix publish workflow and Biome check issues
yanthomasdev Sep 9, 2024
e2dd31f
Move Biome CI into core package only (temporary)
yanthomasdev Sep 9, 2024
b311d37
Fix errors missing message parameter
yanthomasdev Sep 9, 2024
4d61c3b
Attempt at fixing "cannot find module"
yanthomasdev Sep 9, 2024
ea23d95
Move check for file existence up
yanthomasdev Sep 9, 2024
94f7a6c
Handle edge case in finding latest change
yanthomasdev Sep 9, 2024
a66e433
Change a few dependencies
yanthomasdev Sep 9, 2024
e8fa61d
Move configuration loading into class
yanthomasdev Sep 9, 2024
093a352
Update `path-to-regexp`
yanthomasdev Sep 12, 2024
70e39d0
Fix `findFileConfig()` matching
yanthomasdev Sep 12, 2024
d588a82
test: basic caching function
yanthomasdev Sep 14, 2024
205487f
Add git caching
yanthomasdev Sep 14, 2024
ed2b45a
Remove dependency on empathic
yanthomasdev Sep 16, 2024
4e31e85
Merge branch 'v1' of https://github.com/yanthomasdev/lunaria into v1
yanthomasdev Sep 16, 2024
12c4040
Add integrations support
yanthomasdev Oct 4, 2024
0e8a44c
Add tracking tests and fix `@lunaria-ignore`
yanthomasdev Oct 13, 2024
9054a79
Biome format and check
yanthomasdev Oct 13, 2024
ff2cccc
Remove nextra example
yanthomasdev Oct 13, 2024
5b6ee76
Update `jiti` to `v2.3.3` and move async code
yanthomasdev Oct 13, 2024
b55cdcd
Improve a few errors
yanthomasdev Oct 13, 2024
d2f7645
Update Biome to `v1.9.3`
yanthomasdev Oct 13, 2024
dac1d7c
Remove solved `TODO`s
yanthomasdev Oct 13, 2024
92655af
Add dictionaries tests and fix bug
yanthomasdev Oct 14, 2024
cbe2bff
Add external repo support
yanthomasdev Oct 21, 2024
387b1c5
Use async version of `node:fs` modules
yanthomasdev Oct 22, 2024
082caa4
Filter out unlocalizable files
yanthomasdev Oct 22, 2024
a219bee
Make patterns less restrictive
yanthomasdev Nov 6, 2024
0aebd79
Fix support for yaml dictionary files
yanthomasdev Nov 6, 2024
0bf48b2
Add support for custom parameters in patterns
yanthomasdev Nov 7, 2024
c128ce1
Make config readable
yanthomasdev Nov 8, 2024
f52e5c3
Add git hosting links to public API
yanthomasdev Nov 8, 2024
06931b3
Force `pkg.pr.new` rebuild
yanthomasdev Nov 9, 2024
45a07eb
Merge branch 'main' into v1
yanthomasdev Nov 9, 2024
f81d0fb
Update Starlight example
yanthomasdev Nov 11, 2024
3523c58
Fix cache creation issue
yanthomasdev Nov 11, 2024
592e4ff
Await dictionaries individually
yanthomasdev Nov 11, 2024
ae1b8a7
Properly sort entries
yanthomasdev Nov 12, 2024
f00ede3
Fix `findFileConfig()` issue
yanthomasdev Nov 12, 2024
e57b9f7
Fix edge case in path compilation from patterns
yanthomasdev Nov 13, 2024
6d3c5d8
Remove VitePress example
yanthomasdev Nov 13, 2024
32ec387
Fix issue with getting localizedPath
yanthomasdev Nov 13, 2024
8825f83
Fix typo in error message
yanthomasdev Nov 13, 2024
f3e0577
Update examples
yanthomasdev Nov 13, 2024
5f38136
Change `findFileConfig` logic again
yanthomasdev Nov 13, 2024
62a858f
Make Biome happy
yanthomasdev Nov 13, 2024
bef33c0
Rename `findFileConfig` and improve logic
yanthomasdev Nov 22, 2024
2cb6111
Use full picomatch import
yanthomasdev Nov 22, 2024
0c6dcb4
Fix matching issue in `findFilesEntry`
yanthomasdev Nov 22, 2024
59c4b19
Update `external` example
yanthomasdev Nov 22, 2024
add3ea3
Trim lunaria directives' paths and globs
yanthomasdev Nov 22, 2024
267c8e4
Test adding limited concurrency
yanthomasdev Nov 23, 2024
bdbc42a
Revert "Test adding limited concurrency"
yanthomasdev Nov 23, 2024
4c8b9b0
Update dependencies
yanthomasdev Nov 25, 2024
bb58040
Add limited concurrency
yanthomasdev Nov 28, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Fix support for yaml dictionary files
  • Loading branch information
yanthomasdev committed Nov 6, 2024
commit 0aebd793fd490cf47dbf8b4f9c567bedbfec30bf
23 changes: 0 additions & 23 deletions examples/starlight/lunaria.config.json

This file was deleted.

9 changes: 8 additions & 1 deletion examples/starlight/lunaria.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ export default defineConfig({
},
type: 'universal',
},
{
include: ['src/content/i18n/en.yml'],
pattern: {
source: 'src/content/i18n/@lang.yml',
locales: 'src/content/i18n/@lang.yml',
},
type: 'dictionary',
},
],
tracking: {
localizableProperty: 'i18nReady',
Expand All @@ -29,5 +37,4 @@ export default defineConfig({
'i18nIgnore',
],
},
external: true,
});
2 changes: 2 additions & 0 deletions examples/starlight/src/content/i18n/en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
one: test
second: test
1 change: 1 addition & 0 deletions examples/starlight/src/content/i18n/pt.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
one: teste
4 changes: 2 additions & 2 deletions examples/starlight/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ const lunaria = await createLunaria({
force: true,
logLevel: 'debug',
});
const status = await lunaria.getFullStatus();
console.info(status);
const status = await lunaria.getFileStatus('src/content/i18n/en.yml');
console.info(status.localizations[0].missingKeys);
console.timeEnd('Lunaria benchmark');
6 changes: 6 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./utils": {
"types": "./dist/utils/index.d.ts",
"default": "./dist/utils/index.js"
},
"./config": {
"types": "./dist/config/index.d.ts",
"default": "./dist/config/index.js"
Expand All @@ -43,6 +47,7 @@
"dependencies": {
"consola": "^3.2.3",
"jiti": "2.3.3",
"js-yaml": "^4.1.0",
"neotraverse": "^0.6.18",
"path-to-regexp": "6.3.0",
"picomatch": "^4.0.2",
Expand All @@ -52,6 +57,7 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@types/js-yaml": "^4.0.9",
"@types/node": "^22.5.4",
"@types/picomatch": "^3.0.1"
},
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { resolve } from 'node:path';
import { ConfigNotFound, ConfigValidationError } from '../errors/errors.js';
import { moduleLoader } from '../files/loaders.js';
import { loadModule } from '../files/loaders.js';
import { LunariaPreSetupSchema } from '../integrations/schema.js';
import type { CompleteLunariaUserConfig } from '../integrations/types.js';
import { exists, parseWithFriendlyErrors } from '../utils/utils.js';
Expand Down Expand Up @@ -38,7 +38,7 @@ export async function loadConfig() {
throw path;
}

const mod = await moduleLoader(path);
const mod = await loadModule(path);
if (mod instanceof Error) {
throw mod;
}
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ export const SetupOptionsSchema = z.object({
}
>
>,
fileLoader: z.function(z.tuple([z.string()]), z.any()),
});

const LunariaIntegrationSchema = z.object({
Expand Down
19 changes: 13 additions & 6 deletions packages/core/src/errors/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ export const FailedToLoadModule = {
export const InvalidFilesPattern = {
name: 'InvalidFilesPattern',
title: 'Invalid `files` pattern was found.',
message: (pattern: string, parameter: string) =>
`The file pattern \`${pattern}\` is missing the \`${parameter}\` parameter. Add it to your pattern string.`,
message: (pattern: string) =>
`The file pattern \`${pattern}\` is missing a valid path parameter. Be sure to add at least one to your pattern string.`,
} satisfies ErrorContext;

export const FileConfigNotFound = {
Expand All @@ -46,11 +46,18 @@ export const UncommittedFileFound = {
`The file \`${path}\` is being tracked but no commits have been found. Ensure all tracked files in your working branch are committed before running Lunaria.`,
} satisfies ErrorContext;

export const InvalidDictionaryFormat = {
name: 'InvalidDictionaryFormat',
title: 'A file with an invalid dictionary format was found.',
export const InvalidDictionaryStructure = {
name: 'InvalidDictionaryStructure',
title: 'A file with an invalid dictionary structure 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.`,
`The \`type: "dictionary"\` file \`${path}\` has an invalid structure. 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 UnsupportedDictionaryFileFormat = {
name: 'UnsupportedDictionaryFileFormat',
title: 'An unsupported file format was found.',
message: (file: string) =>
`The file \`${file}\` has an unsupported file format. Dictionaries can be Markdown/MDX/Markdoc, JSON, or JavaScript/TypeScript modules. Use one of these file formats or instead track this file without key completion checking by setting it to \`type: "universal"\` instead.`,
} satisfies ErrorContext;

export const UnsupportedIntegrationSelfUpdate = {
Expand Down
29 changes: 19 additions & 10 deletions packages/core/src/files/loaders.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { createJiti } from 'jiti';
import yaml from 'js-yaml';
import { parse } from 'ultramatter';
import { FailedToLoadModule } from '../errors/errors.js';

// TODO: Consider moving this out of the loaders file and directly into the public utils API.

/** Regex to match ESM and CJS JavaScript/TypeScript files. */
export const moduleFileRe = /\.(c|m)?(ts|js)$/;
/** Loader for JavaScript/TypeScript modules (CJS, ESM). */
export async function moduleLoader(path: string) {
export async function loadModule(path: string) {
const resolvedPath = resolve(path);
const jiti = createJiti(import.meta.url);

Expand All @@ -19,9 +22,9 @@ export async function moduleLoader(path: string) {
}

/** Regex to match files that support frontmatter. */
export const fileSupportsFrontmatterRe = /\.(yml|md|markdown|mdx|mdoc)$/;
/** Loader for frontmatter in `.yml`, `.md`, `.markdown`, `.mdx`, `.mdoc`. */
export async function frontmatterLoader(path: string) {
export const frontmatterFileRe = /\.(md|markdown|mdx|mdoc)$/;
/** Loader for frontmatter in `.md`, `.markdown`, `.mdx`, `.mdoc`. */
export async function loadFrontmatter(path: string) {
const resolvedPath = resolve(path);

try {
Expand All @@ -37,7 +40,7 @@ export async function frontmatterLoader(path: string) {
/** Regex to match JSON files. */
export const jsonFileRe = /\.json$/;
/** Loader for JSON files. */
export async function jsonLoader(path: string) {
export async function loadJSON(path: string) {
const resolvedPath = resolve(path);

try {
Expand All @@ -48,9 +51,15 @@ export async function jsonLoader(path: string) {
}
}

/** Loader for JS/TS modules, JSON, and frontmatter. */
export async function fileLoader(path: string) {
if (moduleFileRe.test(path)) return await moduleLoader(path);
if (fileSupportsFrontmatterRe.test(path)) return await frontmatterLoader(path);
if (jsonFileRe.test(path)) return jsonLoader(path);
export const yamlFileRe = /\.(yml|yaml)$/;
/** Loader for YAML files. */
export async function loadYAML(path: string) {
const resolvedPath = resolve(path);

try {
const file = await readFile(resolvedPath, 'utf-8');
return yaml.load(file);
} catch (e) {
if (e instanceof Error) return e;
}
}
6 changes: 3 additions & 3 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,10 @@ class Lunaria {
new Date(latestSourceChanges.latestTrackedChange.date) >
new Date(latestLocaleChanges.latestTrackedChange.date);

const entryTypeData = () => {
const entryTypeData = async () => {
if (fileConfig.type === 'dictionary') {
try {
const missingKeys = getDictionaryCompletion(
const missingKeys = await getDictionaryCompletion(
fileConfig.optionalKeys,
externalSafePath(external, this.#cwd, sourcePath),
externalSafePath(external, this.#cwd, localizedPath),
Expand All @@ -210,7 +210,7 @@ class Lunaria {
path: localizedPath,
git: latestLocaleChanges,
status: isOutdated ? 'outdated' : 'up-to-date',
...entryTypeData(),
...(await entryTypeData()),
};
}),
),
Expand Down
2 changes: 0 additions & 2 deletions packages/core/src/integrations/integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { ConsolaInstance } from 'consola';
import { validateFinalConfig, validateInitialConfig } from '../config/config.js';
import type { LunariaUserConfig } from '../config/types.js';
import { UnsupportedIntegrationSelfUpdate } from '../errors/errors.js';
import { fileLoader } from '../files/loaders.js';
import type { CompleteLunariaUserConfig } from './types.js';

export async function runSetupHook(config: LunariaUserConfig, logger: ConsolaInstance) {
Expand Down Expand Up @@ -37,7 +36,6 @@ export async function runSetupHook(config: LunariaUserConfig, logger: ConsolaIns
});
},
logger,
fileLoader,
});
}

Expand Down
1 change: 0 additions & 1 deletion packages/core/src/integrations/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export interface LunariaIntegration {
config: LunariaUserConfig;
updateConfig: (config: Partial<LunariaUserConfig>) => void;
logger: ConsolaInstance;
fileLoader: (path: string) => Promise<unknown>;
}) => void | Promise<void>;
};
}
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/status/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ export class LunariaGitInstance {
};
}

// TODO: Using an external repo seems to introduce some sort of performance gains, this should be tested to ensure
// its not a bug e.g. not being able to read certain files or the git history not being complete and missing commits
// that are necessary for the status to be accurate.
async handleExternalRepository() {
const { cloneDir, repository } = this.#config;
const { name, hosting, rootDir } = repository;
Expand Down
42 changes: 33 additions & 9 deletions packages/core/src/status/status.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import { Traverse } from 'neotraverse/modern';
import type { OptionalKeys } from '../config/types.js';
import { InvalidDictionaryFormat } from '../errors/errors.js';
import { fileLoader, fileSupportsFrontmatterRe, frontmatterLoader } from '../files/loaders.js';
import { InvalidDictionaryStructure, UnsupportedDictionaryFileFormat } from '../errors/errors.js';
import {
frontmatterFileRe,
jsonFileRe,
loadFrontmatter,
loadJSON,
loadModule,
loadYAML,
moduleFileRe,
yamlFileRe,
} from '../files/loaders.js';
import { DictionarySchema } from './schema.js';
import type { Dictionary } from './types.js';

export async function isFileLocalizable(path: string, localizableProperty: string | undefined) {
// If no localizableProperty is specified, all files are supposed to be localizable.
if (!localizableProperty) return true;
// If the file doesn't support frontmatter, it's automatically supposed to be localizable.
if (!fileSupportsFrontmatterRe.test(path)) return true;
if (!frontmatterFileRe.test(path)) return true;

const frontmatter = await frontmatterLoader(path);
const frontmatter = await loadFrontmatter(path);

if (frontmatter instanceof Error) return frontmatter;

Expand All @@ -31,27 +40,25 @@ export async function getDictionaryCompletion(
sourceDictPath: string,
localeDictPath: string,
) {
const sourceDict = await fileLoader(sourceDictPath);
const localeDict = await fileLoader(localeDictPath);
const [sourceDict, localeDict] = await loadDictionaries(sourceDictPath, localeDictPath);

if (sourceDict instanceof Error || localeDict instanceof Error) {
throw sourceDict instanceof Error ? sourceDict : localeDict;
}

const parsedSourceDict = DictionarySchema.safeParse(sourceDict);
if (parsedSourceDict.error) {
throw new Error(InvalidDictionaryFormat.message(sourceDictPath));
throw new Error(InvalidDictionaryStructure.message(sourceDictPath));
}

const parsedLocaleDict = DictionarySchema.safeParse(localeDict);
if (parsedLocaleDict.error) {
throw new Error(InvalidDictionaryFormat.message(localeDictPath));
throw new Error(InvalidDictionaryStructure.message(localeDictPath));
}

return findMissingKeys(optionalKeys, parsedSourceDict.data, parsedLocaleDict.data);
}

// TODO: Test this function.
export function findMissingKeys(
optionalKeys: OptionalKeys | undefined,
sourceDict: Dictionary,
Expand Down Expand Up @@ -94,3 +101,20 @@ export function findMissingKeys(

return missingKeys;
}

// TODO: Add integration tests for this function
function loadDictionaries(sourcePath: string, localePath: string) {
if (moduleFileRe.test(sourcePath)) {
return Promise.all([loadModule(sourcePath), loadModule(localePath)]);
}

if (yamlFileRe.test(sourcePath)) {
return Promise.all([loadYAML(sourcePath), loadYAML(localePath)]);
}

if (jsonFileRe.test(sourcePath)) {
return Promise.all([loadJSON(sourcePath), loadJSON(localePath)]);
}

throw new Error(UnsupportedDictionaryFileFormat.message(sourcePath));
}
2 changes: 2 additions & 0 deletions packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/** Public facing utils */
export { loadFrontmatter, loadJSON, loadModule, loadYAML } from '../files/loaders.js';
4 changes: 2 additions & 2 deletions packages/core/src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { mkdir, stat, writeFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import type { z } from 'zod';
import { errorMap } from '../errors/zod-map.js';
import { jsonLoader } from '../files/loaders.js';
import { loadJSON } from '../files/loaders.js';

export function isRelative(path: string) {
return path.startsWith('./') || path.startsWith('../');
Expand Down Expand Up @@ -75,7 +75,7 @@ export async function createCache(dir: string, entry: string, hash: string) {
);
};

const contents = async () => await jsonLoader(path);
const contents = async () => await loadJSON(path);

const revalidate = async (hash: string) => {
if ((await contents())?.__validation !== hash) {
Expand Down
Loading
Loading