@cfdez11/vex 0.8.3 → 0.9.1
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/dist/bin/vex.js +3 -0
- package/dist/client/services/cache.js +1 -0
- package/dist/client/services/hmr-client.js +1 -0
- package/dist/client/services/html.js +1 -0
- package/dist/client/services/hydrate-client-components.js +1 -0
- package/dist/client/services/hydrate.js +1 -0
- package/dist/client/services/index.js +1 -0
- package/dist/client/services/navigation/create-layouts.js +1 -0
- package/dist/client/services/navigation/create-navigation.js +1 -0
- package/dist/client/services/navigation/index.js +1 -0
- package/dist/client/services/navigation/link-interceptor.js +1 -0
- package/dist/client/services/navigation/metadata.js +1 -0
- package/dist/client/services/navigation/navigate.js +1 -0
- package/dist/client/services/navigation/prefetch.js +1 -0
- package/dist/client/services/navigation/render-page.js +1 -0
- package/dist/client/services/navigation/render-ssr.js +1 -0
- package/dist/client/services/navigation/router.js +1 -0
- package/dist/client/services/navigation/use-query-params.js +1 -0
- package/dist/client/services/navigation/use-route-params.js +1 -0
- package/dist/client/services/navigation.js +1 -0
- package/dist/client/services/reactive.js +1 -0
- package/dist/server/build-static.js +6 -0
- package/dist/server/index.js +4 -0
- package/dist/server/prebuild.js +1 -0
- package/dist/server/utils/cache.js +1 -0
- package/dist/server/utils/component-processor.js +68 -0
- package/dist/server/utils/data-cache.js +1 -0
- package/dist/server/utils/esbuild-plugin.js +1 -0
- package/dist/server/utils/files.js +28 -0
- package/dist/server/utils/hmr.js +1 -0
- package/dist/server/utils/router.js +11 -0
- package/dist/server/utils/streaming.js +1 -0
- package/dist/server/utils/template.js +1 -0
- package/package.json +8 -7
- package/bin/vex.js +0 -69
- package/client/favicon.ico +0 -0
- package/client/services/cache.js +0 -55
- package/client/services/hmr-client.js +0 -22
- package/client/services/html.js +0 -378
- package/client/services/hydrate-client-components.js +0 -97
- package/client/services/hydrate.js +0 -25
- package/client/services/index.js +0 -9
- package/client/services/navigation/create-layouts.js +0 -172
- package/client/services/navigation/create-navigation.js +0 -103
- package/client/services/navigation/index.js +0 -8
- package/client/services/navigation/link-interceptor.js +0 -39
- package/client/services/navigation/metadata.js +0 -23
- package/client/services/navigation/navigate.js +0 -64
- package/client/services/navigation/prefetch.js +0 -43
- package/client/services/navigation/render-page.js +0 -45
- package/client/services/navigation/render-ssr.js +0 -157
- package/client/services/navigation/router.js +0 -48
- package/client/services/navigation/use-query-params.js +0 -225
- package/client/services/navigation/use-route-params.js +0 -76
- package/client/services/navigation.js +0 -6
- package/client/services/reactive.js +0 -247
- package/server/build-static.js +0 -138
- package/server/index.js +0 -135
- package/server/prebuild.js +0 -13
- package/server/utils/cache.js +0 -89
- package/server/utils/component-processor.js +0 -1631
- package/server/utils/data-cache.js +0 -62
- package/server/utils/delay.js +0 -1
- package/server/utils/esbuild-plugin.js +0 -110
- package/server/utils/files.js +0 -845
- package/server/utils/hmr.js +0 -21
- package/server/utils/router.js +0 -375
- package/server/utils/streaming.js +0 -324
- package/server/utils/template.js +0 -274
- /package/{client → dist/client}/app.webmanifest +0 -0
- /package/{server → dist/server}/root.html +0 -0
package/server/utils/files.js
DELETED
|
@@ -1,845 +0,0 @@
|
|
|
1
|
-
import fs from "fs/promises";
|
|
2
|
-
import { watch, existsSync, statSync, readFileSync } from "fs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import crypto from "crypto";
|
|
5
|
-
import { fileURLToPath, pathToFileURL } from "url";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Absolute path of the current file.
|
|
9
|
-
* Used to resolve project root in ESM context.
|
|
10
|
-
* @private
|
|
11
|
-
*/
|
|
12
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
-
/**
|
|
14
|
-
* Directory name of the current module.
|
|
15
|
-
* @private
|
|
16
|
-
*/
|
|
17
|
-
const __dirname = path.dirname(__filename);
|
|
18
|
-
// Framework's own directory (packages/vexjs/ — 3 levels up from server/utils/)
|
|
19
|
-
const FRAMEWORK_DIR = path.resolve(__dirname, "..", "..");
|
|
20
|
-
// User's project root (where they run the server)
|
|
21
|
-
export const PROJECT_ROOT = process.cwd();
|
|
22
|
-
const ROOT_DIR = PROJECT_ROOT;
|
|
23
|
-
|
|
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
|
-
// Simple names (no /, *, .) are treated as directory names here.
|
|
90
|
-
...(_vexConfig.watchIgnore || []).filter(p => !/[\/\*\.]/.test(p)),
|
|
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
|
-
|
|
106
|
-
export const PAGES_DIR = path.resolve(SRC_DIR, "pages");
|
|
107
|
-
export const SERVER_APP_DIR = path.join(FRAMEWORK_DIR, "server");
|
|
108
|
-
export const CLIENT_DIR = path.join(FRAMEWORK_DIR, "client");
|
|
109
|
-
export const CLIENT_SERVICES_DIR = path.join(CLIENT_DIR, "services");
|
|
110
|
-
// Generated files go to PROJECT_ROOT/.vexjs/
|
|
111
|
-
const GENERATED_DIR = path.join(PROJECT_ROOT, ".vexjs");
|
|
112
|
-
const CACHE_DIR = path.join(GENERATED_DIR, "_cache");
|
|
113
|
-
export const CLIENT_COMPONENTS_DIR = path.join(GENERATED_DIR, "_components");
|
|
114
|
-
export const USER_GENERATED_DIR = path.join(GENERATED_DIR, "user");
|
|
115
|
-
const ROOT_HTML_USER = path.join(PROJECT_ROOT, "root.html");
|
|
116
|
-
const ROOT_HTML_DEFAULT = path.join(FRAMEWORK_DIR, "server", "root.html");
|
|
117
|
-
export const ROOT_HTML_DIR = ROOT_HTML_USER;
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Ensures all required application directories exist.
|
|
121
|
-
*
|
|
122
|
-
* This function initializes:
|
|
123
|
-
* - Client application directory
|
|
124
|
-
* - Server application directory
|
|
125
|
-
* - Server-side HTML cache directory
|
|
126
|
-
*
|
|
127
|
-
* Directories are created recursively and safely if they already exist.
|
|
128
|
-
*
|
|
129
|
-
* @async
|
|
130
|
-
* @private
|
|
131
|
-
* @returns {Promise<boolean|undefined>}
|
|
132
|
-
* Resolves `true` when directories are created successfully.
|
|
133
|
-
*/
|
|
134
|
-
export async function initializeDirectories() {
|
|
135
|
-
try {
|
|
136
|
-
const servicesDir = path.join(GENERATED_DIR, "services");
|
|
137
|
-
await Promise.all([
|
|
138
|
-
fs.mkdir(GENERATED_DIR, { recursive: true }),
|
|
139
|
-
fs.mkdir(CACHE_DIR, { recursive: true }),
|
|
140
|
-
fs.mkdir(CLIENT_COMPONENTS_DIR, { recursive: true }),
|
|
141
|
-
fs.mkdir(USER_GENERATED_DIR, { recursive: true }),
|
|
142
|
-
fs.mkdir(servicesDir, { recursive: true }),
|
|
143
|
-
]);
|
|
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
|
-
|
|
151
|
-
return true;
|
|
152
|
-
} catch (err) {
|
|
153
|
-
console.error("Failed to create cache directory:", err);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Adjusts a client module path and its corresponding import statement.
|
|
159
|
-
*
|
|
160
|
-
* This function modifies the module path if it resides within the server directory,
|
|
161
|
-
* converting it to the corresponding path in the client services directory.
|
|
162
|
-
* If the module path is already within the client services directory, it returns it unchanged.
|
|
163
|
-
*
|
|
164
|
-
* @param {string} modulePath - The original module path to adjust.
|
|
165
|
-
* @param {string} importStatement - The import statement string that references the module.
|
|
166
|
-
* @returns {{
|
|
167
|
-
* path: string,
|
|
168
|
-
* importStatement: string
|
|
169
|
-
* }}
|
|
170
|
-
* An object containing:
|
|
171
|
-
* - `path`: The adjusted module path suitable for client usage.
|
|
172
|
-
* - `importStatement`: The updated import statement reflecting the adjusted path.
|
|
173
|
-
*
|
|
174
|
-
* @example
|
|
175
|
-
* const result = adjustClientModulePath(
|
|
176
|
-
* 'vex/reactive',
|
|
177
|
-
* "import { reactive } from 'vex/reactive';"
|
|
178
|
-
* );
|
|
179
|
-
* console.log(result.path); // '/_vexjs/services/reactive.js'
|
|
180
|
-
*/
|
|
181
|
-
export function adjustClientModulePath(modulePath, importStatement, componentFilePath = null) {
|
|
182
|
-
if (modulePath.startsWith("/_vexjs/")) {
|
|
183
|
-
return { path: modulePath, importStatement };
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// User imports — relative (e.g. "../utils/context") or @ alias (e.g. "@/utils/context")
|
|
187
|
-
// — served via /_vexjs/user/
|
|
188
|
-
const isRelative = (modulePath.startsWith("./") || modulePath.startsWith("../")) && componentFilePath;
|
|
189
|
-
const isAtAlias = modulePath.startsWith("@/") || modulePath === "@";
|
|
190
|
-
if (isRelative || isAtAlias) {
|
|
191
|
-
let resolvedPath;
|
|
192
|
-
if (isAtAlias) {
|
|
193
|
-
resolvedPath = path.resolve(SRC_DIR, modulePath.replace(/^@\//, "").replace(/^@$/, ""));
|
|
194
|
-
} else {
|
|
195
|
-
const componentDir = path.dirname(componentFilePath);
|
|
196
|
-
resolvedPath = path.resolve(componentDir, modulePath);
|
|
197
|
-
}
|
|
198
|
-
if (!path.extname(resolvedPath)) {
|
|
199
|
-
if (existsSync(resolvedPath + ".js")) {
|
|
200
|
-
resolvedPath += ".js";
|
|
201
|
-
} else if (existsSync(path.join(resolvedPath, "index.js"))) {
|
|
202
|
-
resolvedPath = path.join(resolvedPath, "index.js");
|
|
203
|
-
} else {
|
|
204
|
-
resolvedPath += ".js";
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
const relativePath = path.relative(SRC_DIR, resolvedPath).replace(/\\/g, "/");
|
|
208
|
-
const adjustedPath = `/_vexjs/user/${relativePath}`;
|
|
209
|
-
const adjustedImportStatement = importStatement.replace(modulePath, adjustedPath);
|
|
210
|
-
return { path: adjustedPath, importStatement: adjustedImportStatement };
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Framework imports (vex/)
|
|
214
|
-
let relative = modulePath.replace(/^vex\//, "");
|
|
215
|
-
let adjustedPath = `/_vexjs/services/${relative}`;
|
|
216
|
-
|
|
217
|
-
// Auto-resolve directory → index.js, bare name → .js
|
|
218
|
-
const fsPath = path.join(CLIENT_SERVICES_DIR, relative);
|
|
219
|
-
if (existsSync(fsPath) && statSync(fsPath).isDirectory()) {
|
|
220
|
-
adjustedPath += "/index.js";
|
|
221
|
-
} else if (!path.extname(adjustedPath)) {
|
|
222
|
-
adjustedPath += ".js";
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const adjustedImportStatement = importStatement.replace(modulePath, adjustedPath);
|
|
226
|
-
return { path: adjustedPath, importStatement: adjustedImportStatement };
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Gets relative path from one directory to another
|
|
231
|
-
* @param {string} from
|
|
232
|
-
* @param {string} to
|
|
233
|
-
* @returns {string}
|
|
234
|
-
*/
|
|
235
|
-
export function getRelativePath(from, to) {
|
|
236
|
-
return path.relative(from, to);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Gets directory name from a file path
|
|
241
|
-
* @param {string} filePath
|
|
242
|
-
* @returns {string}
|
|
243
|
-
*/
|
|
244
|
-
function getDirectoryName(filePath) {
|
|
245
|
-
return path.dirname(filePath);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Retrieves layout file paths for a given page.
|
|
250
|
-
*
|
|
251
|
-
* Layouts are determined by traversing up the directory tree
|
|
252
|
-
* from the page's location to the pages root, collecting any
|
|
253
|
-
* `layout.html` files found along the way.
|
|
254
|
-
*
|
|
255
|
-
* @async
|
|
256
|
-
* @param {string} pagePath
|
|
257
|
-
* @returns {Promise<string[]>}
|
|
258
|
-
*/
|
|
259
|
-
/**
|
|
260
|
-
*
|
|
261
|
-
* `getLayoutPaths` calls `fs.access` on every ancestor directory of `pagePath`
|
|
262
|
-
* to discover which `layout.html` files exist. The result is deterministic for
|
|
263
|
-
* a given page path — the filesystem structure does not change between requests.
|
|
264
|
-
*
|
|
265
|
-
* Key: absolute page file path
|
|
266
|
-
* Value: array of absolute layout.html paths (innermost → outermost)
|
|
267
|
-
*
|
|
268
|
-
* In production entries live forever (deploy is immutable).
|
|
269
|
-
* In dev the watcher below clears the whole cache whenever any layout.html is
|
|
270
|
-
* created, modified, or deleted, so the next request re-discovers the correct set.
|
|
271
|
-
*/
|
|
272
|
-
const layoutPathsCache = new Map();
|
|
273
|
-
|
|
274
|
-
if (process.env.NODE_ENV !== "production") {
|
|
275
|
-
// Watch the entire pages tree. When a layout.html changes, the set of layouts
|
|
276
|
-
// that exist may have changed — evict all cached entries to be safe.
|
|
277
|
-
watch(PAGES_DIR, { recursive: true }, (_, filename) => {
|
|
278
|
-
if (filename === "layout.vex" || filename?.endsWith(`${path.sep}layout.vex`)) {
|
|
279
|
-
layoutPathsCache.clear();
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
async function _getLayoutPaths(pagePath) {
|
|
285
|
-
const layouts = [];
|
|
286
|
-
const relativePath = getRelativePath(PAGES_DIR, pagePath);
|
|
287
|
-
const pathSegments = getDirectoryName(relativePath).split(path.sep);
|
|
288
|
-
|
|
289
|
-
// Always start with base layout
|
|
290
|
-
const baseLayout = path.join(PAGES_DIR, 'layout.vex');
|
|
291
|
-
if (await fileExists(baseLayout)) {
|
|
292
|
-
layouts.push(baseLayout);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Add nested layouts based on directory structure
|
|
296
|
-
let currentPath = PAGES_DIR;
|
|
297
|
-
for (const segment of pathSegments) {
|
|
298
|
-
if (segment === '.' || segment === '..') continue;
|
|
299
|
-
|
|
300
|
-
currentPath = path.join(currentPath, segment);
|
|
301
|
-
const layoutPath = path.join(currentPath, 'layout.vex');
|
|
302
|
-
|
|
303
|
-
if (await fileExists(layoutPath)) {
|
|
304
|
-
layouts.push(layoutPath);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return layouts;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Cached wrapper around `_getLayoutPaths`.
|
|
313
|
-
*
|
|
314
|
-
* Returns the cached layout list on repeated calls for the same page, avoiding
|
|
315
|
-
* repeated `fs.access` probes on every SSR request.
|
|
316
|
-
*
|
|
317
|
-
* @param {string} pagePath - Absolute path to the page file.
|
|
318
|
-
* @returns {Promise<string[]>}
|
|
319
|
-
*/
|
|
320
|
-
export async function getLayoutPaths(pagePath) {
|
|
321
|
-
if (layoutPathsCache.has(pagePath)) return layoutPathsCache.get(pagePath);
|
|
322
|
-
const result = await _getLayoutPaths(pagePath);
|
|
323
|
-
layoutPathsCache.set(pagePath, result);
|
|
324
|
-
return result;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Normalizes file content before persisting it to disk.
|
|
329
|
-
*
|
|
330
|
-
* - Converts Windows line endings to Unix
|
|
331
|
-
* - Collapses multiple whitespace characters
|
|
332
|
-
* - Trims leading and trailing whitespace
|
|
333
|
-
*
|
|
334
|
-
* Used mainly for generated artifacts (HTML, JS).
|
|
335
|
-
*
|
|
336
|
-
* @param {string} content
|
|
337
|
-
* Raw file content.
|
|
338
|
-
*
|
|
339
|
-
* @returns {string}
|
|
340
|
-
* Normalized content.
|
|
341
|
-
*/
|
|
342
|
-
function formatFileContent(content) {
|
|
343
|
-
return content
|
|
344
|
-
.trim();
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Writes formatted content to disk.
|
|
349
|
-
*
|
|
350
|
-
* Automatically normalizes content before writing.
|
|
351
|
-
*
|
|
352
|
-
* @async
|
|
353
|
-
* @param {string} filePath
|
|
354
|
-
* Absolute path to the output file.
|
|
355
|
-
*
|
|
356
|
-
* @param {string} content
|
|
357
|
-
* File content to write.
|
|
358
|
-
*
|
|
359
|
-
* @returns {Promise<void>}
|
|
360
|
-
*/
|
|
361
|
-
export async function writeFile(filePath, content) {
|
|
362
|
-
const formattedContent = formatFileContent(content);
|
|
363
|
-
return fs.writeFile(filePath, formattedContent, 'utf-8');
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Reads a UTF-8 encoded file from disk.
|
|
368
|
-
*
|
|
369
|
-
* @async
|
|
370
|
-
* @param {string} filePath
|
|
371
|
-
* Absolute path to the file.
|
|
372
|
-
*
|
|
373
|
-
* @returns {Promise<string>}
|
|
374
|
-
* File contents.
|
|
375
|
-
*/
|
|
376
|
-
export function readFile(filePath) {
|
|
377
|
-
return fs.readFile(filePath, 'utf-8');
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* Checks whether a file exists and is accessible.
|
|
383
|
-
*
|
|
384
|
-
* @async
|
|
385
|
-
* @param {string} filePath
|
|
386
|
-
* Absolute path to the file.
|
|
387
|
-
*
|
|
388
|
-
* @returns {Promise<boolean>}
|
|
389
|
-
* True if the file exists, false otherwise.
|
|
390
|
-
*/
|
|
391
|
-
export async function fileExists(filePath) {
|
|
392
|
-
try {
|
|
393
|
-
await fs.access(filePath);
|
|
394
|
-
return true;
|
|
395
|
-
} catch {
|
|
396
|
-
return false;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* Generates a stable, filesystem-safe component identifier
|
|
402
|
-
* from a relative component path.
|
|
403
|
-
*
|
|
404
|
-
* This name is used to:
|
|
405
|
-
* - Create client-side JS module filenames
|
|
406
|
-
* - Reference client components during hydration
|
|
407
|
-
*
|
|
408
|
-
* @param {string} componentPath
|
|
409
|
-
* Relative path to the component from project root.
|
|
410
|
-
*
|
|
411
|
-
* @returns {string}
|
|
412
|
-
* Autogenerated component name prefixed with `_`.
|
|
413
|
-
*/
|
|
414
|
-
function getAutogeneratedComponentName(componentPath) {
|
|
415
|
-
const componentName = componentPath
|
|
416
|
-
.replace(ROOT_DIR + path.sep, '')
|
|
417
|
-
.split(path.sep)
|
|
418
|
-
.filter(Boolean)
|
|
419
|
-
.join('_')
|
|
420
|
-
.replaceAll('.vex', '')
|
|
421
|
-
.replaceAll(path.sep, '_')
|
|
422
|
-
.replaceAll('-', '_')
|
|
423
|
-
.replaceAll(':', '');
|
|
424
|
-
|
|
425
|
-
return `_${componentName}`;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* Generates a unique and deterministic ID for a component or page path.
|
|
430
|
-
*
|
|
431
|
-
* This is useful for naming client component chunks, cached HTML files, or
|
|
432
|
-
* any scenario where a stable, filesystem-safe identifier is needed.
|
|
433
|
-
*
|
|
434
|
-
* The generated ID combines a sanitized base name with a fixed-length SHA-256 hash
|
|
435
|
-
* derived from the relative component path, ensuring uniqueness even across
|
|
436
|
-
* similarly named components in different directories.
|
|
437
|
-
*
|
|
438
|
-
* @param {string} componentPath - Absolute or project-root-relative path of the component.
|
|
439
|
-
* @param {Object} [options] - Optional configuration.
|
|
440
|
-
* @param {number} [options.length=8] - Number of characters from the hash to include in the ID.
|
|
441
|
-
* @param {boolean} [options.prefix=true] - Whether to include the base component name as a prefix.
|
|
442
|
-
*
|
|
443
|
-
* @returns {string} A deterministic, unique, and filesystem-safe component ID.
|
|
444
|
-
*
|
|
445
|
-
* @example
|
|
446
|
-
* generateComponentId("/src/components/Header.jsx");
|
|
447
|
-
* // "_Header_3f1b2a4c"
|
|
448
|
-
*
|
|
449
|
-
* @example
|
|
450
|
-
* generateComponentId("/src/components/Footer.jsx", { length: 12, prefix: false });
|
|
451
|
-
* // "7a9b1c2d5e6f"
|
|
452
|
-
*/
|
|
453
|
-
export function generateComponentId(componentPath, options = {}) {
|
|
454
|
-
const { length = 8, prefix = true } = options;
|
|
455
|
-
|
|
456
|
-
const relativePath = componentPath.replace(ROOT_DIR + path.sep, '');
|
|
457
|
-
|
|
458
|
-
const hash = crypto.createHash("sha256").update(relativePath).digest("hex").slice(0, length);
|
|
459
|
-
|
|
460
|
-
const baseName = getAutogeneratedComponentName(componentPath).replace(/^_/, '');
|
|
461
|
-
|
|
462
|
-
return prefix ? `_${baseName}_${hash}` : hash;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* Resolves the absolute path to a page's main HTML file.
|
|
467
|
-
*
|
|
468
|
-
* @param {string} pageName
|
|
469
|
-
* Page directory name.
|
|
470
|
-
*
|
|
471
|
-
* @returns {string}
|
|
472
|
-
* Absolute path to `page.html`.
|
|
473
|
-
*/
|
|
474
|
-
export const getPagePath = (pageName) =>
|
|
475
|
-
path.resolve(PAGES_DIR, pageName, "page.vex");
|
|
476
|
-
|
|
477
|
-
/**
|
|
478
|
-
* Retrieves the root HTML template.
|
|
479
|
-
*
|
|
480
|
-
* @async
|
|
481
|
-
* @returns {Promise<string>}
|
|
482
|
-
* Root HTML content.
|
|
483
|
-
*/
|
|
484
|
-
export const getRootTemplate = async () => {
|
|
485
|
-
try {
|
|
486
|
-
await fs.access(ROOT_HTML_USER);
|
|
487
|
-
return await fs.readFile(ROOT_HTML_USER, "utf-8");
|
|
488
|
-
} catch {
|
|
489
|
-
return await fs.readFile(ROOT_HTML_DEFAULT, "utf-8");
|
|
490
|
-
}
|
|
491
|
-
};
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* Recursively scans a directory and returns all files found.
|
|
495
|
-
*
|
|
496
|
-
* Each file entry includes:
|
|
497
|
-
* - Absolute path
|
|
498
|
-
* - Relative project path
|
|
499
|
-
* - File name
|
|
500
|
-
*
|
|
501
|
-
* @async
|
|
502
|
-
* @param {string} dir
|
|
503
|
-
* Directory to scan.
|
|
504
|
-
*
|
|
505
|
-
* @returns {Promise<Array<{
|
|
506
|
-
* fullpath: string,
|
|
507
|
-
* name: string,
|
|
508
|
-
* path: string
|
|
509
|
-
* }>>}
|
|
510
|
-
*/
|
|
511
|
-
export async function readDirectoryRecursive(dir) {
|
|
512
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
513
|
-
const files = [];
|
|
514
|
-
|
|
515
|
-
for (const entry of entries) {
|
|
516
|
-
const fullpath = path.join(dir, entry.name);
|
|
517
|
-
|
|
518
|
-
if (entry.isDirectory()) {
|
|
519
|
-
files.push(...await readDirectoryRecursive(fullpath));
|
|
520
|
-
} else {
|
|
521
|
-
files.push({
|
|
522
|
-
path: fullpath.replace(ROOT_DIR, ''),
|
|
523
|
-
fullpath,
|
|
524
|
-
name: entry.name,
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
return files;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
/**
|
|
533
|
-
* Derives a component or page name from its filesystem path.
|
|
534
|
-
*
|
|
535
|
-
* Handles:
|
|
536
|
-
* - Pages inside `/pages`
|
|
537
|
-
* - Nested routes
|
|
538
|
-
* - Standalone components
|
|
539
|
-
*
|
|
540
|
-
* @param {string} fullFilepath
|
|
541
|
-
* Absolute file path.
|
|
542
|
-
*
|
|
543
|
-
* @param {string} fileName
|
|
544
|
-
* File name.
|
|
545
|
-
*
|
|
546
|
-
* @returns {string}
|
|
547
|
-
* Derived component name.
|
|
548
|
-
*/
|
|
549
|
-
|
|
550
|
-
export const getComponentNameFromPath = (fullFilepath, fileName) => {
|
|
551
|
-
const filePath = fullFilepath.replace(ROOT_DIR + path.sep, "");
|
|
552
|
-
const isPage = filePath.startsWith(path.join("pages", path.sep));
|
|
553
|
-
if (isPage) {
|
|
554
|
-
const segments = filePath.split(path.sep);
|
|
555
|
-
if (segments.length === 2) {
|
|
556
|
-
return segments[0].replace(".vex", "");
|
|
557
|
-
} else {
|
|
558
|
-
return segments[segments.length - 2].replace(".vex", "");
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
return fileName.replace(".vex", "");
|
|
562
|
-
};
|
|
563
|
-
|
|
564
|
-
/**
|
|
565
|
-
* Retrieves cached HTML for a component or page from disk.
|
|
566
|
-
*
|
|
567
|
-
* Supports Incremental Static Regeneration (ISR) by returning cached HTML
|
|
568
|
-
* and its metadata. The `isStale` flag can be used to determine if the HTML
|
|
569
|
-
* should be regenerated.
|
|
570
|
-
*
|
|
571
|
-
* @async
|
|
572
|
-
* @param {Object} options
|
|
573
|
-
* @param {string} options.componentPath - Unique identifier or path of the component/page.
|
|
574
|
-
* @returns {Promise<{
|
|
575
|
-
* html: string | null,
|
|
576
|
-
* meta: { generatedAt: number, isStale: boolean } | null
|
|
577
|
-
* }>}
|
|
578
|
-
* - `html`: The cached HTML content, or null if it does not exist.
|
|
579
|
-
* - `meta`: Metadata object containing:
|
|
580
|
-
* - `generatedAt`: Timestamp (ms) of when the HTML was generated.
|
|
581
|
-
* - `isStale`: Boolean indicating if the cache has been manually invalidated.
|
|
582
|
-
*/
|
|
583
|
-
export async function getComponentHtmlDisk({ componentPath }) {
|
|
584
|
-
const filePath = path.join(CACHE_DIR, generateComponentId(componentPath) + ".html");
|
|
585
|
-
const metaPath = filePath + ".meta.json";
|
|
586
|
-
|
|
587
|
-
const [existsHtml, existsMeta] = await Promise.all([fileExists(filePath), fileExists(metaPath)]);
|
|
588
|
-
|
|
589
|
-
if (!existsMeta || !existsHtml) {
|
|
590
|
-
return { html: null, meta: null };
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
const [html, meta] = await Promise.all([
|
|
594
|
-
fs.readFile(filePath, "utf-8"),
|
|
595
|
-
fs.readFile(metaPath, "utf-8")
|
|
596
|
-
]).then(([htmlContent, metaContent]) => [htmlContent, JSON.parse(metaContent)]);
|
|
597
|
-
|
|
598
|
-
return { html, meta };
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
/**
|
|
602
|
-
* Persists server-rendered HTML to disk along with metadata.
|
|
603
|
-
*
|
|
604
|
-
* Metadata includes:
|
|
605
|
-
* - `generatedAt`: timestamp of generation
|
|
606
|
-
* - `isStale`: initially false
|
|
607
|
-
*
|
|
608
|
-
* @async
|
|
609
|
-
* @param {Object} options
|
|
610
|
-
* @param {string} options.componentPath - Unique identifier or path of the component/page.
|
|
611
|
-
* @param {string} options.html - The HTML content to save.
|
|
612
|
-
* @returns {Promise<void>} Resolves when the HTML and metadata have been successfully saved.
|
|
613
|
-
*/
|
|
614
|
-
export async function saveComponentHtmlDisk({ componentPath, html }) {
|
|
615
|
-
const filePath = path.join(CACHE_DIR, generateComponentId(componentPath) + ".html");
|
|
616
|
-
const metaPath = filePath + ".meta.json";
|
|
617
|
-
|
|
618
|
-
const meta = {
|
|
619
|
-
generatedAt: Date.now(),
|
|
620
|
-
isStale: false,
|
|
621
|
-
path: componentPath,
|
|
622
|
-
};
|
|
623
|
-
|
|
624
|
-
await Promise.all([
|
|
625
|
-
writeFile(filePath, html, "utf-8"),
|
|
626
|
-
writeFile(metaPath, JSON.stringify(meta), "utf-8"),
|
|
627
|
-
]);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
/**
|
|
631
|
-
* Marks a cached component/page as stale without regenerating it.
|
|
632
|
-
*
|
|
633
|
-
* Useful for manual revalidation of ISR pages.
|
|
634
|
-
*
|
|
635
|
-
* @async
|
|
636
|
-
* @param {Object} options
|
|
637
|
-
* @param {string} options.componentPath - Unique identifier or path of the component/page to mark as stale.
|
|
638
|
-
* @returns {Promise<void>} Resolves when the cache metadata has been updated.
|
|
639
|
-
*/
|
|
640
|
-
export async function markComponentHtmlStale({ componentPath }) {
|
|
641
|
-
const filePath = path.join(CACHE_DIR, generateComponentId(componentPath) + ".html");
|
|
642
|
-
const metaPath = filePath + ".meta.json";
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
if (!(await fileExists(metaPath))) return;
|
|
646
|
-
|
|
647
|
-
const meta = JSON.parse(await fs.readFile(metaPath, "utf-8"));
|
|
648
|
-
meta.isStale = true;
|
|
649
|
-
|
|
650
|
-
await writeFile(metaPath, JSON.stringify(meta), "utf-8");
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
/**
|
|
654
|
-
* Writes the server-side routes definition file.
|
|
655
|
-
*
|
|
656
|
-
* This file is consumed at runtime by the server router.
|
|
657
|
-
*
|
|
658
|
-
* @async
|
|
659
|
-
* @param {string[]} serverRoutes
|
|
660
|
-
* Serialized server route objects.
|
|
661
|
-
*
|
|
662
|
-
* @returns {Promise<void>}
|
|
663
|
-
*/
|
|
664
|
-
|
|
665
|
-
/**
|
|
666
|
-
* Writes the server-side route registry to `_routes.js`.
|
|
667
|
-
*
|
|
668
|
-
* @param {Array<{
|
|
669
|
-
* path: string,
|
|
670
|
-
* serverPath: string,
|
|
671
|
-
* isNotFound: boolean,
|
|
672
|
-
* meta: { ssr: boolean, requiresAuth: boolean, revalidate: number | string }
|
|
673
|
-
* }>} serverRoutes - Plain route objects.
|
|
674
|
-
*/
|
|
675
|
-
export async function saveServerRoutesFile(serverRoutes) {
|
|
676
|
-
await writeFile(
|
|
677
|
-
path.join(GENERATED_DIR, "_routes.js"),
|
|
678
|
-
`// Auto-generated by prebuild — do not edit manually.\nexport const routes = ${JSON.stringify(serverRoutes, null, 2)};\n`
|
|
679
|
-
);
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
/**
|
|
683
|
-
* Writes the client-side routes definition file.
|
|
684
|
-
*
|
|
685
|
-
* Includes:
|
|
686
|
-
* - Route definitions
|
|
687
|
-
*
|
|
688
|
-
* @async
|
|
689
|
-
* @param {string[]} clientRoutes
|
|
690
|
-
* Serialized client route objects.
|
|
691
|
-
* *
|
|
692
|
-
* @returns {Promise<void>}
|
|
693
|
-
*/
|
|
694
|
-
export async function saveClientRoutesFile(clientRoutes) {
|
|
695
|
-
const commentsClient = `
|
|
696
|
-
/**
|
|
697
|
-
* @typedef {Object} RouteMeta
|
|
698
|
-
* @property {boolean} ssr
|
|
699
|
-
* @property {boolean} requiresAuth
|
|
700
|
-
* @property {number} revalidateSeconds
|
|
701
|
-
*/
|
|
702
|
-
|
|
703
|
-
/**
|
|
704
|
-
* @typedef {Object} Route
|
|
705
|
-
* @property {string} path
|
|
706
|
-
* @property {string} serverPath
|
|
707
|
-
* @property {boolean} isNotFound
|
|
708
|
-
* @property {(marker: HTMLElement) => Promise<{ render: (marker: string) => void, metadata: any}>} [component]
|
|
709
|
-
* @property {RouteMeta} meta
|
|
710
|
-
* @property {Array<{ name: string, importPath: string }>} [layouts]
|
|
711
|
-
*/
|
|
712
|
-
`;
|
|
713
|
-
const clientFileCode = `
|
|
714
|
-
import { loadRouteComponent } from './cache.js';
|
|
715
|
-
|
|
716
|
-
${commentsClient}
|
|
717
|
-
export const routes = [
|
|
718
|
-
${clientRoutes.join(",\n")}
|
|
719
|
-
];
|
|
720
|
-
`;
|
|
721
|
-
|
|
722
|
-
await writeFile(
|
|
723
|
-
path.join(GENERATED_DIR, "services", "_routes.js"),
|
|
724
|
-
clientFileCode
|
|
725
|
-
);
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
/**
|
|
729
|
-
* Converts a page file path into a public-facing route path.
|
|
730
|
-
*
|
|
731
|
-
* Keeps dynamic segments in `[param]` format.
|
|
732
|
-
*
|
|
733
|
-
* @param {string} filePath
|
|
734
|
-
* Absolute page file path.
|
|
735
|
-
*
|
|
736
|
-
* @returns {string}
|
|
737
|
-
* Public route path.
|
|
738
|
-
*/
|
|
739
|
-
|
|
740
|
-
export function getOriginalRoutePath(filePath) {
|
|
741
|
-
let route = filePath.replace(PAGES_DIR, '').replace('/page.vex', '');
|
|
742
|
-
if (!route.startsWith('/')) route = '/' + route;
|
|
743
|
-
return route;
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
/**
|
|
747
|
-
* Retrieves all page files (`page.html`) in the pages directory.
|
|
748
|
-
* Optionally includes layout files (`layout.html`).
|
|
749
|
-
*
|
|
750
|
-
* @param {Object} [options]
|
|
751
|
-
* @param {boolean} [options.layouts=false]
|
|
752
|
-
* Whether to include layout files in the results.
|
|
753
|
-
*
|
|
754
|
-
* @async
|
|
755
|
-
* @returns {Promise<Array<{ fullpath: string, path: string }>>}
|
|
756
|
-
*/
|
|
757
|
-
export async function getPageFiles({ layouts = false } = {}) {
|
|
758
|
-
const pageFiles = await readDirectoryRecursive(PAGES_DIR);
|
|
759
|
-
const htmlFiles = pageFiles.filter((file) =>
|
|
760
|
-
file.fullpath.endsWith("page.vex") || (layouts && file.name === "layout.vex")
|
|
761
|
-
);
|
|
762
|
-
|
|
763
|
-
return htmlFiles;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
/**
|
|
767
|
-
* Converts a page file path into a server routing path.
|
|
768
|
-
*
|
|
769
|
-
* Dynamic segments `[param]` are converted to `:param`
|
|
770
|
-
* for Express-style routing.
|
|
771
|
-
*
|
|
772
|
-
* @param {string} filePath
|
|
773
|
-
* Absolute page file path.
|
|
774
|
-
*
|
|
775
|
-
* @returns {string}
|
|
776
|
-
* Server route path.
|
|
777
|
-
*/
|
|
778
|
-
export function getRoutePath(filePath) {
|
|
779
|
-
let route = filePath.replace(PAGES_DIR, '').replace('/page.vex', '');
|
|
780
|
-
route = route.replace(/\[([^\]]+)\]/g, ':$1'); // [param] -> :param
|
|
781
|
-
|
|
782
|
-
if (!route.startsWith('/')) {
|
|
783
|
-
route = '/' + route;
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
return route;
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
/**
|
|
790
|
-
* Writes a client component JS module to disk.
|
|
791
|
-
*
|
|
792
|
-
* @async
|
|
793
|
-
* @param {string} componentName
|
|
794
|
-
* Autogenerated component name.
|
|
795
|
-
*
|
|
796
|
-
* @param {string} jsModuleCode
|
|
797
|
-
* JavaScript module source.
|
|
798
|
-
*
|
|
799
|
-
* @returns {Promise<void>}
|
|
800
|
-
*/
|
|
801
|
-
export async function saveClientComponentModule(componentName, jsModuleCode) {
|
|
802
|
-
const outputPath = path.join(CLIENT_COMPONENTS_DIR, `${componentName}.js`);
|
|
803
|
-
|
|
804
|
-
await writeFile(outputPath, jsModuleCode, "utf-8");
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
/**
|
|
808
|
-
* Resolves an import path relative to the project root
|
|
809
|
-
* and returns filesystem and file URL representations.
|
|
810
|
-
*
|
|
811
|
-
* @param {string} importPath
|
|
812
|
-
* Import path as declared in source code.
|
|
813
|
-
*
|
|
814
|
-
* @returns {{
|
|
815
|
-
* path: string,
|
|
816
|
-
* fileUrl: string,
|
|
817
|
-
* importPath: string
|
|
818
|
-
* }}
|
|
819
|
-
*/
|
|
820
|
-
export async function getImportData(importPath, callerFilePath = null) {
|
|
821
|
-
let resolvedPath;
|
|
822
|
-
if (importPath.startsWith("vex/server/")) {
|
|
823
|
-
resolvedPath = path.resolve(FRAMEWORK_DIR, importPath.replace("vex/server/", "server/"));
|
|
824
|
-
} else if (importPath.startsWith("vex/")) {
|
|
825
|
-
resolvedPath = path.resolve(FRAMEWORK_DIR, "client/services", importPath.replace("vex/", ""));
|
|
826
|
-
} else if (importPath.startsWith("@/") || importPath === "@") {
|
|
827
|
-
resolvedPath = path.resolve(SRC_DIR, importPath.replace(/^@\//, "").replace(/^@$/, ""));
|
|
828
|
-
} else if ((importPath.startsWith("./") || importPath.startsWith("../")) && callerFilePath) {
|
|
829
|
-
// Relative import — resolve against the caller component's directory, not ROOT_DIR.
|
|
830
|
-
// Without this, `import Foo from './foo.vex'` inside a nested component would be
|
|
831
|
-
// resolved from the project root instead of from the file that contains the import.
|
|
832
|
-
resolvedPath = path.resolve(path.dirname(callerFilePath), importPath);
|
|
833
|
-
} else {
|
|
834
|
-
resolvedPath = path.resolve(ROOT_DIR, importPath);
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
// Auto-resolve directory → index.js
|
|
838
|
-
if (existsSync(resolvedPath) && statSync(resolvedPath).isDirectory()) {
|
|
839
|
-
resolvedPath = path.join(resolvedPath, "index.js");
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
const fileUrl = pathToFileURL(resolvedPath).href;
|
|
843
|
-
return { path: resolvedPath, fileUrl, importPath };
|
|
844
|
-
}
|
|
845
|
-
|