@blinkk/root 1.0.0-alpha.8 → 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,17 +1,29 @@
1
+ import {
2
+ configureServerPlugins,
3
+ getVitePlugins
4
+ } from "./chunk-DTEQ2AIW.js";
5
+ import {
6
+ htmlMinify,
7
+ htmlPretty,
8
+ isValidTagName,
9
+ parseTagNames
10
+ } from "./chunk-GGQGZ7ZE.js";
11
+
1
12
  // src/cli/commands/dev.ts
2
13
  import path4 from "node:path";
3
14
  import { fileURLToPath } from "node:url";
4
15
  import { default as express } from "express";
5
16
  import { createServer as createViteServer } from "vite";
6
17
 
7
- // src/render/vite-plugin-root.ts
8
- import path2 from "path";
9
- import glob from "tiny-glob";
18
+ // src/render/asset-map/dev-asset-map.ts
19
+ import path2 from "node:path";
20
+ import { searchForWorkspaceRoot } from "vite";
10
21
 
11
- // src/core/fsutils.ts
22
+ // src/utils/fsutils.ts
12
23
  import { promises as fs } from "node:fs";
13
24
  import path from "node:path";
14
25
  import fsExtra from "fs-extra";
26
+ import glob from "tiny-glob";
15
27
  function isJsFile(filename) {
16
28
  return !!filename.match(/\.(j|t)sx?$/);
17
29
  }
@@ -27,11 +39,14 @@ async function makeDir(dirpath) {
27
39
  await fs.mkdir(dirpath, { recursive: true });
28
40
  }
29
41
  }
