xpine 0.0.20 → 0.0.22

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.
Files changed (40) hide show
  1. package/TODO +0 -2
  2. package/dist/index.js +357 -128
  3. package/dist/src/build/esbuild/addDotJS.d.ts +5 -0
  4. package/dist/src/build/esbuild/addDotJS.d.ts.map +1 -0
  5. package/dist/src/build/esbuild/getDataFiles.d.ts +5 -0
  6. package/dist/src/build/esbuild/getDataFiles.d.ts.map +1 -0
  7. package/dist/src/build/esbuild/transformTSXFiles.d.ts +6 -0
  8. package/dist/src/build/esbuild/transformTSXFiles.d.ts.map +1 -0
  9. package/dist/src/express.d.ts +3 -0
  10. package/dist/src/express.d.ts.map +1 -1
  11. package/dist/src/runDevServer.d.ts.map +1 -1
  12. package/dist/src/scripts/build.d.ts +5 -1
  13. package/dist/src/scripts/build.d.ts.map +1 -1
  14. package/dist/src/scripts/xpine-build.js +249 -104
  15. package/dist/src/scripts/xpine-dev.js +294 -114
  16. package/dist/src/static/spa.js +2 -1
  17. package/dist/src/util/config-file.d.ts +6 -0
  18. package/dist/src/util/config-file.d.ts.map +1 -0
  19. package/dist/src/util/constants.d.ts +3 -0
  20. package/dist/src/util/constants.d.ts.map +1 -0
  21. package/dist/src/util/get-config.d.ts.map +1 -1
  22. package/dist/src/util/postcss/remove-layers.d.ts +1 -1
  23. package/dist/src/util/regex.d.ts +11 -0
  24. package/dist/src/util/regex.d.ts.map +1 -0
  25. package/dist/src/util/require.d.ts.map +1 -1
  26. package/dist/types.d.ts +31 -2
  27. package/dist/types.d.ts.map +1 -1
  28. package/eslint.config.mjs +27 -0
  29. package/package.json +18 -5
  30. package/tests/e2e/alpine.spec.ts +14 -0
  31. package/tests/e2e/app-build.spec.ts +73 -0
  32. package/tests/e2e/spa.spec.ts +23 -0
  33. package/tests/e2e/tests-examples/demo-todo-app.spec.ts +437 -0
  34. package/tests/eslint.config.mjs +27 -0
  35. package/tests/package-lock.json +8941 -0
  36. package/tests/package.json +89 -0
  37. package/tests/playwright.config.ts +81 -0
  38. package/tests/tsconfig.json +46 -0
  39. package/tests/xpine.config.mjs +1 -0
  40. package/types.ts +34 -2
package/dist/index.js CHANGED
@@ -6,11 +6,10 @@ import EventEmitter from "events";
6
6
  import chokidar from "chokidar";
7
7
 
8
8
  // src/scripts/build.ts
9
- import path3 from "path";
10
- import fs2 from "fs-extra";
9
+ import path5 from "path";
10
+ import fs5 from "fs-extra";
11
11
  import { build } from "esbuild";
12
- import builtinModules from "builtin-modules";
13
- import ts2 from "typescript";
12
+ import ts3 from "typescript";
14
13
 
15
14
  // src/build/typescript-builder.ts
16
15
  import ts from "typescript";
@@ -114,17 +113,6 @@ function removeClientScriptInTSXFile(pathName, source) {
114
113
  clientDataStart
115
114
  };
116
115
  }
