@blinkk/root 1.0.0-alpha.9 → 1.0.0-beta.1

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