30
- async function copyDir(srcdir, dstdir) {
31
- if (!fsExtra.existsSync(srcdir)) {
32
- return;
42
+ async function copyGlob(pattern, srcdir, dstdir) {
43
+ const files = await glob(pattern, { cwd: srcdir });
44
+ if (files.length > 0) {
45
+ await makeDir(dstdir);
33
46
  }
34
- fsExtra.copySync(srcdir, dstdir, { recursive: true, overwrite: true });
47
+ files.forEach((file) => {
48
+ fsExtra.copySync(path.resolve(srcdir, file), path.resolve(dstdir, file));
49
+ });
35
50
  }
36
51
  async function rmDir(dirpath) {
37
52
  await fs.rm(dirpath, { recursive: true, force: true });
@@ -53,152 +68,60 @@ async function isDirectory(dirpath) {
53
68
  function fileExists(filepath) {
54
69
  return fs.access(filepath).then(() => true).catch(() => false);
55
70
  }
56
-
57
- // src/render/vite-plugin-root.ts
58
- var JSX_ELEMENTS_REGEX = /jsxs?\("(\w[\w-]+\w)"/g;
59
- var HTML_ELEMENTS_REGEX = /<(\w[\w-]+\w)/g;
60
- function pluginRoot(options) {
61
- var _a, _b;
62
- const elementsVirtualId = "virtual:root-elements";
63
- const resolvedElementsVirtualId = "\0" + elementsVirtualId;
64
- const rootDir = (options == null ? void 0 : options.rootDir) || process.cwd();
65
- const rootConfig = (options == null ? void 0 : options.rootConfig) || {};
66
- const elementsDirs = [path2.join(rootDir, "elements")];
67
- const includeDirs = ((_a = rootConfig.elements) == null ? void 0 : _a.include) || [];
68
- includeDirs.forEach((dirPath) => {
69
- const elementsDir = path2.resolve(rootDir, dirPath);
70
- if (!elementsDir.startsWith(rootDir)) {
71
- throw new Error(
72
- `the elements dir (${dirPath}) should be relative to the project's root dir (${rootDir})`
73
- );
74
- }
75
- elementsDirs.push(elementsDir);
76
- });
77
- const excludePatterns = ((_b = rootConfig.elements) == null ? void 0 : _b.exclude) || [];
78
- const excludeElement = (moduleId) => {
79
- return excludePatterns.some((pattern) => Boolean(moduleId.match(pattern)));
80
- };
81
- let elementMap;
82
- async function updateElementMap() {
83
- elementMap = {};
84
- await Promise.all(
85
- elementsDirs.map(async (dirPath) => {
86
- const dirExists = await isDirectory(dirPath);
87
- if (!dirExists) {
88
- return;
89
- }
90
- const files = await glob("**/*", { cwd: dirPath });
91
- files.forEach((file) => {
92
- const parts = path2.parse(file);
93
- if (isJsFile(parts.base)) {
94
- const fullPath = path2.join(dirPath, file);
95
- const moduleId = fullPath.slice(rootDir.length);
96
- if (!excludeElement(moduleId)) {
97
- elementMap[parts.name] = moduleId;
98
- }
99
- }
100
- });
101
- })
102
- );
103
- }
104
- async function getElementImport(tagname) {
105
- if (!elementMap) {
106
- await updateElementMap();
107
- }
108
- if (tagname in elementMap) {
109
- return elementMap[tagname];
110
- }
111
- return null;
112
- }
113
- function isCustomElement(id) {
114
- if (!isJsFile(id)) {
115
- return false;
116
- }
117
- return elementsDirs.some((elementsDir) => {
118
- return id.startsWith(elementsDir);
119
- });
120
- }
121
- return {
122
- name: "vite-plugin-root",
123
- resolveId(id) {
124
- if (id === elementsVirtualId) {
125
- return resolvedElementsVirtualId;
126
- }
127
- return null;
128
- },
129
- async load(id) {
130
- if (id === resolvedElementsVirtualId) {
131
- await updateElementMap();
132
- return `export const elementsMap = ${JSON.stringify(elementMap)}`;
133
- }
134
- return null;
135
- },
136
- async transform(src, id) {
137
- if (isCustomElement(id)) {
138
- const idParts = path2.parse(id);
139
- const deps = /* @__PURE__ */ new Set();
140
- const tagnames = [
141
- ...src.matchAll(JSX_ELEMENTS_REGEX),
142
- ...src.matchAll(HTML_ELEMENTS_REGEX)
143
- ];
144
- for (const match of tagnames) {
145
- const tagname = match[1];
146
- if (!tagname.includes("-")) {
147
- continue;
148
- }
149
- if (tagname === idParts.name) {
150
- continue;
151
- }
152
- deps.add(tagname);
153
- }
154
- const importUrls = [];
155
- await Promise.all(
156
- Array.from(deps).map(async (tagname) => {
157
- const importUrl = await getElementImport(tagname);
158
- if (importUrl) {
159
- importUrls.push(importUrl);
160
- }
161
- })
162
- );
163
- if (importUrls.length > 0) {
164
- const importLines = importUrls.map((url) => `import '${url}';`).join("\n");
165
- const code = `${src}
166
-
167
- // autogenerated by root:
168
- ${importLines}
169
- `;
170
- return { code };
171
- }
172
- return null;
173
- }
174
- return null;
175
- }
176
- };
71
+ async function directoryContains(dirpath, subpath) {
72
+ const outer = await fs.realpath(dirpath);
73
+ const inner = await fs.realpath(subpath);
74
+ const rel = path.relative(outer, inner);
75
+ return !rel.startsWith("..");
177
76
  }
178
77
 
179
78
  // src/render/asset-map/dev-asset-map.ts
180
- import path3 from "node:path";
181
79
  var DevServerAssetMap = class {
182
- constructor(moduleGraph) {
80
+ constructor(rootConfig, moduleGraph) {
81
+ this.rootConfig = rootConfig;
183
82
  this.moduleGraph = moduleGraph;
184
83
  }
185
- async get(moduleId) {
186
- const viteModule = await this.moduleGraph.getModuleByUrl(moduleId);
187
- if (!viteModule || !viteModule.id) {
84
+ async get(src) {
85
+ const file = path2.resolve(this.rootConfig.rootDir, src);
86
+ const viteModules = this.moduleGraph.getModulesByFile(file);
87
+ if (viteModules && viteModules.size > 0) {
88
+ const [viteModule] = viteModules;
89
+ return new DevServerAsset(src, {
90
+ assetMap: this,
91
+ viteModule
92
+ });
93
+ }
94
+ if (file.startsWith(this.rootConfig.rootDir)) {
95
+ const assetUrl = file.slice(this.rootConfig.rootDir.length);
188
96
  return {
189
- moduleId,
190
- assetUrl: moduleId,
97
+ src,
98
+ assetUrl,
191
99
  getCssDeps: async () => [],
192
- getJsDeps: async () => [moduleId]
100
+ getJsDeps: async () => [assetUrl]
193
101
  };
194
102
  }
195
- return new DevServerAsset(this, viteModule);
103
+ const workspaceRoot = searchForWorkspaceRoot(this.rootConfig.rootDir);
104
+ if (await directoryContains(workspaceRoot, file)) {
105
+ const assetUrl = `/@fs/${file}`;
106
+ return {
107
+ src,
108
+ assetUrl,
109
+ getCssDeps: async () => [],
110
+ getJsDeps: async () => [assetUrl]
111
+ };
112
+ }
113
+ console.log(`could not find asset in asset map: ${src}`);
114
+ return null;
115
+ }
116
+ filePathToSrc(file) {
117
+ return path2.relative(this.rootConfig.rootDir, file);
196
118
  }
197
119
  };
198
120
  var DevServerAsset = class {
199
- constructor(assetMap, viteModule) {
200
- this.assetMap = assetMap;
201
- this.viteModule = viteModule;
121
+ constructor(src, options) {
122
+ this.src = src;
123
+ this.assetMap = options.assetMap;
124
+ this.viteModule = options.viteModule;
202
125
  this.moduleId = this.viteModule.id;
203
126
  this.assetUrl = this.viteModule.url;
204
127
  }
@@ -228,13 +151,17 @@ var DevServerAsset = class {
228
151
  return;
229
152
  }
230
153
  visited.add(asset.moduleId);
231
- const parts = path3.parse(asset.assetUrl);
154
+ const parts = path2.parse(asset.assetUrl);
232
155
  if ([".js", ".jsx", ".ts", ".tsx"].includes(parts.ext) && asset.moduleId.includes("/elements/")) {
233
156
  urls.add(asset.assetUrl);
234
157
  }
235
158
  asset.getImportedModules().forEach((viteModule) => {
236
- if (viteModule.id) {
237
- const importedAsset = new DevServerAsset(this.assetMap, viteModule);
159
+ if (viteModule.file) {
160
+ const src = this.assetMap.filePathToSrc(viteModule.file);
161
+ const importedAsset = new DevServerAsset(src, {
162
+ assetMap: this.assetMap,
163
+ viteModule
164
+ });
238
165
  this.collectJs(importedAsset, urls, visited);
239
166
  }
240
167
  });
@@ -250,15 +177,19 @@ var DevServerAsset = class {
250
177
  return;
251
178
  }
252
179
  visited.add(asset.assetUrl);
253
- if (asset.moduleId.endsWith(".scss")) {
254
- const parts = path3.parse(asset.assetUrl);
180
+ if (asset.src.endsWith(".scss")) {
181
+ const parts = path2.parse(asset.src);
255
182
  if (!parts.name.startsWith("_")) {
256
183
  urls.add(asset.assetUrl);
257
184
  }
258
185
  }
259
186
  asset.getImportedModules().forEach((viteModule) => {
260
- if (viteModule.id) {
261
- const importedAsset = new DevServerAsset(this.assetMap, viteModule);
187
+ if (viteModule.file) {
188
+ const src = this.assetMap.filePathToSrc(viteModule.file);
189
+ const importedAsset = new DevServerAsset(src, {
190
+ assetMap: this.assetMap,
191
+ viteModule
192
+ });
262
193
  this.collectCss(importedAsset, urls, visited);
263
194
  }
264
195
  });
@@ -278,94 +209,211 @@ async function loadRootConfig(rootDir) {
278
209
  const configBundle = await bundleRequire({
279
210
  filepath: configPath
280
211
  });
281
- return configBundle.mod.default || {};
212
+ return Object.assign({}, configBundle.mod.default || {}, { rootDir });
282
213
  }
283
- return {};
214
+ return { rootDir };
284
215
  }
285
216
 
286
- // src/render/html-minify.ts
287
- import { minify } from "html-minifier-terser";
288
- async function htmlMinify(html) {
289
- try {
290
- const min = await minify(html, {
291
- collapseWhitespace: true,
292
- removeComments: true,
293
- preserveLineBreaks: true
217
+ // src/cli/commands/dev.ts
218
+ import glob3 from "tiny-glob";
219
+ import { dim } from "kleur/colors";
220
+
221
+ // src/core/middleware.ts
222
+ function rootProjectMiddleware(options) {
223
+ return (req, _, next) => {
224
+ req.rootConfig = Object.assign({}, options.rootConfig, {
225
+ rootDir: options.rootDir
294
226
  });
295
- return min.trimStart();
296
- } catch (e) {
297
- console.error("failed to minify html:", e);
298
- return html;
227
+ next();
228
+ };
229
+ }
230
+
231
+ // src/cli/ports.ts
232
+ import { createServer } from "node:net";
233
+ function isPortOpen(port) {
234
+ return new Promise((resolve, reject) => {
235
+ const server = createServer();
236
+ server.on("error", (err) => {
237
+ if (err.code === "EADDRINUSE") {
238
+ resolve(false);
239
+ return;
240
+ }
241
+ reject(err);
242
+ });
243
+ server.on("close", () => {
244
+ resolve(true);
245
+ });
246
+ server.listen(port, () => {
247
+ server.close((err) => {
248
+ if (err) {
249
+ console.log(`error closing server: ${err}`);
250
+ reject(err);
251
+ }
252
+ });
253
+ });
254
+ });
255
+ }
256
+ async function findOpenPort(min, max) {
257
+ let port = min;
258
+ while (port <= max) {
259
+ const isOpen = await isPortOpen(port);
260
+ if (isOpen) {
261
+ return port;
262
+ }
263
+ port += 1;
299
264
  }
265
+ throw new Error(`no ports open between ${min} and ${max}`);
300
266
  }
301
267
 
302
- // src/cli/commands/dev.ts
268
+ // src/core/element-graph.ts
269
+ import fs2 from "node:fs";
270
+ import path3 from "node:path";
271
+ import { searchForWorkspaceRoot as searchForWorkspaceRoot2 } from "vite";
303
272
  import glob2 from "tiny-glob";
304
- import { dim } from "kleur/colors";
305
- var __dirname = path4.dirname(fileURLToPath(import.meta.url));
306
- async function createServer(options) {
307
- const rootDir = (options == null ? void 0 : options.rootDir) || process.cwd();
308
- const app = express();
309
- app.use(express.static("public"));
310
- const middlewares = await getMiddlewares({ rootDir });
311
- middlewares.forEach((middleware) => app.use(middleware));
312
- const port = parseInt(process.env.PORT || "4007");
313
- console.log();
314
- console.log(`${dim("\u2503")} project: ${rootDir}`);
315
- console.log(`${dim("\u2503")} server: http://localhost:${port}`);
316
- console.log();
317
- app.listen(port);
318
- }
319
- async function getMiddlewares(options) {
320
- var _a, _b;
321
- const rootDir = path4.resolve((options == null ? void 0 : options.rootDir) || process.cwd());
322
- const rootConfig = await loadRootConfig(rootDir);
323
- const viteConfig = rootConfig.vite || {};
324
- const renderModulePath = path4.resolve(__dirname, "./render.js");
325
- const pages = [];
326
- if (await isDirectory(path4.join(rootDir, "routes"))) {
327
- const pageFiles = await glob2("routes/**/*", { cwd: rootDir });
328
- pageFiles.forEach((file) => {
329
- const parts = path4.parse(file);
330
- if (!parts.name.startsWith("_") && isJsFile(parts.base)) {
331
- pages.push(file);
273
+ var ElementGraph = class {
274
+ constructor(sourceFiles) {
275
+ this.sourceFiles = {};
276
+ this.deps = {};
277
+ this.sourceFiles = sourceFiles;
278
+ }
279
+ getDeps(tagName, visited) {
280
+ visited ??= /* @__PURE__ */ new Set();
281
+ if (visited.has(tagName)) {
282
+ return [];
283
+ }
284
+ visited.add(tagName);
285
+ this.deps[tagName] ??= this.parseDepsFromSource(tagName);
286
+ const deps = new Set(this.deps[tagName]);
287
+ for (const depTagName of this.deps[tagName]) {
288
+ for (const childDep of this.getDeps(depTagName, visited)) {
289
+ deps.add(childDep);
332
290
  }
333
- });
291
+ }
292
+ return Array.from(deps);
334
293
  }
335
- const elementsDirs = [path4.join(rootDir, "elements")];
294
+ parseDepsFromSource(tagName) {
295
+ const srcFile = this.sourceFiles[tagName];
296
+ if (!srcFile) {
297
+ throw new Error(`could not find file path for tagName <${tagName}>`);
298
+ }
299
+ const src = fs2.readFileSync(srcFile.filePath, "utf-8");
300
+ const tagNames = parseTagNames(src);
301
+ const deps = /* @__PURE__ */ new Set();
302
+ for (const depTagName of tagNames) {
303
+ if (depTagName !== tagName && depTagName in this.sourceFiles) {
304
+ deps.add(depTagName);
305
+ }
306
+ }
307
+ return Array.from(deps);
308
+ }
309
+ };
310
+ async function getElements(rootConfig) {
311
+ var _a, _b;
312
+ const rootDir = rootConfig.rootDir;
313
+ const workspaceRoot = searchForWorkspaceRoot2(rootDir);
314
+ const elementsDirs = [path3.join(rootDir, "elements")];
336
315
  const elementsInclude = ((_a = rootConfig.elements) == null ? void 0 : _a.include) || [];
337
316
  const excludePatterns = ((_b = rootConfig.elements) == null ? void 0 : _b.exclude) || [];
338
317
  const excludeElement = (moduleId) => {
339
318
  return excludePatterns.some((pattern) => Boolean(moduleId.match(pattern)));
340
319
  };
341
320
  for (const dirPath of elementsInclude) {
342
- const elementsDir = path4.resolve(rootDir, dirPath);
343
- if (!elementsDir.startsWith(rootDir)) {
321
+ const elementsDir = path3.resolve(rootDir, dirPath);
322
+ if (!directoryContains(rootDir, elementsDir)) {
344
323
  throw new Error(
345
- `the elements dir (${dirPath}) should be relative to the project's root dir (${rootDir})`
324
+ `the elements dir (${dirPath}) should be within the project's workspace (${workspaceRoot})`
346
325
  );
347
326
  }
348
327
  elementsDirs.push(elementsDir);
349
328
  }
350
- const elements = [];
329
+ const elementFilePaths = {};
351
330
  for (const dirPath of elementsDirs) {
352
331
  if (await isDirectory(dirPath)) {
353
- const elementFiles = await glob2("**/*", { cwd: dirPath });
354
- elementFiles.forEach((file) => {
355
- const parts = path4.parse(file);
356
- if (isJsFile(parts.base)) {
357
- const fullPath = path4.join(dirPath, file);
358
- const moduleId = fullPath.slice(rootDir.length);
359
- if (!excludeElement(moduleId)) {
360
- elements.push(moduleId.slice(1));
332
+ const files = await glob2("**/*", { cwd: dirPath });
333
+ files.forEach((file) => {
334
+ const parts = path3.parse(file);
335
+ if (isJsFile(parts.base) && isValidTagName(parts.name)) {
336
+ const tagName = parts.name;
337
+ const filePath = path3.join(dirPath, file);
338
+ const relPath = path3.relative(rootDir, filePath);
339
+ if (!excludeElement(relPath)) {
340
+ elementFilePaths[tagName] = { filePath, relPath };
361
341
  }
362
342
  }
363
343
  });
364
344
  }
365
345
  }
346
+ const graph = new ElementGraph(elementFilePaths);
347
+ return graph;
348
+ }
349
+
350
+ // src/cli/commands/dev.ts
351
+ var __dirname = path4.dirname(fileURLToPath(import.meta.url));
352
+ async function dev(rootProjectDir) {
353
+ process.env.NODE_ENV = "development";
354
+ const rootDir = path4.resolve(rootProjectDir || process.cwd());
355
+ const defaultPort = parseInt(process.env.PORT || "4007");
356
+ const port = await findOpenPort(defaultPort, defaultPort + 10);
357
+ console.log();
358
+ console.log(`${dim("\u2503")} project: ${rootDir}`);
359
+ console.log(`${dim("\u2503")} server: http://localhost:${port}`);
360
+ console.log(`${dim("\u2503")} mode: development`);
361
+ console.log();
362
+ const server = await createServer2({ rootDir, port });
363
+ server.listen(port);
364
+ }
365
+ async function createServer2(options) {
366
+ const rootDir = path4.resolve((options == null ? void 0 : options.rootDir) || process.cwd());
367
+ const rootConfig = await loadRootConfig(rootDir);
368
+ const port = options == null ? void 0 : options.port;
369
+ const server = express();
370
+ server.disable("x-powered-by");
371
+ server.use(rootProjectMiddleware({ rootDir, rootConfig }));
372
+ server.use(await viteServerMiddleware({ rootDir, rootConfig, port }));
373
+ const plugins = rootConfig.plugins || [];
374
+ await configureServerPlugins(
375
+ server,
376
+ async () => {
377
+ var _a;
378
+ const userMiddlewares = ((_a = rootConfig.server) == null ? void 0 : _a.middlewares) || [];
379
+ for (const middleware of userMiddlewares) {
380
+ server.use(middleware);
381
+ }
382
+ server.use(rootDevServerMiddleware());
383
+ server.use(rootDevServer404Middleware());
384
+ server.use(rootDevServer500Middleware());
385
+ },
386
+ plugins,
387
+ { type: "dev", rootConfig }
388
+ );
389
+ return server;
390
+ }
391
+ async function viteServerMiddleware(options) {
392
+ var _a, _b;
393
+ const rootDir = options.rootDir;
394
+ const rootConfig = options.rootConfig;
395
+ const viteConfig = rootConfig.vite || {};
396
+ let hmrOptions = (_a = viteConfig.server) == null ? void 0 : _a.hmr;
397
+ if (typeof hmrOptions === "undefined" && options.port) {
398
+ hmrOptions = { port: options.port + 10 };
399
+ }
400
+ const routeFiles = [];
401
+ if (await isDirectory(path4.join(rootDir, "routes"))) {
402
+ const pageFiles = await glob3("routes/**/*", { cwd: rootDir });
403
+ pageFiles.forEach((file) => {
404
+ const parts = path4.parse(file);
405
+ if (!parts.name.startsWith("_") && isJsFile(parts.base)) {
406
+ routeFiles.push(file);
407
+ }
408
+ });
409
+ }
410
+ const elementGraph = await getElements(rootConfig);
411
+ const elements = Object.values(elementGraph.sourceFiles).map((sourceFile) => {
412
+ return sourceFile.relPath;
413
+ });
366
414
  const bundleScripts = [];
367
415
  if (await isDirectory(path4.join(rootDir, "bundles"))) {
368
- const bundleFiles = await glob2(path4.join(rootDir, "bundles/*"));
416
+ const bundleFiles = await glob3("bundles/*", { cwd: rootDir });
369
417
  bundleFiles.forEach((file) => {
370
418
  const parts = path4.parse(file);
371
419
  if (isJsFile(parts.base)) {
@@ -377,122 +425,161 @@ async function getMiddlewares(options) {
377
425
  ...viteConfig,
378
426
  mode: "development",
379
427
  root: rootDir,
380
- server: { middlewareMode: true },
428
+ publicDir: path4.join(rootDir, "public"),
429
+ server: {
430
+ ...viteConfig.server || {},
431
+ middlewareMode: true,
432
+ hmr: hmrOptions
433
+ },
381
434
  appType: "custom",
382
435
  optimizeDeps: {
383
- include: [...pages, ...elements, ...bundleScripts]
436
+ ...viteConfig.optimizeDeps || {},
437
+ include: [
438
+ ...elements,
439
+ ...bundleScripts,
440
+ ...((_b = viteConfig.optimizeDeps) == null ? void 0 : _b.include) || []
441
+ ]
384
442
  },
385
443
  ssr: {
444
+ ...viteConfig.ssr || {},
386
445
  noExternal: ["@blinkk/root"]
387
446
  },
388
447
  esbuild: {
448
+ ...viteConfig.esbuild || {},
389
449
  jsx: "automatic",
390
450
  jsxImportSource: "preact"
391
451
  },
392
- plugins: [...viteConfig.plugins || [], pluginRoot({ rootDir, rootConfig })]
452
+ plugins: [
453
+ ...viteConfig.plugins || [],
454
+ ...getVitePlugins(rootConfig.plugins || [])
455
+ ]
393
456
  });
394
- const rootMiddleware = async (req, res, next) => {
395
- const url = req.originalUrl;
396
- let renderer = null;
457
+ return async (req, res, next) => {
397
458
  try {
459
+ req.viteServer = viteServer;
460
+ const renderModulePath = path4.resolve(__dirname, "./render.js");
398
461
  const render = await viteServer.ssrLoadModule(renderModulePath);
399
- renderer = new render.Renderer(rootConfig);
400
- const assetMap = new DevServerAssetMap(viteServer.moduleGraph);
401
- const data = await renderer.render(url, {
402
- assetMap
403
- });
404
- if (data.notFound || !data.html) {
405
- next();
406
- return;
407
- }
408
- let html = await viteServer.transformIndexHtml(url, data.html || "");
409
- if (rootConfig.minifyHtml !== false) {
410
- html = await htmlMinify(html);
411
- }
412
- res.status(200).set({ "Content-Type": "text/html" }).end(html);
462
+ const assetMap = new DevServerAssetMap(
463
+ rootConfig,
464
+ viteServer.moduleGraph
465
+ );
466
+ req.renderer = new render.Renderer(rootConfig, { assetMap, elementGraph });
467
+ viteServer.middlewares(req, res, next);
413
468
  } catch (e) {
414
- viteServer.ssrFixStacktrace(e);
415
- console.error(e);
416
- try {
417
- if (renderer) {
418
- const { html } = await renderer.renderError(e);
419
- res.status(500).set({ "Content-Type": "text/html" }).end(html);
420
- } else {
421
- next(e);
422
- }
423
- } catch (e2) {
424
- console.error("failed to render custom error");
425
- console.error(e2);
426
- next(e);
427
- }
469
+ next(e);
428
470
  }
429
471
  };
430
- const notFoundMiddleware = async (req, res) => {
431
- const url = req.originalUrl;
432
- const ext = path4.extname(url);
433
- if (!ext) {
434
- const render = await viteServer.ssrLoadModule(renderModulePath);
435
- const renderer = new render.Renderer(rootConfig);
436
- const data = await renderer.renderDevNotFound();
437
- const html = data.html || "";
438
- res.status(404).set({ "Content-Type": "text/html" }).end(html);
439
- return;
472
+ }
473
+ function rootDevServerMiddleware() {
474
+ return async (req, res, next) => {
475
+ const renderer = req.renderer;
476
+ const viteServer = req.viteServer;
477
+ try {
478
+ await renderer.handle(req, res, next);
479
+ } catch (err) {
480
+ viteServer.ssrFixStacktrace(err);
481
+ next(err);
482
+ }
483
+ };
484
+ }
485
+ function rootDevServer404Middleware() {
486
+ return async (req, res) => {
487
+ console.error(`\u2753 404 ${req.originalUrl}`);
488
+ if (req.renderer) {
489
+ const url = req.path;
490
+ const ext = path4.extname(url);
491
+ if (!ext) {
492
+ const renderer = req.renderer;
493
+ const data = await renderer.renderDevServer404(req);
494
+ const html = data.html || "";
495
+ res.status(404).set({ "Content-Type": "text/html" }).end(html);
496
+ return;
497
+ }
440
498
  }
441
499
  res.status(404).set({ "Content-Type": "text/plain" }).end("404");
442
500
  };
443
- return [viteServer.middlewares, rootMiddleware, notFoundMiddleware];
444
501
  }
445
- async function dev(rootProjectDir) {
446
- process.env.NODE_ENV = "development";
447
- const rootDir = path4.resolve(rootProjectDir || process.cwd());
448
- try {
449
- await createServer({ rootDir });
450
- } catch (err) {
451
- console.error("an error occurred");
452
- }
502
+ function rootDevServer500Middleware() {
503
+ return async (err, req, res, next) => {
504
+ console.error(`\u2757 500 ${req.originalUrl}`);
505
+ console.error(String(err.stack || err));
506
+ if (req.renderer) {
507
+ const url = req.path;
508
+ const ext = path4.extname(url);
509
+ if (!ext) {
510
+ const renderer = req.renderer;
511
+ const data = await renderer.renderDevServer500(req, err);
512
+ const html = data.html || "";
513
+ res.status(500).set({ "Content-Type": "text/html" }).end(html);
514
+ return;
515
+ }
516
+ }
517
+ next(err);
518
+ };
453
519
  }
454
520
 
455
521
  // src/cli/commands/build.ts
456
- import path5 from "node:path";
522
+ import path6 from "node:path";
457
523
  import { fileURLToPath as fileURLToPath2 } from "node:url";
458
524
  import fsExtra2 from "fs-extra";
459
- import glob3 from "tiny-glob";
525
+ import glob4 from "tiny-glob";
460
526
  import { build as viteBuild } from "vite";
461
527
 
462
528
  // src/render/asset-map/build-asset-map.ts
529
+ import fs3 from "node:fs";
530
+ import path5 from "node:path";
463
531
  var BuildAssetMap = class {
464
- constructor() {
465
- this.moduleIdToAsset = /* @__PURE__ */ new Map();
532
+ constructor(rootConfig) {
533
+ this.rootConfig = rootConfig;
534
+ this.srcToAsset = /* @__PURE__ */ new Map();
466
535
  }
467
- async get(moduleId) {
468
- return this.moduleIdToAsset.get(moduleId) || null;
536
+ async get(src) {
537
+ const asset = this.srcToAsset.get(src);
538
+ if (asset) {
539
+ return asset;
540
+ }
541
+ const realSrc = realPathRelativeTo(this.rootConfig.rootDir, src);
542
+ if (realSrc !== src) {
543
+ const asset2 = this.srcToAsset.get(realSrc);
544
+ if (asset2) {
545
+ return asset2;
546
+ }
547
+ }
548
+ console.log(`could not find build asset: ${src}`);
549
+ return null;
469
550
  }
470
551
  add(asset) {
471
- this.moduleIdToAsset.set(asset.moduleId, asset);
552
+ this.srcToAsset.set(asset.src, asset);
472
553
  }
473
554
  toJson() {
474
555
  const result = {};
475
- for (const moduleId of this.moduleIdToAsset.keys()) {
476
- result[moduleId] = this.moduleIdToAsset.get(moduleId).toJson();
556
+ for (const src of this.srcToAsset.keys()) {
557
+ result[src] = this.srcToAsset.get(src).toJson();
477
558
  }
478
559
  return result;
479
560
  }
480
- static fromViteManifest(viteManifest, elementMap) {
481
- const assetMap = new BuildAssetMap();
482
- const elementModuleIds = /* @__PURE__ */ new Set();
483
- Object.values(elementMap).forEach(
484
- (moduleId) => elementModuleIds.add(moduleId)
485
- );
486
- Object.keys(viteManifest).forEach((manifestKey) => {
487
- const moduleId = `/${manifestKey}`;
488
- const manifestChunk = viteManifest[manifestKey];
489
- const isElement = elementModuleIds.has(moduleId);
561
+ static fromViteManifest(rootConfig, clientManifest, elementsGraph) {
562
+ const assetMap = new BuildAssetMap(rootConfig);
563
+ const elementFiles = /* @__PURE__ */ new Set();
564
+ Object.values(elementsGraph.sourceFiles).forEach((elementSource) => {
565
+ elementFiles.add(elementSource.relPath);
566
+ const realSrc = realPathRelativeTo(
567
+ rootConfig.rootDir,
568
+ elementSource.relPath
569
+ );
570
+ if (realSrc !== elementSource.filePath) {
571
+ elementFiles.add(realSrc);
572
+ }
573
+ });
574
+ Object.keys(clientManifest).forEach((manifestKey) => {
575
+ const src = manifestKey;
576
+ const manifestChunk = clientManifest[manifestKey];
577
+ const isElement = elementFiles.has(src);
578
+ const assetUrl = src.startsWith("routes/") ? "" : `/${manifestChunk.file}`;
490
579
  const assetData = {
491
- moduleId,
492
- assetUrl: `/${manifestChunk.file}`,
493
- importedModules: (manifestChunk.imports || []).map(
494
- (relPath) => `/${relPath}`
495
- ),
580
+ src,
581
+ assetUrl,
582
+ importedModules: manifestChunk.imports || [],
496
583
  importedCss: (manifestChunk.css || []).map((relPath) => `/${relPath}`),
497
584
  isElement
498
585
  };
@@ -500,8 +587,8 @@ var BuildAssetMap = class {
500
587
  });
501
588
  return assetMap;
502
589
  }
503
- static fromRootManifest(rootManifest) {
504
- const assetMap = new BuildAssetMap();
590
+ static fromRootManifest(rootConfig, rootManifest) {
591
+ const assetMap = new BuildAssetMap(rootConfig);
505
592
  Object.keys(rootManifest).forEach((moduleId) => {
506
593
  const assetData = rootManifest[moduleId];
507
594
  assetMap.add(new BuildAsset(assetMap, assetData));
@@ -509,10 +596,18 @@ var BuildAssetMap = class {
509
596
  return assetMap;
510
597
  }
511
598
  };
599
+ function realPathRelativeTo(rootDir, src) {
600
+ const fullPath = path5.resolve(rootDir, src);
601
+ if (!fs3.existsSync(fullPath)) {
602
+ return src;
603
+ }
604
+ const realpath = fs3.realpathSync(path5.resolve(rootDir, src));
605
+ return path5.relative(rootDir, realpath);
606
+ }
512
607
  var BuildAsset = class {
513
608
  constructor(assetMap, assetData) {
514
609
  this.assetMap = assetMap;
515
- this.moduleId = assetData.moduleId;
610
+ this.src = assetData.src;
516
611
  this.assetUrl = assetData.assetUrl;
517
612
  this.importedModules = assetData.importedModules;
518
613
  this.importedCss = assetData.importedCss;
@@ -534,19 +629,19 @@ var BuildAsset = class {
534
629
  if (!asset) {
535
630
  return;
536
631
  }
537
- if (!asset.moduleId) {
632
+ if (!asset.src) {
538
633
  return;
539
634
  }
540
- if (visited.has(asset.moduleId)) {
635
+ if (visited.has(asset.src)) {
541
636
  return;
542
637
  }
543
- visited.add(asset.moduleId);
638
+ visited.add(asset.src);
544
639
  if (asset.isElement) {
545
640
  urls.add(asset.assetUrl);
546
641
  }
547
642
  await Promise.all(
548
- asset.importedModules.map(async (moduleId) => {
549
- const importedAsset = await this.assetMap.get(moduleId);
643
+ asset.importedModules.map(async (src) => {
644
+ const importedAsset = await this.assetMap.get(src);
550
645
  this.collectJs(importedAsset, urls, visited);
551
646
  })
552
647
  );
@@ -555,13 +650,13 @@ var BuildAsset = class {
555
650
  if (!asset) {
556
651
  return;
557
652
  }
558
- if (!asset.assetUrl) {
653
+ if (!asset.src) {
559
654
  return;
560
655
  }
561
- if (visited.has(asset.moduleId)) {
656
+ if (visited.has(asset.src)) {
562
657
  return;
563
658
  }
564
- visited.add(asset.moduleId);
659
+ visited.add(asset.src);
565
660
  if (asset.importedCss) {
566
661
  asset.importedCss.forEach((cssUrl) => urls.add(cssUrl));
567
662
  }
@@ -574,7 +669,7 @@ var BuildAsset = class {
574
669
  }
575
670
  toJson() {
576
671
  return {
577
- moduleId: this.moduleId,
672
+ src: this.src,
578
673
  assetUrl: this.assetUrl,
579
674
  importedModules: this.importedModules,
580
675
  importedCss: this.importedCss,
@@ -584,213 +679,369 @@ var BuildAsset = class {
584
679
  };
585
680
 
586
681
  // src/cli/commands/build.ts
587
- import { dim as dim2 } from "kleur/colors";
588
- var __dirname2 = path5.dirname(fileURLToPath2(import.meta.url));
682
+ import { dim as dim2, cyan } from "kleur/colors";
683
+ var __dirname2 = path6.dirname(fileURLToPath2(import.meta.url));
589
684
  async function build(rootProjectDir, options) {
590
- var _a, _b;
591
- const rootDir = path5.resolve(rootProjectDir || process.cwd());
685
+ var _a, _b, _c, _d, _e, _f, _g, _h;
686
+ const mode = (options == null ? void 0 : options.mode) || "production";
687
+ process.env.NODE_ENV = mode;
688
+ const rootDir = path6.resolve(rootProjectDir || process.cwd());
592
689
  const rootConfig = await loadRootConfig(rootDir);
593
- const distDir = path5.join(rootDir, "dist");
690
+ const distDir = path6.join(rootDir, "dist");
594
691
  const ssrOnly = (options == null ? void 0 : options.ssrOnly) || false;
595
- const mode = (options == null ? void 0 : options.mode) || "production";
596
692
  console.log();
597
693
  console.log(`${dim2("\u2503")} project: ${rootDir}`);
598
694
  console.log(`${dim2("\u2503")} output: ${distDir}/html`);
695
+ console.log(`${dim2("\u2503")} mode: ${mode}`);
599
696
  console.log();
600
697
  await rmDir(distDir);
601
698
  await makeDir(distDir);
602
- const pages = [];
603
- if (await isDirectory(path5.join(rootDir, "routes"))) {
604
- const pageFiles = await glob3(path5.join(rootDir, "routes/**/*"));
699
+ const routeFiles = [];
700
+ if (await isDirectory(path6.join(rootDir, "routes"))) {
701
+ const pageFiles = await glob4("routes/**/*", { cwd: rootDir });
605
702
  pageFiles.forEach((file) => {
606
- const parts = path5.parse(file);
703
+ const parts = path6.parse(file);
607
704
  if (!parts.name.startsWith("_") && isJsFile(parts.base)) {
608
- pages.push(file);
705
+ routeFiles.push(path6.resolve(rootDir, file));
609
706
  }
610
707
  });
611
708
  }
612
- const elementsDirs = [path5.join(rootDir, "elements")];
613
- const elementsInclude = ((_a = rootConfig.elements) == null ? void 0 : _a.include) || [];
614
- const excludePatterns = ((_b = rootConfig.elements) == null ? void 0 : _b.exclude) || [];
615
- const excludeElement = (moduleId) => {
616
- return excludePatterns.some((pattern) => Boolean(moduleId.match(pattern)));
617
- };
618
- for (const dirPath of elementsInclude) {
619
- const elementsDir = path5.resolve(rootDir, dirPath);
620
- if (!elementsDir.startsWith(rootDir)) {
621
- throw new Error(
622
- `the elements dir (${dirPath}) should be relative to the project's root dir (${rootDir})`
623
- );
624
- }
625
- elementsDirs.push(elementsDir);
626
- }
627
- const elements = [];
628
- const elementMap = {};
629
- for (const dirPath of elementsDirs) {
630
- if (await isDirectory(dirPath)) {
631
- const elementFiles = await glob3("**/*", { cwd: dirPath });
632
- elementFiles.forEach((file) => {
633
- const parts = path5.parse(file);
634
- if (isJsFile(parts.base)) {
635
- const fullPath = path5.join(dirPath, file);
636
- const moduleId = fullPath.slice(rootDir.length);
637
- if (!excludeElement(moduleId)) {
638
- elements.push(fullPath);
639
- elementMap[parts.name] = moduleId;
640
- }
641
- }
642
- });
643
- }
644
- }
709
+ const elementGraph = await getElements(rootConfig);
710
+ const elements = Object.values(elementGraph.sourceFiles).map((sourceFile) => {
711
+ return sourceFile.filePath;
712
+ });
645
713
  const bundleScripts = [];
646
- if (await isDirectory(path5.join(rootDir, "bundles"))) {
647
- const bundleFiles = await glob3(path5.join(rootDir, "bundles/*"));
714
+ if (await isDirectory(path6.join(rootDir, "bundles"))) {
715
+ const bundleFiles = await glob4("bundles/*", { cwd: rootDir });
648
716
  bundleFiles.forEach((file) => {
649
- const parts = path5.parse(file);
717
+ const parts = path6.parse(file);
650
718
  if (isJsFile(parts.base)) {
651
- bundleScripts.push(file);
719
+ bundleScripts.push(path6.resolve(rootDir, file));
652
720
  }
653
721
  });
654
722
  }
723
+ const rootPlugins = rootConfig.plugins || [];
655
724
  const viteConfig = rootConfig.vite || {};
725
+ const vitePlugins = [
726
+ ...viteConfig.plugins || [],
727
+ ...getVitePlugins(rootPlugins)
728
+ ];
656
729
  const baseConfig = {
657
730
  ...viteConfig,
658
731
  root: rootDir,
732
+ mode,
659
733
  esbuild: {
734
+ ...viteConfig.esbuild,
660
735
  jsx: "automatic",
661
736
  jsxImportSource: "preact",
662
737
  treeShaking: true
663
738
  },
664
- plugins: [...viteConfig.plugins || [], pluginRoot({ rootDir, rootConfig })]
739
+ plugins: vitePlugins
740
+ };
741
+ const ssrInput = {
742
+ render: path6.resolve(__dirname2, "./render.js")
665
743
  };
744
+ rootPlugins.forEach((plugin) => {
745
+ if (plugin.ssrInput) {
746
+ Object.assign(ssrInput, plugin.ssrInput());
747
+ }
748
+ });
749
+ const noExternalConfig = (_a = viteConfig.ssr) == null ? void 0 : _a.noExternal;
750
+ const noExternal = [];
751
+ if (noExternalConfig) {
752
+ if (Array.isArray(noExternalConfig)) {
753
+ noExternal.push(...noExternalConfig);
754
+ } else {
755
+ noExternal.push(noExternalConfig);
756
+ }
757
+ }
666
758
  await viteBuild({
667
759
  ...baseConfig,
668
- mode,
669
760
  publicDir: false,
670
761
  build: {
762
+ ...viteConfig == null ? void 0 : viteConfig.build,
671
763
  rollupOptions: {
672
- input: [path5.resolve(__dirname2, "./render.js")],
764
+ ...(_b = viteConfig == null ? void 0 : viteConfig.build) == null ? void 0 : _b.rollupOptions,
765
+ input: ssrInput,
673
766
  output: {
674
767
  format: "esm",
675
- chunkFileNames: "chunks/[name].[hash].js",
768
+ chunkFileNames: "chunks/[name].[hash].min.js",
676
769
  assetFileNames: "assets/[name].[hash][extname]"
677
770
  }
678
771
  },
679
- outDir: path5.join(distDir, "server"),
772
+ outDir: path6.join(distDir, "server"),
680
773
  ssr: true,
681
774
  ssrManifest: false,
682
775
  cssCodeSplit: true,
683
776
  target: "esnext",
684
777
  minify: false,
685
- polyfillModulePreload: false,
778
+ modulePreload: { polyfill: false },
686
779
  reportCompressedSize: false
687
780
  },
688
781
  ssr: {
689
- noExternal: ["@blinkk/root"]
782
+ ...viteConfig.ssr,
783
+ target: "node",
784
+ noExternal: ["@blinkk/root", ...noExternal]
690
785
  }
691
786
  });
692
787
  await viteBuild({
693
788
  ...baseConfig,
694
- mode,
695
789
  publicDir: false,
696
790
  build: {
791
+ ...viteConfig == null ? void 0 : viteConfig.build,
697
792
  rollupOptions: {
698
- input: [...pages, ...elements, ...bundleScripts],
793
+ ...(_c = viteConfig == null ? void 0 : viteConfig.build) == null ? void 0 : _c.rollupOptions,
794
+ input: [...routeFiles],
699
795
  output: {
700
796
  format: "esm",
701
- chunkFileNames: "chunks/[name].[hash].js",
702
- assetFileNames: "assets/[name].[hash][extname]"
797
+ entryFileNames: "assets/[name].[hash].min.js",
798
+ chunkFileNames: "chunks/[name].[hash].min.js",
799
+ assetFileNames: "assets/[name].[hash][extname]",
800
+ ...(_e = (_d = viteConfig == null ? void 0 : viteConfig.build) == null ? void 0 : _d.rollupOptions) == null ? void 0 : _e.output
703
801
  }
704
802
  },
705
- outDir: path5.join(distDir, "client"),
803
+ outDir: path6.join(distDir, "routes"),
706
804
  ssr: false,
707
805
  ssrManifest: false,
708
806
  manifest: true,
709
807
  cssCodeSplit: true,
710
808
  target: "esnext",
711
809
  minify: true,
712
- polyfillModulePreload: false,
810
+ modulePreload: { polyfill: false },
713
811
  reportCompressedSize: false
714
812
  }
715
813
  });
716
- const viteManifest = await loadJson(
717
- path5.join(distDir, "client/manifest.json")
814
+ const clientInput = [...elements, ...bundleScripts];
815
+ if (clientInput.length > 0) {
816
+ await viteBuild({
817
+ ...baseConfig,
818
+ publicDir: false,
819
+ build: {
820
+ ...viteConfig == null ? void 0 : viteConfig.build,
821
+ rollupOptions: {
822
+ ...(_f = viteConfig == null ? void 0 : viteConfig.build) == null ? void 0 : _f.rollupOptions,
823
+ input: [...elements, ...bundleScripts],
824
+ output: {
825
+ format: "esm",
826
+ entryFileNames: "assets/[name].[hash].min.js",
827
+ chunkFileNames: "chunks/[name].[hash].min.js",
828
+ assetFileNames: "assets/[name].[hash][extname]",
829
+ ...(_h = (_g = viteConfig == null ? void 0 : viteConfig.build) == null ? void 0 : _g.rollupOptions) == null ? void 0 : _h.output
830
+ }
831
+ },
832
+ outDir: path6.join(distDir, "client"),
833
+ ssr: false,
834
+ ssrManifest: false,
835
+ manifest: true,
836
+ cssCodeSplit: true,
837
+ target: "esnext",
838
+ minify: true,
839
+ modulePreload: { polyfill: false },
840
+ reportCompressedSize: false
841
+ }
842
+ });
843
+ } else {
844
+ await writeFile(path6.join(distDir, "client/manifest.json"), "{}");
845
+ }
846
+ await copyGlob(
847
+ "*.css",
848
+ path6.join(distDir, "routes/assets"),
849
+ path6.join(distDir, "client/assets")
850
+ );
851
+ const routesManifest = await loadJson(
852
+ path6.join(distDir, "routes/manifest.json")
718
853
  );
719
- const assetMap = BuildAssetMap.fromViteManifest(viteManifest, elementMap);
720
- writeFile(
721
- path5.join(distDir, "client/root-manifest.json"),
722
- JSON.stringify(assetMap.toJson(), null, 2)
854
+ const clientManifest = await loadJson(
855
+ path6.join(distDir, "client/manifest.json")
723
856
  );
724
- const buildDir = path5.join(distDir, "html");
725
- const publicDir = path5.join(rootDir, "public");
726
- if (fsExtra2.existsSync(path5.join(rootDir, "public"))) {
727
- fsExtra2.copySync(publicDir, buildDir);
857
+ function collectRouteCss(asset, cssDeps, visited) {
858
+ if (!asset || !asset.file || visited.has(asset.file)) {
859
+ return;
860
+ }
861
+ visited.add(asset.file);
862
+ if (asset.css) {
863
+ asset.css.forEach((dep) => cssDeps.add(dep));
864
+ }
865
+ if (asset.imports) {
866
+ asset.imports.forEach((manifestKey) => {
867
+ collectRouteCss(routesManifest[manifestKey], cssDeps, visited);
868
+ });
869
+ }
870
+ }
871
+ Object.keys(routesManifest).forEach((manifestKey) => {
872
+ const asset = routesManifest[manifestKey];
873
+ if (asset.isEntry) {
874
+ const visited = /* @__PURE__ */ new Set();
875
+ const cssDeps = /* @__PURE__ */ new Set();
876
+ collectRouteCss(asset, cssDeps, visited);
877
+ asset.css = Array.from(cssDeps);
878
+ asset.imports = [];
879
+ clientManifest[manifestKey] = asset;
880
+ } else if (asset.file.endsWith(".css")) {
881
+ clientManifest[manifestKey] = asset;
882
+ }
883
+ });
884
+ const assetMap = BuildAssetMap.fromViteManifest(
885
+ rootConfig,
886
+ clientManifest,
887
+ elementGraph
888
+ );
889
+ const rootManifest = assetMap.toJson();
890
+ await writeFile(
891
+ path6.join(distDir, "client/root-manifest.json"),
892
+ JSON.stringify(rootManifest, null, 2)
893
+ );
894
+ const buildDir = path6.join(distDir, "html");
895
+ const publicDir = path6.join(rootDir, "public");
896
+ if (fsExtra2.existsSync(path6.join(rootDir, "public"))) {
897
+ fsExtra2.copySync(publicDir, buildDir, { dereference: true });
728
898
  } else {
729
899
  makeDir(buildDir);
730
900
  }
731
- copyDir(path5.join(distDir, "client/assets"), path5.join(buildDir, "assets"));
732
- copyDir(path5.join(distDir, "client/chunks"), path5.join(buildDir, "chunks"));
901
+ console.log("\njs/css output:");
902
+ makeDir(path6.join(buildDir, "assets"));
903
+ makeDir(path6.join(buildDir, "chunks"));
904
+ await Promise.all(
905
+ Object.keys(rootManifest).map(async (src) => {
906
+ if (isRouteFile(src)) {
907
+ return;
908
+ }
909
+ const assetData = rootManifest[src];
910
+ const assetRelPath = assetData.assetUrl.slice(1);
911
+ const assetFrom = path6.join(distDir, "client", assetRelPath);
912
+ const assetTo = path6.join(buildDir, assetRelPath);
913
+ if (!await fileExists(assetFrom)) {
914
+ console.log(`${assetFrom} does not exist`);
915
+ return;
916
+ }
917
+ await fsExtra2.copy(assetFrom, assetTo);
918
+ printFileOutput(fileSize(assetTo), "dist/html/", assetRelPath);
919
+ })
920
+ );
733
921
  if (!ssrOnly) {
734
- const render = await import(path5.join(distDir, "server/render.js"));
735
- const renderer = new render.Renderer(rootConfig);
922
+ const render = await import(path6.join(distDir, "server/render.js"));
923
+ const renderer = new render.Renderer(rootConfig, { assetMap, elementGraph });
736
924
  const sitemap = await renderer.getSitemap();
925
+ console.log("\nhtml output:");
737
926
  await Promise.all(
738
927
  Object.keys(sitemap).map(async (urlPath) => {
739
928
  const { route, params } = sitemap[urlPath];
740
929
  const data = await renderer.renderRoute(route, {
741
- assetMap,
742
930
  routeParams: params
743
931
  });
744
- let outPath = path5.join(distDir, `html${urlPath}`, "index.html");
745
- if (outPath.endsWith("404/index.html")) {
746
- outPath = outPath.replace("404/index.html", "404.html");
932
+ if (data.notFound) {
933
+ return;
934
+ }
935
+ let outFilePath = path6.join(urlPath.slice(1), "index.html");
936
+ if (outFilePath.endsWith("404/index.html")) {
937
+ outFilePath = outFilePath.replace("404/index.html", "404.html");
938
+ }
939
+ const outPath = path6.join(buildDir, outFilePath);
940
+ let html = data.html || "";
941
+ if (rootConfig.prettyHtml !== false) {
942
+ html = await htmlPretty(html, rootConfig.prettyHtmlOptions);
943
+ }
944
+ if (rootConfig.minifyHtml !== false) {
945
+ html = await htmlMinify(html, rootConfig.minifyHtmlOptions);
747
946
  }
748
- const html = await htmlMinify(data.html || "");
749
947
  await writeFile(outPath, html);
750
- const relPath = outPath.slice(path5.dirname(distDir).length + 1);
751
- console.log(`saved ${relPath}`);
948
+ printFileOutput(fileSize(outPath), "dist/html/", outFilePath);
752
949
  })
753
950
  );
754
951
  }
755
952
  }
953
+ function isRouteFile(filepath) {
954
+ return filepath.startsWith("routes") && isJsFile(filepath);
955
+ }
956
+ function fileSize(filepath) {
957
+ const stats = fsExtra2.statSync(filepath);
958
+ const bytes = stats.size;
959
+ const k = 1024;
960
+ if (bytes < k) {
961
+ return (bytes / k).toFixed(2) + " kB";
962
+ }
963
+ const units = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
964
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
965
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + units[i];
966
+ }
967
+ function printFileOutput(fileSize2, outputDir, outputFile) {
968
+ const indent = " ".repeat(2);
969
+ const paddedSize = fileSize2.padStart(9, " ");
970
+ console.log(
971
+ `${indent}${dim2(paddedSize)} ${dim2(outputDir)}${cyan(outputFile)}`
972
+ );
973
+ }
756
974
 
757
975
  // src/cli/commands/preview.ts
758
- import path6 from "node:path";
976
+ import path7 from "node:path";
759
977
  import { default as express2 } from "express";
760
978
  import { dim as dim3 } from "kleur/colors";
979
+ import sirv from "sirv";
980
+ import compression from "compression";
761
981
  async function preview(rootProjectDir) {
762
982
  process.env.NODE_ENV = "development";
763
- const rootDir = path6.resolve(rootProjectDir || process.cwd());
983
+ const rootDir = path7.resolve(rootProjectDir || process.cwd());
984
+ const server = await createServer3({ rootDir });
985
+ const port = parseInt(process.env.PORT || "4007");
986
+ console.log();
987
+ console.log(`${dim3("\u2503")} project: ${rootDir}`);
988
+ console.log(`${dim3("\u2503")} server: http://localhost:${port}`);
989
+ console.log(`${dim3("\u2503")} mode: preview`);
990
+ console.log();
991
+ server.listen(port);
992
+ }
993
+ async function createServer3(options) {
994
+ const rootDir = options.rootDir;
764
995
  const rootConfig = await loadRootConfig(rootDir);
765
- const distDir = path6.join(rootDir, "dist");
766
- const render = await import(path6.join(distDir, "server/render.js"));
767
- const renderer = new render.Renderer(rootConfig);
768
- const manifestPath = path6.join(distDir, "client/root-manifest.json");
996
+ const distDir = path7.join(rootDir, "dist");
997
+ const server = express2();
998
+ server.disable("x-powered-by");
999
+ server.use(compression());
1000
+ server.use(rootProjectMiddleware({ rootDir, rootConfig }));
1001
+ server.use(await rootPreviewRendererMiddleware({ rootConfig, distDir }));
1002
+ const plugins = rootConfig.plugins || [];
1003
+ configureServerPlugins(
1004
+ server,
1005
+ async () => {
1006
+ var _a;
1007
+ const userMiddlewares = ((_a = rootConfig.server) == null ? void 0 : _a.middlewares) || [];
1008
+ userMiddlewares.forEach((middleware) => {
1009
+ server.use(middleware);
1010
+ });
1011
+ const publicDir = path7.join(distDir, "html");
1012
+ server.use(sirv(publicDir, { dev: false }));
1013
+ server.use(rootPreviewServerMiddleware());
1014
+ server.use(rootPreviewServer404Middleware());
1015
+ server.use(rootPreviewServer500Middleware());
1016
+ },
1017
+ plugins,
1018
+ { type: "preview", rootConfig }
1019
+ );
1020
+ return server;
1021
+ }
1022
+ async function rootPreviewRendererMiddleware(options) {
1023
+ const { distDir, rootConfig } = options;
1024
+ const render = await import(path7.join(distDir, "server/render.js"));
1025
+ const manifestPath = path7.join(distDir, "client/root-manifest.json");
769
1026
  if (!await fileExists(manifestPath)) {
770
1027
  throw new Error(
771
1028
  `could not find ${manifestPath}. run \`root build\` before \`root preview\`.`
772
1029
  );
773
1030
  }
774
1031
  const rootManifest = await loadJson(manifestPath);
775
- const assetMap = BuildAssetMap.fromRootManifest(rootManifest);
776
- const app = express2();
777
- const publicDir = path6.join(distDir, "html");
778
- app.use(express2.static(publicDir));
779
- app.use(async (req, res, next) => {
1032
+ const assetMap = BuildAssetMap.fromRootManifest(rootConfig, rootManifest);
1033
+ const elementGraph = await getElements(rootConfig);
1034
+ const renderer = new render.Renderer(rootConfig, { assetMap, elementGraph });
1035
+ return async (req, _, next) => {
1036
+ req.renderer = renderer;
1037
+ next();
1038
+ };
1039
+ }
1040
+ function rootPreviewServerMiddleware() {
1041
+ return async (req, res, next) => {
1042
+ const renderer = req.renderer;
780
1043
  try {
781
- const url = req.originalUrl;
782
- const data = await renderer.render(url, {
783
- assetMap
784
- });
785
- if (data.notFound || !data.html) {
786
- next();
787
- return;
788
- }
789
- let html = data.html || "";
790
- if (rootConfig.minifyHtml !== false) {
791
- html = await htmlMinify(html);
792
- }
793
- res.status(200).set({ "Content-Type": "text/html" }).end(html);
1044
+ await renderer.handle(req, res, next);
794
1045
  } catch (e) {
795
1046
  try {
796
1047
  const { html } = await renderer.renderError(e);
@@ -801,52 +1052,110 @@ async function preview(rootProjectDir) {
801
1052
  next(e);
802
1053
  }
803
1054
  }
804
- });
805
- const port = parseInt(process.env.PORT || "4007");
806
- console.log();
807
- console.log(`${dim3("\u2503")} project: ${rootDir}`);
808
- console.log(`${dim3("\u2503")} server: http://localhost:${port}`);
809
- console.log();
810
- app.listen(port);
1055
+ };
1056
+ }
1057
+ function rootPreviewServer404Middleware() {
1058
+ return async (req, res) => {
1059
+ console.error(`\u2753 404 ${req.originalUrl}`);
1060
+ const url = req.path;
1061
+ const ext = path7.extname(url);
1062
+ const renderer = req.renderer;
1063
+ if (!ext) {
1064
+ const data = await renderer.render404();
1065
+ const html = data.html || "";
1066
+ res.status(404).set({ "Content-Type": "text/html" }).end(html);
1067
+ return;
1068
+ }
1069
+ res.status(404).set({ "Content-Type": "text/plain" }).end("404");
1070
+ };
1071
+ }
1072
+ function rootPreviewServer500Middleware() {
1073
+ return async (err, req, res, next) => {
1074
+ console.error(`\u2757 500 ${req.originalUrl}`);
1075
+ console.error(String(err.stack || err));
1076
+ const url = req.path;
1077
+ const ext = path7.extname(url);
1078
+ const renderer = req.renderer;
1079
+ if (!ext) {
1080
+ const data = await renderer.renderError(err);
1081
+ const html = data.html || "";
1082
+ res.status(500).set({ "Content-Type": "text/html" }).end(html);
1083
+ return;
1084
+ }
1085
+ next(err);
1086
+ };
811
1087
  }
812
1088
 
813
1089
  // src/cli/commands/start.ts
814
- import path7 from "node:path";
1090
+ import path8 from "node:path";
815
1091
  import { default as express3 } from "express";
816
1092
  import { dim as dim4 } from "kleur/colors";
1093
+ import sirv2 from "sirv";
1094
+ import compression2 from "compression";
817
1095
  async function start(rootProjectDir) {
818
1096
  process.env.NODE_ENV = "production";
819
- const rootDir = path7.resolve(rootProjectDir || process.cwd());
1097
+ const rootDir = path8.resolve(rootProjectDir || process.cwd());
1098
+ const server = await createServer4({ rootDir });
1099
+ const port = parseInt(process.env.PORT || "4007");
1100
+ console.log();
1101
+ console.log(`${dim4("\u2503")} project: ${rootDir}`);
1102
+ console.log(`${dim4("\u2503")} server: http://localhost:${port}`);
1103
+ console.log(`${dim4("\u2503")} mode: production`);
1104
+ console.log();
1105
+ server.listen(port);
1106
+ }
1107
+ async function createServer4(options) {
1108
+ const rootDir = options.rootDir;
820
1109
  const rootConfig = await loadRootConfig(rootDir);
821
- const distDir = path7.join(rootDir, "dist");
822
- const render = await import(path7.join(distDir, "server/render.js"));
823
- const renderer = new render.Renderer(rootConfig);
824
- const manifestPath = path7.join(distDir, "client/root-manifest.json");
1110
+ const distDir = path8.join(rootDir, "dist");
1111
+ const server = express3();
1112
+ server.disable("x-powered-by");
1113
+ server.use(compression2());
1114
+ server.use(rootProjectMiddleware({ rootDir, rootConfig }));
1115
+ server.use(await rootProdRendererMiddleware({ rootConfig, distDir }));
1116
+ const plugins = rootConfig.plugins || [];
1117
+ configureServerPlugins(
1118
+ server,
1119
+ async () => {
1120
+ var _a;
1121
+ const userMiddlewares = ((_a = rootConfig.server) == null ? void 0 : _a.middlewares) || [];
1122
+ userMiddlewares.forEach((middleware) => {
1123
+ server.use(middleware);
1124
+ });
1125
+ const publicDir = path8.join(distDir, "html");
1126
+ server.use(sirv2(publicDir, { dev: false }));
1127
+ server.use(rootProdServerMiddleware());
1128
+ server.use(rootProdServer404Middleware());
1129
+ server.use(rootProdServer500Middleware());
1130
+ },
1131
+ plugins,
1132
+ { type: "prod", rootConfig }
1133
+ );
1134
+ return server;
1135
+ }
1136
+ async function rootProdRendererMiddleware(options) {
1137
+ const { distDir, rootConfig } = options;
1138
+ const render = await import(path8.join(distDir, "server/render.js"));
1139
+ const manifestPath = path8.join(distDir, "client/root-manifest.json");
825
1140
  if (!await fileExists(manifestPath)) {
826
1141
  throw new Error(
827
1142
  `could not find ${manifestPath}. run \`root build\` before \`root start\`.`
828
1143
  );
829
1144
  }
830
1145
  const rootManifest = await loadJson(manifestPath);
831
- const assetMap = BuildAssetMap.fromRootManifest(rootManifest);
832
- const app = express3();
833
- const publicDir = path7.join(distDir, "html");
834
- app.use(express3.static(publicDir));
835
- app.use(async (req, res, next) => {
1146
+ const assetMap = BuildAssetMap.fromRootManifest(rootConfig, rootManifest);
1147
+ const elementGraph = await getElements(rootConfig);
1148
+ const renderer = new render.Renderer(rootConfig, { assetMap, elementGraph });
1149
+ return async (req, _, next) => {
1150
+ req.renderer = renderer;
1151
+ next();
1152
+ };
1153
+ }
1154
+ function rootProdServerMiddleware() {
1155
+ return async (req, res, next) => {
1156
+ const renderer = req.renderer;
836
1157
  try {
837
- const url = req.originalUrl;
838
- const data = await renderer.render(url, {
839
- assetMap
840
- });
841
- if (data.notFound || !data.html) {
842
- next();
843
- return;
844
- }
845
- let html = data.html || "";
846
- if (rootConfig.minifyHtml !== false) {
847
- html = await htmlMinify(html);
848
- }
849
- res.status(200).set({ "Content-Type": "text/html" }).end(html);
1158
+ await renderer.handle(req, res, next);
850
1159
  } catch (e) {
851
1160
  try {
852
1161
  const { html } = await renderer.renderError(e);
@@ -857,13 +1166,42 @@ async function start(rootProjectDir) {
857
1166
  next(e);
858
1167
  }
859
1168
  }
860
- });
861
- const port = parseInt(process.env.PORT || "4007");
862
- console.log();
863
- console.log(`${dim4("\u2503")} project: ${rootDir}`);
864
- console.log(`${dim4("\u2503")} server: http://localhost:${port}`);
865
- console.log();
866
- app.listen(port);
1169
+ };
1170
+ }
1171
+ function rootProdServer404Middleware() {
1172
+ return async (req, res) => {
1173
+ console.error(`\u2753 404 ${req.originalUrl}`);
1174
+ if (req.renderer) {
1175
+ const url = req.path;
1176
+ const ext = path8.extname(url);
1177
+ if (!ext) {
1178
+ const renderer = req.renderer;
1179
+ const data = await renderer.render404();
1180
+ const html = data.html || "";
1181
+ res.status(404).set({ "Content-Type": "text/html" }).end(html);
1182
+ return;
1183
+ }
1184
+ }
1185
+ res.status(404).set({ "Content-Type": "text/plain" }).end("404");
1186
+ };
1187
+ }
1188
+ function rootProdServer500Middleware() {
1189
+ return async (err, req, res, next) => {
1190
+ console.error(`\u2757 500 ${req.originalUrl}`);
1191
+ console.error(String(err.stack || err));
1192
+ if (req.renderer) {
1193
+ const url = req.path;
1194
+ const ext = path8.extname(url);
1195
+ if (!ext) {
1196
+ const renderer = req.renderer;
1197
+ const data = await renderer.renderError(err);
1198
+ const html = data.html || "";
1199
+ res.status(500).set({ "Content-Type": "text/html" }).end(html);
1200
+ return;
1201
+ }
1202
+ }
1203
+ next(err);
1204
+ };
867
1205
  }
868
1206
  export {
869
1207
  build,