@cfdez11/vex 0.2.1 → 0.4.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 +6 -0
- package/bin/vex.js +29 -1
- package/client/services/reactive.js +20 -4
- package/package.json +1 -1
- package/server/build-static.js +202 -0
- package/server/index.js +45 -1
- package/server/utils/component-processor.js +35 -13
- package/server/utils/files.js +45 -3
- package/server/utils/template.js +9 -6
package/README.md
CHANGED
|
@@ -686,6 +686,12 @@ sequenceDiagram
|
|
|
686
686
|
- [x] `vex.config.json` — configurable `srcDir` and `watchIgnore`
|
|
687
687
|
- [x] Published to npm as `@cfdez11/vex`
|
|
688
688
|
- [x] VS Code extension with syntax highlighting and go-to-definition
|
|
689
|
+
- [ ] Devtools
|
|
690
|
+
- [ ] Typescript in framework
|
|
691
|
+
- [ ] Allow typescript to devs
|
|
692
|
+
- [ ] Improve extension (hightlight, redirects, etc)
|
|
693
|
+
- [ ] Create theme syntax
|
|
694
|
+
- [ ] Create docs page
|
|
689
695
|
- [ ] Authentication middleware
|
|
690
696
|
- [ ] CDN cache integration
|
|
691
697
|
- [ ] Fix Suspense marker replacement with multi-root templates
|
package/bin/vex.js
CHANGED
|
@@ -9,7 +9,25 @@ const serverDir = path.resolve(__dirname, "..", "server");
|
|
|
9
9
|
|
|
10
10
|
const [command] = process.argv.slice(2);
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Available CLI commands.
|
|
14
|
+
*
|
|
15
|
+
* Each entry is a factory function that calls `spawn()` to launch a child
|
|
16
|
+
* process and returns the ChildProcess handle.
|
|
17
|
+
*
|
|
18
|
+
* `spawn(command, args, options)` forks a new OS process running `command`
|
|
19
|
+
* with the given `args`. It is non-blocking: the parent (this CLI) keeps
|
|
20
|
+
* running while the child executes. The returned ChildProcess emits an
|
|
21
|
+
* "exit" event when the child terminates, which we use to forward its exit
|
|
22
|
+
* code so the shell sees the correct status (e.g. for CI).
|
|
23
|
+
*
|
|
24
|
+
* `stdio: "inherit"` wires the child's stdin/stdout/stderr directly to the
|
|
25
|
+
* terminal that launched the CLI. Without it the child's output would be
|
|
26
|
+
* captured internally and never displayed. "inherit" is equivalent to
|
|
27
|
+
* passing [process.stdin, process.stdout, process.stderr].
|
|
28
|
+
*/
|
|
12
29
|
const commands = {
|
|
30
|
+
/** Start the dev server with Node's built-in file watcher (--watch restarts on .js changes). */
|
|
13
31
|
dev: () =>
|
|
14
32
|
spawn(
|
|
15
33
|
"node",
|
|
@@ -17,6 +35,7 @@ const commands = {
|
|
|
17
35
|
{ stdio: "inherit" }
|
|
18
36
|
),
|
|
19
37
|
|
|
38
|
+
/** Run the prebuild: scan pages/, generate component bundles and route registries. */
|
|
20
39
|
build: () =>
|
|
21
40
|
spawn(
|
|
22
41
|
"node",
|
|
@@ -24,16 +43,25 @@ const commands = {
|
|
|
24
43
|
{ stdio: "inherit" }
|
|
25
44
|
),
|
|
26
45
|
|
|
46
|
+
/** Start the production server. Sets NODE_ENV=production to disable HMR and file watchers. */
|
|
27
47
|
start: () =>
|
|
28
48
|
spawn(
|
|
29
49
|
"node",
|
|
30
50
|
[path.join(serverDir, "index.js")],
|
|
31
51
|
{ stdio: "inherit", env: { ...process.env, NODE_ENV: "production" } }
|
|
32
52
|
),
|
|
53
|
+
|
|
54
|
+
/** Run the static build: prebuild + copy assets to dist/ for deployment without a server. */
|
|
55
|
+
"build:static": () =>
|
|
56
|
+
spawn(
|
|
57
|
+
"node",
|
|
58
|
+
[path.join(serverDir, "build-static.js")],
|
|
59
|
+
{ stdio: "inherit" }
|
|
60
|
+
),
|
|
33
61
|
};
|
|
34
62
|
|
|
35
63
|
if (!commands[command]) {
|
|
36
|
-
console.error(`Unknown command: "${command}"\nAvailable: dev, build, start`);
|
|
64
|
+
console.error(`Unknown command: "${command}"\nAvailable: dev, build, build:static, start`);
|
|
37
65
|
process.exit(1);
|
|
38
66
|
}
|
|
39
67
|
|
|
@@ -89,6 +89,14 @@ export function reactive(obj) {
|
|
|
89
89
|
get(target, prop) {
|
|
90
90
|
// Handle primitive value conversion (for template literals, etc.)
|
|
91
91
|
if (target.__isPrimitive && prop === Symbol.toPrimitive) {
|
|
92
|
+
// Track "value" dependency so effects using ${counter} re-run on change
|
|
93
|
+
if (activeEffect) {
|
|
94
|
+
if (!depsMap.has("value")) depsMap.set("value", new Set());
|
|
95
|
+
const depSet = depsMap.get("value");
|
|
96
|
+
depSet.add(activeEffect);
|
|
97
|
+
if (!activeEffect.deps) activeEffect.deps = [];
|
|
98
|
+
activeEffect.deps.push(depSet);
|
|
99
|
+
}
|
|
92
100
|
return () => target.value;
|
|
93
101
|
}
|
|
94
102
|
|
|
@@ -180,11 +188,19 @@ export function computed(getter) {
|
|
|
180
188
|
value = getter();
|
|
181
189
|
});
|
|
182
190
|
|
|
183
|
-
return {
|
|
184
|
-
get
|
|
185
|
-
|
|
191
|
+
return new Proxy({}, {
|
|
192
|
+
get(_, prop) {
|
|
193
|
+
if (prop === Symbol.toPrimitive) {
|
|
194
|
+
return () => value;
|
|
195
|
+
}
|
|
196
|
+
if (prop === "value") {
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
// Delegate any other access (e.g. .map, .length) to the underlying value
|
|
200
|
+
const v = value?.[prop];
|
|
201
|
+
return typeof v === "function" ? v.bind(value) : v;
|
|
186
202
|
},
|
|
187
|
-
};
|
|
203
|
+
});
|
|
188
204
|
}
|
|
189
205
|
|
|
190
206
|
/**
|
package/package.json
CHANGED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { build } from "./utils/component-processor.js";
|
|
4
|
+
import {
|
|
5
|
+
initializeDirectories,
|
|
6
|
+
CLIENT_DIR,
|
|
7
|
+
SRC_DIR,
|
|
8
|
+
PROJECT_ROOT,
|
|
9
|
+
getRootTemplate,
|
|
10
|
+
WATCH_IGNORE,
|
|
11
|
+
generateComponentId,
|
|
12
|
+
} from "./utils/files.js";
|
|
13
|
+
|
|
14
|
+
const GENERATED_DIR = path.join(PROJECT_ROOT, ".vexjs");
|
|
15
|
+
const DIST_DIR = path.join(PROJECT_ROOT, "dist");
|
|
16
|
+
|
|
17
|
+
console.log("🔨 Starting static build...");
|
|
18
|
+
|
|
19
|
+
// Step 1: Prebuild (components + routes)
|
|
20
|
+
console.log("📁 Initializing directories...");
|
|
21
|
+
await initializeDirectories();
|
|
22
|
+
|
|
23
|
+
console.log("⚙️ Generating components and routes...");
|
|
24
|
+
const { serverRoutes } = await build();
|
|
25
|
+
|
|
26
|
+
// Step 2: Create dist/ structure (clean start)
|
|
27
|
+
console.log("🗂️ Creating dist/ structure...");
|
|
28
|
+
await fs.rm(DIST_DIR, { recursive: true, force: true });
|
|
29
|
+
await fs.mkdir(path.join(DIST_DIR, "_vexjs", "_components"), { recursive: true });
|
|
30
|
+
await fs.mkdir(path.join(DIST_DIR, "_vexjs", "user"), { recursive: true });
|
|
31
|
+
|
|
32
|
+
// Step 3: Generate dist/index.html shell
|
|
33
|
+
console.log("📄 Generating index.html shell...");
|
|
34
|
+
const rootTemplate = await getRootTemplate();
|
|
35
|
+
let shell = rootTemplate
|
|
36
|
+
.replace(/\{\{metadata\.title\}\}/g, "App")
|
|
37
|
+
.replace(/\{\{metadata\.description\}\}/g, "")
|
|
38
|
+
.replace(/\{\{props\.children\}\}/g, "");
|
|
39
|
+
|
|
40
|
+
const frameworkScripts = [
|
|
41
|
+
`<script type="module" src="/_vexjs/services/index.js"></script>`,
|
|
42
|
+
`<script src="/_vexjs/services/hydrate-client-components.js"></script>`,
|
|
43
|
+
`<script src="/_vexjs/services/hydrate.js" id="hydrate-script"></script>`,
|
|
44
|
+
].join("\n ");
|
|
45
|
+
|
|
46
|
+
shell = shell.replace("</head>", ` ${frameworkScripts}\n</head>`);
|
|
47
|
+
await fs.writeFile(path.join(DIST_DIR, "index.html"), shell, "utf-8");
|
|
48
|
+
|
|
49
|
+
// Step 4: Copy framework client files → dist/_vexjs/
|
|
50
|
+
console.log("📦 Copying framework client files...");
|
|
51
|
+
await fs.cp(CLIENT_DIR, path.join(DIST_DIR, "_vexjs"), { recursive: true });
|
|
52
|
+
|
|
53
|
+
// Step 5: Copy generated component bundles → dist/_vexjs/_components/
|
|
54
|
+
console.log("📦 Copying component bundles...");
|
|
55
|
+
await fs.cp(
|
|
56
|
+
path.join(GENERATED_DIR, "_components"),
|
|
57
|
+
path.join(DIST_DIR, "_vexjs", "_components"),
|
|
58
|
+
{ recursive: true }
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Step 6: Copy generated services (includes _routes.js) → dist/_vexjs/services/
|
|
62
|
+
// This overwrites the framework-level services dir copy with the generated routes
|
|
63
|
+
console.log("📦 Copying generated services...");
|
|
64
|
+
await fs.cp(
|
|
65
|
+
path.join(GENERATED_DIR, "services"),
|
|
66
|
+
path.join(DIST_DIR, "_vexjs", "services"),
|
|
67
|
+
{ recursive: true }
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Step 7: Copy user JS files with import rewriting → dist/_vexjs/user/
|
|
71
|
+
console.log("📦 Processing user JS files...");
|
|
72
|
+
await copyUserJsFiles(SRC_DIR, path.join(DIST_DIR, "_vexjs", "user"));
|
|
73
|
+
|
|
74
|
+
// Step 8: Copy public/ → dist/ (static assets, CSS)
|
|
75
|
+
console.log("📦 Copying public assets...");
|
|
76
|
+
const publicDir = path.join(PROJECT_ROOT, "public");
|
|
77
|
+
try {
|
|
78
|
+
await fs.cp(publicDir, DIST_DIR, { recursive: true });
|
|
79
|
+
} catch {
|
|
80
|
+
// no public/ directory — that's fine
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Step 9: Copy pre-rendered HTML for SSG routes (revalidate: 'never')
|
|
84
|
+
const CACHE_DIR = path.join(GENERATED_DIR, "_cache");
|
|
85
|
+
const ssgRoutes = serverRoutes.filter(
|
|
86
|
+
(r) => r.meta.revalidate === "never" || r.meta.revalidate === false
|
|
87
|
+
);
|
|
88
|
+
if (ssgRoutes.length > 0) {
|
|
89
|
+
console.log("📄 Copying pre-rendered SSG pages...");
|
|
90
|
+
for (const route of ssgRoutes) {
|
|
91
|
+
const cacheFile = path.join(CACHE_DIR, `${generateComponentId(route.serverPath)}.html`);
|
|
92
|
+
try {
|
|
93
|
+
const html = await fs.readFile(cacheFile, "utf-8");
|
|
94
|
+
const routeSegment = route.serverPath === "/" ? "" : route.serverPath;
|
|
95
|
+
const destPath = path.join(DIST_DIR, routeSegment, "index.html");
|
|
96
|
+
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
97
|
+
await fs.writeFile(destPath, html, "utf-8");
|
|
98
|
+
console.log(` ✓ ${route.serverPath}`);
|
|
99
|
+
} catch {
|
|
100
|
+
console.warn(` ✗ ${route.serverPath} (no cached HTML found)`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Step 10: Report SSR-only routes that were skipped
|
|
106
|
+
const ssrOnlyRoutes = serverRoutes.filter((r) => r.meta.ssr);
|
|
107
|
+
if (ssrOnlyRoutes.length > 0) {
|
|
108
|
+
console.warn("\n⚠️ The following routes require a server and were NOT included in the static build:");
|
|
109
|
+
for (const r of ssrOnlyRoutes) {
|
|
110
|
+
console.warn(` ${r.path} (SSR)`);
|
|
111
|
+
}
|
|
112
|
+
console.warn(" These routes will show a 404 in the static build.\n");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log("✅ Static build complete! Output: dist/");
|
|
116
|
+
console.log("\nTo serve locally: npx serve dist");
|
|
117
|
+
console.log("Static host note: configure your host to serve dist/index.html for all 404s (SPA fallback).");
|
|
118
|
+
|
|
119
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Recursively walks SRC_DIR, rewrites imports in every .js file,
|
|
123
|
+
* and writes results to destDir preserving the relative path structure.
|
|
124
|
+
*
|
|
125
|
+
* Skips directories listed in WATCH_IGNORE (node_modules, dist, .vexjs, etc.).
|
|
126
|
+
*
|
|
127
|
+
* @param {string} srcDir Absolute path to user source root (SRC_DIR)
|
|
128
|
+
* @param {string} destDir Absolute path to dist/_vexjs/user/
|
|
129
|
+
*/
|
|
130
|
+
async function copyUserJsFiles(srcDir, destDir) {
|
|
131
|
+
let entries;
|
|
132
|
+
try {
|
|
133
|
+
entries = await fs.readdir(srcDir, { withFileTypes: true });
|
|
134
|
+
} catch {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const entry of entries) {
|
|
139
|
+
if (WATCH_IGNORE.has(entry.name)) continue;
|
|
140
|
+
|
|
141
|
+
const fullSrc = path.join(srcDir, entry.name);
|
|
142
|
+
const relToSrcDir = path.relative(SRC_DIR, fullSrc).replace(/\\/g, "/");
|
|
143
|
+
const fullDest = path.join(destDir, relToSrcDir);
|
|
144
|
+
|
|
145
|
+
if (entry.isDirectory()) {
|
|
146
|
+
await copyUserJsFiles(fullSrc, destDir);
|
|
147
|
+
} else if (entry.name.endsWith(".js")) {
|
|
148
|
+
let content;
|
|
149
|
+
try {
|
|
150
|
+
content = await fs.readFile(fullSrc, "utf-8");
|
|
151
|
+
} catch {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
content = rewriteUserImports(content, fullSrc, srcDir);
|
|
156
|
+
|
|
157
|
+
await fs.mkdir(path.dirname(fullDest), { recursive: true });
|
|
158
|
+
await fs.writeFile(fullDest, content, "utf-8");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Rewrites import paths in a user JS file so they work in the browser.
|
|
165
|
+
* Mirrors the runtime rewriting done by the /_vexjs/user/* Express handler.
|
|
166
|
+
*
|
|
167
|
+
* - `vex/` and `.app/` → `/_vexjs/services/`
|
|
168
|
+
* - `@/` (project alias) → `/_vexjs/user/`
|
|
169
|
+
* - relative `./` or `../` → `/_vexjs/user/`
|
|
170
|
+
* - external bare specifiers (e.g. npm packages) → left as-is
|
|
171
|
+
*
|
|
172
|
+
* @param {string} content File source
|
|
173
|
+
* @param {string} filePath Absolute path of the file being rewritten
|
|
174
|
+
* @param {string} srcDir Absolute SRC_DIR root
|
|
175
|
+
* @returns {string} Rewritten source
|
|
176
|
+
*/
|
|
177
|
+
function rewriteUserImports(content, filePath, srcDir) {
|
|
178
|
+
return content.replace(
|
|
179
|
+
/^(\s*import\s+[^'"]*from\s+)['"]([^'"]+)['"]/gm,
|
|
180
|
+
(match, prefix, modulePath) => {
|
|
181
|
+
if (modulePath.startsWith("vex/") || modulePath.startsWith(".app/")) {
|
|
182
|
+
let mod = modulePath.replace(/^vex\//, "").replace(/^\.app\//, "");
|
|
183
|
+
if (!path.extname(mod)) mod += ".js";
|
|
184
|
+
return `${prefix}'/_vexjs/services/${mod}'`;
|
|
185
|
+
}
|
|
186
|
+
if (modulePath.startsWith("@/") || modulePath === "@") {
|
|
187
|
+
let resolved = path.resolve(srcDir, modulePath.replace(/^@\//, "").replace(/^@$/, ""));
|
|
188
|
+
if (!path.extname(resolved)) resolved += ".js";
|
|
189
|
+
const rel = path.relative(srcDir, resolved).replace(/\\/g, "/");
|
|
190
|
+
return `${prefix}'/_vexjs/user/${rel}'`;
|
|
191
|
+
}
|
|
192
|
+
if (modulePath.startsWith("./") || modulePath.startsWith("../")) {
|
|
193
|
+
const fileDir = path.dirname(filePath);
|
|
194
|
+
let resolved = path.resolve(fileDir, modulePath);
|
|
195
|
+
if (!path.extname(resolved)) resolved += ".js";
|
|
196
|
+
const rel = path.relative(srcDir, resolved).replace(/\\/g, "/");
|
|
197
|
+
return `${prefix}'/_vexjs/user/${rel}'`;
|
|
198
|
+
}
|
|
199
|
+
return match;
|
|
200
|
+
}
|
|
201
|
+
);
|
|
202
|
+
}
|
package/server/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
1
2
|
import express from "express";
|
|
2
3
|
import path from "path";
|
|
3
4
|
import { pathToFileURL } from "url";
|
|
4
5
|
import { handlePageRequest, revalidatePath } from "./utils/router.js";
|
|
5
|
-
import { initializeDirectories, CLIENT_DIR } from "./utils/files.js";
|
|
6
|
+
import { initializeDirectories, CLIENT_DIR, SRC_DIR } from "./utils/files.js";
|
|
6
7
|
|
|
7
8
|
await initializeDirectories();
|
|
8
9
|
|
|
@@ -63,6 +64,49 @@ app.use(
|
|
|
63
64
|
})
|
|
64
65
|
);
|
|
65
66
|
|
|
67
|
+
// Serve user JS utility files at /_vexjs/user/* with import rewriting
|
|
68
|
+
app.get("/_vexjs/user/*splat", async (req, res) => {
|
|
69
|
+
const splat = req.params.splat;
|
|
70
|
+
const relPath = Array.isArray(splat) ? splat.join("/") : splat;
|
|
71
|
+
const filePath = path.resolve(path.join(SRC_DIR, relPath));
|
|
72
|
+
// Prevent path traversal outside SRC_DIR
|
|
73
|
+
if (!filePath.startsWith(SRC_DIR + path.sep) && filePath !== SRC_DIR) {
|
|
74
|
+
return res.status(403).send("Forbidden");
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
let content = await fs.readFile(filePath, "utf-8");
|
|
78
|
+
// Rewrite imports to browser-accessible paths
|
|
79
|
+
content = content.replace(
|
|
80
|
+
/^(\s*import\s+[^'"]*from\s+)['"]([^'"]+)['"]/gm,
|
|
81
|
+
(match, prefix, modulePath) => {
|
|
82
|
+
if (modulePath.startsWith("vex/") || modulePath.startsWith(".app/")) {
|
|
83
|
+
let mod = modulePath.replace(/^vex\//, "").replace(/^\.app\//, "");
|
|
84
|
+
if (!path.extname(mod)) mod += ".js";
|
|
85
|
+
return `${prefix}'/_vexjs/services/${mod}'`;
|
|
86
|
+
}
|
|
87
|
+
if (modulePath.startsWith("@/") || modulePath === "@") {
|
|
88
|
+
let resolved = path.resolve(SRC_DIR, modulePath.replace(/^@\//, "").replace(/^@$/, ""));
|
|
89
|
+
if (!path.extname(resolved)) resolved += ".js";
|
|
90
|
+
const rel = path.relative(SRC_DIR, resolved).replace(/\\/g, "/");
|
|
91
|
+
return `${prefix}'/_vexjs/user/${rel}'`;
|
|
92
|
+
}
|
|
93
|
+
if (modulePath.startsWith("./") || modulePath.startsWith("../")) {
|
|
94
|
+
const fileDir = path.dirname(filePath);
|
|
95
|
+
let resolved = path.resolve(fileDir, modulePath);
|
|
96
|
+
if (!path.extname(resolved)) resolved += ".js";
|
|
97
|
+
const rel = path.relative(SRC_DIR, resolved).replace(/\\/g, "/");
|
|
98
|
+
return `${prefix}'/_vexjs/user/${rel}'`;
|
|
99
|
+
}
|
|
100
|
+
return match;
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
res.setHeader("Content-Type", "application/javascript");
|
|
104
|
+
res.send(content);
|
|
105
|
+
} catch {
|
|
106
|
+
res.status(404).send("Not found");
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
66
110
|
// Serve user's public directory at /
|
|
67
111
|
app.use("/", express.static(path.join(process.cwd(), "public")));
|
|
68
112
|
|
|
@@ -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, SRC_DIR, WATCH_IGNORE } 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, WATCH_IGNORE_FILES } 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";
|
|
@@ -83,8 +83,14 @@ if (process.env.NODE_ENV !== "production") {
|
|
|
83
83
|
// Watch SRC_DIR (configured via vex.config.json `srcDir`, defaults to project root).
|
|
84
84
|
// Skip any path segment that appears in WATCH_IGNORE to avoid reacting to
|
|
85
85
|
// changes inside node_modules, build outputs, or other non-source directories.
|
|
86
|
+
// Individual file patterns can be excluded via `watchIgnoreFiles` in vex.config.json.
|
|
86
87
|
watch(SRC_DIR, { recursive: true }, async (_, filename) => {
|
|
87
|
-
if (
|
|
88
|
+
if (!filename) return;
|
|
89
|
+
if (filename.split(path.sep).some(part => WATCH_IGNORE.has(part))) return;
|
|
90
|
+
const normalizedFilename = filename.replace(/\\/g, "/");
|
|
91
|
+
if (WATCH_IGNORE_FILES.some(pattern => path.matchesGlob(normalizedFilename, pattern))) return;
|
|
92
|
+
|
|
93
|
+
if (filename.endsWith(".vex")) {
|
|
88
94
|
const fullPath = path.join(SRC_DIR, filename);
|
|
89
95
|
|
|
90
96
|
// 1. Evict all in-memory caches for this file
|
|
@@ -100,6 +106,9 @@ if (process.env.NODE_ENV !== "production") {
|
|
|
100
106
|
|
|
101
107
|
// 3. Notify connected browsers to reload
|
|
102
108
|
hmrEmitter.emit("reload", filename);
|
|
109
|
+
} else if (filename.endsWith(".js")) {
|
|
110
|
+
// User utility file changed — reload browsers (served dynamically, no bundle to regenerate)
|
|
111
|
+
hmrEmitter.emit("reload", filename);
|
|
103
112
|
}
|
|
104
113
|
});
|
|
105
114
|
|
|
@@ -145,7 +154,7 @@ const DEFAULT_METADATA = {
|
|
|
145
154
|
* }>
|
|
146
155
|
* }>}
|
|
147
156
|
*/
|
|
148
|
-
const getScriptImports = async (script, isClientSide = false) => {
|
|
157
|
+
const getScriptImports = async (script, isClientSide = false, filePath = null) => {
|
|
149
158
|
const componentRegistry = new Map();
|
|
150
159
|
const imports = {};
|
|
151
160
|
const clientImports = {};
|
|
@@ -182,21 +191,22 @@ const getScriptImports = async (script, isClientSide = false) => {
|
|
|
182
191
|
}
|
|
183
192
|
} else if (defaultImport) {
|
|
184
193
|
// client side default imports and named imports
|
|
185
|
-
const adjustedClientModule = adjustClientModulePath(modulePath, importStatement);
|
|
194
|
+
const adjustedClientModule = adjustClientModulePath(modulePath, importStatement, filePath);
|
|
186
195
|
clientImports[defaultImport || namedImports] = {
|
|
187
196
|
fileUrl,
|
|
188
197
|
originalPath: adjustedClientModule.path,
|
|
189
198
|
importStatement: adjustedClientModule.importStatement,
|
|
199
|
+
originalImportStatement: importStatement,
|
|
190
200
|
};
|
|
191
201
|
} else {
|
|
192
202
|
namedImports.split(",").forEach((name) => {
|
|
193
203
|
const trimmedName = name.trim();
|
|
194
|
-
|
|
195
|
-
const adjustedClientModule = adjustClientModulePath(modulePath, importStatement);
|
|
204
|
+
const adjustedClientModule = adjustClientModulePath(modulePath, importStatement, filePath);
|
|
196
205
|
clientImports[trimmedName] = {
|
|
197
|
-
fileUrl,
|
|
206
|
+
fileUrl,
|
|
198
207
|
originalPath: adjustedClientModule.path,
|
|
199
208
|
importStatement: adjustedClientModule.importStatement,
|
|
209
|
+
originalImportStatement: importStatement,
|
|
200
210
|
};
|
|
201
211
|
});
|
|
202
212
|
}
|
|
@@ -311,7 +321,7 @@ async function _processHtmlFile(filePath) {
|
|
|
311
321
|
}
|
|
312
322
|
|
|
313
323
|
if (clientMatch) {
|
|
314
|
-
const { componentRegistry, clientImports: newClientImports } = await getScriptImports(clientMatch[1], true);
|
|
324
|
+
const { componentRegistry, clientImports: newClientImports } = await getScriptImports(clientMatch[1], true, filePath);
|
|
315
325
|
clientComponents = componentRegistry;
|
|
316
326
|
clientImports = newClientImports;
|
|
317
327
|
}
|
|
@@ -412,6 +422,7 @@ export async function renderHtmlFile(filePath, context = {}, extraComponentData
|
|
|
412
422
|
*/
|
|
413
423
|
function generateClientScriptTags({
|
|
414
424
|
clientCode,
|
|
425
|
+
clientImports = {},
|
|
415
426
|
clientComponentsScripts = [],
|
|
416
427
|
clientComponents = new Map(),
|
|
417
428
|
}) {
|
|
@@ -421,6 +432,13 @@ function generateClientScriptTags({
|
|
|
421
432
|
clientCode = clientCode.replace(`${importStatement};`, '').replace(importStatement, "");
|
|
422
433
|
}
|
|
423
434
|
|
|
435
|
+
// Rewrite framework and user utility imports to browser-accessible paths
|
|
436
|
+
for (const importData of Object.values(clientImports)) {
|
|
437
|
+
if (importData.originalImportStatement && importData.importStatement !== importData.originalImportStatement) {
|
|
438
|
+
clientCode = clientCode.replace(importData.originalImportStatement, importData.importStatement);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
424
442
|
const clientCodeWithoutComponentImports = clientCode
|
|
425
443
|
.split("\n")
|
|
426
444
|
.filter((line) => !/^\s*import\s+.*['"].*\.vex['"]/.test(line))
|
|
@@ -459,11 +477,12 @@ function generateClientScriptTags({
|
|
|
459
477
|
* }>
|
|
460
478
|
*/
|
|
461
479
|
async function renderPage(pagePath, ctx, awaitSuspenseComponents = false, extraComponentData = {}) {
|
|
462
|
-
const {
|
|
463
|
-
html,
|
|
464
|
-
metadata,
|
|
465
|
-
clientCode,
|
|
466
|
-
|
|
480
|
+
const {
|
|
481
|
+
html,
|
|
482
|
+
metadata,
|
|
483
|
+
clientCode,
|
|
484
|
+
clientImports,
|
|
485
|
+
serverComponents,
|
|
467
486
|
clientComponents,
|
|
468
487
|
} = await renderHtmlFile(pagePath, ctx, extraComponentData);
|
|
469
488
|
|
|
@@ -482,6 +501,7 @@ async function renderPage(pagePath, ctx, awaitSuspenseComponents = false, extraC
|
|
|
482
501
|
html: htmlWithComponents,
|
|
483
502
|
metadata,
|
|
484
503
|
clientCode,
|
|
504
|
+
clientImports,
|
|
485
505
|
serverComponents,
|
|
486
506
|
clientComponents,
|
|
487
507
|
suspenseComponents,
|
|
@@ -582,6 +602,7 @@ export async function renderPageWithLayout(pagePath, ctx = {}, awaitSuspenseComp
|
|
|
582
602
|
html: pageHtml,
|
|
583
603
|
metadata,
|
|
584
604
|
clientCode,
|
|
605
|
+
clientImports,
|
|
585
606
|
serverComponents,
|
|
586
607
|
clientComponents,
|
|
587
608
|
suspenseComponents,
|
|
@@ -592,6 +613,7 @@ export async function renderPageWithLayout(pagePath, ctx = {}, awaitSuspenseComp
|
|
|
592
613
|
// Wrap in layout
|
|
593
614
|
const clientScripts = generateClientScriptTags({
|
|
594
615
|
clientCode,
|
|
616
|
+
clientImports,
|
|
595
617
|
clientComponentsScripts,
|
|
596
618
|
clientComponents,
|
|
597
619
|
});
|
package/server/utils/files.js
CHANGED
|
@@ -85,10 +85,24 @@ export const WATCH_IGNORE = new Set([
|
|
|
85
85
|
".next", ".nuxt", ".svelte-kit", ".astro",
|
|
86
86
|
// misc
|
|
87
87
|
"tmp", "temp", ".cache", ".claude",
|
|
88
|
-
// user-defined extras from vex.config.json
|
|
89
|
-
|
|
88
|
+
// user-defined extras from vex.config.json.
|
|
89
|
+
// Simple names (no /, *, .) are treated as directory names here.
|
|
90
|
+
...(_vexConfig.watchIgnore || []).filter(p => !/[\/\*\.]/.test(p)),
|
|
90
91
|
]);
|
|
91
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Glob patterns derived from `watchIgnore` entries in vex.config.json that
|
|
95
|
+
* contain path separators, wildcards, or dots — i.e. file-level patterns.
|
|
96
|
+
*
|
|
97
|
+
* Simple directory names in the same array go to WATCH_IGNORE instead.
|
|
98
|
+
*
|
|
99
|
+
* "watchIgnore": ["utils/legacy.js", "components/wip/**", "wip"]
|
|
100
|
+
* → "wip" added to WATCH_IGNORE (directory name)
|
|
101
|
+
* → "utils/legacy.js" added to WATCH_IGNORE_FILES (glob pattern)
|
|
102
|
+
* → "components/wip/**" added to WATCH_IGNORE_FILES (glob pattern)
|
|
103
|
+
*/
|
|
104
|
+
export const WATCH_IGNORE_FILES = (_vexConfig.watchIgnore || []).filter(p => /[\/\*\.]/.test(p));
|
|
105
|
+
|
|
92
106
|
export const PAGES_DIR = path.resolve(SRC_DIR, "pages");
|
|
93
107
|
export const SERVER_APP_DIR = path.join(FRAMEWORK_DIR, "server");
|
|
94
108
|
export const CLIENT_DIR = path.join(FRAMEWORK_DIR, "client");
|
|
@@ -158,11 +172,39 @@ export async function initializeDirectories() {
|
|
|
158
172
|
* console.log(result.importStatement);
|
|
159
173
|
* // "import userController from '/.app/client/services/reactive.js';"
|
|
160
174
|
*/
|
|
161
|
-
export function adjustClientModulePath(modulePath, importStatement) {
|
|
175
|
+
export function adjustClientModulePath(modulePath, importStatement, componentFilePath = null) {
|
|
162
176
|
if (modulePath.startsWith("/_vexjs/")) {
|
|
163
177
|
return { path: modulePath, importStatement };
|
|
164
178
|
}
|
|
165
179
|
|
|
180
|
+
// User imports — relative (e.g. "../utils/context") or @ alias (e.g. "@/utils/context")
|
|
181
|
+
// — served via /_vexjs/user/
|
|
182
|
+
const isRelative = (modulePath.startsWith("./") || modulePath.startsWith("../")) && componentFilePath;
|
|
183
|
+
const isAtAlias = modulePath.startsWith("@/") || modulePath === "@";
|
|
184
|
+
if (isRelative || isAtAlias) {
|
|
185
|
+
let resolvedPath;
|
|
186
|
+
if (isAtAlias) {
|
|
187
|
+
resolvedPath = path.resolve(SRC_DIR, modulePath.replace(/^@\//, "").replace(/^@$/, ""));
|
|
188
|
+
} else {
|
|
189
|
+
const componentDir = path.dirname(componentFilePath);
|
|
190
|
+
resolvedPath = path.resolve(componentDir, modulePath);
|
|
191
|
+
}
|
|
192
|
+
if (!path.extname(resolvedPath)) {
|
|
193
|
+
if (existsSync(resolvedPath + ".js")) {
|
|
194
|
+
resolvedPath += ".js";
|
|
195
|
+
} else if (existsSync(path.join(resolvedPath, "index.js"))) {
|
|
196
|
+
resolvedPath = path.join(resolvedPath, "index.js");
|
|
197
|
+
} else {
|
|
198
|
+
resolvedPath += ".js";
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const relativePath = path.relative(SRC_DIR, resolvedPath).replace(/\\/g, "/");
|
|
202
|
+
const adjustedPath = `/_vexjs/user/${relativePath}`;
|
|
203
|
+
const adjustedImportStatement = importStatement.replace(modulePath, adjustedPath);
|
|
204
|
+
return { path: adjustedPath, importStatement: adjustedImportStatement };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Framework imports (vex/ and .app/)
|
|
166
208
|
let relative = modulePath.replace(/^vex\//, "").replace(/^\.app\//, "");
|
|
167
209
|
let adjustedPath = `/_vexjs/services/${relative}`;
|
|
168
210
|
|
package/server/utils/template.js
CHANGED
|
@@ -77,9 +77,12 @@ function parseHTMLToNodes(html) {
|
|
|
77
77
|
*/
|
|
78
78
|
function processNode(node, scope, previousRendered = false) {
|
|
79
79
|
if (node.type === "text") {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
// Replace {{expr}} with its value from scope (SSR interpolation).
|
|
81
|
+
// The lookbehind (?<!\\) skips escaped \{{expr}}, which are then
|
|
82
|
+
// unescaped to literal {{expr}} by the second replace.
|
|
83
|
+
node.data = node.data
|
|
84
|
+
.replace(/(?<!\\)\{\{(.+?)\}\}/g, (_, expr) => getDataValue(expr.trim(), scope))
|
|
85
|
+
.replace(/\\\{\{/g, "{{");
|
|
83
86
|
return node;
|
|
84
87
|
}
|
|
85
88
|
|
|
@@ -88,9 +91,9 @@ function processNode(node, scope, previousRendered = false) {
|
|
|
88
91
|
|
|
89
92
|
for (const [attrName, attrValue] of Object.entries(attrs)) {
|
|
90
93
|
if (typeof attrValue === "string") {
|
|
91
|
-
attrs[attrName] = attrValue
|
|
92
|
-
getDataValue(expr.trim(), scope)
|
|
93
|
-
|
|
94
|
+
attrs[attrName] = attrValue
|
|
95
|
+
.replace(/(?<!\\)\{\{(.+?)\}\}/g, (_, expr) => getDataValue(expr.trim(), scope))
|
|
96
|
+
.replace(/\\\{\{/g, "{{");
|
|
94
97
|
}
|
|
95
98
|
}
|
|
96
99
|
|