mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-22 23:15:56 +08:00
181 lines
5.2 KiB
TypeScript
181 lines
5.2 KiB
TypeScript
/**
|
|
* @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
|
|
*/
|
|
import { Compiler, loader } from 'webpack';
|
|
import { CachedSource, ConcatSource, OriginalSource, RawSource, Source } from 'webpack-sources';
|
|
import { interpolateName } from 'loader-utils';
|
|
import * as path from 'path';
|
|
|
|
const Chunk = require('webpack/lib/Chunk');
|
|
const EntryPoint = require('webpack/lib/Entrypoint');
|
|
|
|
export interface ScriptsWebpackPluginOptions {
|
|
name: string;
|
|
sourceMap: boolean;
|
|
scripts: string[];
|
|
filename: string;
|
|
basePath: string;
|
|
}
|
|
|
|
interface ScriptOutput {
|
|
filename: string;
|
|
source: CachedSource;
|
|
}
|
|
|
|
function addDependencies(compilation: any, scripts: string[]): void {
|
|
if (compilation.fileDependencies.add) {
|
|
// Webpack 4+ uses a Set
|
|
for (const script of scripts) {
|
|
compilation.fileDependencies.add(script);
|
|
}
|
|
} else {
|
|
// Webpack 3
|
|
compilation.fileDependencies.push(...scripts);
|
|
}
|
|
}
|
|
|
|
function hook(compiler: any, action: (compilation: any, callback: (err?: Error) => void) => void) {
|
|
if (compiler.hooks) {
|
|
// Webpack 4
|
|
compiler.hooks.thisCompilation.tap('scripts-webpack-plugin', (compilation: any) => {
|
|
compilation.hooks.additionalAssets.tapAsync(
|
|
'scripts-webpack-plugin',
|
|
(callback: (err?: Error) => void) => action(compilation, callback),
|
|
);
|
|
});
|
|
} else {
|
|
// Webpack 3
|
|
compiler.plugin('this-compilation', (compilation: any) => {
|
|
compilation.plugin(
|
|
'additional-assets',
|
|
(callback: (err?: Error) => void) => action(compilation, callback),
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
export class ScriptsWebpackPlugin {
|
|
private _lastBuildTime?: number;
|
|
private _cachedOutput?: ScriptOutput;
|
|
|
|
constructor(private options: Partial<ScriptsWebpackPluginOptions> = {}) {}
|
|
|
|
shouldSkip(compilation: any, scripts: string[]): boolean {
|
|
if (this._lastBuildTime == undefined) {
|
|
this._lastBuildTime = Date.now();
|
|
return false;
|
|
}
|
|
|
|
for (let i = 0; i < scripts.length; i++) {
|
|
let scriptTime;
|
|
if (compilation.fileTimestamps.get) {
|
|
// Webpack 4+ uses a Map
|
|
scriptTime = compilation.fileTimestamps.get(scripts[i]);
|
|
} else {
|
|
// Webpack 3
|
|
scriptTime = compilation.fileTimestamps[scripts[i]];
|
|
}
|
|
if (!scriptTime || scriptTime > this._lastBuildTime) {
|
|
this._lastBuildTime = Date.now();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private _insertOutput(compilation: any, { filename, source }: ScriptOutput, cached = false) {
|
|
const chunk = new Chunk(this.options.name);
|
|
chunk.rendered = !cached;
|
|
chunk.id = this.options.name;
|
|
chunk.ids = [chunk.id];
|
|
chunk.files.push(filename);
|
|
|
|
const entrypoint = new EntryPoint(this.options.name);
|
|
entrypoint.pushChunk(chunk);
|
|
|
|
compilation.entrypoints.set(this.options.name, entrypoint);
|
|
compilation.chunks.push(chunk);
|
|
compilation.assets[filename] = source;
|
|
}
|
|
|
|
apply(compiler: Compiler): void {
|
|
if (!this.options.scripts || this.options.scripts.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const scripts = this.options.scripts
|
|
.filter(script => !!script)
|
|
.map(script => path.resolve(this.options.basePath || '', script));
|
|
|
|
hook(compiler, (compilation, callback) => {
|
|
if (this.shouldSkip(compilation, scripts)) {
|
|
if (this._cachedOutput) {
|
|
this._insertOutput(compilation, this._cachedOutput, true);
|
|
}
|
|
|
|
addDependencies(compilation, scripts);
|
|
callback();
|
|
|
|
return;
|
|
}
|
|
|
|
const sourceGetters = scripts.map(fullPath => {
|
|
return new Promise<Source>((resolve, reject) => {
|
|
compilation.inputFileSystem.readFile(fullPath, (err: Error, data: Buffer) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
const content = data.toString();
|
|
|
|
let source;
|
|
if (this.options.sourceMap) {
|
|
// TODO: Look for source map file (for '.min' scripts, etc.)
|
|
|
|
let adjustedPath = fullPath;
|
|
if (this.options.basePath) {
|
|
adjustedPath = path.relative(this.options.basePath, fullPath);
|
|
}
|
|
source = new OriginalSource(content, adjustedPath);
|
|
} else {
|
|
source = new RawSource(content);
|
|
}
|
|
|
|
resolve(source);
|
|
});
|
|
});
|
|
});
|
|
|
|
Promise.all(sourceGetters)
|
|
.then(sources => {
|
|
const concatSource = new ConcatSource();
|
|
sources.forEach(source => {
|
|
concatSource.add(source);
|
|
concatSource.add('\n;');
|
|
});
|
|
|
|
const combinedSource = new CachedSource(concatSource);
|
|
const filename = interpolateName(
|
|
{ resourcePath: 'scripts.js' } as loader.LoaderContext,
|
|
this.options.filename,
|
|
{ content: combinedSource.source() },
|
|
);
|
|
|
|
const output = { filename, source: combinedSource };
|
|
this._insertOutput(compilation, output);
|
|
this._cachedOutput = output;
|
|
addDependencies(compilation, scripts);
|
|
|
|
callback();
|
|
})
|
|
.catch((err: Error) => callback(err));
|
|
});
|
|
}
|
|
}
|