Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 10 additions & 10 deletions packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import * as path from 'node:path';
import { maxWorkers, useTypeChecking } from '../../../utils/environment-options';
import { AngularHostOptions } from '../../angular/angular-host';
import { AngularCompilation, DiagnosticModes, NoopCompilation } from '../../angular/compilation';
import { type PersistentCacheStore, createPersistentCacheStore } from '../cache';
import { JavaScriptTransformer } from '../javascript-transformer';
import { LoadResultCache, createCachedLoad } from '../load-result-cache';
import { logCumulativeDurations, profileAsync, resetCumulativeDurations } from '../profiling';
Expand Down Expand Up @@ -62,9 +63,7 @@ export interface CompilerPluginOptions {
export function createCompilerPlugin(
pluginOptions: CompilerPluginOptions,
compilationContextOrCompilation:
| AngularCompilationContext
| AngularCompilation
| (() => Promise<AngularCompilation>),
AngularCompilationContext | AngularCompilation | (() => Promise<AngularCompilation>),
stylesheetBundler: ComponentStylesheetBundler,
): Plugin {
return {
Expand All @@ -76,20 +75,21 @@ export function createCompilerPlugin(

// Initialize a worker pool for JavaScript transformations.
// Webcontainers currently do not support this persistent cache store.
let cacheStore: import('../lmdb-cache-store').LmdbCacheStore | undefined;
let cacheStore: PersistentCacheStore | undefined;
if (pluginOptions.sourceFileCache?.persistentCachePath && !process.versions.webcontainer) {
try {
const { LmdbCacheStore } = await import('../lmdb-cache-store');
cacheStore = new LmdbCacheStore(
path.join(pluginOptions.sourceFileCache.persistentCachePath, 'angular-compiler.db'),
cacheStore = await createPersistentCacheStore(
path.join(pluginOptions.sourceFileCache.persistentCachePath, 'angular-compiler'),
);
} catch (e) {
setupWarnings.push({
text: 'Unable to initialize JavaScript cache storage.',
location: null,
notes: [
// Only show first line of lmdb load error which has platform support listed
{ text: (e as Error)?.message.split('\n')[0] ?? `${e}` },
...(e as Error).message
.split('\n')
.slice(1)
.map((text) => ({ text })),
Comment thread
clydin marked this conversation as resolved.
{
text: 'This will not affect the build output content but may result in slower builds.',
},
Expand Down Expand Up @@ -666,7 +666,7 @@ function createCompilerOptionsTransformer(
// If 'useDefineForClassFields' is already defined in the users project leave the value as is.
// Otherwise fallback to false due to https://github.com/microsoft/TypeScript/issues/45995
// which breaks the deprecated `@Effects` NGRX decorator and potentially other existing code as well.
compilerOptions.target = 9 /** ES2022 */;
compilerOptions.target = 9; /** ES2022 */
compilerOptions.useDefineForClassFields ??= false;

// Only add the warning on the initial build
Expand Down
46 changes: 46 additions & 0 deletions packages/angular/build/src/tools/esbuild/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
* Provides infrastructure for common caching functionality within the build system.
*/

import { assertIsError } from '../../utils/error';

/**
* A backing data store for one or more Cache instances.
* The interface is intentionally designed to support using a JavaScript
Expand Down Expand Up @@ -38,6 +40,16 @@ export interface CacheStore<V> {
set(key: string, value: V): this | Promise<this>;
}

/**
* A persistent backing data store that supports namespace partitioning
* and manual lifecycle close operations.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface PersistentCacheStore<V = any> extends CacheStore<V> {
createCache<T = V>(namespace: string): Cache<T>;
close(): void | Promise<void>;
}

/**
* A cache object that allows accessing and storing key/value pairs in
* an underlying CacheStore. This class is the primary method for consumers
Expand Down Expand Up @@ -224,3 +236,37 @@ export class MemoryCache<V> extends Cache<V, Map<string, V>> {
return this.store.entries();
}
}

/**
* Creates and returns a persistent cache store.
* Attempts to use the native LMDB store first, and falls back to the built-in SQLite store
* if LMDB fails to initialize.
*
* @param baseCachePath The base path of the cache file/directory without suffix/extension.
* @returns A promise resolving to a PersistentCacheStore instance.
*/
export async function createPersistentCacheStore(
baseCachePath: string,
): Promise<PersistentCacheStore> {
try {
const { LmdbCacheStore } = await import('./lmdb-cache-store');

return new LmdbCacheStore(baseCachePath + '.db');
} catch (lmdbError) {
try {
const { SqliteCacheStore } = await import('./sqlite-cache-store');

return new SqliteCacheStore(baseCachePath + '-sqlite.db');
} catch (sqliteError) {
assertIsError(lmdbError);
assertIsError(sqliteError);

throw new Error(
'Unable to initialize JavaScript cache storage.\n' +
`LMDB error: ${lmdbError.message.split('\n')[0]}\n` +
`SQLite error: ${sqliteError.message.split('\n')[0]}`,
{ cause: sqliteError },
);
Comment thread
clydin marked this conversation as resolved.
}
}
}
14 changes: 7 additions & 7 deletions packages/angular/build/src/tools/esbuild/i18n-inliner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { createHash } from 'node:crypto';
import { extname, join } from 'node:path';
import { WorkerPool } from '../../utils/worker-pool';
import { type BuildOutputFile, BuildOutputFileType, createOutputFile } from './bundler-files';
import type { LmdbCacheStore } from './lmdb-cache-store';
import { type PersistentCacheStore, createPersistentCacheStore } from './cache';

/**
* A keyword used to indicate if a JavaScript file may require inlining of translations.
Expand All @@ -38,7 +38,7 @@ export interface I18nInlinerOptions {
export class I18nInliner {
#cacheInitFailed = false;
#workerPool: WorkerPool;
#cache: LmdbCacheStore | undefined;
#cache: PersistentCacheStore | undefined;
readonly #localizeFiles: ReadonlyMap<string, BuildOutputFile>;
readonly #unmodifiedFiles: Array<BuildOutputFile>;

Expand Down Expand Up @@ -162,8 +162,10 @@ export class I18nInliner {

const result = await this.#workerPool.run({ filename, locale, translation });
if (this.#cache && cacheKey) {
// Failure to set the value should not fail the transform
await this.#cache.set(cacheKey, result).catch(() => {});
try {
// Failure to set the value should not fail the transform
await this.#cache.set(cacheKey, result);
} catch {}
}

return result;
Expand Down Expand Up @@ -273,9 +275,7 @@ export class I18nInliner {

// Initialize a persistent cache for i18n transformations.
try {
const { LmdbCacheStore } = await import('./lmdb-cache-store');

this.#cache = new LmdbCacheStore(join(persistentCachePath, 'angular-i18n.db'));
this.#cache = await createPersistentCacheStore(join(persistentCachePath, 'angular-i18n'));
} catch {
this.#cacheInitFailed = true;

Expand Down
4 changes: 2 additions & 2 deletions packages/angular/build/src/tools/esbuild/lmdb-cache-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
*/

import { RootDatabase, open } from 'lmdb';
import { Cache, CacheStore } from './cache';
import { Cache, PersistentCacheStore } from './cache';

export class LmdbCacheStore implements CacheStore<unknown> {
export class LmdbCacheStore implements PersistentCacheStore<unknown> {
readonly #cacheFileUrl;
#db: RootDatabase | undefined;

Expand Down
116 changes: 116 additions & 0 deletions packages/angular/build/src/tools/esbuild/sqlite-cache-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import { DatabaseSync, StatementSync } from 'node:sqlite';
import { Cache, PersistentCacheStore } from './cache';

export class SqliteCacheStore implements PersistentCacheStore<unknown> {
#db: DatabaseSync | undefined;
#getStmt: StatementSync | undefined;
#hasStmt: StatementSync | undefined;
#setStmt: StatementSync | undefined;
#updateAccessedStmt: StatementSync | undefined;

constructor(
readonly cachePath: string,
private readonly maxPayloadSize = 1024 * 1024 * 1024,
private readonly ttlDays = 14,
) {}

#ensureDb(): DatabaseSync {
if (!this.#db) {
this.#db = new DatabaseSync(this.cachePath);
// Optimize SQLite for cache usage
this.#db.exec('PRAGMA auto_vacuum = FULL;');
this.#db.exec('PRAGMA journal_mode = WAL;');
this.#db.exec('PRAGMA synchronous = NORMAL;');
this.#db.exec(
'CREATE TABLE IF NOT EXISTS cache (key TEXT PRIMARY KEY, value TEXT, last_accessed INTEGER NOT NULL) WITHOUT ROWID;',
);

this.#getStmt = this.#db.prepare('SELECT value FROM cache WHERE key = ?');
this.#hasStmt = this.#db.prepare('SELECT 1 FROM cache WHERE key = ?');
this.#setStmt = this.#db.prepare(
'INSERT OR REPLACE INTO cache (key, value, last_accessed) VALUES (?, ?, unixepoch())',
);
this.#updateAccessedStmt = this.#db.prepare(
'UPDATE cache SET last_accessed = unixepoch() WHERE key = ?',
);
}

return this.#db;
}
Comment thread
clydin marked this conversation as resolved.

// eslint-disable-next-line @typescript-eslint/no-explicit-any
async get(key: string): Promise<any> {
this.#ensureDb();
const row = this.#getStmt?.get(key) as { value: string } | undefined;

if (row) {
this.#updateAccessedStmt?.run(key);

try {
return JSON.parse(row.value);
} catch {
return undefined;
}
}

return undefined;
}

has(key: string): boolean {
this.#ensureDb();

return !!this.#hasStmt?.get(key);
}

async set(key: string, value: unknown): Promise<this> {
this.#ensureDb();
this.#setStmt?.run(key, JSON.stringify(value));

return this;
}
Comment thread
clydin marked this conversation as resolved.

createCache<V = unknown>(namespace: string): Cache<V> {
return new Cache(this, namespace);
}

close(): void {
if (this.#db) {
try {
// 1. Delete items older than N days
this.#db
.prepare("DELETE FROM cache WHERE last_accessed < unixepoch('now', ?);")
.run(`-${this.ttlDays} days`);

// 2. Prune oldest items if payload exceeds maxPayloadSize
const pruneStmt = this.#db.prepare(`
DELETE FROM cache WHERE key IN (
SELECT key FROM (
SELECT key,
sum(length(key) + length(value)) OVER (ORDER BY last_accessed DESC, key DESC) as running_size
FROM cache
) WHERE running_size > ?
);
`);
pruneStmt.run(this.maxPayloadSize);
} catch {
// Pruning errors should not block build success
} finally {
this.#getStmt = undefined;
this.#hasStmt = undefined;
this.#setStmt = undefined;
this.#updateAccessedStmt = undefined;

this.#db.close();
this.#db = undefined;
}
Comment thread
clydin marked this conversation as resolved.
}
}
}
Loading
Loading