117
- function createStaticFile(pathName, source) {
118
- source.forEachChild((child) => {
119
- if (child.kind === ts.SyntaxKind.ExpressionStatement) {
120
- const text = child.getText(source);
121
- const cleanedText = text.replace(/["';]/g, "");
122
- if (cleanedText === "xpine-static") {
123
- console.log("make this file static", pathName);
124
- }
125
- }
126
- });
127
- }
128
116
 
129
117
  // src/scripts/build.ts
130
118
  import { globSync } from "glob";
@@ -140,6 +128,10 @@ var require_default = createRequire(import.meta.url);
140
128
  function getXPineDistDir() {
141
129
  const dir = import.meta.dirname;
142
130
  const splitDir = dir.split("/xpine/dist");
131
+ if (splitDir.length === 1) {
132
+ const splitDirSingle = dir.split("/xpine/");
133
+ return splitDirSingle[0] + "/xpine/dist";
134
+ }
143
135
  return splitDir[0] + "/xpine/dist";
144
136
  }
145
137
 
@@ -181,6 +173,9 @@ var configDefaults = {
181
173
  get pagesDir() {
182
174
  return path2.join(this.srcDir, "./pages");
183
175
  },
176
+ get distPagesDir() {
177
+ return path2.join(this.distDir, "./pages");
178
+ },
184
179
  get publicDir() {
185
180
  return path2.join(this.srcDir, "./public");
186
181
  },
@@ -216,21 +211,152 @@ var plugin = (opts = {}) => {
216
211
  plugin.postcss = true;
217
212
  var remove_layers_default = plugin;
218
213
 
214
+ // src/build/esbuild/transformTSXFiles.ts
215
+ import fs2 from "fs-extra";
216
+ import ts2 from "typescript";
217
+
218
+ // src/util/regex.ts
219
+ var regex_default = {
220
+ dotTsx: /.tsx/,
221
+ hasLetters: /[A-Za-z0-9]/g,
222
+ configFile: /\+config\.[tj]sx?/g,
223
+ dynamicRoutes: /(\[)(.*?)(\])/g,
224
+ isDynamicRoute: /\[(.*)\]/g,
225
+ endsWithTSX: /\.tsx$/,
226
+ endsWithJSX: /\.jsx$/
227
+ };
228
+
229
+ // src/util/config-file.ts
230
+ import path3 from "path";
231
+ function sourcePathToDistPath(sourcePath) {
232
+ return sourcePath?.replace(config.srcDir, config.distDir)?.replace(/\.ts$/, ".js")?.replace(/\.tsx$/, ".js");
233
+ }
234
+ function getConfigFiles(pathName, pageConfigFiles, returnDistPaths = true) {
235
+ const configs = [];
236
+ for (const configFile of pageConfigFiles) {
237
+ const result = path3.relative(pathName, configFile)?.replace(regex_default.configFile, "");
238
+ const hasLetters = result.match(regex_default.hasLetters);
239
+ if (!hasLetters) {
240
+ configs.push(returnDistPaths ? sourcePathToDistPath(configFile) : configFile);
241
+ }
242
+ }
243
+ if (!configs.length) return null;
244
+ return configs.sort((a, b) => b.length - a.length);
245
+ }
246
+ async function getCompleteConfig(configFiles, cacheKey) {
247
+ let config2 = {};
248
+ for (const configFile of configFiles) {
249
+ config2 = {
250
+ ...(await import(configFile + `?cache=${cacheKey}`)).default,
251
+ ...config2
252
+ };
253
+ }
254
+ return config2;
255
+ }
256
+ function isAConfigFile(pathName) {
257
+ return !!pathName.match(regex_default.configFile);
258
+ }
259
+
260
+ // src/build/esbuild/transformTSXFiles.ts
261
+ function transformTSXFiles(componentData, pageConfigFiles) {
262
+ return {
263
+ name: "transform-tsx-files",
264
+ setup(build2) {
265
+ build2.onLoad({ filter: regex_default.dotTsx }, async (args) => {
266
+ const content = fs2.readFileSync(args.path, "utf-8");
267
+ const source = ts2.createSourceFile(
268
+ args.path,
269
+ content,
270
+ ts2.ScriptTarget.Latest
271
+ );
272
+ const cleanedContent = removeClientScriptInTSXFile(args.path, source);
273
+ const htmlImportStart = "import { html } from 'xpine';\n";
274
+ const newContent = `${htmlImportStart}${cleanedContent.content}`;
275
+ componentData.push({
276
+ ...args,
277
+ contents: `${htmlImportStart}${cleanedContent.fullContent}`,
278
+ clientContent: cleanedContent.clientContent,
279
+ configFiles: isAConfigFile(args?.path) ? null : getConfigFiles(args.path, pageConfigFiles)
280
+ });
281
+ return {
282
+ contents: newContent,
283
+ loader: "tsx"
284
+ };
285
+ });
286
+ }
287
+ };
288
+ }
289
+
290
+ // src/build/esbuild/addDotJS.ts
291
+ import fs3 from "fs-extra";
292
+ import path4 from "path";
293
+ import builtinModules from "builtin-modules";
294
+ function addDotJS(allPackages2, extensions2, isDev) {
295
+ const allPackagesIncludingNode = allPackages2.concat(builtinModules);
296
+ return {
297
+ name: "add-dot-js",
298
+ setup(build2) {
299
+ build2.onResolve({ filter: /.*/ }, (args) => {
300
+ const hasAtSign = args.path.startsWith("@");
301
+ const isPackage = hasAtSign ? allPackagesIncludingNode.includes(args.path) : allPackagesIncludingNode.includes(args.path.split("/").shift());
302
+ if (args.importer && !isPackage) {
303
+ const calculatedDir = path4.join(args.resolveDir, args.path);
304
+ let existsAsFile = false;
305
+ for (const extension of extensions2) {
306
+ const asFile = calculatedDir + extension;
307
+ const exists = fs3.existsSync(asFile);
308
+ if (exists) existsAsFile = true;
309
+ }
310
+ let outputPath = args.path + (existsAsFile ? "" : "/index") + ".js";
311
+ outputPath = args.path.endsWith(".js") || args.path.endsWith(".mjs") ? args.path : outputPath;
312
+ return { path: outputPath + (isDev ? `?cache=${Date.now()}` : ""), external: true };
313
+ }
314
+ });
315
+ }
316
+ };
317
+ }
318
+
319
+ // src/build/esbuild/getDataFiles.ts
320
+ import fs4 from "fs-extra";
321
+ function getDataFiles(dataFiles) {
322
+ return {
323
+ name: "get-data-files",
324
+ setup(build2) {
325
+ build2.onLoad({ filter: /\.data\.(js|mjs|ts)$/ }, (args) => {
326
+ const contents = fs4.readFileSync(args.path, "utf-8");
327
+ dataFiles.push({
328
+ ...args,
329
+ contents
330
+ });
331
+ return {
332
+ contents,
333
+ loader: "ts"
334
+ };
335
+ });
336
+ }
337
+ };
338
+ }
339
+
340
+ // src/util/constants.ts
341
+ var doctypeHTML = "<!DOCTYPE html>";
342
+ var staticComment = "<!-- static -->";
343
+
219
344
  // src/scripts/build.ts
220
345
  var extensions = [".ts", ".tsx"];
221
- var packageJson = JSON.parse(fs2.readFileSync(config.packageJsonPath, "utf-8"));
346
+ var packageJson = JSON.parse(fs5.readFileSync(config.packageJsonPath, "utf-8"));
222
347
  var allPackages = Object.keys(packageJson.devDependencies).concat(Object.keys(packageJson.dependencies));
223
- var allPackagesIncludingNode = allPackages.concat(builtinModules);
224
348
  var xpineDistDir = getXPineDistDir();
225
- async function buildApp(isDev = false) {
349
+ async function buildApp(isDev = false, removePreviousBuild = false) {
226
350
  try {
351
+ if (removePreviousBuild) fs5.removeSync(config.distDir);
227
352
  const srcDirFiles = globSync(config.srcDir + "/**/*.{js,ts,tsx,jsx}");
228
353
  const { componentData, dataFiles } = await buildAppFiles(srcDirFiles, isDev);
229
354
  const alpineDataFile = await buildAlpineDataFile(componentData, dataFiles);
230
355
  await buildClientSideFiles([alpineDataFile], isDev);
231
- fs2.removeSync(config.distTempFolder);
356
+ fs5.removeSync(config.distTempFolder);
232
357
  await buildCSS();
233
358
  await buildPublicFolderSymlinks();
359
+ if (!isDev) await buildFilesWithConfigs(componentData);
234
360
  } catch (err) {
235
361
  console.error("Build failed");
236
362
  console.error(err);
@@ -239,8 +365,12 @@ async function buildApp(isDev = false) {
239
365
  async function buildAppFiles(files, isDev) {
240
366
  const componentData = [];
241
367
  const dataFiles = [];
368
+ const pageConfigFiles = files.filter((file) => {
369
+ const fileName = file.split("/").at(-1).split(".").shift();
370
+ return fileName === "+config";
371
+ });
242
372
  const backendFiles = files.filter((file) => extensions.find((ext) => file.endsWith(ext))).filter((file) => !file.startsWith(config.publicDir));
243
- fs2.ensureDirSync(config.distDir);
373
+ fs5.ensureDirSync(config.distDir);
244
374
  await build({
245
375
  entryPoints: backendFiles,
246
376
  format: "esm",
@@ -252,69 +382,9 @@ async function buildAppFiles(files, isDev) {
252
382
  jsx: "transform",
253
383
  minify: !isDev,
254
384
  plugins: [
255
- {
256
- name: "add-dot-js",
257
- setup(build2) {
258
- build2.onResolve({ filter: /.*/ }, (args) => {
259
- const hasAtSign = args.path.startsWith("@");
260
- const isPackage = hasAtSign ? allPackagesIncludingNode.includes(args.path) : allPackagesIncludingNode.includes(args.path.split("/").shift());
261
- if (args.importer && !isPackage) {
262
- const calculatedDir = path3.join(args.resolveDir, args.path);
263
- let existsAsFile = false;
264
- for (const extension of extensions) {
265
- const asFile = calculatedDir + extension;
266
- const exists = fs2.existsSync(asFile);
267
- if (exists) existsAsFile = true;
268
- }
269
- let outputPath = args.path + (existsAsFile ? "" : "/index") + ".js";
270
- outputPath = args.path.endsWith(".js") || args.path.endsWith(".mjs") ? args.path : outputPath;
271
- return { path: outputPath + (isDev ? `?cache=${Date.now()}` : ""), external: true };
272
- }
273
- });
274
- }
275
- },
276
- {
277
- name: "insert-html-banner-and-remove-client-scripts",
278
- setup(build2) {
279
- build2.onLoad({ filter: /.tsx/ }, (args) => {
280
- const content = fs2.readFileSync(args.path, "utf-8");
281
- const source = ts2.createSourceFile(
282
- args.path,
283
- content,
284
- ts2.ScriptTarget.Latest
285
- );
286
- const cleanedContent = removeClientScriptInTSXFile(args.path, source);
287
- createStaticFile(args.path, source);
288
- const htmlImportStart = "import { html } from 'xpine';\n";
289
- const newContent = `${htmlImportStart}${cleanedContent.content}`;
290
- componentData.push({
291
- ...args,
292
- contents: `${htmlImportStart}${cleanedContent.fullContent}`,
293
- clientContent: cleanedContent.clientContent
294
- });
295
- return {
296
- contents: newContent,
297
- loader: "tsx"
298
- };
299
- });
300
- }
301
- },
302
- {
303
- name: "get-data-files",
304
- setup(build2) {
305
- build2.onLoad({ filter: /\.data\.(js|mjs|ts)$/ }, (args) => {
306
- const contents = fs2.readFileSync(args.path, "utf-8");
307
- dataFiles.push({
308
- ...args,
309
- contents
310
- });
311
- return {
312
- contents,
313
- loader: "ts"
314
- };
315
- });
316
- }
317
- }
385
+ addDotJS(allPackages, extensions, isDev),
386
+ transformTSXFiles(componentData, pageConfigFiles),
387
+ getDataFiles(dataFiles)
318
388
  ]
319
389
  });
320
390
  await logSize(config.distDir, "app");
@@ -324,8 +394,8 @@ async function buildAppFiles(files, isDev) {
324
394
  };
325
395
  }
326
396
  async function buildClientSideFiles(alpineDataFiles = [], isDev) {
327
- const tempFilePath = path3.join(config.distTempFolder, "./app.ts");
328
- fs2.ensureFileSync(tempFilePath);
397
+ const tempFilePath = path5.join(config.distTempFolder, "./app.ts");
398
+ fs5.ensureFileSync(tempFilePath);
329
399
  const pagesScriptsGlob = config.publicDir + "/scripts/pages/**/*.{js,ts}";
330
400
  const clientFiles = globSync(
331
401
  config.publicDir + "/**/*.{js,ts}",
@@ -354,16 +424,14 @@ async function buildClientSideFiles(alpineDataFiles = [], isDev) {
354
424
  await logSize(config.distPublicDir, "client");
355
425
  }
356
426
  function writeDevServerClientSideCode(tempFilePath) {
357
- const devServerPath = path3.join(xpineDistDir, "./src/static/dev-server.js");
358
- const content = fs2.readFileSync(devServerPath, "utf-8");
359
- fs2.appendFileSync(tempFilePath, `
360
- ` + content);
427
+ const devServerPath = path5.join(xpineDistDir, "./src/static/dev-server.js");
428
+ const content = fs5.readFileSync(devServerPath, "utf-8");
429
+ fs5.appendFileSync(tempFilePath, "\n" + content);
361
430
  }
362
431
  function writeSpaClientSideCode(tempFilePath) {
363
- const spaPath = path3.join(xpineDistDir, "./src/static/spa.js");
364
- const content = fs2.readFileSync(spaPath, "utf-8");
365
- fs2.appendFileSync(tempFilePath, `
366
- ` + content);
432
+ const spaPath = path5.join(xpineDistDir, "./src/static/spa.js");
433
+ const content = fs5.readFileSync(spaPath, "utf-8");
434
+ fs5.appendFileSync(tempFilePath, "\n" + content);
367
435
  }
368
436
  async function buildAlpineDataFile(componentData, dataFiles) {
369
437
  const output = {
@@ -382,10 +450,10 @@ async function buildAlpineDataFile(componentData, dataFiles) {
382
450
  };
383
451
  const componentsAndDataFiles = componentData.concat(dataFiles);
384
452
  for (const component of componentsAndDataFiles) {
385
- const sourceFile = ts2.createSourceFile(
453
+ const sourceFile = ts3.createSourceFile(
386
454
  component.path,
387
455
  component.contents,
388
- ts2.ScriptTarget.Latest
456
+ ts3.ScriptTarget.Latest
389
457
  );
390
458
  const dataFunctionResult = findDataAttributesAndFunctions(sourceFile, sourceFile);
391
459
  dataFunctionResults.foundDataAttributes.push(...dataFunctionResult.foundDataAttributes);
@@ -407,18 +475,18 @@ async function buildAlpineDataFile(componentData, dataFiles) {
407
475
  output.code.push(`Alpine.data('${dataFunction.name}', ${dataFunction.name});`);
408
476
  }
409
477
  const result = output.imports.join("\n") + "\n" + output.code.join("\n") + "\n" + output.end.join("\n");
410
- fs2.ensureFileSync(config.alpineDataPath);
411
- fs2.writeFileSync(config.alpineDataPath, result);
478
+ fs5.ensureFileSync(config.alpineDataPath);
479
+ fs5.writeFileSync(config.alpineDataPath, result);
412
480
  return config.alpineDataPath;
413
481
  }
414
482
  async function buildCSS() {
415
483
  const cssFiles = globSync(config.srcDir + "/**/*.css");
416
484
  for (const file of cssFiles) {
417
- const fileContents = fs2.readFileSync(file, "utf-8");
485
+ const fileContents = fs5.readFileSync(file, "utf-8");
418
486
  const result = await postcss([tailwindPostcss(), remove_layers_default()]).process(fileContents, { from: file });
419
487
  const newPath = file.replace(config.srcDir, config.distDir);
420
- fs2.ensureFileSync(newPath);
421
- fs2.writeFileSync(newPath, result.css);
488
+ fs5.ensureFileSync(newPath);
489
+ fs5.writeFileSync(newPath, result.css);
422
490
  }
423
491
  logSize(config.distPublicDir, "css");
424
492
  }
@@ -432,8 +500,8 @@ async function buildPublicFolderSymlinks() {
432
500
  const splitNewPath = newPath.split("/");
433
501
  splitNewPath.pop();
434
502
  const newDir = splitNewPath.join("/");
435
- fs2.ensureDirSync(newDir);
436
- fs2.symlinkSync(file, newPath);
503
+ fs5.ensureDirSync(newDir);
504
+ fs5.symlinkSync(file, newPath);
437
505
  } catch {
438
506
  }
439
507
  }
@@ -444,7 +512,7 @@ async function logSize(pathName, type, validExtensions = [".js", ".css"]) {
444
512
  if (!validExtensions.find((ext) => file.endsWith(ext))) return false;
445
513
  return {
446
514
  file,
447
- size: fs2.statSync(file).size / (1024 * 1e3)
515
+ size: fs5.statSync(file).size / (1024 * 1e3)
448
516
  };
449
517
  }).filter(Boolean);
450
518
  const totalSize = fileSizes.reduce((total, current) => {
@@ -452,17 +520,91 @@ async function logSize(pathName, type, validExtensions = [".js", ".css"]) {
452
520
  }, 0);
453
521
  console.info(`[${totalSize.toFixed(3)} MB] Built ${type}`);
454
522
  }
523
+ async function buildFilesWithConfigs(componentData) {
524
+ const now = Date.now();
525
+ const componentsWithConfigs = componentData.filter((item) => item.configFiles);
526
+ for (const component of componentsWithConfigs) {
527
+ let config2 = await getCompleteConfig(component.configFiles, now);
528
+ const builtComponentPath = sourcePathToDistPath(component.path);
529
+ const componentImport = await import(builtComponentPath + `?cache=${Date.now()}`);
530
+ if (componentImport?.config) {
531
+ config2 = {
532
+ ...config2,
533
+ ...componentImport.config
534
+ };
535
+ }
536
+ if (config2?.staticPaths) buildStaticFiles(config2, component, componentImport, builtComponentPath);
537
+ }
538
+ }
539
+ async function buildStaticFiles(config2, component, componentImport, builtComponentPath) {
540
+ if (!config2?.staticPaths) return;
541
+ let componentFileName = component.path.split("/").pop().replace(regex_default.endsWithJSX, "").replace(regex_default.endsWithTSX, "");
542
+ const isDynamicRoute = component.path.match(regex_default.isDynamicRoute);
543
+ if (isDynamicRoute) {
544
+ componentFileName = component.path.split("/").filter((dir) => dir.match(regex_default.isDynamicRoute)).join("/").replace(regex_default.endsWithJSX, "").replace(regex_default.endsWithTSX, "");
545
+ }
546
+ const componentDynamicPaths = getComponentDynamicPaths(componentFileName);
547
+ const componentFn = componentImport.default;
548
+ const outputPath = componentDynamicPaths?.length ? componentDynamicPaths.reduce((total, current) => {
549
+ return total.replace(`/[${current}]`, "");
550
+ }, path5.dirname(builtComponentPath)) : path5.dirname(builtComponentPath);
551
+ if (typeof config2?.staticPaths === "boolean") {
552
+ try {
553
+ const req = { params: {} };
554
+ const data = config2?.data ? await config2.data(req) : null;
555
+ const staticComponentOutput = await componentFn({ data });
556
+ fs5.writeFileSync(
557
+ path5.join(outputPath, "./index.html"),
558
+ doctypeHTML + (config2?.wrapper ? await config2.wrapper({ req, children: staticComponentOutput, config: config2, data }) : staticComponentOutput) + staticComment
559
+ );
560
+ } catch (err) {
561
+ console.error(err);
562
+ console.log("Could not build static component", component.path);
563
+ }
564
+ } else if (typeof config2?.staticPaths === "function") {
565
+ const dynamicPaths = await config2.staticPaths();
566
+ for (const dynamicPath of dynamicPaths) {
567
+ try {
568
+ const req = {
569
+ params: {
570
+ ...componentDynamicPaths?.length ? dynamicPath : {}
571
+ }
572
+ };
573
+ const data = config2?.data ? await config2.data(req) : null;
574
+ const staticComponentOutput = await componentFn({ req, data });
575
+ const updatedOutDir = path5.join(outputPath, `./${componentDynamicPaths.map((key) => dynamicPath[key]).join("/")}`);
576
+ fs5.ensureDirSync(updatedOutDir);
577
+ fs5.writeFileSync(
578
+ path5.join(updatedOutDir, "./index.html"),
579
+ doctypeHTML + (config2?.wrapper ? await config2.wrapper({ req, children: staticComponentOutput, config: config2, data }) : staticComponentOutput) + staticComment
580
+ );
581
+ } catch (err) {
582
+ console.log("Could not build static component", component.path);
583
+ console.error(err);
584
+ }
585
+ }
586
+ }
587
+ }
588
+ function getComponentDynamicPaths(componentPath) {
589
+ const matches = [...componentPath.matchAll(regex_default.dynamicRoutes)];
590
+ if (!matches?.length) return null;
591
+ const output = [];
592
+ for (const match of matches) {
593
+ output.push(match[2]);
594
+ }
595
+ return output;
596
+ }
455
597
 
456
598
  // src/runDevServer.ts
457
- import path5 from "path";
599
+ import path7 from "path";
458
600
 
459
601
  // src/util/env.ts
460
602
  import dotenv from "dotenv";
461
- import path4 from "path";
603
+ import path6 from "path";
462
604
  import { GetSecretValueCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
463
605
  async function setupEnv() {
464
606
  if (process.env.HAS_SETUP_ENV) return;
465
- dotenv.config({ path: path4.join(config.rootDir, `./.env.${process.env.STAGE || "dev"}`) });
607
+ dotenv.config({ path: path6.join(config.rootDir, `./.env.${process.env.STAGE || "dev"}`) });
466
608
  await loadSecretsManagerSecrets();
467
609
  process.env.HAS_SETUP_ENV = "true";
468
610
  }
@@ -491,26 +633,33 @@ async function loadSecretsManagerSecrets() {
491
633
  await setupEnv();
492
634
  async function runDevServer() {
493
635
  process.env.NODE_ENV = "development";
494
- const startServer = (await import(config.serverDistAppPath + `?cache=${Date.now()}`)).default;
636
+ const rebuildEmitter = createRebuildEmitter();
495
637
  await buildApp(true);
638
+ const startServer = (await import(config.serverDistAppPath + `?cache=${Date.now()}`)).default;
496
639
  let appServer = await startServer();
497
640
  const watcher = chokidar.watch(config.srcDir, {
498
641
  ignoreInitial: true,
499
642
  // Ignore map and prisma files
500
- ignored: (pathName) => pathName.endsWith(".map") || pathName.startsWith(path5.join(config.serverDir, "./prisma"))
643
+ ignored: (pathName) => pathName.endsWith(".map") || pathName.startsWith(path7.join(config.serverDir, "./prisma"))
501
644
  });
502
- watcher.on("all", async (event, path6) => {
503
- const shouldReloadServer = path6.startsWith(config.serverDir) && !path6.startsWith(config.runDir) || ["add", "unlink"].includes(event) && path6.startsWith(config.pagesDir);
645
+ watcher.on("all", async (event, path9) => {
646
+ const shouldReloadServer = path9.startsWith(config.serverDir) && !path9.startsWith(config.runDir) || ["add", "unlink"].includes(event) && path9.startsWith(config.pagesDir);
504
647
  if (shouldReloadServer) {
505
- await appServer.server.close();
506
- await buildApp(true);
507
- const startServer2 = (await import(config.serverDistAppPath + `?cache=${Date.now()}`)).default;
508
- appServer = await startServer2();
648
+ await asyncServerClose(appServer.server);
649
+ rebuildEmitter.emit("rebuild-server");
509
650
  return;
510
651
  }
511
652
  await buildApp(true);
512
653
  refreshEmitter.emit("refresh");
513
654
  });
655
+ rebuildEmitter.on("done", async () => {
656
+ await rebuildServer();
657
+ });
658
+ async function rebuildServer() {
659
+ await buildApp(true);
660
+ const startServer2 = (await import(config.serverDistAppPath + `?cache=${Date.now()}`)).default;
661
+ appServer = await startServer2();
662
+ }
514
663
  const wsApp = express();
515
664
  const wsServer = http.createServer(wsApp);
516
665
  expressWs(wsApp, wsServer);
@@ -530,6 +679,37 @@ async function runDevServer() {
530
679
  console.info(`Dev server listening on port ${wsPort}`);
531
680
  });
532
681
  }
682
+ function asyncServerClose(server) {
683
+ return new Promise((resolve, reject) => {
684
+ server.close();
685
+ resolve(true);
686
+ });
687
+ }
688
+ function createRebuildEmitter(delay = 500) {
689
+ class RebuildEmitter extends EventEmitter {
690
+ }
691
+ ;
692
+ const rebuildEmitter = new RebuildEmitter();
693
+ let start = 0;
694
+ let hasEmitted = false;
695
+ rebuildEmitter.on("rebuild-server", () => {
696
+ hasEmitted = false;
697
+ trigger();
698
+ });
699
+ function trigger() {
700
+ start = Date.now();
701
+ const interval = setInterval(() => {
702
+ if (hasEmitted) return;
703
+ const now = Date.now();
704
+ if (now - start >= delay) {
705
+ rebuildEmitter.emit("done");
706
+ clearInterval(interval);
707
+ hasEmitted = true;
708
+ }
709
+ }, 100);
710
+ }
711
+ return rebuildEmitter;
712
+ }
533
713
 
534
714
  // src/express.ts
535
715
  import express2 from "express";
@@ -574,33 +754,48 @@ function getTokenFromRequest(req) {
574
754
 
575
755
  // src/express.ts
576
756
  import requestIP from "request-ip";
577
- var doctypeHTML = "<!DOCTYPE html>";
757
+ import fs6 from "fs-extra";
758
+ import path8 from "path";
578
759
  async function createRouter() {
760
+ const isDev = process.env.NODE_ENV === "development";
579
761
  const methods = ["get", "post", "put", "patch", "delete"];
580
762
  const router = express2.Router();
581
763
  const routes = globSync2(config.pagesDir + "/**/*.{tsx,ts}");
582
764
  const routeMap = routes.map((route) => {
765
+ const routeFormatted = route.split(config.pagesDir).pop().replace(".tsx", "").replace(".js", "").replace(".ts", "");
766
+ if (routeFormatted.endsWith("+config")) return;
767
+ const routeFormattedWithIndex = routeFormatted.replace(/\/index$/g, "");
583
768
  return {
584
- route: route.split(config.pagesDir).pop().replace(".tsx", "").replace(".js", "").replace(".ts", "").replace("/index", "/"),
769
+ route: routeFormattedWithIndex,
585
770
  path: route.replace(config.srcDir, config.distDir).replace(".tsx", ".js").replace(".ts", ".js"),
586
771
  originalRoute: route
587
772
  };
588
- });
773
+ }).filter(Boolean);
589
774
  const routeResults = [];
775
+ const configFiles = globSync2(config.pagesDir + "/**/+config.{tsx,ts}");
590
776
  for (const route of routeMap) {
591
777
  const isJSX = route.originalRoute.endsWith(".tsx") || route.originalRoute.endsWith(".jsx");
592
- const routeItem = process.env.NODE_ENV === "development" ? null : (await import(route.path)).default;
593
- const slugRoute = route.route.toLowerCase().replace(/[ ]/g, "");
778
+ const slugRoute = route.route.replace(/[ ]/g, "");
594
779
  const foundMethod = methods.find((method) => slugRoute.endsWith(`.${method}`));
595
- const isDynamicRoute = slugRoute.match(/\[(.*)\]/g);
780
+ const isDynamicRoute = slugRoute.match(regex_default.isDynamicRoute);
596
781
  let formattedRouteItem = slugRoute;
597
782
  if (foundMethod) formattedRouteItem = formattedRouteItem.split(".").shift();
598
783
  if (isDynamicRoute) {
599
- const result = [...formattedRouteItem.matchAll(/(\[)(.*?)(\])/g)];
784
+ const result = [...formattedRouteItem.matchAll(regex_default.dynamicRoutes)];
600
785
  for (const match of result) {
601
786
  formattedRouteItem = formattedRouteItem.replace(match[0], ":" + match[2]);
602
787
  }
603
788
  }
789
+ const componentImport = isDev ? null : await import(route.path);
790
+ const componentFn = componentImport?.default;
791
+ const configFilePaths = getConfigFiles(route.originalRoute, configFiles);
792
+ let config2 = configFilePaths && await getCompleteConfig(configFilePaths, Date.now());
793
+ if (componentImport?.config) {
794
+ config2 = {
795
+ ...config2,
796
+ ...componentImport.config
797
+ };
798
+ }
604
799
  routeResults.push({
605
800
  formattedRouteItem,
606
801
  foundMethod,
@@ -608,19 +803,38 @@ async function createRouter() {
608
803
  });
609
804
  router[foundMethod || "get"](formattedRouteItem, async (req, res) => {
610
805
  try {
611
- if (routeItem) {
806
+ const staticPath = routeHasStaticPath(formattedRouteItem, req.params);
807
+ if (staticPath && !isDev) {
808
+ res.sendFile(staticPath);
809
+ return;
810
+ }
811
+ if (componentFn && !isDev) {
612
812
  if (isJSX) {
613
- res.send(doctypeHTML + await routeItem(req, res));
813
+ const data = config2?.data ? await config2.data(req) : null;
814
+ const originalResult = await componentFn({ req, res, data });
815
+ const output = config2?.wrapper ? await config2.wrapper({ req, children: originalResult, config: config2, data }) : originalResult;
816
+ res.send(doctypeHTML + output);
614
817
  } else {
615
- await routeItem(req, res);
818
+ await componentFn(req, res);
616
819
  }
617
820
  return;
618
821
  }
619
- const defaultRouteImport = (await import(route.path + `?cache=${Date.now()}`)).default;
822
+ const componentImportDev = await import(route.path + `?cache=${Date.now()}`);
823
+ const componentFnDev = componentImportDev.default;
620
824
  if (isJSX) {
621
- res.send(doctypeHTML + await defaultRouteImport(req, res));
825
+ let config3 = configFilePaths && await getCompleteConfig(configFilePaths, Date.now());
826
+ if (componentImportDev?.config) {
827
+ config3 = {
828
+ ...config3,
829
+ ...componentImportDev.config
830
+ };
831
+ }
832
+ const data = config3?.data ? await config3.data(req) : null;
833
+ const originalResult = await componentFnDev({ req, res, data });
834
+ const output = config3?.wrapper ? await config3.wrapper({ req, children: originalResult, config: config3, data }) : originalResult;
835
+ res.send(doctypeHTML + output);
622
836
  } else {
623
- await defaultRouteImport(req, res);
837
+ await componentFnDev(req, res);
624
838
  }
625
839
  } catch (err) {
626
840
  console.error(err);
@@ -655,7 +869,7 @@ async function createXPineRouter(app, beforeErrorRoute) {
655
869
  router(req, res, next);
656
870
  });
657
871
  const found404 = routeResults?.find((item) => item?.formattedRouteItem === "/404");
658
- const import404 = process.env.NODE_ENV === "development" ? null : (await import(found404.route.path)).default;
872
+ const import404 = process.env.NODE_ENV === "development" || !found404 ? null : (await import(found404.route.path)).default;
659
873
  if (beforeErrorRoute) beforeErrorRoute(app);
660
874
  app.use(async function(req, res) {
661
875
  res.status(404);
@@ -669,6 +883,17 @@ async function createXPineRouter(app, beforeErrorRoute) {
669
883
  }
670
884
  });
671
885
  }
886
+ function routeHasStaticPath(route, params) {
887
+ const paramEntries = Object.entries(params);
888
+ let routeToStaticPath = route;
889
+ for (const [key, value] of paramEntries) {
890
+ routeToStaticPath = routeToStaticPath.replace(`:${key}`, value);
891
+ }
892
+ routeToStaticPath += "/index.html";
893
+ const outputPath = path8.join(config.distPagesDir, routeToStaticPath);
894
+ if (fs6.existsSync(outputPath)) return outputPath;
895
+ return false;
896
+ }
672
897
 
673
898
  // src/util/html.ts
674
899
  var html = class {
@@ -699,14 +924,18 @@ export {
699
924
  JSXRuntime,
700
925
  buildApp,
701
926
  buildCSS,
927
+ buildFilesWithConfigs,
702
928
  buildPublicFolderSymlinks,
929
+ buildStaticFiles,
703
930
  config,
704
931
  createRouter,
705
932
  createXPineRouter,
706
933
  fromRoot,
934
+ getComponentDynamicPaths,
707
935
  getTokenFromRequest,
708
936
  html,
709
937
  logSize,
938
+ routeHasStaticPath,
710
939
  runDevServer,
711
940
  setupEnv,
712
941
  signUser,