"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/**
 * @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 crypto_1 = require("crypto");
const fs = require("fs");
const path = require("path");
const source_map_1 = require("source-map");
const terser_1 = require("terser");
const mangle_options_1 = require("./mangle-options");
const cacache = require('cacache');
let cachePath;
function setup(options) {
    cachePath = options.cachePath;
}
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}/, '-es5');
    const downlevel = !options.optimizeOnly;
    // if code size is larger than 1 MB, manually handle sourcemaps with newer source-map package.
    const codeSize = Buffer.byteLength(options.code);
    const mapSize = options.map ? Buffer.byteLength(options.map) : 0;
    const manualSourceMaps = codeSize >= 500 * 1024 || mapSize >= 500 * 1024;
    const sourceCode = options.code;
    const sourceMap = options.map ? JSON.parse(options.map) : false;
    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: options.filename,
            // using false ensures that babel will NOT search and process sourcemap comments (large memory usage)
            inputSourceMap: manualSourceMaps ? false : sourceMap,
            babelrc: 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'],
                    },
                ],
            ],
            minified: options.optimize,
            // `false` ensures it is disabled and prevents large file warnings
            compact: options.optimize || false,
            sourceMaps: !!sourceMap,
        });
        if (!transformResult || !transformResult.code) {
            throw new Error(`Unknown error occurred processing bundle for "${options.filename}".`);
        }
        downlevelCode = transformResult.code;
        if (manualSourceMaps && sourceMap && transformResult.map) {
            downlevelMap = await mergeSourcemaps(sourceMap, transformResult.map);
        }
        else {
            // undefined is needed here to normalize the property type
            downlevelMap = transformResult.map || undefined;
        }
    }
    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(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;
    // 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 (optimize) {
        result = terserMangle(code, {
            filename,
            map: rawMap,
            compress: !isOriginal,
            ecma: isOriginal ? 6 : 5,
        });
    }
    else {
        if (rawMap) {
            rawMap.file = filename;
        }
        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;
}
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 = terser_1.minify(code, {
        compress: options.compress || false,
        ecma: options.ecma || 5,
        mangle: !mangle_options_1.manglingDisabled,
        safari10: true,
        output: {
            ascii_only: true,
            webkit: true,
        },
        sourceMap: !!options.map &&
            {
                filename: options.filename,
                // terser uses an old version of the sourcemap typings
                // tslint:disable-next-line: no-any
                content: options.map,
                asObject: true,
            },
    });
    if (minifyOutput.error) {
        throw minifyOutput.error;
    }
    // tslint:disable-next-line: no-non-null-assertion
    return { code: minifyOutput.code, map: minifyOutput.map };
}
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}\./, '   "-es5.');
    return {
        original: await processBundle({
            ...options,
            code: originalCode,
            isOriginal: true,
        }),
        downlevel: await processBundle({
            ...options,
            code: downlevelCode,
            filename: options.filename.replace(/\-es20\d{2}/, '-es5'),
            isOriginal: false,
        }),
    };
}
