@angular/build
Version:
Official build system for Angular
266 lines • 11.6 kB
JavaScript
;
/**
* @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
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.I18nInliner = void 0;
const node_assert_1 = __importDefault(require("node:assert"));
const node_crypto_1 = require("node:crypto");
const node_path_1 = require("node:path");
const worker_pool_1 = require("../../utils/worker-pool");
const bundler_context_1 = require("./bundler-context");
const utils_1 = require("./utils");
/**
* A keyword used to indicate if a JavaScript file may require inlining of translations.
* This keyword is used to avoid processing files that would not otherwise need i18n processing.
*/
const LOCALIZE_KEYWORD = '$localize';
/**
* A class that performs i18n translation inlining of JavaScript code.
* A worker pool is used to distribute the transformation actions and allow
* parallel processing. Inlining is only performed on code that contains the
* localize function (`$localize`).
*/
class I18nInliner {
options;
#cacheInitFailed = false;
#workerPool;
#cache;
#localizeFiles;
#unmodifiedFiles;
constructor(options, maxThreads) {
this.options = options;
this.#unmodifiedFiles = [];
const { outputFiles, shouldOptimize, missingTranslation } = options;
const files = new Map();
const pendingMaps = [];
for (const file of outputFiles) {
if (file.type === bundler_context_1.BuildOutputFileType.Root || file.type === bundler_context_1.BuildOutputFileType.ServerRoot) {
// Skip also the server entry-point.
// Skip stats and similar files.
continue;
}
const fileExtension = (0, node_path_1.extname)(file.path);
if (fileExtension === '.js' || fileExtension === '.mjs') {
// Check if localizations are present
const contentBuffer = Buffer.isBuffer(file.contents)
? file.contents
: Buffer.from(file.contents.buffer, file.contents.byteOffset, file.contents.byteLength);
const hasLocalize = contentBuffer.includes(LOCALIZE_KEYWORD);
if (hasLocalize) {
files.set(file.path, file);
continue;
}
}
else if (fileExtension === '.map') {
// The related JS file may not have been checked yet. To ensure that map files are not
// missed, store any pending map files and check them after all output files.
pendingMaps.push(file);
continue;
}
this.#unmodifiedFiles.push(file);
}
// Check if any pending map files should be processed by checking if the parent JS file is present
for (const file of pendingMaps) {
if (files.has(file.path.slice(0, -4))) {
files.set(file.path, file);
}
else {
this.#unmodifiedFiles.push(file);
}
}
this.#localizeFiles = files;
this.#workerPool = new worker_pool_1.WorkerPool({
filename: require.resolve('./i18n-inliner-worker'),
maxThreads,
// Extract options to ensure only the named options are serialized and sent to the worker
workerData: {
missingTranslation,
shouldOptimize,
// A Blob is an immutable data structure that allows sharing the data between workers
// without copying until the data is actually used within a Worker. This is useful here
// since each file may not actually be processed in each Worker and the Blob avoids
// unneeded repeat copying of potentially large JavaScript files.
files: new Map(Array.from(files, ([name, file]) => [name, new Blob([file.contents])])),
},
});
}
/**
* Performs inlining of translations for the provided locale and translations. The files that
* are processed originate from the files passed to the class constructor and filter by presence
* of the localize function keyword.
* @param locale The string representing the locale to inline.
* @param translation The translation messages to use when inlining.
* @returns A promise that resolves to an array of OutputFiles representing a translated result.
*/
async inlineForLocale(locale, translation) {
await this.initCache();
const { shouldOptimize, missingTranslation } = this.options;
// Request inlining for each file that contains localize calls
const requests = [];
let fileCacheKeyBase;
for (const [filename, file] of this.#localizeFiles) {
let cacheKey;
if (filename.endsWith('.map')) {
continue;
}
let cacheResultPromise = Promise.resolve(null);
if (this.#cache) {
fileCacheKeyBase ??= Buffer.from(JSON.stringify({ locale, translation, missingTranslation, shouldOptimize }), 'utf-8');
// NOTE: If additional options are added, this may need to be updated.
// TODO: Consider xxhash or similar instead of SHA256
cacheKey = (0, node_crypto_1.createHash)('sha256')
.update(file.hash)
.update(filename)
.update(fileCacheKeyBase)
.digest('hex');
// Failure to get the value should not fail the transform
cacheResultPromise = this.#cache.get(cacheKey).catch(() => null);
}
const fileResult = cacheResultPromise.then(async (cachedResult) => {
if (cachedResult) {
return cachedResult;
}
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(() => { });
}
return result;
});
requests.push(fileResult);
}
// Wait for all file requests to complete
const rawResults = await Promise.all(requests);
// Convert raw results to output file objects and include all unmodified files
const errors = [];
const warnings = [];
const outputFiles = [
...rawResults.flatMap(({ file, code, map, messages }) => {
const type = this.#localizeFiles.get(file)?.type;
(0, node_assert_1.default)(type !== undefined, 'localized file should always have a type' + file);
const resultFiles = [(0, utils_1.createOutputFile)(file, code, type)];
if (map) {
resultFiles.push((0, utils_1.createOutputFile)(file + '.map', map, type));
}
for (const message of messages) {
if (message.type === 'error') {
errors.push(message.message);
}
else {
warnings.push(message.message);
}
}
return resultFiles;
}),
...this.#unmodifiedFiles.map((file) => file.clone()),
];
return {
outputFiles,
errors,
warnings,
};
}
async inlineTemplateUpdate(locale, translation, templateCode, templateId) {
const hasLocalize = templateCode.includes(LOCALIZE_KEYWORD);
if (!hasLocalize) {
return {
code: templateCode,
errors: [],
warnings: [],
};
}
const { output, messages } = await this.#workerPool.run({ code: templateCode, filename: templateId, locale, translation }, { name: 'inlineCode' });
const errors = [];
const warnings = [];
for (const message of messages) {
if (message.type === 'error') {
errors.push(message.message);
}
else {
warnings.push(message.message);
}
}
return {
code: output,
errors,
warnings,
};
}
/**
* Stops all active transformation tasks and shuts down all workers.
* @returns A void promise that resolves when closing is complete.
*/
close() {
return this.#workerPool.destroy();
}
/**
* Initializes the cache for storing translated bundles.
* If the cache is already initialized, it does nothing.
*
* @returns A promise that resolves once the cache initialization process is complete.
*/
async initCache() {
if (this.#cache || this.#cacheInitFailed) {
return;
}
const { persistentCachePath } = this.options;
// Webcontainers currently do not support this persistent cache store.
if (!persistentCachePath || process.versions.webcontainer) {
return;
}
// Initialize a persistent cache for i18n transformations.
try {
const { LmdbCacheStore } = await Promise.resolve().then(() => __importStar(require('./lmdb-cache-store')));
this.#cache = new LmdbCacheStore((0, node_path_1.join)(persistentCachePath, 'angular-i18n.db'));
}
catch {
this.#cacheInitFailed = true;
// eslint-disable-next-line no-console
console.warn('Unable to initialize JavaScript cache storage.\n' +
'This will not affect the build output content but may result in slower builds.');
}
}
}
exports.I18nInliner = I18nInliner;
//# sourceMappingURL=i18n-inliner.js.map