UNPKG

19.2 kBJavaScriptView Raw
1import { build as buildProject } from "snowpack";
2import { dirname, resolve } from "path";
3import glob from "globby";
4import arg from "arg";
5import { rollup } from "rollup";
6import { performance } from "perf_hooks";
7import { green, dim } from "kleur/colors";
8import styles from "rollup-plugin-styles";
9import esbuild from "esbuild";
10import module from "module";
11const { createRequire, builtinModules: builtins } = module;
12const require = createRequire(import.meta.url);
13import { STAGING_DIR, SSR_DIR, OUT_DIR_NO_BASE, OUT_DIR, importDataMethods, applyDataMethods, preactImportTransformer, proxyImportTransformer, getFileNameFromPath, hashContentSync, emitFile, renderPage, emitFinalAsset, copyAssetToFinal, stripWithHydrate, preactToCDN, setBasePath, CACHE_DIR, } from "../utils/build.js";
14import { rmdir, mkdir, copyDir, copyFile } from "../utils/fs.js";
15import { statSync } from "fs";
16import { resolveNormalizedBasePath, loadConfiguration, } from "../utils/command.js";
17function parseArgs(argv) {
18 return arg({
19 "--debug-hydration": Boolean,
20 "--no-clean": Boolean,
21 "--no-open": Boolean,
22 "--serve": Boolean,
23 "--base-path": String,
24 }, { permissive: true, argv });
25}
26export default async function build(argvOrParsedArgs) {
27 const args = Array.isArray(argvOrParsedArgs)
28 ? parseArgs(argvOrParsedArgs)
29 : argvOrParsedArgs;
30 let basePath = resolveNormalizedBasePath(args);
31 setBasePath(basePath);
32 const config = await loadConfiguration("build");
33 const buildStart = performance.now();
34 await Promise.all([prepare(), buildProject({ config, lockfile: null })]);
35 let pages = await glob(resolve(STAGING_DIR, "src/pages/**/*.js"));
36 let globalEntryPoint = resolve(STAGING_DIR, "src/global/index.js");
37 let globalStyle = resolve(STAGING_DIR, "src/global/index.css");
38 try {
39 globalEntryPoint = statSync(globalEntryPoint) ? globalEntryPoint : null;
40 }
41 catch (e) {
42 globalEntryPoint = null;
43 }
44 try {
45 globalStyle = statSync(globalStyle) ? globalStyle : null;
46 }
47 catch (e) {
48 globalStyle = null;
49 }
50 pages = pages.filter((page) => !page.endsWith(".proxy.js"));
51 let [manifest, routeData] = await Promise.all([
52 bundlePagesForSSR(globalEntryPoint ? [...pages, globalEntryPoint] : pages),
53 fetchRouteData(pages.filter((page) => !page.endsWith("_document.js"))),
54 ]);
55 if (globalStyle) {
56 manifest = manifest.map((entry) => (Object.assign(Object.assign({}, entry), { hydrateStyleBindings: [
57 "_static/styles/_global.css",
58 ...(entry.hydrateStyleBindings || []),
59 ] })));
60 }
61 await Promise.all([
62 ssr(manifest, routeData, {
63 basePath,
64 debug: args["--debug-hydration"],
65 hasGlobalScript: globalEntryPoint !== null,
66 }),
67 copyHydrateAssets(manifest, globalStyle),
68 ]);
69 const buildEnd = performance.now();
70 console.log(`${green("✔")} build complete ${dim(`[${((buildEnd - buildStart) / 1000).toFixed(2)}s]`)}`);
71 // TODO: print tree of generated files
72 if (!args["--no-clean"])
73 await cleanup();
74 if (args["--serve"]) {
75 const toForward = ["--base-path", "--no-open"];
76 let forwardArgs = {};
77 for (const arg of toForward) {
78 forwardArgs[arg] = args[arg];
79 }
80 return import("./microsite-serve.js").then(({ default: serve }) => serve(forwardArgs));
81 }
82 process.exit(0);
83}
84async function prepare() {
85 const paths = [SSR_DIR];
86 await Promise.all([...paths, OUT_DIR_NO_BASE].map((p) => rmdir(p)));
87 await Promise.all([...paths].map((p) => mkdir(p)));
88 await copyDir(resolve(process.cwd(), "./public"), resolve(process.cwd(), `./${OUT_DIR}`));
89}
90async function copyHydrateAssets(manifest, globalStyle) {
91 let tasks = [];
92 const transform = async (source) => {
93 source = stripWithHydrate(source);
94 source = await preactToCDN(source);
95 const result = await esbuild.transform(source, {
96 minify: true,
97 minifyIdentifiers: false,
98 });
99 return result.code;
100 };
101 if (globalStyle) {
102 tasks.push(copyFile(globalStyle, resolve(OUT_DIR, "_static/styles/_global.css")));
103 }
104 if (manifest.some((entry) => entry.hydrateBindings && Object.keys(entry.hydrateBindings).length > 0)) {
105 const transformInit = async (source) => {
106 source = await preactToCDN(source);
107 const result = await esbuild.transform(source, {
108 minify: true,
109 });
110 return result.code;
111 };
112 tasks.push(copyFile(require.resolve("microsite/assets/microsite-runtime.js"), resolve(OUT_DIR, "_static/vendor/microsite.js"), { transform: transformInit }));
113 }
114 const jsAssets = await glob([
115 resolve(SSR_DIR, "_hydrate/**/*.js"),
116 resolve(SSR_DIR, "_static/**/*.js"),
117 ]);
118 const hydrateStyleAssets = await glob([
119 resolve(SSR_DIR, "_hydrate/**/*.css"),
120 resolve(SSR_DIR, "_static/**/*.css"),
121 ]);
122 await Promise.all([
123 ...tasks,
124 ...jsAssets.map((asset) => copyAssetToFinal(asset, transform)),
125 ...hydrateStyleAssets.map((asset) => copyAssetToFinal(asset)),
126 ]);
127 return;
128}
129async function fetchRouteData(paths) {
130 let routeData = [];
131 await Promise.all(paths.map((path) => importDataMethods(path)
132 .then((handlers) => applyDataMethods(path.replace(resolve(process.cwd(), `./${STAGING_DIR}/src/pages`), ""), handlers))
133 .then((entry) => {
134 routeData = routeData.concat(...entry);
135 })));
136 return routeData;
137}
138/**
139 * This function runs rollup on Snowpack's output to
140 * extract the hydrated chunks and prepare the pages to be
141 * server-side rendered.
142 */
143async function bundlePagesForSSR(paths) {
144 const bundle = await rollup({
145 input: paths.reduce((acc, page) => {
146 if (/pages\//.test(page)) {
147 return Object.assign(Object.assign({}, acc), { [page.slice(page.indexOf("pages/"), -3)]: page });
148 }
149 if (/global\/index\.js/.test(page)) {
150 return Object.assign(Object.assign({}, acc), { "_static/chunks/_global": page });
151 }
152 }, {}),
153 external: (source) => {
154 return (builtins.includes(source) ||
155 source.startsWith("microsite") ||
156 source.startsWith("preact"));
157 },
158 plugins: [
159 rewriteCssProxies(),
160 rewritePreact(),
161 styles({
162 config: true,
163 mode: "extract",
164 minimize: true,
165 autoModules: true,
166 modules: {
167 generateScopedName: `[local]_[hash:5]`,
168 },
169 sourceMap: false,
170 }),
171 ],
172 onwarn(warning, handler) {
173 // unresolved import happens for anything just called server-side
174 if (warning.code === "UNRESOLVED_IMPORT")
175 return;
176 handler(warning);
177 },
178 });
179 let entries = new Set();
180 let entryHydrations = {};
181 let sharedModuleCssProxyEntries = new Set();
182 const { output } = await bundle.generate({
183 dir: SSR_DIR,
184 format: "esm",
185 sourcemap: false,
186 hoistTransitiveImports: false,
187 minifyInternalExports: false,
188 chunkFileNames: "[name].js",
189 assetFileNames: "[name][extname]",
190 /**
191 * This is where most of the magic happens...
192 * We loop through all the modules and group any hydrated components
193 * based on the entryPoint which imported them.
194 *
195 * Components reused for multiple routes are placed in a shared chunk.
196 *
197 * All code from '_snowpack/pkg' is placed in a vendor chunk.
198 */
199 manualChunks(id, { getModuleInfo }) {
200 const info = getModuleInfo(id);
201 if (id.endsWith(".css") && !id.endsWith(".module.css"))
202 return;
203 if (info.importers.length === 1) {
204 // If we only import this module in global/index.js, inline to _global chunk
205 if (/global\/index\.js$/.test(info.importers[0]))
206 return `_static/vendor/global`;
207 }
208 if (/_snowpack\/pkg/.test(info.id))
209 return `_static/vendor/index`;
210 const dependentStaticEntryPoints = [];
211 const dependentHydrateEntryPoints = [];
212 const target = info.importedIds.includes("microsite/hydrate")
213 ? dependentHydrateEntryPoints
214 : dependentStaticEntryPoints;
215 const idsToHandle = new Set([
216 ...info.importers,
217 ...info.dynamicImporters,
218 ]);
219 let moduleInfoById = {};
220 for (const moduleId of idsToHandle) {
221 const moduleInfo = getModuleInfo(moduleId);
222 moduleInfoById[moduleId] = moduleInfo;
223 for (const importerId of moduleInfo.importers)
224 idsToHandle.add(importerId);
225 }
226 for (const moduleId of idsToHandle) {
227 const moduleInfo = moduleInfoById[moduleId];
228 const { isEntry, dynamicImporters, importers } = moduleInfo;
229 // TODO: naive check to see if module is a "facade" to only export sub-modules (something like `/components/index.ts`)
230 // const isFacade = (basename(moduleId, extname(moduleId)) === 'index') && !isEntry && importedIds.every(m => dirname(m).startsWith(dirname(moduleId)));
231 if (isEntry || [...importers, ...dynamicImporters].length > 0)
232 target.push(moduleId);
233 if (isEntry) {
234 entries.add(moduleId);
235 }
236 }
237 let manualChunkId;
238 if (dependentHydrateEntryPoints.length > 1) {
239 // All shared components should go in the same chunk (for now)
240 // Eventually this could be optimized to split into a few chunks based on how many entry points rely on them
241 manualChunkId = `_hydrate/chunks/_shared`;
242 }
243 if (dependentStaticEntryPoints.length > 1) {
244 if (id.endsWith(".module.css")) {
245 dependentStaticEntryPoints.forEach((entry) => sharedModuleCssProxyEntries.add(entry.replace(/^.*\/pages\//gim, "pages/")));
246 manualChunkId = `_static/chunks/_classnames`;
247 }
248 manualChunkId = "_static/chunks/_shared";
249 }
250 if (dependentHydrateEntryPoints.length === 1) {
251 const { code } = getModuleInfo(dependentHydrateEntryPoints[0]);
252 const hash = hashContentSync(code, 8);
253 const filename = `${getFileNameFromPath(dependentHydrateEntryPoints[0]).replace(/^pages\//, "")}-${hash}`;
254 manualChunkId = `_hydrate/chunks/${filename}`;
255 }
256 for (const moduleId of dependentHydrateEntryPoints) {
257 if (entries.has(moduleId)) {
258 if (!(moduleId in entryHydrations)) {
259 entryHydrations[moduleId] = new Set();
260 }
261 entryHydrations[moduleId].add(manualChunkId);
262 }
263 }
264 return manualChunkId;
265 },
266 });
267 const hydrationExports = output.reduce((acc, chunkOrAsset) => {
268 if (chunkOrAsset.type !== 'asset' &&
269 chunkOrAsset.name.startsWith("_hydrate/")) {
270 return Object.assign(Object.assign({}, acc), { [chunkOrAsset.name]: chunkOrAsset.exports });
271 }
272 return acc;
273 }, {});
274 const manifest = [];
275 /**
276 * Here we're manually emitting the files so we have a chance
277 * to generate a manifest detailing any dependent styles or
278 * hydrated chunks per entry-point.
279 *
280 * Later, we'll pass the manifest to the SSR function.
281 */
282 await Promise.all(output.map((chunkOrAsset) => {
283 var _a;
284 if (chunkOrAsset.type === "asset") {
285 if (chunkOrAsset.name.startsWith("_hydrate")) {
286 const finalAssetName = chunkOrAsset.name.replace(/\bchunks\b/, "styles");
287 manifest.forEach((entry) => {
288 let binding = chunkOrAsset.name.replace(/\.css$/, ".js");
289 if (entry.hydrateBindings && entry.hydrateBindings[binding]) {
290 entry.hydrateStyleBindings = Array.from(new Set([...entry.hydrateStyleBindings, finalAssetName]));
291 }
292 });
293 return emitFile(finalAssetName, chunkOrAsset.source);
294 }
295 else if (chunkOrAsset.name.endsWith("_classnames.css")) {
296 const finalAssetName = "_static/styles/_modules.css";
297 for (const entryName of sharedModuleCssProxyEntries.values()) {
298 const inManifest = manifest.find((entry) => entry.name === entryName);
299 if (inManifest) {
300 manifest.forEach((entry) => {
301 if (entry.name === entryName) {
302 entry.hydrateStyleBindings = Array.from(new Set([
303 ...entry.hydrateStyleBindings,
304 `${finalAssetName}?m=${hashContentSync(chunkOrAsset.source.toString(), 8)}`,
305 ]));
306 }
307 });
308 }
309 else {
310 manifest.push({
311 name: entryName,
312 hydrateStyleBindings: [
313 `${finalAssetName}?m=${hashContentSync(chunkOrAsset.source.toString(), 8)}`,
314 ],
315 hydrateBindings: {},
316 });
317 }
318 }
319 return emitFile(finalAssetName, chunkOrAsset.source);
320 }
321 else {
322 let entryName = chunkOrAsset.name.replace(/\.css$/, ".js");
323 let finalAssetName = chunkOrAsset.name
324 .replace(/^pages/, "_hydrate/styles")
325 .replace(/\bchunks\b/, "styles");
326 const inManifest = manifest.find((entry) => entry.name === entryName);
327 if (inManifest) {
328 manifest.forEach((entry) => {
329 if (entry.name === entryName) {
330 entry.hydrateStyleBindings = Array.from(new Set([
331 ...entry.hydrateStyleBindings,
332 `${finalAssetName}?m=${hashContentSync(chunkOrAsset.source.toString(), 8)}`,
333 ]));
334 }
335 });
336 }
337 else {
338 manifest.push({
339 name: entryName,
340 hydrateStyleBindings: [],
341 hydrateBindings: {},
342 });
343 }
344 return emitFile(finalAssetName, chunkOrAsset.source);
345 }
346 }
347 else {
348 if (chunkOrAsset.name.startsWith("_hydrate/") ||
349 chunkOrAsset.name.startsWith("_static/")) {
350 return emitFile(`${chunkOrAsset.name}.js`, chunkOrAsset.code);
351 }
352 else if (chunkOrAsset.isEntry) {
353 let hydrateBindings = {};
354 for (const [file, exports] of Object.entries(chunkOrAsset.importedBindings)) {
355 if (file.startsWith("_hydrate/")) {
356 hydrateBindings = Object.assign(hydrateBindings, {
357 [file]: exports,
358 });
359 }
360 }
361 const id = chunkOrAsset.facadeModuleId;
362 if (id in entryHydrations) {
363 for (const hydration of entryHydrations[id]) {
364 const exports = (_a = hydrationExports[hydration]) !== null && _a !== void 0 ? _a : [];
365 const file = `${hydration}.js`;
366 hydrateBindings = Object.assign(hydrateBindings, {
367 [file]: exports,
368 });
369 }
370 }
371 const entryName = `${chunkOrAsset.name}.js`;
372 const inManifest = manifest.find((entry) => entry.name === entryName);
373 if (inManifest) {
374 manifest.forEach((entry) => {
375 if (entry.name === entryName) {
376 entry.hydrateBindings = Object.assign(entry.hydrateBindings || {}, hydrateBindings);
377 }
378 });
379 }
380 else {
381 manifest.push({
382 name: entryName,
383 hydrateStyleBindings: [],
384 hydrateBindings,
385 });
386 }
387 emitFile(entryName, chunkOrAsset.code);
388 }
389 else {
390 console.log(`Unexpected chunk: ${chunkOrAsset.name}`, chunkOrAsset.code);
391 }
392 }
393 }));
394 return manifest
395 .filter(({ name }) => name !== "pages/_document.js")
396 .map((entry) => {
397 if (Object.keys(entry.hydrateBindings).length === 0)
398 entry.hydrateBindings = null;
399 if (entry.hydrateStyleBindings.length === 0)
400 entry.hydrateStyleBindings = null;
401 return entry;
402 });
403}
404/**
405 * Snowpack rewrites CSS to a `.css.proxy.js` file.
406 * Great for dev, but we need to revert to the actual CSS file
407 */
408const rewriteCssProxies = () => {
409 return {
410 name: "@microsite/rollup-rewrite-css-proxies",
411 resolveId(source, importer) {
412 if (!proxyImportTransformer.filter(source))
413 return null;
414 return resolve(dirname(importer), proxyImportTransformer.transform(source));
415 },
416 };
417};
418/**
419 * Snowpack rewrites CSS to a `.css.proxy.js` file.
420 * Great for dev, but we need to revert to the actual CSS file
421 */
422const rewritePreact = () => {
423 return {
424 name: "@microsite/rollup-rewrite-preact",
425 resolveId(source) {
426 if (!preactImportTransformer.filter(source))
427 return null;
428 return preactImportTransformer.transform(source);
429 },
430 };
431};
432async function ssr(manifest, routeData, { basePath = "/", debug = false, hasGlobalScript = false } = {}) {
433 return Promise.all(routeData.map((entry) => renderPage(entry, manifest.find((route) => route.name.replace(/^pages/, "") === entry.name), { basePath, debug, hasGlobalScript })
434 .then(({ name, contents }) => {
435 return { name, contents };
436 })
437 .then(({ name, contents }) => emitFinalAsset(name, contents))));
438}
439async function cleanup() {
440 const paths = [STAGING_DIR, SSR_DIR, CACHE_DIR];
441 await Promise.all(paths.map((p) => rmdir(p)));
442}