@cfdez11/vex 0.1.0 → 0.2.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/bin/vex.js ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "child_process";
4
+ import { fileURLToPath } from "url";
5
+ import path from "path";
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const serverDir = path.resolve(__dirname, "..", "server");
9
+
10
+ const [command] = process.argv.slice(2);
11
+
12
+ const commands = {
13
+ dev: () =>
14
+ spawn(
15
+ "node",
16
+ ["--watch", path.join(serverDir, "index.js")],
17
+ { stdio: "inherit" }
18
+ ),
19
+
20
+ build: () =>
21
+ spawn(
22
+ "node",
23
+ [path.join(serverDir, "prebuild.js")],
24
+ { stdio: "inherit" }
25
+ ),
26
+
27
+ start: () =>
28
+ spawn(
29
+ "node",
30
+ [path.join(serverDir, "index.js")],
31
+ { stdio: "inherit", env: { ...process.env, NODE_ENV: "production" } }
32
+ ),
33
+ };
34
+
35
+ if (!commands[command]) {
36
+ console.error(`Unknown command: "${command}"\nAvailable: dev, build, start`);
37
+ process.exit(1);
38
+ }
39
+
40
+ const child = commands[command]();
41
+ child.on("exit", code => process.exit(code ?? 0));
package/package.json CHANGED
@@ -1,13 +1,17 @@
1
1
  {
2
2
  "name": "@cfdez11/vex",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "A vanilla JavaScript meta-framework with file-based routing, SSR/CSR/SSG/ISR and Vue-like reactivity",
5
5
  "type": "module",
6
6
  "main": "./server/index.js",
7
7
  "exports": {
8
8
  ".": "./server/index.js"
9
9
  },
10
+ "bin": {
11
+ "vex": "./bin/vex.js"
12
+ },
10
13
  "files": [
14
+ "bin",
11
15
  "server",
12
16
  "client"
13
17
  ],
@@ -1,7 +1,7 @@
1
1
  import { watch } from "fs";
2
2
  import path from "path";
3
3
  import { compileTemplateToHTML } from "./template.js";
4
- import { getOriginalRoutePath, getPageFiles, getRoutePath, saveClientComponentModule, saveClientRoutesFile, saveComponentHtmlDisk, saveServerRoutesFile, readFile, getImportData, generateComponentId, adjustClientModulePath, PAGES_DIR, ROOT_HTML_DIR, getLayoutPaths } from "./files.js";
4
+ import { getOriginalRoutePath, getPageFiles, getRoutePath, saveClientComponentModule, saveClientRoutesFile, saveComponentHtmlDisk, saveServerRoutesFile, readFile, getImportData, generateComponentId, adjustClientModulePath, PAGES_DIR, ROOT_HTML_DIR, getLayoutPaths, SRC_DIR, WATCH_IGNORE } from "./files.js";
5
5
  import { renderComponents } from "./streaming.js";
6
6
  import { getRevalidateSeconds } from "./cache.js";
7
7
  import { withCache } from "./data-cache.js";
@@ -80,11 +80,12 @@ if (process.env.NODE_ENV !== "production") {
80
80
  // Lazy import — hmr.js is never loaded in production
81
81
  const { hmrEmitter } = await import("./hmr.js");
82
82
 
83
- const watchDirs = [PAGES_DIR, path.join(path.dirname(PAGES_DIR), "components")];
84
- for (const dir of watchDirs) {
85
- watch(dir, { recursive: true }, async (_, filename) => {
86
- if (filename?.endsWith(".vex")) {
87
- const fullPath = path.join(dir, filename);
83
+ // Watch SRC_DIR (configured via vex.config.json `srcDir`, defaults to project root).
84
+ // Skip any path segment that appears in WATCH_IGNORE to avoid reacting to
85
+ // changes inside node_modules, build outputs, or other non-source directories.
86
+ watch(SRC_DIR, { recursive: true }, async (_, filename) => {
87
+ if (filename?.endsWith(".vex") && !filename.split(path.sep).some(part => WATCH_IGNORE.has(part))) {
88
+ const fullPath = path.join(SRC_DIR, filename);
88
89
 
89
90
  // 1. Evict all in-memory caches for this file
90
91
  processHtmlFileCache.delete(fullPath);
@@ -101,7 +102,6 @@ if (process.env.NODE_ENV !== "production") {
101
102
  hmrEmitter.emit("reload", filename);
102
103
  }
103
104
  });
104
- }
105
105
 
106
106
  // root.html is a single file — watch it directly
107
107
  watch(ROOT_HTML_DIR, async () => {
@@ -971,19 +971,9 @@ async function generateServerComponentHTML(componentPath) {
971
971
  export async function processClientComponent(componentName, originalPath, props = {}) {
972
972
  const targetId = `client-${componentName}-${Date.now()}`;
973
973
 
974
- const propsEntries = Object.entries(props)
975
- .map(([key, value]) => {
976
- // if starts with ${ remove quotes
977
- if (typeof value === "string" && value.startsWith("${")) {
978
- return `${key}:${value.replace(/^\$\{|\}$/g, "")}`;
979
- } else {
980
- return `${key}:${JSON.stringify(value)}`;
981
- }
982
- })
983
- .join(",");
984
-
985
974
  const componentImport = generateComponentId(originalPath)
986
- const html = `<template id="${targetId}" data-client:component="${componentImport}" data-client:props='${propsEntries ? `\${JSON.stringify({${propsEntries}})}` : "{}"}'></template>`;
975
+ const propsJson = JSON.stringify(props);
976
+ const html = `<template id="${targetId}" data-client:component="${componentImport}" data-client:props='${propsJson}'></template>`;
987
977
 
988
978
  return html;
989
979
  }
@@ -1,5 +1,5 @@
1
1
  import fs from "fs/promises";
2
- import { watch, existsSync, statSync } from "fs";
2
+ import { watch, existsSync, statSync, readFileSync } from "fs";
3
3
  import path from "path";
4
4
  import crypto from "crypto";
5
5
  import { fileURLToPath, pathToFileURL } from "url";
@@ -18,10 +18,78 @@ const __dirname = path.dirname(__filename);
18
18
  // Framework's own directory (packages/vexjs/ — 3 levels up from server/utils/)
19
19
  const FRAMEWORK_DIR = path.resolve(__dirname, "..", "..");
20
20
  // User's project root (where they run the server)
21
- const PROJECT_ROOT = process.cwd();
21
+ export const PROJECT_ROOT = process.cwd();
22
22
  const ROOT_DIR = PROJECT_ROOT;
23
23
 
24
- export const PAGES_DIR = path.resolve(PROJECT_ROOT, "pages");
24
+ /**
25
+ * User configuration loaded from `vex.config.json` at the project root.
26
+ *
27
+ * Supported fields:
28
+ * - `srcDir` {string} Subfolder that contains pages/, components/ and
29
+ * all user .vex code. Defaults to "." (project root).
30
+ * Example: "app" → pages live at app/pages/
31
+ * - `watchIgnore` {string[]} Additional directory names to exclude from the
32
+ * dev file watcher, merged with the built-in list.
33
+ * Example: ["dist", "coverage"]
34
+ *
35
+ * The file is optional — if absent, all values fall back to their defaults.
36
+ */
37
+ let _vexConfig = {};
38
+ try {
39
+ _vexConfig = JSON.parse(readFileSync(path.join(PROJECT_ROOT, "vex.config.json"), "utf-8"));
40
+ } catch {}
41
+
42
+ /**
43
+ * Absolute path to the directory that contains the user's source files
44
+ * (pages/, components/, and any other .vex folders).
45
+ *
46
+ * Derived from `srcDir` in vex.config.json, resolved relative to PROJECT_ROOT.
47
+ * Defaults to PROJECT_ROOT when `srcDir` is not set.
48
+ *
49
+ * Changing this allows users to organise all their app code in a single
50
+ * subfolder (e.g. `app/`) so the dev watcher only needs to observe that
51
+ * folder instead of the entire project root.
52
+ */
53
+ export const SRC_DIR = path.resolve(PROJECT_ROOT, _vexConfig.srcDir || ".");
54
+
55
+ /**
56
+ * Set of directory *names* (not paths) that the dev file watcher will skip
57
+ * when scanning for .vex changes.
58
+ *
59
+ * The check is applied to every segment of the changed file's relative path,
60
+ * so a directory named "dist" is ignored regardless of nesting depth.
61
+ *
62
+ * Built-in ignored directories (always excluded):
63
+ * - Build outputs: dist, build, out, .output
64
+ * - Framework generated: .vexjs, public
65
+ * - Dependencies: node_modules
66
+ * - Version control: .git, .svn
67
+ * - Test coverage: coverage, .nyc_output
68
+ * - Other fw caches: .next, .nuxt, .svelte-kit, .astro
69
+ * - Misc: tmp, temp, .cache, .claude
70
+ *
71
+ * Extended via `watchIgnore` in vex.config.json.
72
+ */
73
+ export const WATCH_IGNORE = new Set([
74
+ // build outputs
75
+ "dist", "build", "out", ".output",
76
+ // framework generated
77
+ ".vexjs", "public",
78
+ // dependencies
79
+ "node_modules",
80
+ // vcs
81
+ ".git", ".svn",
82
+ // test coverage
83
+ "coverage", ".nyc_output",
84
+ // other framework caches
85
+ ".next", ".nuxt", ".svelte-kit", ".astro",
86
+ // misc
87
+ "tmp", "temp", ".cache", ".claude",
88
+ // user-defined extras from vex.config.json
89
+ ...(_vexConfig.watchIgnore || []),
90
+ ]);
91
+
92
+ export const PAGES_DIR = path.resolve(SRC_DIR, "pages");
25
93
  export const SERVER_APP_DIR = path.join(FRAMEWORK_DIR, "server");
26
94
  export const CLIENT_DIR = path.join(FRAMEWORK_DIR, "client");
27
95
  export const CLIENT_SERVICES_DIR = path.join(CLIENT_DIR, "services");
@@ -98,10 +166,12 @@ export function adjustClientModulePath(modulePath, importStatement) {
98
166
  let relative = modulePath.replace(/^vex\//, "").replace(/^\.app\//, "");
99
167
  let adjustedPath = `/_vexjs/services/${relative}`;
100
168
 
101
- // Auto-resolve directory → index.js
169
+ // Auto-resolve directory → index.js, bare name → .js
102
170
  const fsPath = path.join(CLIENT_SERVICES_DIR, relative);
103
171
  if (existsSync(fsPath) && statSync(fsPath).isDirectory()) {
104
172
  adjustedPath += "/index.js";
173
+ } else if (!path.extname(adjustedPath)) {
174
+ adjustedPath += ".js";
105
175
  }
106
176
 
107
177
  const adjustedImportStatement = importStatement.replace(modulePath, adjustedPath);
@@ -295,6 +295,7 @@ export async function handlePageRequest(req, res, route) {
295
295
  try {
296
296
  await renderAndSendPage({ pageName, context, route });
297
297
  } catch (e) {
298
+ console.error(`[500] Error rendering page "${route.path}":`, e);
298
299
  // redirect() in a server script throws a structured error.
299
300
  // Intercept it before the generic 500 handler so the browser gets a proper redirect.
300
301
  if (e.redirect) {
@@ -321,6 +322,7 @@ export async function handlePageRequest(req, res, route) {
321
322
  route,
322
323
  });
323
324
  } catch (err) {
325
+ console.warn('error}}}}}}}}}}', err)
324
326
  console.error(`Failed to render error page: ${err.message}`);
325
327
  sendResponse(res, 500, FALLBACK_ERROR_HTML);
326
328
  }
@@ -84,7 +84,7 @@ async function processServerComponents(html, serverComponents) {
84
84
  const replacements = [];
85
85
  let match;
86
86
 
87
- while ((match = componentRegex.exec(html)) !== null) {
87
+ while ((match = componentRegex.exec(processedHtml)) !== null) {
88
88
  replacements.push({
89
89
  name: componentName,
90
90
  attrs: parseAttributes(match[1]),