@cfdez11/vex 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +409 -1101
- package/bin/vex.js +41 -0
- package/package.json +5 -1
- package/server/utils/component-processor.js +9 -19
- package/server/utils/files.js +74 -4
- package/server/utils/router.js +2 -0
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.
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
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
|
}
|
package/server/utils/files.js
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/server/utils/router.js
CHANGED
|
@@ -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
|
}
|