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
Add git caching
  • Loading branch information
yanthomasdev committed Sep 16, 2024
commit 205487f58f47b2bcd5c6e80043997184ee787004
68 changes: 31 additions & 37 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ import { createPathResolver } from './files/paths.js';
import { LunariaGitInstance } from './status/git.js';
import { getDictionaryCompletion, isFileLocalizable } from './status/status.js';
import type { LunariaStatus, StatusLocalizationEntry } from './status/types.js';

// Additional data to ensure we can force rebuild the cache.
// Bump this whenever there are breaking changes to the status output.
const CACHE_VERSION = '1.0.0';
import { Cache, md5 } from './utils/utils.js';

// Logging levels available for the console.
// Used to translate consola's numeric values into human-readable strings.
Expand All @@ -35,18 +32,13 @@ export class Lunaria {
#config: LunariaConfig;
#git: LunariaGitInstance;
#logger: ConsolaInstance;
// Force a fresh status build, ignoring the cache.
#force: boolean;
// Hash built out of the cache version + latest commit hash.
// If either changed, the status will be rebuilt.
#cacheHash?: string;
#hash: string;

constructor({ logLevel = 'info', force = false, config }: LunariaOpts = {}) {
const logger = createConsola({
this.#logger = createConsola({
level: CONSOLE_LEVELS[logLevel],
});

this.#logger = logger;
this.#force = force;

try {
Expand All @@ -56,34 +48,16 @@ export class Lunaria {
process.exit(1);
}

this.#git = new LunariaGitInstance(this.#config, logger);
// Hash used to revalidate the cache -- the tracking properties manipulate how the changes are tracked,
// therefore we have to account for them so that the cache is fresh.
this.#hash = md5(
`ignoredKeywords::${this.#config.tracking.ignoredKeywords.join('|')}:localizableProperty::${this.#config.tracking.localizableProperty}`,
);

this.#git = new LunariaGitInstance(this.#config, this.#logger, this.#force, this.#hash);
}

async getFullStatus() {
/** Uncomment when working in caching
const latestCommitHash = await this.#git.revparse(['HEAD']);
// The configuration has to be accounted to invalidate the cache
// since it can affect the status output.
const configString = JSON.stringify(this.#config);
const cacheHash = md5(CACHE_VERSION + latestCommitHash + configString);

const cachePath = join(this.#config.cacheDir, 'status.json');

if (existsSync(cachePath) && cacheHash === this.#cacheHash && !this.#force) {
try {
const statusJSON = readFileSync(cachePath, {
encoding: 'utf-8',
});
const status = JSON.parse(statusJSON);
this.#logger.success('Successfully loaded status from cache.');

return status as LunariaStatus;
} catch (e) {
this.#logger.warn('Failed to read status from cache, rebuilding...');
}
}
*/

const { files } = this.#config;

const status: LunariaStatus = [];
Expand Down Expand Up @@ -124,16 +98,31 @@ export class Lunaria {
/** We use `Promise.all` to allow the promises to run in parallel, increasing the performance considerably. */
await Promise.all(
sourceFilePaths.sort().map(async (path) => {
const fileStatus = await this.getFileStatus(path);
const fileStatus = await this.#getFileStatus(path, false);
if (fileStatus) status.push(fileStatus);
}),
);
}

// Save the existing git data into the cache for next builds.
if (!this.#force) {
new Cache(this.#config.cacheDir, 'git', this.#hash).write(this.#git.cache);
}

return status;
}

// The existence of both a public and private `getFileStatus()` is to hide
// the cache parameter from the public API. We do that so when we invoke
// it from `getFullStatus()` we only write to the cache once, considerably
// increasing performance (1 cache write instead of one for each file).
// Otherwise, when users invoke this method, they will also want to enjoy
// caching normally, unless they explicitly want to force a fresh status.
async getFileStatus(path: string) {
return this.#getFileStatus(path, !this.#force);
}

async #getFileStatus(path: string, cache: boolean) {
const fileConfig = this.findFileConfig(path);

if (!fileConfig) {
Expand All @@ -155,6 +144,11 @@ export class Lunaria {

const latestSourceChanges = await this.#git.getFileLatestChanges(sourcePath);

// Save the existing git data into the cache for next builds.
if (cache) {
new Cache(this.#config.cacheDir, 'git', this.#hash).write(this.#git.cache);
}

return {
...fileConfig,
source: {
Expand Down
55 changes: 30 additions & 25 deletions packages/core/src/status/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,54 +4,55 @@ import type { ConsolaInstance } from 'consola';
import picomatch from 'picomatch';
import { type DefaultLogFields, type ListLogLine, simpleGit } from 'simple-git';
import type { LunariaConfig } from '../config/types.js';
import { FileCommitsNotFound, UncommittedFileFound } from '../errors/errors.js';
import { UncommittedFileFound } from '../errors/errors.js';
import type { RegExpGroups } from '../utils/types.js';
import { Cache } from '../utils/utils.js';

export class LunariaGitInstance {
#git = simpleGit({
maxConcurrentProcesses: Math.max(2, Math.min(32, cpus().length)),
});
#config: LunariaConfig;
#logger: ConsolaInstance;
#force: boolean;
#cache: Record<string, string>;

constructor(config: LunariaConfig, logger: ConsolaInstance) {
constructor(config: LunariaConfig, logger: ConsolaInstance, force: boolean, hash: string) {
this.#logger = logger;
this.#config = config;
}
this.#force = force;

// TODO: Try to cache the latest changes hash for each file so that you don't have to fetch the entire history every run, only new ones.
async #getFileHistory(path: string) {
try {
const log = await this.#git.log({
file: resolve(path),
strictDate: true,
});

return log;
} catch (e) {
this.#logger.error(FileCommitsNotFound.message(path));
throw e;
if (this.#force) {
this.#cache = {};
} else {
const cache = new Cache(this.#config.cacheDir, 'git', hash);
this.#cache = cache.contents;
}
}

async getFileLatestChanges(path: string) {
const logHistory = await this.#getFileHistory(path);

const latestChange = logHistory.latest;
/**
* Edge case: it might be possible all the changes for a file have
* been purposefully ignored in Lunaria, therefore we need to define
* the latest change as the latest tracked change.
* TODO: Check if this is not an stupid assumption.
*/
// The cache will keep the latest tracked change hash, that means it will be able
// to completely skip looking into older commits, considerably increasing performance.
const log = await this.#git.log({
file: resolve(path),
strictDate: true,
from: this.#cache[path] ? `${this.#cache[path]}^` : undefined,
});

const latestChange = log.latest;
// Edge case: sometimes all the changes for a file (or the only one)
// have been purposefully ignored in Lunaria, therefore we need to
// define the latest change as the latest tracked change.
const latestTrackedChange =
findLatestTrackedCommit(this.#config.tracking, path, logHistory.all) ?? latestChange;
findLatestTrackedCommit(this.#config.tracking, path, log.all) ?? latestChange;

if (!latestChange || !latestTrackedChange) {
this.#logger.error(UncommittedFileFound.message(path));
process.exit(1);
}

if (!this.#force) this.#cache[path] = latestTrackedChange.hash;

return {
latestChange: {
date: latestChange.date,
Expand All @@ -65,6 +66,10 @@ export class LunariaGitInstance {
},
};
}

get cache() {
return this.#cache;
}
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/status/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export type Dictionary = {
[k: string]: string | Dictionary;
};

type FileGitData = {
export type FileGitData = {
latestChange: {
message: string;
date: string;
Expand Down
44 changes: 44 additions & 0 deletions packages/core/src/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { createHash } from 'node:crypto';
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { jsonLoader } from '../files/loaders.js';

export function isRelative(path: string) {
return path.startsWith('./') || path.startsWith('../');
Expand All @@ -23,3 +26,44 @@ export function stringFromFormat(format: string, placeholders: Record<string, st
}
return formatResult;
}

export class Cache {
#dir: string;
#file: string;
#path: string;
#hash: string;

constructor(dir: string, entry: string, hash: string) {
this.#file = `${entry}.json`;
this.#dir = resolve(dir);
this.#path = join(this.#dir, this.#file);
this.#hash = hash;

if (!existsSync(this.#path)) {
mkdirSync(this.#dir, { recursive: true });
this.write({ __validation: this.#hash });
} else {
this.#revalidate(this.#hash);
}
}

get contents() {
return jsonLoader(this.#path);
}

write(contents: Record<string, string>) {
writeFileSync(
this.#path,
JSON.stringify({
__validation: this.#hash,
...contents,
}),
);
}

#revalidate(hash: string) {
if (this.contents?.__validation !== hash) {
this.write({});
}
}
}