@cfdez11/vex 0.3.0 ā 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/package.json +1 -1
- package/server/build-static.js +202 -0
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
|
|
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
|
+
}
|