@cfdez11/vex 0.2.0 → 0.3.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.
|
@@ -89,6 +89,14 @@ export function reactive(obj) {
|
|
|
89
89
|
get(target, prop) {
|
|
90
90
|
// Handle primitive value conversion (for template literals, etc.)
|
|
91
91
|
if (target.__isPrimitive && prop === Symbol.toPrimitive) {
|
|
92
|
+
// Track "value" dependency so effects using ${counter} re-run on change
|
|
93
|
+
if (activeEffect) {
|
|
94
|
+
if (!depsMap.has("value")) depsMap.set("value", new Set());
|
|
95
|
+
const depSet = depsMap.get("value");
|
|
96
|
+
depSet.add(activeEffect);
|
|
97
|
+
if (!activeEffect.deps) activeEffect.deps = [];
|
|
98
|
+
activeEffect.deps.push(depSet);
|
|
99
|
+
}
|
|
92
100
|
return () => target.value;
|
|
93
101
|
}
|
|
94
102
|
|
|
@@ -180,11 +188,19 @@ export function computed(getter) {
|
|
|
180
188
|
value = getter();
|
|
181
189
|
});
|
|
182
190
|
|
|
183
|
-
return {
|
|
184
|
-
get
|
|
185
|
-
|
|
191
|
+
return new Proxy({}, {
|
|
192
|
+
get(_, prop) {
|
|
193
|
+
if (prop === Symbol.toPrimitive) {
|
|
194
|
+
return () => value;
|
|
195
|
+
}
|
|
196
|
+
if (prop === "value") {
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
// Delegate any other access (e.g. .map, .length) to the underlying value
|
|
200
|
+
const v = value?.[prop];
|
|
201
|
+
return typeof v === "function" ? v.bind(value) : v;
|
|
186
202
|
},
|
|
187
|
-
};
|
|
203
|
+
});
|
|
188
204
|
}
|
|
189
205
|
|
|
190
206
|
/**
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
1
2
|
import express from "express";
|
|
2
3
|
import path from "path";
|
|
3
4
|
import { pathToFileURL } from "url";
|
|
4
5
|
import { handlePageRequest, revalidatePath } from "./utils/router.js";
|
|
5
|
-
import { initializeDirectories, CLIENT_DIR } from "./utils/files.js";
|
|
6
|
+
import { initializeDirectories, CLIENT_DIR, SRC_DIR } from "./utils/files.js";
|
|
6
7
|
|
|
7
8
|
await initializeDirectories();
|
|
8
9
|
|
|
@@ -63,6 +64,49 @@ app.use(
|
|
|
63
64
|
})
|
|
64
65
|
);
|
|
65
66
|
|
|
67
|
+
// Serve user JS utility files at /_vexjs/user/* with import rewriting
|
|
68
|
+
app.get("/_vexjs/user/*splat", async (req, res) => {
|
|
69
|
+
const splat = req.params.splat;
|
|
70
|
+
const relPath = Array.isArray(splat) ? splat.join("/") : splat;
|
|
71
|
+
const filePath = path.resolve(path.join(SRC_DIR, relPath));
|
|
72
|
+
// Prevent path traversal outside SRC_DIR
|
|
73
|
+
if (!filePath.startsWith(SRC_DIR + path.sep) && filePath !== SRC_DIR) {
|
|
74
|
+
return res.status(403).send("Forbidden");
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
let content = await fs.readFile(filePath, "utf-8");
|
|
78
|
+
// Rewrite imports to browser-accessible paths
|
|
79
|
+
content = content.replace(
|
|
80
|
+
/^(\s*import\s+[^'"]*from\s+)['"]([^'"]+)['"]/gm,
|
|
81
|
+
(match, prefix, modulePath) => {
|
|
82
|
+
if (modulePath.startsWith("vex/") || modulePath.startsWith(".app/")) {
|
|
83
|
+
let mod = modulePath.replace(/^vex\//, "").replace(/^\.app\//, "");
|
|
84
|
+
if (!path.extname(mod)) mod += ".js";
|
|
85
|
+
return `${prefix}'/_vexjs/services/${mod}'`;
|
|
86
|
+
}
|
|
87
|
+
if (modulePath.startsWith("@/") || modulePath === "@") {
|
|
88
|
+
let resolved = path.resolve(SRC_DIR, modulePath.replace(/^@\//, "").replace(/^@$/, ""));
|
|
89
|
+
if (!path.extname(resolved)) resolved += ".js";
|
|
90
|
+
const rel = path.relative(SRC_DIR, resolved).replace(/\\/g, "/");
|
|
91
|
+
return `${prefix}'/_vexjs/user/${rel}'`;
|
|
92
|
+
}
|
|
93
|
+
if (modulePath.startsWith("./") || modulePath.startsWith("../")) {
|
|
94
|
+
const fileDir = path.dirname(filePath);
|
|
95
|
+
let resolved = path.resolve(fileDir, modulePath);
|
|
96
|
+
if (!path.extname(resolved)) resolved += ".js";
|
|
97
|
+
const rel = path.relative(SRC_DIR, resolved).replace(/\\/g, "/");
|
|
98
|
+
return `${prefix}'/_vexjs/user/${rel}'`;
|
|
99
|
+
}
|
|
100
|
+
return match;
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
res.setHeader("Content-Type", "application/javascript");
|
|
104
|
+
res.send(content);
|
|
105
|
+
} catch {
|
|
106
|
+
res.status(404).send("Not found");
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
66
110
|
// Serve user's public directory at /
|
|
67
111
|
app.use("/", express.static(path.join(process.cwd(), "public")));
|
|
68
112
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { watch } from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { compileTemplateToHTML } from "./template.js";
|
|
4
|
-
import { getOriginalRoutePath, getPageFiles, getRoutePath, saveClientComponentModule, saveClientRoutesFile, saveComponentHtmlDisk, saveServerRoutesFile, readFile, getImportData, generateComponentId, adjustClientModulePath, PAGES_DIR, ROOT_HTML_DIR, getLayoutPaths, SRC_DIR, WATCH_IGNORE } from "./files.js";
|
|
4
|
+
import { getOriginalRoutePath, getPageFiles, getRoutePath, saveClientComponentModule, saveClientRoutesFile, saveComponentHtmlDisk, saveServerRoutesFile, readFile, getImportData, generateComponentId, adjustClientModulePath, PAGES_DIR, ROOT_HTML_DIR, getLayoutPaths, SRC_DIR, WATCH_IGNORE, WATCH_IGNORE_FILES } from "./files.js";
|
|
5
5
|
import { renderComponents } from "./streaming.js";
|
|
6
6
|
import { getRevalidateSeconds } from "./cache.js";
|
|
7
7
|
import { withCache } from "./data-cache.js";
|
|
@@ -83,8 +83,14 @@ if (process.env.NODE_ENV !== "production") {
|
|
|
83
83
|
// Watch SRC_DIR (configured via vex.config.json `srcDir`, defaults to project root).
|
|
84
84
|
// Skip any path segment that appears in WATCH_IGNORE to avoid reacting to
|
|
85
85
|
// changes inside node_modules, build outputs, or other non-source directories.
|
|
86
|
+
// Individual file patterns can be excluded via `watchIgnoreFiles` in vex.config.json.
|
|
86
87
|
watch(SRC_DIR, { recursive: true }, async (_, filename) => {
|
|
87
|
-
if (
|
|
88
|
+
if (!filename) return;
|
|
89
|
+
if (filename.split(path.sep).some(part => WATCH_IGNORE.has(part))) return;
|
|
90
|
+
const normalizedFilename = filename.replace(/\\/g, "/");
|
|
91
|
+
if (WATCH_IGNORE_FILES.some(pattern => path.matchesGlob(normalizedFilename, pattern))) return;
|
|
92
|
+
|
|
93
|
+
if (filename.endsWith(".vex")) {
|
|
88
94
|
const fullPath = path.join(SRC_DIR, filename);
|
|
89
95
|
|
|
90
96
|
// 1. Evict all in-memory caches for this file
|
|
@@ -100,6 +106,9 @@ if (process.env.NODE_ENV !== "production") {
|
|
|
100
106
|
|
|
101
107
|
// 3. Notify connected browsers to reload
|
|
102
108
|
hmrEmitter.emit("reload", filename);
|
|
109
|
+
} else if (filename.endsWith(".js")) {
|
|
110
|
+
// User utility file changed — reload browsers (served dynamically, no bundle to regenerate)
|
|
111
|
+
hmrEmitter.emit("reload", filename);
|
|
103
112
|
}
|
|
104
113
|
});
|
|
105
114
|
|
|
@@ -145,7 +154,7 @@ const DEFAULT_METADATA = {
|
|
|
145
154
|
* }>
|
|
146
155
|
* }>}
|
|
147
156
|
*/
|
|
148
|
-
const getScriptImports = async (script, isClientSide = false) => {
|
|
157
|
+
const getScriptImports = async (script, isClientSide = false, filePath = null) => {
|
|
149
158
|
const componentRegistry = new Map();
|
|
150
159
|
const imports = {};
|
|
151
160
|
const clientImports = {};
|
|
@@ -182,21 +191,22 @@ const getScriptImports = async (script, isClientSide = false) => {
|
|
|
182
191
|
}
|
|
183
192
|
} else if (defaultImport) {
|
|
184
193
|
// client side default imports and named imports
|
|
185
|
-
const adjustedClientModule = adjustClientModulePath(modulePath, importStatement);
|
|
194
|
+
const adjustedClientModule = adjustClientModulePath(modulePath, importStatement, filePath);
|
|
186
195
|
clientImports[defaultImport || namedImports] = {
|
|
187
196
|
fileUrl,
|
|
188
197
|
originalPath: adjustedClientModule.path,
|
|
189
198
|
importStatement: adjustedClientModule.importStatement,
|
|
199
|
+
originalImportStatement: importStatement,
|
|
190
200
|
};
|
|
191
201
|
} else {
|
|
192
202
|
namedImports.split(",").forEach((name) => {
|
|
193
203
|
const trimmedName = name.trim();
|
|
194
|
-
|
|
195
|
-
const adjustedClientModule = adjustClientModulePath(modulePath, importStatement);
|
|
204
|
+
const adjustedClientModule = adjustClientModulePath(modulePath, importStatement, filePath);
|
|
196
205
|
clientImports[trimmedName] = {
|
|
197
|
-
fileUrl,
|
|
206
|
+
fileUrl,
|
|
198
207
|
originalPath: adjustedClientModule.path,
|
|
199
208
|
importStatement: adjustedClientModule.importStatement,
|
|
209
|
+
originalImportStatement: importStatement,
|
|
200
210
|
};
|
|
201
211
|
});
|
|
202
212
|
}
|
|
@@ -311,7 +321,7 @@ async function _processHtmlFile(filePath) {
|
|
|
311
321
|
}
|
|
312
322
|
|
|
313
323
|
if (clientMatch) {
|
|
314
|
-
const { componentRegistry, clientImports: newClientImports } = await getScriptImports(clientMatch[1], true);
|
|
324
|
+
const { componentRegistry, clientImports: newClientImports } = await getScriptImports(clientMatch[1], true, filePath);
|
|
315
325
|
clientComponents = componentRegistry;
|
|
316
326
|
clientImports = newClientImports;
|
|
317
327
|
}
|
|
@@ -412,6 +422,7 @@ export async function renderHtmlFile(filePath, context = {}, extraComponentData
|
|
|
412
422
|
*/
|
|
413
423
|
function generateClientScriptTags({
|
|
414
424
|
clientCode,
|
|
425
|
+
clientImports = {},
|
|
415
426
|
clientComponentsScripts = [],
|
|
416
427
|
clientComponents = new Map(),
|
|
417
428
|
}) {
|
|
@@ -421,6 +432,13 @@ function generateClientScriptTags({
|
|
|
421
432
|
clientCode = clientCode.replace(`${importStatement};`, '').replace(importStatement, "");
|
|
422
433
|
}
|
|
423
434
|
|
|
435
|
+
// Rewrite framework and user utility imports to browser-accessible paths
|
|
436
|
+
for (const importData of Object.values(clientImports)) {
|
|
437
|
+
if (importData.originalImportStatement && importData.importStatement !== importData.originalImportStatement) {
|
|
438
|
+
clientCode = clientCode.replace(importData.originalImportStatement, importData.importStatement);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
424
442
|
const clientCodeWithoutComponentImports = clientCode
|
|
425
443
|
.split("\n")
|
|
426
444
|
.filter((line) => !/^\s*import\s+.*['"].*\.vex['"]/.test(line))
|
|
@@ -459,11 +477,12 @@ function generateClientScriptTags({
|
|
|
459
477
|
* }>
|
|
460
478
|
*/
|
|
461
479
|
async function renderPage(pagePath, ctx, awaitSuspenseComponents = false, extraComponentData = {}) {
|
|
462
|
-
const {
|
|
463
|
-
html,
|
|
464
|
-
metadata,
|
|
465
|
-
clientCode,
|
|
466
|
-
|
|
480
|
+
const {
|
|
481
|
+
html,
|
|
482
|
+
metadata,
|
|
483
|
+
clientCode,
|
|
484
|
+
clientImports,
|
|
485
|
+
serverComponents,
|
|
467
486
|
clientComponents,
|
|
468
487
|
} = await renderHtmlFile(pagePath, ctx, extraComponentData);
|
|
469
488
|
|
|
@@ -482,6 +501,7 @@ async function renderPage(pagePath, ctx, awaitSuspenseComponents = false, extraC
|
|
|
482
501
|
html: htmlWithComponents,
|
|
483
502
|
metadata,
|
|
484
503
|
clientCode,
|
|
504
|
+
clientImports,
|
|
485
505
|
serverComponents,
|
|
486
506
|
clientComponents,
|
|
487
507
|
suspenseComponents,
|
|
@@ -582,6 +602,7 @@ export async function renderPageWithLayout(pagePath, ctx = {}, awaitSuspenseComp
|
|
|
582
602
|
html: pageHtml,
|
|
583
603
|
metadata,
|
|
584
604
|
clientCode,
|
|
605
|
+
clientImports,
|
|
585
606
|
serverComponents,
|
|
586
607
|
clientComponents,
|
|
587
608
|
suspenseComponents,
|
|
@@ -592,6 +613,7 @@ export async function renderPageWithLayout(pagePath, ctx = {}, awaitSuspenseComp
|
|
|
592
613
|
// Wrap in layout
|
|
593
614
|
const clientScripts = generateClientScriptTags({
|
|
594
615
|
clientCode,
|
|
616
|
+
clientImports,
|
|
595
617
|
clientComponentsScripts,
|
|
596
618
|
clientComponents,
|
|
597
619
|
});
|
package/server/utils/files.js
CHANGED
|
@@ -85,10 +85,24 @@ export const WATCH_IGNORE = new Set([
|
|
|
85
85
|
".next", ".nuxt", ".svelte-kit", ".astro",
|
|
86
86
|
// misc
|
|
87
87
|
"tmp", "temp", ".cache", ".claude",
|
|
88
|
-
// user-defined extras from vex.config.json
|
|
89
|
-
|
|
88
|
+
// user-defined extras from vex.config.json.
|
|
89
|
+
// Simple names (no /, *, .) are treated as directory names here.
|
|
90
|
+
...(_vexConfig.watchIgnore || []).filter(p => !/[\/\*\.]/.test(p)),
|
|
90
91
|
]);
|
|
91
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Glob patterns derived from `watchIgnore` entries in vex.config.json that
|
|
95
|
+
* contain path separators, wildcards, or dots — i.e. file-level patterns.
|
|
96
|
+
*
|
|
97
|
+
* Simple directory names in the same array go to WATCH_IGNORE instead.
|
|
98
|
+
*
|
|
99
|
+
* "watchIgnore": ["utils/legacy.js", "components/wip/**", "wip"]
|
|
100
|
+
* → "wip" added to WATCH_IGNORE (directory name)
|
|
101
|
+
* → "utils/legacy.js" added to WATCH_IGNORE_FILES (glob pattern)
|
|
102
|
+
* → "components/wip/**" added to WATCH_IGNORE_FILES (glob pattern)
|
|
103
|
+
*/
|
|
104
|
+
export const WATCH_IGNORE_FILES = (_vexConfig.watchIgnore || []).filter(p => /[\/\*\.]/.test(p));
|
|
105
|
+
|
|
92
106
|
export const PAGES_DIR = path.resolve(SRC_DIR, "pages");
|
|
93
107
|
export const SERVER_APP_DIR = path.join(FRAMEWORK_DIR, "server");
|
|
94
108
|
export const CLIENT_DIR = path.join(FRAMEWORK_DIR, "client");
|
|
@@ -158,11 +172,39 @@ export async function initializeDirectories() {
|
|
|
158
172
|
* console.log(result.importStatement);
|
|
159
173
|
* // "import userController from '/.app/client/services/reactive.js';"
|
|
160
174
|
*/
|
|
161
|
-
export function adjustClientModulePath(modulePath, importStatement) {
|
|
175
|
+
export function adjustClientModulePath(modulePath, importStatement, componentFilePath = null) {
|
|
162
176
|
if (modulePath.startsWith("/_vexjs/")) {
|
|
163
177
|
return { path: modulePath, importStatement };
|
|
164
178
|
}
|
|
165
179
|
|
|
180
|
+
// User imports — relative (e.g. "../utils/context") or @ alias (e.g. "@/utils/context")
|
|
181
|
+
// — served via /_vexjs/user/
|
|
182
|
+
const isRelative = (modulePath.startsWith("./") || modulePath.startsWith("../")) && componentFilePath;
|
|
183
|
+
const isAtAlias = modulePath.startsWith("@/") || modulePath === "@";
|
|
184
|
+
if (isRelative || isAtAlias) {
|
|
185
|
+
let resolvedPath;
|
|
186
|
+
if (isAtAlias) {
|
|
187
|
+
resolvedPath = path.resolve(SRC_DIR, modulePath.replace(/^@\//, "").replace(/^@$/, ""));
|
|
188
|
+
} else {
|
|
189
|
+
const componentDir = path.dirname(componentFilePath);
|
|
190
|
+
resolvedPath = path.resolve(componentDir, modulePath);
|
|
191
|
+
}
|
|
192
|
+
if (!path.extname(resolvedPath)) {
|
|
193
|
+
if (existsSync(resolvedPath + ".js")) {
|
|
194
|
+
resolvedPath += ".js";
|
|
195
|
+
} else if (existsSync(path.join(resolvedPath, "index.js"))) {
|
|
196
|
+
resolvedPath = path.join(resolvedPath, "index.js");
|
|
197
|
+
} else {
|
|
198
|
+
resolvedPath += ".js";
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const relativePath = path.relative(SRC_DIR, resolvedPath).replace(/\\/g, "/");
|
|
202
|
+
const adjustedPath = `/_vexjs/user/${relativePath}`;
|
|
203
|
+
const adjustedImportStatement = importStatement.replace(modulePath, adjustedPath);
|
|
204
|
+
return { path: adjustedPath, importStatement: adjustedImportStatement };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Framework imports (vex/ and .app/)
|
|
166
208
|
let relative = modulePath.replace(/^vex\//, "").replace(/^\.app\//, "");
|
|
167
209
|
let adjustedPath = `/_vexjs/services/${relative}`;
|
|
168
210
|
|
|
@@ -84,7 +84,7 @@ async function processServerComponents(html, serverComponents) {
|
|
|
84
84
|
const replacements = [];
|
|
85
85
|
let match;
|
|
86
86
|
|
|
87
|
-
while ((match = componentRegex.exec(
|
|
87
|
+
while ((match = componentRegex.exec(processedHtml)) !== null) {
|
|
88
88
|
replacements.push({
|
|
89
89
|
name: componentName,
|
|
90
90
|
attrs: parseAttributes(match[1]),
|
package/server/utils/template.js
CHANGED
|
@@ -77,9 +77,12 @@ function parseHTMLToNodes(html) {
|
|
|
77
77
|
*/
|
|
78
78
|
function processNode(node, scope, previousRendered = false) {
|
|
79
79
|
if (node.type === "text") {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
// Replace {{expr}} with its value from scope (SSR interpolation).
|
|
81
|
+
// The lookbehind (?<!\\) skips escaped \{{expr}}, which are then
|
|
82
|
+
// unescaped to literal {{expr}} by the second replace.
|
|
83
|
+
node.data = node.data
|
|
84
|
+
.replace(/(?<!\\)\{\{(.+?)\}\}/g, (_, expr) => getDataValue(expr.trim(), scope))
|
|
85
|
+
.replace(/\\\{\{/g, "{{");
|
|
83
86
|
return node;
|
|
84
87
|
}
|
|
85
88
|
|
|
@@ -88,9 +91,9 @@ function processNode(node, scope, previousRendered = false) {
|
|
|
88
91
|
|
|
89
92
|
for (const [attrName, attrValue] of Object.entries(attrs)) {
|
|
90
93
|
if (typeof attrValue === "string") {
|
|
91
|
-
attrs[attrName] = attrValue
|
|
92
|
-
getDataValue(expr.trim(), scope)
|
|
93
|
-
|
|
94
|
+
attrs[attrName] = attrValue
|
|
95
|
+
.replace(/(?<!\\)\{\{(.+?)\}\}/g, (_, expr) => getDataValue(expr.trim(), scope))
|
|
96
|
+
.replace(/\\\{\{/g, "{{");
|
|
94
97
|
}
|
|
95
98
|
}
|
|
96
99
|
|