@angular-devkit/build-angular
Version:
Angular Webpack Build Facade
318 lines (317 loc) • 14.7 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.inlineLocales = inlineLocales;
const remapping_1 = __importDefault(require("@ampproject/remapping"));
const core_1 = require("@babel/core");
const fs = __importStar(require("node:fs/promises"));
const path = __importStar(require("node:path"));
const node_worker_threads_1 = require("node:worker_threads");
const environment_options_1 = require("./environment-options");
const error_1 = require("./error");
const load_esm_1 = require("./load-esm");
// Lazy loaded webpack-sources object
// Webpack is only imported if needed during the processing
let webpackSources;
const { i18n } = (node_worker_threads_1.workerData || {});
/**
* Internal flag to enable the direct usage of the `@angular/localize` translation plugins.
* Their usage is currently several times slower than the string manipulation method.
* Future work to optimize the plugins should enable plugin usage as the default.
*/
const USE_LOCALIZE_PLUGINS = false;
/**
* Cached instance of the `@angular/localize/tools` module.
* This is used to remove the need to repeatedly import the module per file translation.
*/
let localizeToolsModule;
/**
* Attempts to load the `@angular/localize/tools` module containing the functionality to
* perform the file translations.
* This module must be dynamically loaded as it is an ESM module and this file is CommonJS.
*/
async function loadLocalizeTools() {
if (localizeToolsModule !== undefined) {
return localizeToolsModule;
}
// Load ESM `@angular/localize/tools` using the TypeScript dynamic import workaround.
// Once TypeScript provides support for keeping the dynamic import this workaround can be
// changed to a direct dynamic import.
return (0, load_esm_1.loadEsmModule)('@angular/localize/tools');
}
async function createI18nPlugins(locale, translation, missingTranslation, shouldInline, localeDataContent) {
const { Diagnostics, makeEs2015TranslatePlugin, makeLocalePlugin } = await loadLocalizeTools();
const plugins = [];
const diagnostics = new Diagnostics();
if (shouldInline) {
plugins.push(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
makeEs2015TranslatePlugin(diagnostics, (translation || {}), {
missingTranslation: translation === undefined ? 'ignore' : missingTranslation,
}));
}
plugins.push(makeLocalePlugin(locale));
if (localeDataContent) {
plugins.push({
visitor: {
Program(path) {
path.unshiftContainer('body', core_1.template.ast(localeDataContent));
},
},
});
}
return { diagnostics, plugins };
}
const localizeName = '$localize';
async function inlineLocales(options) {
if (!i18n || i18n.inlineLocales.size === 0) {
return { file: options.filename, diagnostics: [], count: 0 };
}
if (i18n.flatOutput && i18n.inlineLocales.size > 1) {
throw new Error('Flat output is only supported when inlining one locale.');
}
const hasLocalizeName = options.code.includes(localizeName);
if (!hasLocalizeName && !options.setLocale) {
return inlineCopyOnly(options);
}
await loadLocalizeTools();
let ast;
try {
ast = (0, core_1.parseSync)(options.code, {
babelrc: false,
configFile: false,
sourceType: 'unambiguous',
filename: options.filename,
});
}
catch (error) {
(0, error_1.assertIsError)(error);
// Make the error more readable.
// Same errors will contain the full content of the file as the error message
// Which makes it hard to find the actual error message.
const index = error.message.indexOf(')\n');
const msg = index !== -1 ? error.message.slice(0, index + 1) : error.message;
throw new Error(`${msg}\nAn error occurred inlining file "${options.filename}"`);
}
if (!ast) {
throw new Error(`Unknown error occurred inlining file "${options.filename}"`);
}
if (!USE_LOCALIZE_PLUGINS) {
return inlineLocalesDirect(ast, options);
}
const diagnostics = [];
for (const locale of i18n.inlineLocales) {
const isSourceLocale = locale === i18n.sourceLocale;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const translations = isSourceLocale ? {} : i18n.locales[locale].translation || {};
let localeDataContent;
if (options.setLocale) {
// If locale data is provided, load it and prepend to file
const localeDataPath = i18n.locales[locale]?.dataPath;
if (localeDataPath) {
localeDataContent = await loadLocaleData(localeDataPath, true);
}
}
const { diagnostics: localeDiagnostics, plugins } = await createI18nPlugins(locale, translations, isSourceLocale ? 'ignore' : options.missingTranslation || 'warning', true, localeDataContent);
const transformResult = (0, core_1.transformFromAstSync)(ast, options.code, {
filename: options.filename,
// using false ensures that babel will NOT search and process sourcemap comments (large memory usage)
// The types do not include the false option even though it is valid
// eslint-disable-next-line @typescript-eslint/no-explicit-any
inputSourceMap: false,
babelrc: false,
configFile: false,
plugins,
compact: !environment_options_1.shouldBeautify,
sourceMaps: !!options.map,
});
diagnostics.push(...localeDiagnostics.messages);
if (!transformResult || !transformResult.code) {
throw new Error(`Unknown error occurred processing bundle for "${options.filename}".`);
}
const subPath = i18n.locales[locale].subPath;
const outputPath = path.join(options.outputPath, i18n.flatOutput ? '' : subPath, options.filename);
await fs.writeFile(outputPath, transformResult.code);
if (options.map && transformResult.map) {
const outputMap = (0, remapping_1.default)([transformResult.map, options.map], () => null);
await fs.writeFile(outputPath + '.map', JSON.stringify(outputMap));
}
}
return { file: options.filename, diagnostics };
}
async function inlineLocalesDirect(ast, options) {
if (!i18n || i18n.inlineLocales.size === 0) {
return { file: options.filename, diagnostics: [], count: 0 };
}
const { default: generate } = await Promise.resolve().then(() => __importStar(require('@babel/generator')));
const localizeDiag = await loadLocalizeTools();
const diagnostics = new localizeDiag.Diagnostics();
const positions = findLocalizePositions(ast, options, localizeDiag);
if (positions.length === 0 && !options.setLocale) {
return inlineCopyOnly(options);
}
const inputMap = !!options.map && JSON.parse(options.map);
// Cleanup source root otherwise it will be added to each source entry
const mapSourceRoot = inputMap && inputMap.sourceRoot;
if (inputMap) {
delete inputMap.sourceRoot;
}
// Load Webpack only when needed
if (webpackSources === undefined) {
webpackSources = (await Promise.resolve().then(() => __importStar(require('webpack')))).sources;
}
const { ConcatSource, OriginalSource, ReplaceSource, SourceMapSource } = webpackSources;
for (const locale of i18n.inlineLocales) {
const content = new ReplaceSource(inputMap
? new SourceMapSource(options.code, options.filename, inputMap)
: new OriginalSource(options.code, options.filename));
const isSourceLocale = locale === i18n.sourceLocale;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const translations = isSourceLocale ? {} : i18n.locales[locale].translation || {};
for (const position of positions) {
const translated = localizeDiag.translate(diagnostics, translations, position.messageParts, position.expressions, isSourceLocale ? 'ignore' : options.missingTranslation || 'warning');
const expression = localizeDiag.buildLocalizeReplacement(translated[0], translated[1]);
const { code } = generate(expression);
content.replace(position.start, position.end - 1, code);
}
let outputSource = content;
if (options.setLocale) {
const setLocaleText = `globalThis.$localize=Object.assign(globalThis.$localize || {},{locale:"${locale}"});\n`;
// If locale data is provided, load it and prepend to file
let localeDataSource;
const localeDataPath = i18n.locales[locale] && i18n.locales[locale].dataPath;
if (localeDataPath) {
const localeDataContent = await loadLocaleData(localeDataPath, true);
localeDataSource = new OriginalSource(localeDataContent, path.basename(localeDataPath));
}
outputSource = localeDataSource
? // The semicolon ensures that there is no syntax error between statements
new ConcatSource(setLocaleText, localeDataSource, ';\n', content)
: new ConcatSource(setLocaleText, content);
}
const { source: outputCode, map: outputMap } = outputSource.sourceAndMap();
const subPath = i18n.locales[locale].subPath;
const outputPath = path.join(options.outputPath, i18n.flatOutput ? '' : subPath, options.filename);
await fs.writeFile(outputPath, outputCode);
if (inputMap && outputMap) {
outputMap.file = options.filename;
if (mapSourceRoot) {
outputMap.sourceRoot = mapSourceRoot;
}
await fs.writeFile(outputPath + '.map', JSON.stringify(outputMap));
}
}
return { file: options.filename, diagnostics: diagnostics.messages, count: positions.length };
}
async function inlineCopyOnly(options) {
if (!i18n) {
throw new Error('i18n options are missing');
}
for (const locale of i18n.inlineLocales) {
const subPath = i18n.locales[locale].subPath;
const outputPath = path.join(options.outputPath, i18n.flatOutput ? '' : subPath, options.filename);
await fs.writeFile(outputPath, options.code);
if (options.map) {
await fs.writeFile(outputPath + '.map', options.map);
}
}
return { file: options.filename, diagnostics: [], count: 0 };
}
function findLocalizePositions(ast, options, utils) {
const positions = [];
// Workaround to ensure a path hub is present for traversal
const { File } = require('@babel/core');
const file = new File({}, { code: options.code, ast });
(0, core_1.traverse)(file.ast, {
TaggedTemplateExpression(path) {
if (core_1.types.isIdentifier(path.node.tag) && path.node.tag.name === localizeName) {
const [messageParts, expressions] = unwrapTemplateLiteral(path, utils);
positions.push({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
start: path.node.start,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
end: path.node.end,
messageParts,
expressions,
});
}
},
});
return positions;
}
function unwrapTemplateLiteral(path, utils) {
const [messageParts] = utils.unwrapMessagePartsFromTemplateLiteral(path.get('quasi').get('quasis'));
const [expressions] = utils.unwrapExpressionsFromTemplateLiteral(path.get('quasi'));
return [messageParts, expressions];
}
async function loadLocaleData(path, optimize) {
// The path is validated during option processing before the build starts
const content = await fs.readFile(path, 'utf8');
// Downlevel and optimize the data
const transformResult = await (0, core_1.transformAsync)(content, {
filename: path,
// The types do not include the false option even though it is valid
// eslint-disable-next-line @typescript-eslint/no-explicit-any
inputSourceMap: false,
babelrc: false,
configFile: false,
presets: [
[
require.resolve('@babel/preset-env'),
{
bugfixes: true,
targets: { esmodules: true },
},
],
],
minified: environment_options_1.allowMinify && optimize,
compact: !environment_options_1.shouldBeautify && optimize,
comments: !optimize,
});
if (!transformResult || !transformResult.code) {
throw new Error(`Unknown error occurred processing bundle for "${path}".`);
}
return transformResult.code;
}
;