UNPKG

@angular-devkit/build-angular

Version:
656 lines (655 loc) 28.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.inlineLocales = exports.createI18nPlugins = exports.process = exports.setup = void 0; /** * @license * Copyright Google Inc. 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.io/license */ const core_1 = require("@babel/core"); const template_1 = require("@babel/template"); const crypto_1 = require("crypto"); const fs = require("fs"); const path = require("path"); const semver_1 = require("semver"); const source_map_1 = require("source-map"); const terser_1 = require("terser"); const v8 = require("v8"); const webpack_sources_1 = require("webpack-sources"); const environment_options_1 = require("./environment-options"); const cacache = require('cacache'); const deserialize = v8.deserialize; // If code size is larger than 500KB, consider lower fidelity but faster sourcemap merge const FAST_SOURCEMAP_THRESHOLD = 500 * 1024; let cachePath; let i18n; function setup(data) { const options = Array.isArray(data) ? deserialize(Buffer.from(data)) : data; cachePath = options.cachePath; i18n = options.i18n; } exports.setup = setup; async function cachePut(content, key, integrity) { if (cachePath && key) { await cacache.put(cachePath, key || null, content, { metadata: { integrity }, }); } } async function process(options) { if (!options.cacheKeys) { options.cacheKeys = []; } const result = { name: options.name }; if (options.integrityAlgorithm) { // Store unmodified code integrity value -- used for SRI value replacement result.integrity = generateIntegrityValue(options.integrityAlgorithm, options.code); } // Runtime chunk requires specialized handling if (options.runtime) { return { ...result, ...(await processRuntime(options)) }; } const basePath = path.dirname(options.filename); const filename = path.basename(options.filename); const downlevelFilename = filename.replace(/\-(es20\d{2}|esnext)/, '-es5'); const downlevel = !options.optimizeOnly; const sourceCode = options.code; const sourceMap = options.map ? JSON.parse(options.map) : undefined; let downlevelCode; let downlevelMap; if (downlevel) { const { supportedBrowsers: targets = [] } = options; // todo: revisit this in version 10, when we update our defaults browserslist // Without this workaround bundles will not be downlevelled because Babel doesn't know handle to 'op_mini all' // See: https://github.com/babel/babel/issues/11155 if (Array.isArray(targets) && targets.includes('op_mini all')) { targets.push('ie_mob 11'); } else if ('op_mini' in targets) { targets['ie_mob'] = '11'; } // Downlevel the bundle const transformResult = await core_1.transformAsync(sourceCode, { 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 // tslint:disable-next-line: no-any inputSourceMap: false, babelrc: false, configFile: false, presets: [[ require.resolve('@babel/preset-env'), { // browserslist-compatible query or object of minimum environment versions to support targets, // modules aren't needed since the bundles use webpack's custom module loading modules: false, // 'transform-typeof-symbol' generates slower code exclude: ['transform-typeof-symbol'], }, ]], plugins: [ createIifeWrapperPlugin(), ...(options.replacements ? [createReplacePlugin(options.replacements)] : []), ], minified: environment_options_1.allowMinify && !!options.optimize, compact: !environment_options_1.shouldBeautify && !!options.optimize, sourceMaps: !!sourceMap, }); if (!transformResult || !transformResult.code) { throw new Error(`Unknown error occurred processing bundle for "${options.filename}".`); } downlevelCode = transformResult.code; if (sourceMap && transformResult.map) { // String length is used as an estimate for byte length const fastSourceMaps = sourceCode.length > FAST_SOURCEMAP_THRESHOLD; downlevelMap = await mergeSourceMaps(sourceCode, sourceMap, downlevelCode, transformResult.map, filename, // When not optimizing, the sourcemaps are significantly less complex // and can use the higher fidelity merge !!options.optimize && fastSourceMaps); } } if (downlevelCode) { result.downlevel = await processBundle({ ...options, code: downlevelCode, map: downlevelMap, filename: path.join(basePath, downlevelFilename), isOriginal: false, }); } if (!result.original && !options.ignoreOriginal) { result.original = await processBundle({ ...options, isOriginal: true, }); } return result; } exports.process = process; async function mergeSourceMaps(inputCode, inputSourceMap, resultCode, resultSourceMap, filename, fast = false) { if (fast) { return mergeSourceMapsFast(inputSourceMap, resultSourceMap); } // SourceMapSource produces high-quality sourcemaps // The last argument is not yet in the typings // tslint:disable-next-line: no-any return new webpack_sources_1.SourceMapSource(resultCode, filename, resultSourceMap, inputCode, inputSourceMap, true).map(); } async function mergeSourceMapsFast(first, second) { const sourceRoot = first.sourceRoot; const generator = new source_map_1.SourceMapGenerator(); // sourcemap package adds the sourceRoot to all position source paths if not removed delete first.sourceRoot; await source_map_1.SourceMapConsumer.with(first, null, originalConsumer => { return source_map_1.SourceMapConsumer.with(second, null, newConsumer => { newConsumer.eachMapping(mapping => { if (mapping.originalLine === null) { return; } const originalPosition = originalConsumer.originalPositionFor({ line: mapping.originalLine, column: mapping.originalColumn, }); if (originalPosition.line === null || originalPosition.column === null || originalPosition.source === null) { return; } generator.addMapping({ generated: { line: mapping.generatedLine, column: mapping.generatedColumn, }, name: originalPosition.name || undefined, original: { line: originalPosition.line, column: originalPosition.column, }, source: originalPosition.source, }); }); }); }); const map = generator.toJSON(); map.file = second.file; map.sourceRoot = sourceRoot; // Add source content if present if (first.sourcesContent) { // Source content array is based on index of sources const sourceContentMap = new Map(); for (let i = 0; i < first.sources.length; i++) { // make paths "absolute" so they can be compared (`./a.js` and `a.js` are equivalent) sourceContentMap.set(path.resolve('/', first.sources[i]), i); } map.sourcesContent = []; for (let i = 0; i < map.sources.length; i++) { const contentIndex = sourceContentMap.get(path.resolve('/', map.sources[i])); if (contentIndex === undefined) { map.sourcesContent.push(''); } else { map.sourcesContent.push(first.sourcesContent[contentIndex]); } } } // Put the sourceRoot back if (sourceRoot) { first.sourceRoot = sourceRoot; } return map; } async function processBundle(options) { const { optimize, isOriginal, code, map, filename: filepath, hiddenSourceMaps, cacheKeys = [], integrityAlgorithm, } = options; const rawMap = typeof map === 'string' ? JSON.parse(map) : map; const filename = path.basename(filepath); let result; if (rawMap) { rawMap.file = filename; } if (optimize) { result = await terserMangle(code, { filename, map: rawMap, compress: !isOriginal, ecma: isOriginal ? 2015 : 5, }); } else { result = { map: rawMap, code, }; } let mapContent; if (result.map) { if (!hiddenSourceMaps) { result.code += `\n//# sourceMappingURL=${filename}.map`; } mapContent = JSON.stringify(result.map); await cachePut(mapContent, cacheKeys[isOriginal ? 1 /* OriginalMap */ : 3 /* DownlevelMap */]); fs.writeFileSync(filepath + '.map', mapContent); } const fileResult = createFileEntry(filepath, result.code, mapContent, integrityAlgorithm); await cachePut(result.code, cacheKeys[isOriginal ? 0 /* OriginalCode */ : 2 /* DownlevelCode */], fileResult.integrity); fs.writeFileSync(filepath, result.code); return fileResult; } async function terserMangle(code, options = {}) { // Note: Investigate converting the AST instead of re-parsing // estree -> terser is already supported; need babel -> estree/terser // Mangle downlevel code const minifyOutput = await terser_1.minify(options.filename ? { [options.filename]: code } : code, { compress: environment_options_1.allowMinify && !!options.compress, ecma: options.ecma || 5, mangle: environment_options_1.allowMangle, safari10: true, format: { ascii_only: true, webkit: true, beautify: environment_options_1.shouldBeautify, }, sourceMap: !!options.map && { asObject: true, }, }); // tslint:disable-next-line: no-non-null-assertion const outputCode = minifyOutput.code; let outputMap; if (options.map && minifyOutput.map) { outputMap = await mergeSourceMaps(code, options.map, outputCode, minifyOutput.map, options.filename || '0', code.length > FAST_SOURCEMAP_THRESHOLD); } return { code: outputCode, map: outputMap }; } function createFileEntry(filename, code, map, integrityAlgorithm) { return { filename: filename, size: Buffer.byteLength(code), integrity: integrityAlgorithm && generateIntegrityValue(integrityAlgorithm, code), map: !map ? undefined : { filename: filename + '.map', size: Buffer.byteLength(map), }, }; } function generateIntegrityValue(hashAlgorithm, code) { return (hashAlgorithm + '-' + crypto_1.createHash(hashAlgorithm) .update(code) .digest('base64')); } // The webpack runtime chunk is already ES5. // However, two variants are still needed due to lazy routing and SRI differences // NOTE: This should eventually be a babel plugin async function processRuntime(options) { let originalCode = options.code; let downlevelCode = options.code; // Replace integrity hashes with updated values if (options.integrityAlgorithm && options.runtimeData) { for (const data of options.runtimeData) { if (!data.integrity) { continue; } if (data.original && data.original.integrity) { originalCode = originalCode.replace(data.integrity, data.original.integrity); } if (data.downlevel && data.downlevel.integrity) { downlevelCode = downlevelCode.replace(data.integrity, data.downlevel.integrity); } } } // Adjust lazy loaded scripts to point to the proper variant // Extra spacing is intentional to align source line positions downlevelCode = downlevelCode.replace(/"\-(es20\d{2}|esnext)\./, ' "-es5.'); return { original: await processBundle({ ...options, code: originalCode, isOriginal: true, }), downlevel: await processBundle({ ...options, code: downlevelCode, filename: options.filename.replace(/\-(es20\d{2}|esnext)/, '-es5'), isOriginal: false, }), }; } function createReplacePlugin(replacements) { return { visitor: { StringLiteral(path) { for (const replacement of replacements) { if (path.node.value === replacement[0]) { path.node.value = replacement[1]; } } }, }, }; } function createIifeWrapperPlugin() { return { visitor: { Program: { exit(path) { // Save existing body and directives const { body, directives } = path.node; // Clear out body and directives for wrapper path.node.body = []; path.node.directives = []; // Create the wrapper - "(function() { ... })();" const wrapper = core_1.types.expressionStatement(core_1.types.callExpression(core_1.types.parenthesizedExpression(core_1.types.functionExpression(undefined, [], core_1.types.blockStatement(body, directives))), [])); // Insert the wrapper path.pushContainer('body', wrapper); }, }, }, }; } const USE_LOCALIZE_PLUGINS = false; async function createI18nPlugins(locale, translation, missingTranslation, localeDataContent) { const plugins = []; const localizeDiag = await Promise.resolve().then(() => require('@angular/localize/src/tools/src/diagnostics')); const diagnostics = new localizeDiag.Diagnostics(); const es2015 = await Promise.resolve().then(() => require( // tslint:disable-next-line: trailing-comma no-implicit-dependencies '@angular/localize/src/tools/src/translate/source_files/es2015_translate_plugin')); plugins.push( // tslint:disable-next-line: no-any es2015.makeEs2015TranslatePlugin(diagnostics, (translation || {}), { missingTranslation: translation === undefined ? 'ignore' : missingTranslation, })); const es5 = await Promise.resolve().then(() => require( // tslint:disable-next-line: trailing-comma no-implicit-dependencies '@angular/localize/src/tools/src/translate/source_files/es5_translate_plugin')); plugins.push( // tslint:disable-next-line: no-any es5.makeEs5TranslatePlugin(diagnostics, (translation || {}), { missingTranslation: translation === undefined ? 'ignore' : missingTranslation, })); const inlineLocale = await Promise.resolve().then(() => require( // tslint:disable-next-line: trailing-comma no-implicit-dependencies '@angular/localize/src/tools/src/translate/source_files/locale_plugin')); plugins.push(inlineLocale.makeLocalePlugin(locale)); if (localeDataContent) { plugins.push({ visitor: { Program(path) { path.unshiftContainer('body', template_1.default.ast(localeDataContent)); }, }, }); } return { diagnostics, plugins }; } exports.createI18nPlugins = createI18nPlugins; const localizeName = '$localize'; async function inlineLocales(options) { var _a; 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); } let ast; try { ast = core_1.parseSync(options.code, { babelrc: false, configFile: false, sourceType: 'script', filename: options.filename, }); } catch (error) { if (error.message) { // 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.substr(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 = []; const inputMap = options.map && JSON.parse(options.map); for (const locale of i18n.inlineLocales) { const isSourceLocale = locale === i18n.sourceLocale; // tslint:disable-next-line: no-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 = (_a = i18n.locales[locale]) === null || _a === void 0 ? void 0 : _a.dataPath; if (localeDataPath) { localeDataContent = await loadLocaleData(localeDataPath, true, options.es5); } } const { diagnostics: localeDiagnostics, plugins } = await createI18nPlugins(locale, translations, isSourceLocale ? 'ignore' : options.missingTranslation || 'warning', localeDataContent); const transformResult = await 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 // tslint:disable-next-line: no-any inputSourceMap: false, babelrc: false, configFile: false, plugins, compact: !environment_options_1.shouldBeautify, sourceMaps: !!inputMap, }); diagnostics.push(...localeDiagnostics.messages); if (!transformResult || !transformResult.code) { throw new Error(`Unknown error occurred processing bundle for "${options.filename}".`); } const outputPath = path.join(options.outputPath, i18n.flatOutput ? '' : locale, options.filename); fs.writeFileSync(outputPath, transformResult.code); if (inputMap && transformResult.map) { const outputMap = await mergeSourceMaps(options.code, inputMap, transformResult.code, transformResult.map, options.filename, options.code.length > FAST_SOURCEMAP_THRESHOLD); fs.writeFileSync(outputPath + '.map', JSON.stringify(outputMap)); } } return { file: options.filename, diagnostics }; } exports.inlineLocales = inlineLocales; 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(() => require('@babel/generator')); const utils = await Promise.resolve().then(() => require('@angular/localize/src/tools/src/source_file_utils')); const localizeDiag = await Promise.resolve().then(() => require('@angular/localize/src/tools/src/diagnostics')); const diagnostics = new localizeDiag.Diagnostics(); const positions = findLocalizePositions(ast, options, utils); 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; } for (const locale of i18n.inlineLocales) { const content = new webpack_sources_1.ReplaceSource(inputMap ? // tslint:disable-next-line: no-any new webpack_sources_1.SourceMapSource(options.code, options.filename, inputMap) : new webpack_sources_1.OriginalSource(options.code, options.filename)); const isSourceLocale = locale === i18n.sourceLocale; // tslint:disable-next-line: no-any const translations = isSourceLocale ? {} : i18n.locales[locale].translation || {}; for (const position of positions) { const translated = utils.translate(diagnostics, translations, position.messageParts, position.expressions, isSourceLocale ? 'ignore' : options.missingTranslation || 'warning'); const expression = utils.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 = `var $localize=Object.assign(void 0===$localize?{}:$localize,{locale:"${locale}"});\n`; // If locale data is provided, load it and prepend to file let localeDataSource = null; const localeDataPath = i18n.locales[locale] && i18n.locales[locale].dataPath; if (localeDataPath) { const localeDataContent = await loadLocaleData(localeDataPath, true, options.es5); localeDataSource = new webpack_sources_1.OriginalSource(localeDataContent, path.basename(localeDataPath)); } outputSource = localeDataSource // The semicolon ensures that there is no syntax error between statements ? new webpack_sources_1.ConcatSource(setLocaleText, localeDataSource, ';\n', content) : new webpack_sources_1.ConcatSource(setLocaleText, content); } const { source: outputCode, map: outputMap } = outputSource.sourceAndMap(); const outputPath = path.join(options.outputPath, i18n.flatOutput ? '' : locale, options.filename); fs.writeFileSync(outputPath, outputCode); if (inputMap && outputMap) { outputMap.file = options.filename; if (mapSourceRoot) { outputMap.sourceRoot = mapSourceRoot; } fs.writeFileSync(outputPath + '.map', JSON.stringify(outputMap)); } } return { file: options.filename, diagnostics: diagnostics.messages, count: positions.length }; } function inlineCopyOnly(options) { if (!i18n) { throw new Error('i18n options are missing'); } for (const locale of i18n.inlineLocales) { const outputPath = path.join(options.outputPath, i18n.flatOutput ? '' : locale, options.filename); fs.writeFileSync(outputPath, options.code); if (options.map) { fs.writeFileSync(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 }); if (options.es5) { core_1.traverse(file.ast, { CallExpression(path) { const callee = path.get('callee'); if (callee.isIdentifier() && callee.node.name === localizeName && utils.isGlobalIdentifier(callee)) { const [messageParts, expressions] = unwrapLocalizeCall(path, utils); positions.push({ // tslint:disable-next-line: no-non-null-assertion start: path.node.start, // tslint:disable-next-line: no-non-null-assertion end: path.node.end, messageParts, expressions, }); } }, }); } else { 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({ // tslint:disable-next-line: no-non-null-assertion start: path.node.start, // tslint:disable-next-line: no-non-null-assertion end: path.node.end, messageParts, expressions, }); } }, }); } return positions; } // TODO: Remove this for v11. // This check allows the CLI to support both FW 10.0 and 10.1 let localizeOld; function unwrapTemplateLiteral(path, utils) { if (localizeOld === undefined) { const { version: localizeVersion } = require('@angular/localize/package.json'); localizeOld = semver_1.lt(localizeVersion, '10.1.0-rc.0', { includePrerelease: true }); } if (localizeOld) { // tslint:disable-next-line: no-any const messageParts = utils.unwrapMessagePartsFromTemplateLiteral(path.node.quasi.quasis); return [messageParts, path.node.quasi.expressions]; } const [messageParts] = utils.unwrapMessagePartsFromTemplateLiteral(path.get('quasi').get('quasis')); const [expressions] = utils.unwrapExpressionsFromTemplateLiteral(path.get('quasi')); return [messageParts, expressions]; } function unwrapLocalizeCall(path, utils) { if (localizeOld === undefined) { const { version: localizeVersion } = require('@angular/localize/package.json'); localizeOld = semver_1.lt(localizeVersion, '10.1.0-rc.0', { includePrerelease: true }); } if (localizeOld) { const messageParts = utils.unwrapMessagePartsFromLocalizeCall(path); // tslint:disable-next-line: no-any const expressions = utils.unwrapSubstitutionsFromLocalizeCall(path.node); return [ messageParts, expressions, ]; } const [messageParts] = utils.unwrapMessagePartsFromLocalizeCall(path); const [expressions] = utils.unwrapSubstitutionsFromLocalizeCall(path); return [messageParts, expressions]; } async function loadLocaleData(path, optimize, es5) { // The path is validated during option processing before the build starts const content = fs.readFileSync(path, 'utf8'); // Downlevel and optimize the data const transformResult = await core_1.transformAsync(content, { filename: path, // The types do not include the false option even though it is valid // tslint:disable-next-line: no-any inputSourceMap: false, babelrc: false, configFile: false, presets: [ [ require.resolve('@babel/preset-env'), { bugfixes: true, // IE 9 is the oldest supported browser targets: es5 ? { ie: '9' } : { 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; }