1 | import { build as buildProject } from "snowpack";
|
2 | import { dirname, resolve } from "path";
|
3 | import glob from "globby";
|
4 | import arg from "arg";
|
5 | import { rollup } from "rollup";
|
6 | import { performance } from "perf_hooks";
|
7 | import { green, dim } from "kleur/colors";
|
8 | import styles from "rollup-plugin-styles";
|
9 | import esbuild from "esbuild";
|
10 | import module from "module";
|
11 | const { createRequire, builtinModules: builtins } = module;
|
12 | const require = createRequire(import.meta.url);
|
13 | import { 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";
|
14 | import { rmdir, mkdir, copyDir, copyFile } from "../utils/fs.js";
|
15 | import { statSync } from "fs";
|
16 | import { resolveNormalizedBasePath, loadConfiguration, } from "../utils/command.js";
|
17 | function 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 | }
|
26 | export 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 |
|
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 | }
|
84 | async 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 | }
|
90 | async 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 | }
|
129 | async 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 |
|
140 |
|
141 |
|
142 |
|
143 | async 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 |
|
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 |
|
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
197 |
|
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 |
|
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 |
|
230 |
|
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 |
|
240 |
|
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 |
|
277 |
|
278 |
|
279 |
|
280 |
|
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 |
|
406 |
|
407 |
|
408 | const 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 |
|
420 |
|
421 |
|
422 | const 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 | };
|
432 | async 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 | }
|
439 | async function cleanup() {
|
440 | const paths = [STAGING_DIR, SSR_DIR, CACHE_DIR];
|
441 | await Promise.all(paths.map((p) => rmdir(p)));
|
442 | }
|