@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfdez11/vex",
3
- "version": "0.3.0",
3
+ "version": "0.4.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",
@@ -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
+ }