-
Notifications
You must be signed in to change notification settings - Fork 10
/
bundle.js
289 lines (236 loc) · 11.2 KB
/
bundle.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
/* eslint-disable max-depth, max-len */
import fs from 'fs/promises';
import { getRollupConfigForApis, getRollupConfigForScriptResources, getRollupConfigForSsr } from '../config/rollup.config.js';
import { getAppTemplate, getPageTemplate, getUserScripts } from '../lib/templating-utils.js';
import { hashString } from '../lib/hashing-utils.js';
import { checkResourceExists, mergeResponse, normalizePathnameForWindows } from '../lib/resource-utils.js';
import path from 'path';
import { rollup } from 'rollup';
async function emitResources(compilation) {
const { outputDir } = compilation.context;
const { resources, graph } = compilation;
// https://stackoverflow.com/a/56150320/417806
await fs.writeFile(new URL('./resources.json', outputDir), JSON.stringify(resources, (key, value) => {
if (value instanceof Map) {
return {
dataType: 'Map',
value: [...value]
};
} else {
return value;
}
}));
await fs.writeFile(new URL('./graph.json', outputDir), JSON.stringify(graph));
}
async function cleanUpResources(compilation) {
const { outputDir } = compilation.context;
for (const resource of compilation.resources.values()) {
const { src, optimizedFileName, optimizationAttr } = resource;
const optConfig = ['inline', 'static'].indexOf(compilation.config.optimization) >= 0;
const optAttr = ['inline', 'static'].indexOf(optimizationAttr) >= 0;
if (optimizedFileName && (!src || (optAttr || optConfig))) {
await fs.unlink(new URL(`./${optimizedFileName}`, outputDir));
}
}
}
async function optimizeStaticPages(compilation, plugins) {
const { scratchDir, outputDir } = compilation.context;
return Promise.all(compilation.graph
.filter(page => !page.isSSR || (page.isSSR && page.data.static) || (page.isSSR && compilation.config.prerender))
.map(async (page) => {
const { route, outputPath } = page;
const outputDirUrl = new URL(`.${route}`, outputDir);
const url = new URL(`http://localhost:${compilation.config.port}${route}`);
const contents = await fs.readFile(new URL(`./${outputPath}`, scratchDir), 'utf-8');
const headers = new Headers({ 'Content-Type': 'text/html' });
let response = new Response(contents, { headers });
if (!await checkResourceExists(outputDirUrl)) {
await fs.mkdir(outputDirUrl, {
recursive: true
});
}
for (const plugin of plugins) {
if (plugin.shouldOptimize && await plugin.shouldOptimize(url, response.clone())) {
const currentResponse = await plugin.optimize(url, response.clone());
response = mergeResponse(response.clone(), currentResponse.clone());
}
}
// clean up optimization markers
const body = (await response.text()).replace(/data-gwd-opt=".*[a-z]"/g, '');
await fs.writeFile(new URL(`./${outputPath}`, outputDir), body);
})
);
}
async function bundleStyleResources(compilation, resourcePlugins) {
const { outputDir } = compilation.context;
for (const resource of compilation.resources.values()) {
const { contents, optimizationAttr, src = '', type } = resource;
if (['style', 'link'].includes(type)) {
const resourceKey = resource.sourcePathURL.pathname;
const srcPath = src && src.replace(/\.\.\//g, '').replace('./', '');
let optimizedFileName;
let optimizedFileContents;
if (src) {
const basename = path.basename(srcPath);
const basenamePieces = path.basename(srcPath).split('.');
const fileNamePieces = srcPath.split('/').filter(piece => piece !== ''); // normalize by removing any leading /'s
optimizedFileName = srcPath.indexOf('/node_modules') >= 0
? `${basenamePieces[0]}.${hashString(contents)}.css`
: fileNamePieces.join('/').replace(basename, `${basenamePieces[0]}.${hashString(contents)}.css`);
} else {
optimizedFileName = `${hashString(contents)}.css`;
}
const outputPathRoot = new URL(`./${optimizedFileName}`, outputDir)
.pathname
.split('/')
.slice(0, -1)
.join('/')
.concat('/');
const outputPathRootUrl = new URL(`file://${outputPathRoot}`);
if (!await checkResourceExists(outputPathRootUrl)) {
await fs.mkdir(new URL(`file://${outputPathRoot}`), {
recursive: true
});
}
if (compilation.config.optimization === 'none' || optimizationAttr === 'none') {
optimizedFileContents = contents;
} else {
const url = resource.sourcePathURL;
const contentType = 'text/css';
const headers = new Headers({ 'Content-Type': contentType });
const request = new Request(url, { headers });
const initResponse = new Response(contents, { headers });
let response = await resourcePlugins.reduce(async (responsePromise, plugin) => {
const intermediateResponse = await responsePromise;
const shouldIntercept = plugin.shouldIntercept && await plugin.shouldIntercept(url, request, intermediateResponse.clone());
if (shouldIntercept) {
const currentResponse = await plugin.intercept(url, request, intermediateResponse.clone());
const mergedResponse = mergeResponse(intermediateResponse.clone(), currentResponse.clone());
if (mergedResponse.headers.get('Content-Type').indexOf(contentType) >= 0) {
return Promise.resolve(mergedResponse.clone());
}
}
return Promise.resolve(responsePromise);
}, Promise.resolve(initResponse));
response = await resourcePlugins.reduce(async (responsePromise, plugin) => {
const intermediateResponse = await responsePromise;
const shouldOptimize = plugin.shouldOptimize && await plugin.shouldOptimize(url, intermediateResponse.clone());
return shouldOptimize
? Promise.resolve(await plugin.optimize(url, intermediateResponse.clone()))
: Promise.resolve(responsePromise);
}, Promise.resolve(response.clone()));
optimizedFileContents = await response.text();
}
compilation.resources.set(resourceKey, {
...compilation.resources.get(resourceKey),
optimizedFileName,
optimizedFileContents
});
await fs.writeFile(new URL(`./${optimizedFileName}`, outputDir), optimizedFileContents);
}
}
}
async function bundleApiRoutes(compilation) {
// https://rollupjs.org/guide/en/#differences-to-the-javascript-api
const [rollupConfig] = await getRollupConfigForApis(compilation);
if (rollupConfig.input.length !== 0) {
const bundle = await rollup(rollupConfig);
await bundle.write(rollupConfig.output);
}
}
async function bundleSsrPages(compilation) {
// https://rollupjs.org/guide/en/#differences-to-the-javascript-api
// TODO context plugins for SSR ?
// const contextPlugins = compilation.config.plugins.filter((plugin) => {
// return plugin.type === 'context';
// }).map((plugin) => {
// return plugin.provider(compilation);
// });
const hasSSRPages = compilation.graph.filter(page => page.isSSR).length > 0;
const input = [];
if (!compilation.config.prerender && hasSSRPages) {
const htmlOptimizer = compilation.config.plugins.find(plugin => plugin.name === 'plugin-standard-html').provider(compilation);
const { executeModuleUrl } = compilation.config.plugins.find(plugin => plugin.type === 'renderer').provider();
const { executeRouteModule } = await import(executeModuleUrl);
const { pagesDir, scratchDir } = compilation.context;
for (const page of compilation.graph) {
if (page.isSSR && !page.data.static) {
const { filename, imports, route, template, title } = page;
const entryFileUrl = new URL(`./_${filename}`, scratchDir);
const moduleUrl = new URL(`./${filename}`, pagesDir);
// TODO getTemplate has to be static (for now?)
// https://github.com/ProjectEvergreen/greenwood/issues/955
const data = await executeRouteModule({ moduleUrl, compilation, page, prerender: false, htmlContents: null, scripts: [] });
let staticHtml = '';
staticHtml = data.template ? data.template : await getPageTemplate(staticHtml, compilation.context, template, []);
staticHtml = await getAppTemplate(staticHtml, compilation.context, imports, [], false, title);
staticHtml = await getUserScripts(staticHtml, compilation.context);
staticHtml = await (await htmlOptimizer.optimize(new URL(`http://localhost:8080${route}`), new Response(staticHtml))).text();
// better way to write out this inline code?
await fs.writeFile(entryFileUrl, `
import { executeRouteModule } from '${normalizePathnameForWindows(executeModuleUrl)}';
export async function handler(request) {
const compilation = JSON.parse('${JSON.stringify(compilation)}');
const page = JSON.parse('${JSON.stringify(page)}');
const moduleUrl = '___GWD_ENTRY_FILE_URL=${filename}___';
const data = await executeRouteModule({ moduleUrl, compilation, page });
let staticHtml = \`${staticHtml}\`;
if (data.body) {
staticHtml = staticHtml.replace(\/\<content-outlet>(.*)<\\/content-outlet>\/s, data.body);
}
return new Response(staticHtml, {
headers: {
'Content-Type': 'text/html'
}
});
}
`);
input.push(normalizePathnameForWindows(moduleUrl));
input.push(normalizePathnameForWindows(entryFileUrl));
}
}
const [rollupConfig] = await getRollupConfigForSsr(compilation, input);
if (rollupConfig.input.length > 0) {
const bundle = await rollup(rollupConfig);
await bundle.write(rollupConfig.output);
}
}
}
async function bundleScriptResources(compilation) {
// https://rollupjs.org/guide/en/#differences-to-the-javascript-api
const [rollupConfig] = await getRollupConfigForScriptResources(compilation);
if (rollupConfig.input.length !== 0) {
const bundle = await rollup(rollupConfig);
await bundle.write(rollupConfig.output);
}
}
const bundleCompilation = async (compilation) => {
return new Promise(async (resolve, reject) => {
try {
const optimizeResourcePlugins = compilation.config.plugins.filter((plugin) => {
return plugin.type === 'resource';
}).map((plugin) => {
return plugin.provider(compilation);
}).filter((provider) => {
return provider.shouldIntercept && provider.intercept
|| provider.shouldOptimize && provider.optimize;
});
console.info('bundling static assets...');
await Promise.all([
await bundleApiRoutes(compilation),
await bundleScriptResources(compilation),
await bundleStyleResources(compilation, optimizeResourcePlugins)
]);
// bundleSsrPages depends on bundleScriptResources having run first
await bundleSsrPages(compilation);
console.info('optimizing static pages....');
await optimizeStaticPages(compilation, optimizeResourcePlugins);
await cleanUpResources(compilation);
await emitResources(compilation);
resolve();
} catch (err) {
reject(err);
}
});
};
export { bundleCompilation };