@cfdez11/vex 0.6.0 → 0.8.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 +22 -11
- package/client/services/navigation.js +6 -0
- package/package.json +1 -1
- package/server/build-static.js +37 -103
- package/server/index.js +22 -11
- package/server/utils/component-processor.js +60 -8
- package/server/utils/esbuild-plugin.js +46 -22
- package/server/utils/files.js +19 -14
package/README.md
CHANGED
|
@@ -128,7 +128,7 @@ Optional file at the project root.
|
|
|
128
128
|
```html
|
|
129
129
|
<!-- pages/example/page.vex -->
|
|
130
130
|
<script server>
|
|
131
|
-
import UserCard from "components/user-card.vex";
|
|
131
|
+
import UserCard from "@/components/user-card.vex";
|
|
132
132
|
|
|
133
133
|
const metadata = { title: "My Page", description: "Page description" };
|
|
134
134
|
|
|
@@ -138,7 +138,7 @@ Optional file at the project root.
|
|
|
138
138
|
</script>
|
|
139
139
|
|
|
140
140
|
<script client>
|
|
141
|
-
import Counter from "components/counter.vex";
|
|
141
|
+
import Counter from "@/components/counter.vex";
|
|
142
142
|
</script>
|
|
143
143
|
|
|
144
144
|
<template>
|
|
@@ -204,11 +204,11 @@ Import them in any page or component:
|
|
|
204
204
|
|
|
205
205
|
```html
|
|
206
206
|
<script server>
|
|
207
|
-
import UserCard from "components/user-card.vex";
|
|
207
|
+
import UserCard from "@/components/user-card.vex";
|
|
208
208
|
</script>
|
|
209
209
|
|
|
210
210
|
<script client>
|
|
211
|
-
import Counter from "components/counter.vex";
|
|
211
|
+
import Counter from "@/components/counter.vex";
|
|
212
212
|
</script>
|
|
213
213
|
|
|
214
214
|
<template>
|
|
@@ -361,8 +361,8 @@ Streams a fallback immediately while a slow component loads:
|
|
|
361
361
|
|
|
362
362
|
```html
|
|
363
363
|
<script server>
|
|
364
|
-
import SlowCard
|
|
365
|
-
import SkeletonCard from "components/skeleton-card.vex";
|
|
364
|
+
import SlowCard from "@/components/slow-card.vex";
|
|
365
|
+
import SkeletonCard from "@/components/skeleton-card.vex";
|
|
366
366
|
</script>
|
|
367
367
|
|
|
368
368
|
<template>
|
|
@@ -553,12 +553,23 @@ Reference the stylesheet in `root.html`:
|
|
|
553
553
|
|
|
554
554
|
## 🔧 Framework API
|
|
555
555
|
|
|
556
|
-
###
|
|
556
|
+
### Import conventions
|
|
557
557
|
|
|
558
|
-
|
|
|
559
|
-
|
|
560
|
-
| `vex/reactive` |
|
|
561
|
-
| `
|
|
558
|
+
| Pattern | Example | Behaviour |
|
|
559
|
+
|---------|---------|-----------|
|
|
560
|
+
| `vex/*` | `import { reactive } from "vex/reactive"` | Framework singleton — shared instance across all components |
|
|
561
|
+
| `@/*` | `import store from "@/utils/store.js"` | Project alias for your source root — also a singleton |
|
|
562
|
+
| `./` / `../` | `import { fn } from "./helpers.js"` | Relative user file — also a singleton |
|
|
563
|
+
| npm bare specifier | `import { format } from "date-fns"` | Bundled inline by esbuild |
|
|
564
|
+
|
|
565
|
+
All user JS files (`@/` and relative) are pre-bundled at startup: npm packages are inlined, while `vex/*`, `@/*`, and relative imports stay external. The browser's ES module cache guarantees every import of the same file returns the same instance — enabling shared reactive state across components without a dedicated store library.
|
|
566
|
+
|
|
567
|
+
### Client script imports
|
|
568
|
+
|
|
569
|
+
| Import | Description |
|
|
570
|
+
|--------|-------------|
|
|
571
|
+
| `vex/reactive` | Reactivity engine (`reactive`, `computed`, `effect`, `watch`) |
|
|
572
|
+
| `vex/navigation` | Router utilities (`useRouteParams`, `useQueryParams`) |
|
|
562
573
|
|
|
563
574
|
### Server script hooks
|
|
564
575
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Barrel file for vex/navigation imports.
|
|
2
|
+
// Components import { useRouteParams } from "vex/navigation" which esbuild
|
|
3
|
+
// rewrites to /_vexjs/services/navigation.js (external). All re-exports go
|
|
4
|
+
// through navigation/index.js so the browser module cache ensures the same
|
|
5
|
+
// runtime instance is shared with the index.js bootstrap.
|
|
6
|
+
export { useRouteParams, useQueryParams, navigate } from "./navigation/index.js";
|
package/package.json
CHANGED
package/server/build-static.js
CHANGED
|
@@ -5,11 +5,10 @@ import { build } from "./utils/component-processor.js";
|
|
|
5
5
|
import {
|
|
6
6
|
initializeDirectories,
|
|
7
7
|
CLIENT_DIR,
|
|
8
|
-
SRC_DIR,
|
|
9
8
|
PROJECT_ROOT,
|
|
10
9
|
getRootTemplate,
|
|
11
|
-
WATCH_IGNORE,
|
|
12
10
|
generateComponentId,
|
|
11
|
+
USER_GENERATED_DIR,
|
|
13
12
|
} from "./utils/files.js";
|
|
14
13
|
|
|
15
14
|
const GENERATED_DIR = path.join(PROJECT_ROOT, ".vexjs");
|
|
@@ -47,32 +46,50 @@ const frameworkScripts = [
|
|
|
47
46
|
shell = shell.replace("</head>", ` ${frameworkScripts}\n</head>`);
|
|
48
47
|
await fs.writeFile(path.join(DIST_DIR, "index.html"), shell, "utf-8");
|
|
49
48
|
|
|
50
|
-
// Step 4: Copy framework
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
// Step 4: Copy static framework assets (favicon.ico, app.webmanifest) → dist/_vexjs/
|
|
50
|
+
// JS runtime files live in .vexjs/services/ (copied in step 5) — no need to
|
|
51
|
+
// copy CLIENT_DIR/services/ separately.
|
|
52
|
+
console.log("📦 Copying framework assets...");
|
|
53
|
+
for (const asset of ["favicon.ico", "app.webmanifest"]) {
|
|
54
|
+
try {
|
|
55
|
+
await fs.copyFile(
|
|
56
|
+
path.join(CLIENT_DIR, asset),
|
|
57
|
+
path.join(DIST_DIR, "_vexjs", asset)
|
|
58
|
+
);
|
|
59
|
+
} catch {
|
|
60
|
+
// asset not present — skip
|
|
61
|
+
}
|
|
62
|
+
}
|
|
53
63
|
|
|
54
|
-
// Step 5: Copy generated
|
|
55
|
-
|
|
64
|
+
// Step 5: Copy generated services → dist/_vexjs/services/
|
|
65
|
+
// .vexjs/services/ already contains both framework JS (copied by initializeDirectories)
|
|
66
|
+
// and generated files (_routes.js). One copy covers everything.
|
|
67
|
+
console.log("📦 Copying services...");
|
|
56
68
|
await fs.cp(
|
|
57
|
-
path.join(GENERATED_DIR, "
|
|
58
|
-
path.join(DIST_DIR, "_vexjs", "
|
|
69
|
+
path.join(GENERATED_DIR, "services"),
|
|
70
|
+
path.join(DIST_DIR, "_vexjs", "services"),
|
|
59
71
|
{ recursive: true }
|
|
60
72
|
);
|
|
61
73
|
|
|
62
|
-
// Step 6: Copy generated
|
|
63
|
-
|
|
64
|
-
console.log("📦 Copying generated services...");
|
|
74
|
+
// Step 6: Copy generated component bundles → dist/_vexjs/_components/
|
|
75
|
+
console.log("📦 Copying component bundles...");
|
|
65
76
|
await fs.cp(
|
|
66
|
-
path.join(GENERATED_DIR, "
|
|
67
|
-
path.join(DIST_DIR, "_vexjs", "
|
|
77
|
+
path.join(GENERATED_DIR, "_components"),
|
|
78
|
+
path.join(DIST_DIR, "_vexjs", "_components"),
|
|
68
79
|
{ recursive: true }
|
|
69
80
|
);
|
|
70
81
|
|
|
71
|
-
// Step 7: Copy user JS files
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
// Step 7: Copy pre-bundled user JS files → dist/_vexjs/user/
|
|
83
|
+
// build() already ran esbuild on every user .js file → USER_GENERATED_DIR.
|
|
84
|
+
// npm packages are bundled inline; vex/*, @/*, relative imports stay external.
|
|
85
|
+
console.log("📦 Copying pre-bundled user JS files...");
|
|
86
|
+
try {
|
|
87
|
+
await fs.cp(USER_GENERATED_DIR, path.join(DIST_DIR, "_vexjs", "user"), { recursive: true });
|
|
88
|
+
} catch {
|
|
89
|
+
// no user JS files — that's fine
|
|
90
|
+
}
|
|
74
91
|
|
|
75
|
-
// Step 8: Copy public/ → dist/
|
|
92
|
+
// Step 8: Copy public/ → dist/
|
|
76
93
|
console.log("📦 Copying public assets...");
|
|
77
94
|
const publicDir = path.join(PROJECT_ROOT, "public");
|
|
78
95
|
try {
|
|
@@ -81,7 +98,7 @@ try {
|
|
|
81
98
|
// no public/ directory — that's fine
|
|
82
99
|
}
|
|
83
100
|
|
|
84
|
-
// Step 9: Copy pre-rendered
|
|
101
|
+
// Step 9: Copy pre-rendered SSG pages
|
|
85
102
|
const CACHE_DIR = path.join(GENERATED_DIR, "_cache");
|
|
86
103
|
const ssgRoutes = serverRoutes.filter(
|
|
87
104
|
(r) => r.meta.revalidate === "never" || r.meta.revalidate === false
|
|
@@ -103,7 +120,7 @@ if (ssgRoutes.length > 0) {
|
|
|
103
120
|
}
|
|
104
121
|
}
|
|
105
122
|
|
|
106
|
-
// Step 10: Report SSR-only routes
|
|
123
|
+
// Step 10: Report SSR-only routes (skipped in static build)
|
|
107
124
|
const ssrOnlyRoutes = serverRoutes.filter((r) => r.meta.ssr);
|
|
108
125
|
if (ssrOnlyRoutes.length > 0) {
|
|
109
126
|
console.warn("\n⚠️ The following routes require a server and were NOT included in the static build:");
|
|
@@ -117,87 +134,4 @@ console.log("✅ Static build complete! Output: dist/");
|
|
|
117
134
|
console.log("\nTo serve locally: npx serve dist");
|
|
118
135
|
console.log("Static host note: configure your host to serve dist/index.html for all 404s (SPA fallback).");
|
|
119
136
|
|
|
120
|
-
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Recursively walks SRC_DIR, rewrites imports in every .js file,
|
|
124
|
-
* and writes results to destDir preserving the relative path structure.
|
|
125
|
-
*
|
|
126
|
-
* Skips directories listed in WATCH_IGNORE (node_modules, dist, .vexjs, etc.).
|
|
127
|
-
*
|
|
128
|
-
* @param {string} srcDir Absolute path to user source root (SRC_DIR)
|
|
129
|
-
* @param {string} destDir Absolute path to dist/_vexjs/user/
|
|
130
|
-
*/
|
|
131
|
-
async function copyUserJsFiles(srcDir, destDir) {
|
|
132
|
-
let entries;
|
|
133
|
-
try {
|
|
134
|
-
entries = await fs.readdir(srcDir, { withFileTypes: true });
|
|
135
|
-
} catch {
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
for (const entry of entries) {
|
|
140
|
-
if (WATCH_IGNORE.has(entry.name)) continue;
|
|
141
|
-
|
|
142
|
-
const fullSrc = path.join(srcDir, entry.name);
|
|
143
|
-
const relToSrcDir = path.relative(SRC_DIR, fullSrc).replace(/\\/g, "/");
|
|
144
|
-
const fullDest = path.join(destDir, relToSrcDir);
|
|
145
|
-
|
|
146
|
-
if (entry.isDirectory()) {
|
|
147
|
-
await copyUserJsFiles(fullSrc, destDir);
|
|
148
|
-
} else if (entry.name.endsWith(".js")) {
|
|
149
|
-
let content;
|
|
150
|
-
try {
|
|
151
|
-
content = await fs.readFile(fullSrc, "utf-8");
|
|
152
|
-
} catch {
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
137
|
|
|
156
|
-
content = rewriteUserImports(content, fullSrc, srcDir);
|
|
157
|
-
|
|
158
|
-
await fs.mkdir(path.dirname(fullDest), { recursive: true });
|
|
159
|
-
await fs.writeFile(fullDest, content, "utf-8");
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Rewrites import paths in a user JS file so they work in the browser.
|
|
166
|
-
* Mirrors the runtime rewriting done by the /_vexjs/user/* Express handler.
|
|
167
|
-
*
|
|
168
|
-
* - `vex/` and `.app/` → `/_vexjs/services/`
|
|
169
|
-
* - `@/` (project alias) → `/_vexjs/user/`
|
|
170
|
-
* - relative `./` or `../` → `/_vexjs/user/`
|
|
171
|
-
* - external bare specifiers (e.g. npm packages) → left as-is
|
|
172
|
-
*
|
|
173
|
-
* @param {string} content File source
|
|
174
|
-
* @param {string} filePath Absolute path of the file being rewritten
|
|
175
|
-
* @param {string} srcDir Absolute SRC_DIR root
|
|
176
|
-
* @returns {string} Rewritten source
|
|
177
|
-
*/
|
|
178
|
-
function rewriteUserImports(content, filePath, srcDir) {
|
|
179
|
-
return content.replace(
|
|
180
|
-
/^(\s*import\s+[^'"]*from\s+)['"]([^'"]+)['"]/gm,
|
|
181
|
-
(match, prefix, modulePath) => {
|
|
182
|
-
if (modulePath.startsWith("vex/") || modulePath.startsWith(".app/")) {
|
|
183
|
-
let mod = modulePath.replace(/^vex\//, "").replace(/^\.app\//, "");
|
|
184
|
-
if (!path.extname(mod)) mod += ".js";
|
|
185
|
-
return `${prefix}'/_vexjs/services/${mod}'`;
|
|
186
|
-
}
|
|
187
|
-
if (modulePath.startsWith("@/") || modulePath === "@") {
|
|
188
|
-
let resolved = path.resolve(srcDir, modulePath.replace(/^@\//, "").replace(/^@$/, ""));
|
|
189
|
-
if (!path.extname(resolved)) resolved += ".js";
|
|
190
|
-
const rel = path.relative(srcDir, resolved).replace(/\\/g, "/");
|
|
191
|
-
return `${prefix}'/_vexjs/user/${rel}'`;
|
|
192
|
-
}
|
|
193
|
-
if (modulePath.startsWith("./") || modulePath.startsWith("../")) {
|
|
194
|
-
const fileDir = path.dirname(filePath);
|
|
195
|
-
let resolved = path.resolve(fileDir, modulePath);
|
|
196
|
-
if (!path.extname(resolved)) resolved += ".js";
|
|
197
|
-
const rel = path.relative(srcDir, resolved).replace(/\\/g, "/");
|
|
198
|
-
return `${prefix}'/_vexjs/user/${rel}'`;
|
|
199
|
-
}
|
|
200
|
-
return match;
|
|
201
|
-
}
|
|
202
|
-
);
|
|
203
|
-
}
|
package/server/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import express from "express";
|
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { pathToFileURL } from "url";
|
|
5
5
|
import { handlePageRequest, revalidatePath } from "./utils/router.js";
|
|
6
|
-
import { initializeDirectories, CLIENT_DIR } from "./utils/files.js";
|
|
6
|
+
import { initializeDirectories, CLIENT_DIR, USER_GENERATED_DIR } from "./utils/files.js";
|
|
7
7
|
|
|
8
8
|
await initializeDirectories();
|
|
9
9
|
|
|
@@ -29,8 +29,6 @@ if (process.env.NODE_ENV === "production") {
|
|
|
29
29
|
const app = express();
|
|
30
30
|
|
|
31
31
|
// Serve generated client component bundles at /_vexjs/_components/
|
|
32
|
-
// Must be registered before the broader /_vexjs static mount below so that
|
|
33
|
-
// .vexjs/_components/ takes priority over anything in CLIENT_DIR/_components/.
|
|
34
32
|
app.use(
|
|
35
33
|
"/_vexjs/_components",
|
|
36
34
|
express.static(path.join(process.cwd(), ".vexjs", "_components"), {
|
|
@@ -42,9 +40,9 @@ app.use(
|
|
|
42
40
|
})
|
|
43
41
|
);
|
|
44
42
|
|
|
45
|
-
// Serve generated
|
|
46
|
-
//
|
|
47
|
-
//
|
|
43
|
+
// Serve framework runtime JS + generated files (_routes.js) at /_vexjs/services/
|
|
44
|
+
// initializeDirectories() pre-populates this dir with framework files; build()
|
|
45
|
+
// adds generated files (_routes.js). Single source of truth for all /_vexjs/services/*.
|
|
48
46
|
app.use(
|
|
49
47
|
"/_vexjs/services",
|
|
50
48
|
express.static(path.join(process.cwd(), ".vexjs", "services"), {
|
|
@@ -56,10 +54,24 @@ app.use(
|
|
|
56
54
|
})
|
|
57
55
|
);
|
|
58
56
|
|
|
59
|
-
// Serve
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
57
|
+
// Serve pre-bundled user JS utility files at /_vexjs/user/
|
|
58
|
+
// Registered before the generic /_vexjs mount so requests don't fall through
|
|
59
|
+
// to CLIENT_DIR unnecessarily. esbuild bundles each file with npm packages
|
|
60
|
+
// inlined; vex/*, @/*, and relative user imports stay external (singletons).
|
|
61
|
+
app.use(
|
|
62
|
+
"/_vexjs/user",
|
|
63
|
+
express.static(USER_GENERATED_DIR, {
|
|
64
|
+
setHeaders(res, filePath) {
|
|
65
|
+
if (filePath.endsWith(".js")) {
|
|
66
|
+
res.setHeader("Content-Type", "application/javascript");
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Serve static framework assets (favicon.ico, app.webmanifest) from CLIENT_DIR.
|
|
73
|
+
// Runtime JS files (reactive.js, index.js, etc.) are already in .vexjs/services/
|
|
74
|
+
// via initializeDirectories() and are served by the /_vexjs/services route above.
|
|
63
75
|
app.use(
|
|
64
76
|
"/_vexjs",
|
|
65
77
|
express.static(CLIENT_DIR, {
|
|
@@ -71,7 +83,6 @@ app.use(
|
|
|
71
83
|
})
|
|
72
84
|
);
|
|
73
85
|
|
|
74
|
-
|
|
75
86
|
// Serve user's public directory at /
|
|
76
87
|
app.use("/", express.static(path.join(process.cwd(), "public")));
|
|
77
88
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { watch } from "fs";
|
|
2
|
+
import fs from "fs/promises";
|
|
2
3
|
import path from "path";
|
|
3
4
|
import esbuild from "esbuild";
|
|
4
5
|
import { compileTemplateToHTML } from "./template.js";
|
|
5
|
-
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, CLIENT_COMPONENTS_DIR } from "./files.js";
|
|
6
|
+
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, CLIENT_COMPONENTS_DIR, USER_GENERATED_DIR } from "./files.js";
|
|
6
7
|
import { renderComponents } from "./streaming.js";
|
|
7
8
|
import { getRevalidateSeconds } from "./cache.js";
|
|
8
9
|
import { withCache } from "./data-cache.js";
|
|
@@ -109,15 +110,12 @@ if (process.env.NODE_ENV !== "production") {
|
|
|
109
110
|
// 3. Notify connected browsers to reload
|
|
110
111
|
hmrEmitter.emit("reload", filename);
|
|
111
112
|
} else if (filename.endsWith(".js")) {
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
// re-bundling all components — we cannot know which bundles include this
|
|
115
|
-
// file without tracking the full import graph. Rebuilding all components
|
|
116
|
-
// is fast enough with esbuild (sub-millisecond per file).
|
|
113
|
+
// Rebuild the changed user JS file so npm imports are re-bundled.
|
|
114
|
+
const fullPath = path.join(SRC_DIR, filename);
|
|
117
115
|
try {
|
|
118
|
-
await
|
|
116
|
+
await buildUserFile(fullPath);
|
|
119
117
|
} catch (e) {
|
|
120
|
-
console.error(`[HMR]
|
|
118
|
+
console.error(`[HMR] Failed to rebuild user file ${filename}:`, e.message);
|
|
121
119
|
}
|
|
122
120
|
hmrEmitter.emit("reload", filename);
|
|
123
121
|
}
|
|
@@ -1557,6 +1555,59 @@ export async function generateRoutes() {
|
|
|
1557
1555
|
return { serverRoutes };
|
|
1558
1556
|
}
|
|
1559
1557
|
|
|
1558
|
+
/**
|
|
1559
|
+
* Bundles a single user JS file with esbuild so npm bare-specifier imports
|
|
1560
|
+
* are resolved and inlined, while vex/*, @/*, and relative user imports stay
|
|
1561
|
+
* external (singletons served at /_vexjs/user/*).
|
|
1562
|
+
*
|
|
1563
|
+
* Output is written to USER_GENERATED_DIR preserving the SRC_DIR-relative path.
|
|
1564
|
+
*
|
|
1565
|
+
* @param {string} filePath - Absolute path to the user .js file.
|
|
1566
|
+
*/
|
|
1567
|
+
async function buildUserFile(filePath) {
|
|
1568
|
+
const rel = path.relative(SRC_DIR, filePath).replace(/\\/g, "/");
|
|
1569
|
+
const outfile = path.join(USER_GENERATED_DIR, rel);
|
|
1570
|
+
await esbuild.build({
|
|
1571
|
+
entryPoints: [filePath],
|
|
1572
|
+
bundle: true,
|
|
1573
|
+
format: "esm",
|
|
1574
|
+
outfile,
|
|
1575
|
+
plugins: [createVexAliasPlugin()],
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
/**
|
|
1580
|
+
* Recursively finds all .js files in SRC_DIR (excluding WATCH_IGNORE dirs)
|
|
1581
|
+
* and prebundles each one via buildUserFile.
|
|
1582
|
+
*
|
|
1583
|
+
* Called during build() so that user utility files are ready before the server
|
|
1584
|
+
* starts serving /_vexjs/user/* from the pre-built static output.
|
|
1585
|
+
*/
|
|
1586
|
+
async function buildUserFiles() {
|
|
1587
|
+
const collect = async (dir) => {
|
|
1588
|
+
let entries;
|
|
1589
|
+
try {
|
|
1590
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1591
|
+
} catch {
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
await Promise.all(entries.map(async (entry) => {
|
|
1595
|
+
if (WATCH_IGNORE.has(entry.name)) return;
|
|
1596
|
+
const full = path.join(dir, entry.name);
|
|
1597
|
+
if (entry.isDirectory()) {
|
|
1598
|
+
await collect(full);
|
|
1599
|
+
} else if (entry.name.endsWith(".js")) {
|
|
1600
|
+
try {
|
|
1601
|
+
await buildUserFile(full);
|
|
1602
|
+
} catch (e) {
|
|
1603
|
+
console.error(`[build] Failed to bundle user file ${full}:`, e.message);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
}));
|
|
1607
|
+
};
|
|
1608
|
+
await collect(SRC_DIR);
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1560
1611
|
/**
|
|
1561
1612
|
* Single-pass build entry point.
|
|
1562
1613
|
*
|
|
@@ -1573,5 +1624,6 @@ export async function generateRoutes() {
|
|
|
1573
1624
|
*/
|
|
1574
1625
|
export async function build() {
|
|
1575
1626
|
await generateComponentsAndFillCache();
|
|
1627
|
+
await buildUserFiles();
|
|
1576
1628
|
return generateRoutes();
|
|
1577
1629
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
-
import { SRC_DIR } from "./files.js";
|
|
2
|
+
import { SRC_DIR, PROJECT_ROOT } from "./files.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Creates the VexJS esbuild alias plugin.
|
|
@@ -17,9 +17,8 @@ import { SRC_DIR } from "./files.js";
|
|
|
17
17
|
*
|
|
18
18
|
* ─── Three categories of imports ────────────────────────────────────────────
|
|
19
19
|
*
|
|
20
|
-
* 1. Framework singletons (vex/*
|
|
20
|
+
* 1. Framework singletons (vex/*)
|
|
21
21
|
* Examples: `import { reactive } from 'vex/reactive'`
|
|
22
|
-
* `import { html } from '.app/html'`
|
|
23
22
|
*
|
|
24
23
|
* These are framework runtime files served statically at /_vexjs/services/.
|
|
25
24
|
* They MUST be marked external so every component shares the same instance
|
|
@@ -33,16 +32,20 @@ import { SRC_DIR } from "./files.js";
|
|
|
33
32
|
* 2. Project alias (@/*)
|
|
34
33
|
* Example: `import { counter } from '@/utils/counter'`
|
|
35
34
|
*
|
|
36
|
-
* @/ is a shorthand for the project SRC_DIR root. These are
|
|
37
|
-
*
|
|
38
|
-
*
|
|
35
|
+
* @/ is a shorthand for the project SRC_DIR root. These files are served as
|
|
36
|
+
* singleton modules at /_vexjs/user/ — the browser's ES module cache ensures
|
|
37
|
+
* all components share the same instance (same reactive state, same store).
|
|
39
38
|
*
|
|
40
39
|
* 3. Relative imports (./ and ../)
|
|
41
40
|
* Example: `import { fn } from './helpers'`
|
|
42
41
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
42
|
+
* Treated the same as @/ — marked external and served at /_vexjs/user/.
|
|
43
|
+
* This gives the same singleton guarantee as @/ imports: two components that
|
|
44
|
+
* import the same file via different relative paths both resolve to the same
|
|
45
|
+
* URL, so the browser module cache returns the same instance.
|
|
46
|
+
*
|
|
47
|
+
* Note: .vex component imports are stripped from clientImports before
|
|
48
|
+
* reaching esbuild, so this hook only fires for .js user utility files.
|
|
46
49
|
*
|
|
47
50
|
* 4. npm packages (bare specifiers like 'lodash', 'date-fns')
|
|
48
51
|
* Also resolved automatically by esbuild via node_modules lookup.
|
|
@@ -63,23 +66,44 @@ export function createVexAliasPlugin() {
|
|
|
63
66
|
return { path: `/_vexjs/services/${mod}`, external: true };
|
|
64
67
|
});
|
|
65
68
|
|
|
66
|
-
// ── Category
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
// ── Category 2: @/ project alias ─────────────────────────────────────
|
|
70
|
+
// Matches: '@/utils/counter', '@/store/ui-state', etc.
|
|
71
|
+
//
|
|
72
|
+
// These are user JS utilities that must behave as singletons — all
|
|
73
|
+
// components on a page must share the SAME module instance (same reactive
|
|
74
|
+
// state, same store). If esbuild inlined them, each component bundle would
|
|
75
|
+
// get its own copy and reactive state would not propagate across components.
|
|
76
|
+
//
|
|
77
|
+
// Solution: mark as external and rewrite to the browser-accessible URL
|
|
78
|
+
// /_vexjs/user/<path>.js. The dev server serves those files on-the-fly with
|
|
79
|
+
// import rewriting; the static build pre-copies them to dist/_vexjs/user/.
|
|
80
|
+
// The browser's ES module cache ensures a single instance is shared.
|
|
81
|
+
build.onResolve({ filter: /^@\// }, (args) => {
|
|
82
|
+
let mod = args.path.slice(2); // strip leading @/
|
|
71
83
|
if (!path.extname(mod)) mod += ".js";
|
|
72
|
-
return { path: `/_vexjs/
|
|
84
|
+
return { path: `/_vexjs/user/${mod}`, external: true };
|
|
73
85
|
});
|
|
74
86
|
|
|
75
|
-
// ── Category
|
|
76
|
-
// Matches: '
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
87
|
+
// ── Category 3: relative imports (./ and ../) ─────────────────────────
|
|
88
|
+
// Matches: './helpers', '../utils/format', etc.
|
|
89
|
+
//
|
|
90
|
+
// User JS files imported relatively are also served as singleton modules
|
|
91
|
+
// at /_vexjs/user/<resolved-path>.js. This mirrors Vue + Vite: every source
|
|
92
|
+
// file gets its own URL, and the browser module cache ensures the same file
|
|
93
|
+
// is always the same instance regardless of how it was imported.
|
|
94
|
+
//
|
|
95
|
+
// Files outside SRC_DIR (e.g. node_modules reached via ../../) fall through
|
|
96
|
+
// to esbuild's default resolver and are bundled inline as usual.
|
|
97
|
+
build.onResolve({ filter: /^\.\.?\// }, (args) => {
|
|
98
|
+
let resolved = path.resolve(args.resolveDir, args.path);
|
|
81
99
|
if (!path.extname(resolved)) resolved += ".js";
|
|
82
|
-
|
|
100
|
+
|
|
101
|
+
// Only intercept .js user files — anything else (CSS, JSON, non-user) falls through
|
|
102
|
+
if (!resolved.endsWith(".js")) return;
|
|
103
|
+
if (!resolved.startsWith(SRC_DIR) && !resolved.startsWith(PROJECT_ROOT)) return;
|
|
104
|
+
|
|
105
|
+
const rel = path.relative(SRC_DIR, resolved).replace(/\\/g, "/");
|
|
106
|
+
return { path: `/_vexjs/user/${rel}`, external: true };
|
|
83
107
|
});
|
|
84
108
|
},
|
|
85
109
|
};
|
package/server/utils/files.js
CHANGED
|
@@ -111,7 +111,7 @@ export const CLIENT_SERVICES_DIR = path.join(CLIENT_DIR, "services");
|
|
|
111
111
|
const GENERATED_DIR = path.join(PROJECT_ROOT, ".vexjs");
|
|
112
112
|
const CACHE_DIR = path.join(GENERATED_DIR, "_cache");
|
|
113
113
|
export const CLIENT_COMPONENTS_DIR = path.join(GENERATED_DIR, "_components");
|
|
114
|
-
const
|
|
114
|
+
export const USER_GENERATED_DIR = path.join(GENERATED_DIR, "user");
|
|
115
115
|
const ROOT_HTML_USER = path.join(PROJECT_ROOT, "root.html");
|
|
116
116
|
const ROOT_HTML_DEFAULT = path.join(FRAMEWORK_DIR, "server", "root.html");
|
|
117
117
|
export const ROOT_HTML_DIR = ROOT_HTML_USER;
|
|
@@ -133,13 +133,21 @@ export const ROOT_HTML_DIR = ROOT_HTML_USER;
|
|
|
133
133
|
*/
|
|
134
134
|
export async function initializeDirectories() {
|
|
135
135
|
try {
|
|
136
|
+
const servicesDir = path.join(GENERATED_DIR, "services");
|
|
136
137
|
await Promise.all([
|
|
137
138
|
fs.mkdir(GENERATED_DIR, { recursive: true }),
|
|
138
139
|
fs.mkdir(CACHE_DIR, { recursive: true }),
|
|
139
140
|
fs.mkdir(CLIENT_COMPONENTS_DIR, { recursive: true }),
|
|
140
|
-
fs.mkdir(
|
|
141
|
+
fs.mkdir(USER_GENERATED_DIR, { recursive: true }),
|
|
142
|
+
fs.mkdir(servicesDir, { recursive: true }),
|
|
141
143
|
]);
|
|
142
144
|
|
|
145
|
+
// Copy framework client runtime files into .vexjs/services/ so they are
|
|
146
|
+
// served by the /_vexjs/services static route alongside generated files
|
|
147
|
+
// like _routes.js. Generated files (prefixed with _) are written later by
|
|
148
|
+
// the build step and overwrite any stale copies here.
|
|
149
|
+
await fs.cp(CLIENT_SERVICES_DIR, servicesDir, { recursive: true });
|
|
150
|
+
|
|
143
151
|
return true;
|
|
144
152
|
} catch (err) {
|
|
145
153
|
console.error("Failed to create cache directory:", err);
|
|
@@ -165,12 +173,10 @@ export async function initializeDirectories() {
|
|
|
165
173
|
*
|
|
166
174
|
* @example
|
|
167
175
|
* const result = adjustClientModulePath(
|
|
168
|
-
* '
|
|
169
|
-
* "import
|
|
176
|
+
* 'vex/reactive',
|
|
177
|
+
* "import { reactive } from 'vex/reactive';"
|
|
170
178
|
* );
|
|
171
|
-
* console.log(result.path); // '
|
|
172
|
-
* console.log(result.importStatement);
|
|
173
|
-
* // "import userController from '/.app/client/services/reactive.js';"
|
|
179
|
+
* console.log(result.path); // '/_vexjs/services/reactive.js'
|
|
174
180
|
*/
|
|
175
181
|
export function adjustClientModulePath(modulePath, importStatement, componentFilePath = null) {
|
|
176
182
|
if (modulePath.startsWith("/_vexjs/")) {
|
|
@@ -204,8 +210,8 @@ export function adjustClientModulePath(modulePath, importStatement, componentFil
|
|
|
204
210
|
return { path: adjustedPath, importStatement: adjustedImportStatement };
|
|
205
211
|
}
|
|
206
212
|
|
|
207
|
-
// Framework imports (vex/
|
|
208
|
-
let relative = modulePath.replace(/^vex\//, "")
|
|
213
|
+
// Framework imports (vex/)
|
|
214
|
+
let relative = modulePath.replace(/^vex\//, "");
|
|
209
215
|
let adjustedPath = `/_vexjs/services/${relative}`;
|
|
210
216
|
|
|
211
217
|
// Auto-resolve directory → index.js, bare name → .js
|
|
@@ -817,10 +823,8 @@ export async function getImportData(importPath, callerFilePath = null) {
|
|
|
817
823
|
resolvedPath = path.resolve(FRAMEWORK_DIR, importPath.replace("vex/server/", "server/"));
|
|
818
824
|
} else if (importPath.startsWith("vex/")) {
|
|
819
825
|
resolvedPath = path.resolve(FRAMEWORK_DIR, "client/services", importPath.replace("vex/", ""));
|
|
820
|
-
} else if (importPath.startsWith("
|
|
821
|
-
resolvedPath = path.resolve(
|
|
822
|
-
} else if (importPath.startsWith(".app/")) {
|
|
823
|
-
resolvedPath = path.resolve(FRAMEWORK_DIR, importPath.replace(".app/", ""));
|
|
826
|
+
} else if (importPath.startsWith("@/") || importPath === "@") {
|
|
827
|
+
resolvedPath = path.resolve(SRC_DIR, importPath.replace(/^@\//, "").replace(/^@$/, ""));
|
|
824
828
|
} else if ((importPath.startsWith("./") || importPath.startsWith("../")) && callerFilePath) {
|
|
825
829
|
// Relative import — resolve against the caller component's directory, not ROOT_DIR.
|
|
826
830
|
// Without this, `import Foo from './foo.vex'` inside a nested component would be
|
|
@@ -837,4 +841,5 @@ export async function getImportData(importPath, callerFilePath = null) {
|
|
|
837
841
|
|
|
838
842
|
const fileUrl = pathToFileURL(resolvedPath).href;
|
|
839
843
|
return { path: resolvedPath, fileUrl, importPath };
|
|
840
|
-
}
|
|
844
|
+
}
|
|
845
|
+
|