538 lines
27 KiB
JavaScript
Executable File
538 lines
27 KiB
JavaScript
Executable File
"use strict";
|
|
/**
|
|
* @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.io/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 (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.AngularWebpackPlugin = exports.imageDomains = void 0;
|
|
const assert_1 = require("assert");
|
|
const ts = __importStar(require("typescript"));
|
|
const paths_plugin_1 = require("../paths-plugin");
|
|
const resource_loader_1 = require("../resource_loader");
|
|
const cache_1 = require("./cache");
|
|
const diagnostics_1 = require("./diagnostics");
|
|
const host_1 = require("./host");
|
|
const paths_1 = require("./paths");
|
|
const symbol_1 = require("./symbol");
|
|
const system_1 = require("./system");
|
|
const transformation_1 = require("./transformation");
|
|
/**
|
|
* The threshold used to determine whether Angular file diagnostics should optimize for full programs
|
|
* or single files. If the number of affected files for a build is more than the threshold, full
|
|
* program optimization will be used.
|
|
*/
|
|
const DIAGNOSTICS_AFFECTED_THRESHOLD = 1;
|
|
exports.imageDomains = new Set();
|
|
const PLUGIN_NAME = 'angular-compiler';
|
|
const compilationFileEmitters = new WeakMap();
|
|
class AngularWebpackPlugin {
|
|
pluginOptions;
|
|
compilerCliModule;
|
|
watchMode;
|
|
ngtscNextProgram;
|
|
builder;
|
|
sourceFileCache;
|
|
webpackCache;
|
|
webpackCreateHash;
|
|
fileDependencies = new Map();
|
|
requiredFilesToEmit = new Set();
|
|
requiredFilesToEmitCache = new Map();
|
|
fileEmitHistory = new Map();
|
|
constructor(options = {}) {
|
|
this.pluginOptions = {
|
|
emitClassMetadata: false,
|
|
emitNgModuleScope: false,
|
|
jitMode: false,
|
|
fileReplacements: {},
|
|
substitutions: {},
|
|
directTemplateLoading: true,
|
|
tsconfig: 'tsconfig.json',
|
|
...options,
|
|
};
|
|
}
|
|
get compilerCli() {
|
|
// The compilerCliModule field is guaranteed to be defined during a compilation
|
|
// due to the `beforeCompile` hook. Usage of this property accessor prior to the
|
|
// hook execution is an implementation error.
|
|
assert_1.strict.ok(this.compilerCliModule, `'@angular/compiler-cli' used prior to Webpack compilation.`);
|
|
return this.compilerCliModule;
|
|
}
|
|
get options() {
|
|
return this.pluginOptions;
|
|
}
|
|
apply(compiler) {
|
|
const { NormalModuleReplacementPlugin, WebpackError, util } = compiler.webpack;
|
|
this.webpackCreateHash = util.createHash;
|
|
// Setup file replacements with webpack
|
|
for (const [key, value] of Object.entries(this.pluginOptions.fileReplacements)) {
|
|
new NormalModuleReplacementPlugin(new RegExp('^' + key.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&') + '$'), value).apply(compiler);
|
|
}
|
|
// Set resolver options
|
|
const pathsPlugin = new paths_plugin_1.TypeScriptPathsPlugin();
|
|
compiler.hooks.afterResolvers.tap(PLUGIN_NAME, (compiler) => {
|
|
compiler.resolverFactory.hooks.resolveOptions
|
|
.for('normal')
|
|
.tap(PLUGIN_NAME, (resolveOptions) => {
|
|
resolveOptions.plugins ??= [];
|
|
resolveOptions.plugins.push(pathsPlugin);
|
|
return resolveOptions;
|
|
});
|
|
});
|
|
// Load the compiler-cli if not already available
|
|
compiler.hooks.beforeCompile.tapPromise(PLUGIN_NAME, () => this.initializeCompilerCli());
|
|
const compilationState = { pathsPlugin };
|
|
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
|
|
try {
|
|
this.setupCompilation(compilation, compilationState);
|
|
}
|
|
catch (error) {
|
|
(0, diagnostics_1.addError)(compilation, `Failed to initialize Angular compilation - ${error instanceof Error ? error.message : error}`);
|
|
}
|
|
});
|
|
}
|
|
setupCompilation(compilation, state) {
|
|
const compiler = compilation.compiler;
|
|
// Register plugin to ensure deterministic emit order in multi-plugin usage
|
|
const emitRegistration = this.registerWithCompilation(compilation);
|
|
this.watchMode = compiler.watchMode;
|
|
// Initialize webpack cache
|
|
if (!this.webpackCache && compilation.options.cache) {
|
|
this.webpackCache = compilation.getCache(PLUGIN_NAME);
|
|
}
|
|
// Initialize the resource loader if not already setup
|
|
if (!state.resourceLoader) {
|
|
state.resourceLoader = new resource_loader_1.WebpackResourceLoader(this.watchMode);
|
|
}
|
|
// Setup and read TypeScript and Angular compiler configuration
|
|
const { compilerOptions, rootNames, errors } = this.loadConfiguration();
|
|
// Create diagnostics reporter and report configuration file errors
|
|
const diagnosticsReporter = (0, diagnostics_1.createDiagnosticsReporter)(compilation, (diagnostic) => this.compilerCli.formatDiagnostics([diagnostic]));
|
|
diagnosticsReporter(errors);
|
|
// Update TypeScript path mapping plugin with new configuration
|
|
state.pathsPlugin.update(compilerOptions);
|
|
// Create a Webpack-based TypeScript compiler host
|
|
const system = (0, system_1.createWebpackSystem)(
|
|
// Webpack lacks an InputFileSytem type definition with sync functions
|
|
compiler.inputFileSystem, (0, paths_1.normalizePath)(compiler.context));
|
|
const host = ts.createIncrementalCompilerHost(compilerOptions, system);
|
|
// Setup source file caching and reuse cache from previous compilation if present
|
|
let cache = this.sourceFileCache;
|
|
let changedFiles;
|
|
if (cache) {
|
|
changedFiles = new Set();
|
|
for (const changedFile of [
|
|
...(compiler.modifiedFiles ?? []),
|
|
...(compiler.removedFiles ?? []),
|
|
]) {
|
|
const normalizedChangedFile = (0, paths_1.normalizePath)(changedFile);
|
|
// Invalidate file dependencies
|
|
this.fileDependencies.delete(normalizedChangedFile);
|
|
// Invalidate existing cache
|
|
cache.invalidate(normalizedChangedFile);
|
|
changedFiles.add(normalizedChangedFile);
|
|
}
|
|
}
|
|
else {
|
|
// Initialize a new cache
|
|
cache = new cache_1.SourceFileCache();
|
|
// Only store cache if in watch mode
|
|
if (this.watchMode) {
|
|
this.sourceFileCache = cache;
|
|
}
|
|
}
|
|
(0, host_1.augmentHostWithCaching)(host, cache);
|
|
const moduleResolutionCache = ts.createModuleResolutionCache(host.getCurrentDirectory(), host.getCanonicalFileName.bind(host), compilerOptions);
|
|
// Setup source file dependency collection
|
|
(0, host_1.augmentHostWithDependencyCollection)(host, this.fileDependencies, moduleResolutionCache);
|
|
// Setup resource loading
|
|
state.resourceLoader.update(compilation, changedFiles);
|
|
(0, host_1.augmentHostWithResources)(host, state.resourceLoader, {
|
|
directTemplateLoading: this.pluginOptions.directTemplateLoading,
|
|
inlineStyleFileExtension: this.pluginOptions.inlineStyleFileExtension,
|
|
});
|
|
// Setup source file adjustment options
|
|
(0, host_1.augmentHostWithReplacements)(host, this.pluginOptions.fileReplacements, moduleResolutionCache);
|
|
(0, host_1.augmentHostWithSubstitutions)(host, this.pluginOptions.substitutions);
|
|
// Create the file emitter used by the webpack loader
|
|
const { fileEmitter, builder, internalFiles } = this.pluginOptions.jitMode
|
|
? this.updateJitProgram(compilerOptions, rootNames, host, diagnosticsReporter)
|
|
: this.updateAotProgram(compilerOptions, rootNames, host, diagnosticsReporter, state.resourceLoader);
|
|
// Set of files used during the unused TypeScript file analysis
|
|
const currentUnused = new Set();
|
|
for (const sourceFile of builder.getSourceFiles()) {
|
|
if (internalFiles?.has(sourceFile)) {
|
|
continue;
|
|
}
|
|
// Ensure all program files are considered part of the compilation and will be watched.
|
|
// Webpack does not normalize paths. Therefore, we need to normalize the path with FS seperators.
|
|
compilation.fileDependencies.add((0, paths_1.externalizePath)(sourceFile.fileName));
|
|
// Add all non-declaration files to the initial set of unused files. The set will be
|
|
// analyzed and pruned after all Webpack modules are finished building.
|
|
if (!sourceFile.isDeclarationFile) {
|
|
currentUnused.add((0, paths_1.normalizePath)(sourceFile.fileName));
|
|
}
|
|
}
|
|
compilation.hooks.finishModules.tapPromise(PLUGIN_NAME, async (modules) => {
|
|
// Rebuild any remaining AOT required modules
|
|
await this.rebuildRequiredFiles(modules, compilation, fileEmitter);
|
|
// Clear out the Webpack compilation to avoid an extra retaining reference
|
|
state.resourceLoader?.clearParentCompilation();
|
|
// Analyze program for unused files
|
|
if (compilation.errors.length > 0) {
|
|
return;
|
|
}
|
|
for (const webpackModule of modules) {
|
|
const resource = webpackModule.resource;
|
|
if (resource) {
|
|
this.markResourceUsed((0, paths_1.normalizePath)(resource), currentUnused);
|
|
}
|
|
}
|
|
for (const unused of currentUnused) {
|
|
if (state.previousUnused?.has(unused)) {
|
|
continue;
|
|
}
|
|
(0, diagnostics_1.addWarning)(compilation, `${unused} is part of the TypeScript compilation but it's unused.\n` +
|
|
`Add only entry points to the 'files' or 'include' properties in your tsconfig.`);
|
|
}
|
|
state.previousUnused = currentUnused;
|
|
});
|
|
// Store file emitter for loader usage
|
|
emitRegistration.update(fileEmitter);
|
|
}
|
|
registerWithCompilation(compilation) {
|
|
let fileEmitters = compilationFileEmitters.get(compilation);
|
|
if (!fileEmitters) {
|
|
fileEmitters = new symbol_1.FileEmitterCollection();
|
|
compilationFileEmitters.set(compilation, fileEmitters);
|
|
compilation.compiler.webpack.NormalModule.getCompilationHooks(compilation).loader.tap(PLUGIN_NAME, (loaderContext) => {
|
|
loaderContext[symbol_1.AngularPluginSymbol] = fileEmitters;
|
|
});
|
|
}
|
|
const emitRegistration = fileEmitters.register();
|
|
return emitRegistration;
|
|
}
|
|
markResourceUsed(normalizedResourcePath, currentUnused) {
|
|
if (!currentUnused.has(normalizedResourcePath)) {
|
|
return;
|
|
}
|
|
currentUnused.delete(normalizedResourcePath);
|
|
const dependencies = this.fileDependencies.get(normalizedResourcePath);
|
|
if (!dependencies) {
|
|
return;
|
|
}
|
|
for (const dependency of dependencies) {
|
|
this.markResourceUsed((0, paths_1.normalizePath)(dependency), currentUnused);
|
|
}
|
|
}
|
|
async rebuildRequiredFiles(modules, compilation, fileEmitter) {
|
|
if (this.requiredFilesToEmit.size === 0) {
|
|
return;
|
|
}
|
|
const filesToRebuild = new Set();
|
|
for (const requiredFile of this.requiredFilesToEmit) {
|
|
const history = await this.getFileEmitHistory(requiredFile);
|
|
if (history) {
|
|
const emitResult = await fileEmitter(requiredFile);
|
|
if (emitResult?.content === undefined ||
|
|
history.length !== emitResult.content.length ||
|
|
emitResult.hash === undefined ||
|
|
Buffer.compare(history.hash, emitResult.hash) !== 0) {
|
|
// New emit result is different so rebuild using new emit result
|
|
this.requiredFilesToEmitCache.set(requiredFile, emitResult);
|
|
filesToRebuild.add(requiredFile);
|
|
}
|
|
}
|
|
else {
|
|
// No emit history so rebuild
|
|
filesToRebuild.add(requiredFile);
|
|
}
|
|
}
|
|
if (filesToRebuild.size > 0) {
|
|
const rebuild = (webpackModule) => new Promise((resolve) => compilation.rebuildModule(webpackModule, () => resolve()));
|
|
const modulesToRebuild = [];
|
|
for (const webpackModule of modules) {
|
|
const resource = webpackModule.resource;
|
|
if (resource && filesToRebuild.has((0, paths_1.normalizePath)(resource))) {
|
|
modulesToRebuild.push(webpackModule);
|
|
}
|
|
}
|
|
await Promise.all(modulesToRebuild.map((webpackModule) => rebuild(webpackModule)));
|
|
}
|
|
this.requiredFilesToEmit.clear();
|
|
this.requiredFilesToEmitCache.clear();
|
|
}
|
|
loadConfiguration() {
|
|
const { options: compilerOptions, rootNames, errors, } = this.compilerCli.readConfiguration(this.pluginOptions.tsconfig, this.pluginOptions.compilerOptions);
|
|
compilerOptions.noEmitOnError = false;
|
|
compilerOptions.suppressOutputPathCheck = true;
|
|
compilerOptions.outDir = undefined;
|
|
compilerOptions.inlineSources = compilerOptions.sourceMap;
|
|
compilerOptions.inlineSourceMap = false;
|
|
compilerOptions.mapRoot = undefined;
|
|
compilerOptions.sourceRoot = undefined;
|
|
compilerOptions.allowEmptyCodegenFiles = false;
|
|
compilerOptions.annotationsAs = 'decorators';
|
|
compilerOptions.enableResourceInlining = false;
|
|
return { compilerOptions, rootNames, errors };
|
|
}
|
|
updateAotProgram(compilerOptions, rootNames, host, diagnosticsReporter, resourceLoader) {
|
|
// Create the Angular specific program that contains the Angular compiler
|
|
const angularProgram = new this.compilerCli.NgtscProgram(rootNames, compilerOptions, host, this.ngtscNextProgram);
|
|
const angularCompiler = angularProgram.compiler;
|
|
// The `ignoreForEmit` return value can be safely ignored when emitting. Only files
|
|
// that will be bundled (requested by Webpack) will be emitted. Combined with TypeScript's
|
|
// eliding of type only imports, this will cause type only files to be automatically ignored.
|
|
// Internal Angular type check files are also not resolvable by the bundler. Even if they
|
|
// were somehow errantly imported, the bundler would error before an emit was attempted.
|
|
// Diagnostics are still collected for all files which requires using `ignoreForDiagnostics`.
|
|
const { ignoreForDiagnostics, ignoreForEmit } = angularCompiler;
|
|
// SourceFile versions are required for builder programs.
|
|
// The wrapped host inside NgtscProgram adds additional files that will not have versions.
|
|
const typeScriptProgram = angularProgram.getTsProgram();
|
|
(0, host_1.augmentProgramWithVersioning)(typeScriptProgram);
|
|
let builder;
|
|
if (this.watchMode) {
|
|
builder = this.builder = ts.createEmitAndSemanticDiagnosticsBuilderProgram(typeScriptProgram, host, this.builder);
|
|
this.ngtscNextProgram = angularProgram;
|
|
}
|
|
else {
|
|
// When not in watch mode, the startup cost of the incremental analysis can be avoided by
|
|
// using an abstract builder that only wraps a TypeScript program.
|
|
builder = ts.createAbstractBuilder(typeScriptProgram, host);
|
|
}
|
|
// Update semantic diagnostics cache
|
|
const affectedFiles = new Set();
|
|
// Analyze affected files when in watch mode for incremental type checking
|
|
if ('getSemanticDiagnosticsOfNextAffectedFile' in builder) {
|
|
// eslint-disable-next-line no-constant-condition
|
|
while (true) {
|
|
const result = builder.getSemanticDiagnosticsOfNextAffectedFile(undefined, (sourceFile) => {
|
|
// If the affected file is a TTC shim, add the shim's original source file.
|
|
// This ensures that changes that affect TTC are typechecked even when the changes
|
|
// are otherwise unrelated from a TS perspective and do not result in Ivy codegen changes.
|
|
// For example, changing @Input property types of a directive used in another component's
|
|
// template.
|
|
if (ignoreForDiagnostics.has(sourceFile) &&
|
|
sourceFile.fileName.endsWith('.ngtypecheck.ts')) {
|
|
// This file name conversion relies on internal compiler logic and should be converted
|
|
// to an official method when available. 15 is length of `.ngtypecheck.ts`
|
|
const originalFilename = sourceFile.fileName.slice(0, -15) + '.ts';
|
|
const originalSourceFile = builder.getSourceFile(originalFilename);
|
|
if (originalSourceFile) {
|
|
affectedFiles.add(originalSourceFile);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
if (!result) {
|
|
break;
|
|
}
|
|
affectedFiles.add(result.affected);
|
|
}
|
|
}
|
|
// Collect program level diagnostics
|
|
const diagnostics = [
|
|
...angularCompiler.getOptionDiagnostics(),
|
|
...builder.getOptionsDiagnostics(),
|
|
...builder.getGlobalDiagnostics(),
|
|
];
|
|
diagnosticsReporter(diagnostics);
|
|
// Collect source file specific diagnostics
|
|
for (const sourceFile of builder.getSourceFiles()) {
|
|
if (!ignoreForDiagnostics.has(sourceFile)) {
|
|
diagnosticsReporter(builder.getSyntacticDiagnostics(sourceFile));
|
|
diagnosticsReporter(builder.getSemanticDiagnostics(sourceFile));
|
|
}
|
|
}
|
|
const transformers = (0, transformation_1.createAotTransformers)(builder, this.pluginOptions, exports.imageDomains);
|
|
const getDependencies = (sourceFile) => {
|
|
const dependencies = [];
|
|
for (const resourcePath of angularCompiler.getResourceDependencies(sourceFile)) {
|
|
dependencies.push(resourcePath,
|
|
// Retrieve all dependencies of the resource (stylesheet imports, etc.)
|
|
...resourceLoader.getResourceDependencies(resourcePath));
|
|
}
|
|
return dependencies;
|
|
};
|
|
// Required to support asynchronous resource loading
|
|
// Must be done before creating transformers or getting template diagnostics
|
|
const pendingAnalysis = angularCompiler
|
|
.analyzeAsync()
|
|
.then(() => {
|
|
this.requiredFilesToEmit.clear();
|
|
for (const sourceFile of builder.getSourceFiles()) {
|
|
if (sourceFile.isDeclarationFile) {
|
|
continue;
|
|
}
|
|
// Collect sources that are required to be emitted
|
|
if (!ignoreForEmit.has(sourceFile) &&
|
|
!angularCompiler.incrementalCompilation.safeToSkipEmit(sourceFile)) {
|
|
this.requiredFilesToEmit.add((0, paths_1.normalizePath)(sourceFile.fileName));
|
|
// If required to emit, diagnostics may have also changed
|
|
if (!ignoreForDiagnostics.has(sourceFile)) {
|
|
affectedFiles.add(sourceFile);
|
|
}
|
|
}
|
|
else if (this.sourceFileCache &&
|
|
!affectedFiles.has(sourceFile) &&
|
|
!ignoreForDiagnostics.has(sourceFile)) {
|
|
// Use cached Angular diagnostics for unchanged and unaffected files
|
|
const angularDiagnostics = this.sourceFileCache.getAngularDiagnostics(sourceFile);
|
|
if (angularDiagnostics) {
|
|
diagnosticsReporter(angularDiagnostics);
|
|
}
|
|
}
|
|
}
|
|
// Collect new Angular diagnostics for files affected by changes
|
|
const OptimizeFor = this.compilerCli.OptimizeFor;
|
|
const optimizeDiagnosticsFor = affectedFiles.size <= DIAGNOSTICS_AFFECTED_THRESHOLD
|
|
? OptimizeFor.SingleFile
|
|
: OptimizeFor.WholeProgram;
|
|
for (const affectedFile of affectedFiles) {
|
|
const angularDiagnostics = angularCompiler.getDiagnosticsForFile(affectedFile, optimizeDiagnosticsFor);
|
|
diagnosticsReporter(angularDiagnostics);
|
|
this.sourceFileCache?.updateAngularDiagnostics(affectedFile, angularDiagnostics);
|
|
}
|
|
return {
|
|
emitter: this.createFileEmitter(builder, (0, transformation_1.mergeTransformers)(angularCompiler.prepareEmit().transformers, transformers), getDependencies, (sourceFile) => {
|
|
this.requiredFilesToEmit.delete((0, paths_1.normalizePath)(sourceFile.fileName));
|
|
angularCompiler.incrementalCompilation.recordSuccessfulEmit(sourceFile);
|
|
}),
|
|
};
|
|
})
|
|
.catch((err) => ({ errorMessage: err instanceof Error ? err.message : `${err}` }));
|
|
const analyzingFileEmitter = async (file) => {
|
|
const analysis = await pendingAnalysis;
|
|
if ('errorMessage' in analysis) {
|
|
throw new Error(analysis.errorMessage);
|
|
}
|
|
return analysis.emitter(file);
|
|
};
|
|
return {
|
|
fileEmitter: analyzingFileEmitter,
|
|
builder,
|
|
internalFiles: ignoreForEmit,
|
|
};
|
|
}
|
|
updateJitProgram(compilerOptions, rootNames, host, diagnosticsReporter) {
|
|
let builder;
|
|
if (this.watchMode) {
|
|
builder = this.builder = ts.createEmitAndSemanticDiagnosticsBuilderProgram(rootNames, compilerOptions, host, this.builder);
|
|
}
|
|
else {
|
|
// When not in watch mode, the startup cost of the incremental analysis can be avoided by
|
|
// using an abstract builder that only wraps a TypeScript program.
|
|
builder = ts.createAbstractBuilder(rootNames, compilerOptions, host);
|
|
}
|
|
const diagnostics = [
|
|
...builder.getOptionsDiagnostics(),
|
|
...builder.getGlobalDiagnostics(),
|
|
...builder.getSyntacticDiagnostics(),
|
|
// Gather incremental semantic diagnostics
|
|
...builder.getSemanticDiagnostics(),
|
|
];
|
|
diagnosticsReporter(diagnostics);
|
|
const transformers = (0, transformation_1.createJitTransformers)(builder, this.compilerCli, this.pluginOptions);
|
|
return {
|
|
fileEmitter: this.createFileEmitter(builder, transformers, () => []),
|
|
builder,
|
|
internalFiles: undefined,
|
|
};
|
|
}
|
|
createFileEmitter(program, transformers = {}, getExtraDependencies, onAfterEmit) {
|
|
return async (file) => {
|
|
const filePath = (0, paths_1.normalizePath)(file);
|
|
if (this.requiredFilesToEmitCache.has(filePath)) {
|
|
return this.requiredFilesToEmitCache.get(filePath);
|
|
}
|
|
const sourceFile = program.getSourceFile(filePath);
|
|
if (!sourceFile) {
|
|
return undefined;
|
|
}
|
|
let content;
|
|
let map;
|
|
program.emit(sourceFile, (filename, data) => {
|
|
if (filename.endsWith('.map')) {
|
|
map = data;
|
|
}
|
|
else if (filename.endsWith('.js')) {
|
|
content = data;
|
|
}
|
|
}, undefined, undefined, transformers);
|
|
onAfterEmit?.(sourceFile);
|
|
// Capture emit history info for Angular rebuild analysis
|
|
const hash = content ? (await this.addFileEmitHistory(filePath, content)).hash : undefined;
|
|
const dependencies = [
|
|
...(this.fileDependencies.get(filePath) || []),
|
|
...getExtraDependencies(sourceFile),
|
|
].map(paths_1.externalizePath);
|
|
return { content, map, dependencies, hash };
|
|
};
|
|
}
|
|
async initializeCompilerCli() {
|
|
if (this.compilerCliModule) {
|
|
return;
|
|
}
|
|
// This uses a dynamic import to load `@angular/compiler-cli` which may be ESM.
|
|
// CommonJS code can load ESM code via a dynamic import. Unfortunately, TypeScript
|
|
// will currently, unconditionally downlevel dynamic import into a require call.
|
|
// require calls cannot load ESM code and will result in a runtime error. To workaround
|
|
// this, a Function constructor is used to prevent TypeScript from changing the dynamic import.
|
|
// Once TypeScript provides support for keeping the dynamic import this workaround can
|
|
// be dropped.
|
|
this.compilerCliModule = await new Function(`return import('@angular/compiler-cli');`)();
|
|
}
|
|
async addFileEmitHistory(filePath, content) {
|
|
assert_1.strict.ok(this.webpackCreateHash, 'File emitter is used prior to Webpack compilation');
|
|
const historyData = {
|
|
length: content.length,
|
|
hash: this.webpackCreateHash('xxhash64').update(content).digest(),
|
|
};
|
|
if (this.webpackCache) {
|
|
const history = await this.getFileEmitHistory(filePath);
|
|
if (!history || Buffer.compare(history.hash, historyData.hash) !== 0) {
|
|
// Hash doesn't match or item doesn't exist.
|
|
await this.webpackCache.storePromise(filePath, null, historyData);
|
|
}
|
|
}
|
|
else if (this.watchMode) {
|
|
// The in memory file emit history is only required during watch mode.
|
|
this.fileEmitHistory.set(filePath, historyData);
|
|
}
|
|
return historyData;
|
|
}
|
|
async getFileEmitHistory(filePath) {
|
|
return this.webpackCache
|
|
? this.webpackCache.getPromise(filePath, null)
|
|
: this.fileEmitHistory.get(filePath);
|
|
}
|
|
}
|
|
exports.AngularWebpackPlugin = AngularWebpackPlugin;
|