-
Notifications
You must be signed in to change notification settings - Fork 12k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(@angular-devkit/build-angular): add experimental Rollup concaten…
…ation option
- Loading branch information
1 parent
b4d5921
commit 4f31795
Showing
11 changed files
with
390 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
142 changes: 142 additions & 0 deletions
142
packages/angular_devkit/build_angular/src/angular-cli-files/plugins/webpack-rollup-loader.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
/** | ||
* @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 | ||
*/ | ||
|
||
// Adapted from https://github.com/erikdesjardins/webpack-rollup-loader/blob/master/index.js | ||
|
||
import { VirtualFileSystemDecorator } from '@ngtools/webpack/src/virtual_file_system_decorator'; | ||
import { dirname, join } from 'path'; | ||
import { OutputAsset, OutputChunk, rollup } from 'rollup'; | ||
import { RawSourceMap } from 'source-map'; | ||
import webpack = require('webpack'); | ||
|
||
function splitRequest(request: string) { | ||
const inx = request.lastIndexOf('!'); | ||
if (inx === -1) { | ||
return { | ||
loaders: '', | ||
resource: request, | ||
}; | ||
} else { | ||
return { | ||
loaders: request.slice(0, inx + 1), | ||
resource: request.slice(inx + 1), | ||
}; | ||
} | ||
} | ||
|
||
// Load resolve paths using Webpack. | ||
function webpackResolutionPlugin( | ||
loaderContext: webpack.loader.LoaderContext, | ||
entryId: string, | ||
entryIdCodeAndMap: { code: string, map: RawSourceMap }, | ||
) { | ||
return { | ||
name: 'webpack-resolution-plugin', | ||
resolveId: (id: string, importerId: string) => { | ||
if (id === entryId) { | ||
return entryId; | ||
} else { | ||
return new Promise((resolve, reject) => { | ||
// split apart resource paths because Webpack's this.resolve() can't handle `loader!` | ||
// prefixes | ||
const parts = splitRequest(id); | ||
const importerParts = splitRequest(importerId); | ||
|
||
// resolve the full path of the imported file with Webpack's module loader | ||
// this will figure out node_modules imports, Webpack aliases, etc. | ||
loaderContext.resolve( | ||
dirname(importerParts.resource), | ||
parts.resource, | ||
(err, fullPath) => err ? reject(err) : resolve(parts.loaders + fullPath), | ||
); | ||
}); | ||
} | ||
}, | ||
load: (id: string) => { | ||
if (id === entryId) { | ||
return entryIdCodeAndMap; | ||
} | ||
|
||
return new Promise((resolve, reject) => { | ||
// load the module with Webpack | ||
// this will apply all relevant loaders, etc. | ||
loaderContext.loadModule( | ||
id, | ||
(err, source, map) => err ? reject(err) : resolve({ code: source, map: map }), | ||
); | ||
}); | ||
}, | ||
}; | ||
} | ||
|
||
export default function webpackRollupLoader( | ||
this: webpack.loader.LoaderContext, | ||
source: string, | ||
sourceMap: RawSourceMap, | ||
) { | ||
// Note: this loader isn't cacheable because it will add the lazy chunks to the | ||
// virtual file system on completion. | ||
const callback = this.async(); | ||
if (!callback) { | ||
throw new Error('Async loader support is required.'); | ||
} | ||
const options = this.query || {}; | ||
const entryId = this.resourcePath; | ||
const sourcemap = this.sourceMap; | ||
|
||
// Get the VirtualFileSystemDecorator that AngularCompilerPlugin added so we can write to it. | ||
// Since we use webpackRollupLoader as a post loader, this should be there. | ||
// TODO: we should be able to do this in a more elegant way by again decorating webpacks | ||
// input file system inside a custom WebpackRollupPlugin, modelled after AngularCompilerPlugin. | ||
const vfs = this._compiler.inputFileSystem as VirtualFileSystemDecorator; | ||
const virtualWrite = (path: string, data: string) => | ||
vfs.getWebpackCompilerHost().writeFile(path, data, false); | ||
|
||
// Bundle with Rollup | ||
const rollupOptions = { | ||
...options, | ||
input: entryId, | ||
plugins: [ | ||
...(options.plugins || []), | ||
webpackResolutionPlugin(this, entryId, { code: source, map: sourceMap }), | ||
], | ||
}; | ||
|
||
rollup(rollupOptions) | ||
.then(build => build.generate({ format: 'es', sourcemap })) | ||
.then( | ||
(result) => { | ||
const [mainChunk, ...otherChunksOrAssets] = result.output; | ||
|
||
// Write other chunks and assets to the virtual file system so that webpack can load them. | ||
const resultDir = dirname(entryId); | ||
otherChunksOrAssets.forEach(chunkOrAsset => { | ||
const { fileName, type } = chunkOrAsset; | ||
if (type == 'chunk') { | ||
const { code, map } = chunkOrAsset as OutputChunk; | ||
virtualWrite(join(resultDir, fileName), code); | ||
if (map) { | ||
// Also write the map if there's one. | ||
// Probably need scriptsSourceMap set on CLI to load it. | ||
virtualWrite(join(resultDir, `${fileName}.map`), map.toString()); | ||
} | ||
} else if (type == 'asset') { | ||
const { source } = chunkOrAsset as OutputAsset; | ||
// Source might be a Buffer. Just assuming it's a string for now. | ||
virtualWrite(join(resultDir, fileName), source as string); | ||
} | ||
}); | ||
|
||
// Always return the main chunk from webpackRollupLoader. | ||
// Cast to any here is needed because of a typings incompatibility between source-map versions. | ||
// tslint:disable-next-line:no-any | ||
callback(null, mainChunk.code, (mainChunk as any).map); | ||
}, | ||
(err) => callback(err), | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
102 changes: 102 additions & 0 deletions
102
packages/angular_devkit/build_angular/test/browser/rollup_spec_large.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
/** | ||
* @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 { Architect } from '@angular-devkit/architect'; | ||
import { | ||
BrowserBuildOutput, | ||
browserBuild, | ||
createArchitect, | ||
host, | ||
lazyModuleFiles, | ||
lazyModuleFnImport, | ||
} from '../utils'; | ||
|
||
|
||
describe('Browser Builder Rollup Concatenation test', () => { | ||
const target = { project: 'app', target: 'build' }; | ||
const overrides = { | ||
experimentalRollupPass: true, | ||
// JIT Rollup bundles will include require calls to .css and .html file, that have lost their | ||
// path context. AOT code already inlines resources so that's not a problem. | ||
aot: true, | ||
// Webpack can't separate rolled-up modules into chunks. | ||
vendorChunk: false, | ||
commonChunk: false, | ||
namedChunks: false, | ||
}; | ||
const prodOverrides = { | ||
// Usual prod options. | ||
fileReplacements: [{ | ||
replace: 'src/environments/environment.ts', | ||
with: 'src/environments/environment.prod.ts', | ||
}], | ||
optimization: true, | ||
sourceMap: false, | ||
extractCss: true, | ||
namedChunks: false, | ||
aot: true, | ||
extractLicenses: true, | ||
vendorChunk: false, | ||
buildOptimizer: true, | ||
// Extra prod options we need for experimentalRollupPass. | ||
commonChunk: false, | ||
// Just for convenience. | ||
outputHashing: 'none', | ||
}; | ||
const rollupProdOverrides = { | ||
...prodOverrides, | ||
experimentalRollupPass: true, | ||
}; | ||
let architect: Architect; | ||
|
||
const getOutputSize = async (output: BrowserBuildOutput) => | ||
(await Promise.all( | ||
Object.keys(output.files) | ||
.filter(name => name.endsWith('.js') && | ||
// These aren't concatenated by Rollup so no point comparing. | ||
!['runtime.js', 'polyfills.js'].includes(name)) | ||
.map(name => output.files[name]), | ||
)) | ||
.map(content => content.length) | ||
.reduce((acc, curr) => acc + curr, 0); | ||
|
||
beforeEach(async () => { | ||
await host.initialize().toPromise(); | ||
architect = (await createArchitect(host.root())).architect; | ||
}); | ||
|
||
afterEach(async () => host.restore().toPromise()); | ||
|
||
it('works', async () => { | ||
await browserBuild(architect, host, target, overrides); | ||
}); | ||
|
||
it('works with lazy modules', async () => { | ||
host.writeMultipleFiles(lazyModuleFiles); | ||
host.writeMultipleFiles(lazyModuleFnImport); | ||
await browserBuild(architect, host, target, overrides); | ||
}); | ||
|
||
it('creates smaller or same size bundles for app without lazy bundles', async () => { | ||
const prodOutput = await browserBuild(architect, host, target, prodOverrides); | ||
const prodSize = await getOutputSize(prodOutput); | ||
const rollupProdOutput = await browserBuild(architect, host, target, rollupProdOverrides); | ||
const rollupProd = await getOutputSize(rollupProdOutput); | ||
expect(prodSize).toBeGreaterThan(rollupProd); | ||
}); | ||
|
||
it('creates smaller bundles for apps with lazy bundles', async () => { | ||
host.writeMultipleFiles(lazyModuleFiles); | ||
host.writeMultipleFiles(lazyModuleFnImport); | ||
const prodOutput = await browserBuild(architect, host, target, prodOverrides); | ||
const prodSize = await getOutputSize(prodOutput); | ||
const rollupProdOutput = await browserBuild(architect, host, target, rollupProdOverrides); | ||
const rollupProd = await getOutputSize(rollupProdOutput); | ||
expect(prodSize).toBeGreaterThan(rollupProd); | ||
}); | ||
}); |
Oops, something went wrong.