UNPKG

7.01 kBPlain TextView Raw
1import webpack from 'webpack';
2import { join } from 'path';
3import { mergeWith, flatten } from 'lodash';
4import { writeFileSync, realpathSync } from 'fs';
5import { compile, registerHelper } from 'handlebars';
6import jsStringEscape from 'js-string-escape';
7import { BundleDependencies } from './splitter';
8import { BundlerHook, BuildResult } from './bundler';
9import BundleConfig from './bundle-config';
10import { ensureDirSync } from 'fs-extra';
11
12registerHelper('js-string-escape', jsStringEscape);
13
14const entryTemplate = compile(`
15if (typeof document !== 'undefined') {
16 {{#if publicAssetURL}}
17 __webpack_public_path__ = '{{js-string-escape publicAssetURL}}';
18 {{else}}
19 {{!
20 locate the webpack lazy loaded chunks relative to the currently executing
21 script. The last <script> in DOM should be us, assuming that we are being
22 synchronously loaded, which is the normal thing to do. If people are doing
23 weirder things than that, they may need to explicitly set a publicAssetURL
24 instead.
25 }}
26 __webpack_public_path__ = (function(){
27 var scripts = document.querySelectorAll('script');
28 return scripts[scripts.length - 1].src.replace(/\\/[^/]*$/, '/');
29 })();
30 {{/if}}
31}
32
33module.exports = (function(){
34 var d = _eai_d;
35 var r = _eai_r;
36 window.emberAutoImportDynamic = function(specifier) {
37 return r('_eai_dyn_' + specifier);
38 };
39 {{#each staticImports as |module|}}
40 d('{{js-string-escape module.specifier}}', [], function() { return require('{{js-string-escape module.entrypoint}}'); });
41 {{/each}}
42 {{#each dynamicImports as |module|}}
43 d('_eai_dyn_{{js-string-escape module.specifier}}', [], function() { return import('{{js-string-escape module.entrypoint}}'); });
44 {{/each}}
45})();
46`);
47
48// this goes in a file by itself so we can tell webpack not to parse it. That
49// allows us to grab the "require" and "define" from our enclosing scope without
50// webpack messing with them.
51//
52// It's important that we're using our enclosing scope and not jumping directly
53// to window.require (which would be easier), because the entire Ember app may be
54// inside a closure with a "require" that isn't the same as "window.require".
55const loader = `
56window._eai_r = require;
57window._eai_d = define;
58`;
59
60export default class WebpackBundler implements BundlerHook {
61 private stagingDir: string;
62 private webpack: webpack.Compiler;
63 private outputDir: string;
64
65 constructor(
66 bundles : BundleConfig,
67 environment: 'production' | 'development' | 'test',
68 extraWebpackConfig: webpack.Configuration | undefined,
69 private consoleWrite: (message: string) => void,
70 private publicAssetURL: string | undefined,
71 tempArea: string
72 ) {
73 // resolve the real path, because we're going to do path comparisons later
74 // that could fail if this is not canonical.
75 tempArea = realpathSync(tempArea);
76
77 this.stagingDir = join(tempArea, 'staging');
78 ensureDirSync(this.stagingDir);
79 this.outputDir = join(tempArea, 'output');
80 ensureDirSync(this.outputDir);
81 let entry: { [name: string]: string[] } = {};
82 bundles.names.forEach(bundle => {
83 entry[bundle] = [join(this.stagingDir, 'l.js'), join(this.stagingDir, `${bundle}.js`)];
84 });
85
86 let config: webpack.Configuration = {
87 mode: environment === 'production' ? 'production' : 'development',
88 entry,
89 output: {
90 path: this.outputDir,
91 filename: `chunk.[chunkhash].js`,
92 chunkFilename: `chunk.[chunkhash].js`,
93 libraryTarget: 'var',
94 library: '__ember_auto_import__'
95 },
96 optimization: {
97 splitChunks: {
98 chunks: 'all'
99 }
100 },
101 module: {
102 noParse: (file) => file === join(this.stagingDir, 'l.js'),
103 rules: []
104 },
105 };
106 if (extraWebpackConfig) {
107 mergeConfig(config, extraWebpackConfig);
108 }
109 this.webpack = webpack(config);
110 }
111
112 async build(bundleDeps: Map<string, BundleDependencies>) {
113 for (let [bundle, deps] of bundleDeps.entries()) {
114 this.writeEntryFile(bundle, deps);
115 }
116 this.writeLoaderFile();
117 let stats = await this.runWebpack();
118 return this.summarizeStats(stats);
119 }
120
121 private summarizeStats(_stats: webpack.Stats): BuildResult {
122 let stats = _stats.toJson();
123 let output = {
124 entrypoints: new Map(),
125 lazyAssets: [] as string[],
126 dir: this.outputDir
127 };
128 let nonLazyAssets: Set<string> = new Set();
129 for (let id of Object.keys(stats.entrypoints)) {
130 let entrypoint = stats.entrypoints[id];
131 output.entrypoints.set(id, entrypoint.assets);
132 entrypoint.assets.forEach((asset: string) => nonLazyAssets.add(asset));
133 }
134 for (let asset of stats.assets) {
135 if (!nonLazyAssets.has(asset.name)) {
136 output.lazyAssets.push(asset.name);
137 }
138 }
139 return output;
140 }
141
142 private writeEntryFile(name: string, deps: BundleDependencies) {
143 writeFileSync(
144 join(this.stagingDir, `${name}.js`),
145 entryTemplate({
146 staticImports: deps.staticImports,
147 dynamicImports: deps.dynamicImports,
148 publicAssetURL: this.publicAssetURL
149 })
150 );
151 }
152
153 private writeLoaderFile() {
154 writeFileSync(
155 join(this.stagingDir, `l.js`),
156 loader
157 );
158 }
159
160 private async runWebpack(): Promise<webpack.Stats> {
161 return new Promise((resolve, reject) => {
162 this.webpack.run((err, stats) => {
163 if (err) {
164 this.consoleWrite(stats.toString());
165 reject(err);
166 return;
167 }
168 if (stats.hasErrors()) {
169 this.consoleWrite(stats.toString());
170 reject(new Error('webpack returned errors to ember-auto-import'));
171 return;
172 }
173 if (stats.hasWarnings() || process.env.AUTO_IMPORT_VERBOSE) {
174 this.consoleWrite(stats.toString());
175 }
176 resolve(stats);
177 });
178 }) as Promise<webpack.Stats>;
179 }
180}
181
182export function mergeConfig(dest: object, ...srcs: object[]) {
183 return mergeWith(dest, ...srcs, combine);
184}
185
186function combine(objValue: any, srcValue: any, key: string) {
187 if (key === 'noParse') {
188 return eitherPattern(objValue, srcValue);
189 }
190
191 // arrays concat
192 if (Array.isArray(objValue)) {
193 return objValue.concat(srcValue);
194 }
195}
196
197// webpack configs have several places where they accept:
198// - RegExp
199// - [RegExp]
200// - (resource: string) => boolean
201// - string
202// - [string]
203// This function combines any of these with a logical OR.
204function eitherPattern(...patterns: any[]): (resource: string) => boolean {
205 let flatPatterns = flatten(patterns);
206 return function(resource) {
207 for (let pattern of flatPatterns) {
208 if (pattern instanceof RegExp) {
209 if (pattern.test(resource)) {
210 return true;
211 }
212 } else if (typeof pattern === 'string') {
213 if (pattern === resource) {
214 return true;
215 }
216 } else if (typeof pattern === 'function') {
217 if (pattern(resource)) {
218 return true;
219 }
220 }
221 }
222 return false;
223 };
224}