@cfdez11/vex 0.7.0 → 0.8.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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfdez11/vex",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
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",
@@ -38,6 +38,7 @@ let shell = rootTemplate
38
38
  .replace(/\{\{props\.children\}\}/g, "");
39
39
 
40
40
  const frameworkScripts = [
41
+ `<style>vex-root { display: contents; }</style>`,
41
42
  `<script type="module" src="/_vexjs/services/index.js"></script>`,
42
43
  `<script src="/_vexjs/services/hydrate-client-components.js"></script>`,
43
44
  `<script src="/_vexjs/services/hydrate.js" id="hydrate-script"></script>`,
@@ -46,24 +47,36 @@ const frameworkScripts = [
46
47
  shell = shell.replace("</head>", ` ${frameworkScripts}\n</head>`);
47
48
  await fs.writeFile(path.join(DIST_DIR, "index.html"), shell, "utf-8");
48
49
 
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 });
50
+ // Step 4: Copy static framework assets (favicon.ico, app.webmanifest) → dist/_vexjs/
51
+ // JS runtime files live in .vexjs/services/ (copied in step 5) — no need to
52
+ // copy CLIENT_DIR/services/ separately.
53
+ console.log("šŸ“¦ Copying framework assets...");
54
+ for (const asset of ["favicon.ico", "app.webmanifest"]) {
55
+ try {
56
+ await fs.copyFile(
57
+ path.join(CLIENT_DIR, asset),
58
+ path.join(DIST_DIR, "_vexjs", asset)
59
+ );
60
+ } catch {
61
+ // asset not present — skip
62
+ }
63
+ }
52
64
 
53
- // Step 5: Copy generated component bundles → dist/_vexjs/_components/
54
- console.log("šŸ“¦ Copying component bundles...");
65
+ // Step 5: Copy generated services → dist/_vexjs/services/
66
+ // .vexjs/services/ already contains both framework JS (copied by initializeDirectories)
67
+ // and generated files (_routes.js). One copy covers everything.
68
+ console.log("šŸ“¦ Copying services...");
55
69
  await fs.cp(
56
- path.join(GENERATED_DIR, "_components"),
57
- path.join(DIST_DIR, "_vexjs", "_components"),
70
+ path.join(GENERATED_DIR, "services"),
71
+ path.join(DIST_DIR, "_vexjs", "services"),
58
72
  { recursive: true }
59
73
  );
60
74
 
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...");
75
+ // Step 6: Copy generated component bundles → dist/_vexjs/_components/
76
+ console.log("šŸ“¦ Copying component bundles...");
64
77
  await fs.cp(
65
- path.join(GENERATED_DIR, "services"),
66
- path.join(DIST_DIR, "_vexjs", "services"),
78
+ path.join(GENERATED_DIR, "_components"),
79
+ path.join(DIST_DIR, "_vexjs", "_components"),
67
80
  { recursive: true }
68
81
  );
69
82
 
@@ -77,7 +90,7 @@ try {
77
90
  // no user JS files — that's fine
78
91
  }
79
92
 
80
- // Step 8: Copy public/ → dist/ (static assets, CSS)
93
+ // Step 8: Copy public/ → dist/
81
94
  console.log("šŸ“¦ Copying public assets...");
82
95
  const publicDir = path.join(PROJECT_ROOT, "public");
83
96
  try {
@@ -86,7 +99,7 @@ try {
86
99
  // no public/ directory — that's fine
87
100
  }
88
101
 
89
- // Step 9: Copy pre-rendered HTML for SSG routes (revalidate: 'never')
102
+ // Step 9: Copy pre-rendered SSG pages
90
103
  const CACHE_DIR = path.join(GENERATED_DIR, "_cache");
91
104
  const ssgRoutes = serverRoutes.filter(
92
105
  (r) => r.meta.revalidate === "never" || r.meta.revalidate === false
@@ -108,7 +121,7 @@ if (ssgRoutes.length > 0) {
108
121
  }
109
122
  }
110
123
 
111
- // Step 10: Report SSR-only routes that were skipped
124
+ // Step 10: Report SSR-only routes (skipped in static build)
112
125
  const ssrOnlyRoutes = serverRoutes.filter((r) => r.meta.ssr);
113
126
  if (ssrOnlyRoutes.length > 0) {
114
127
  console.warn("\nāš ļø The following routes require a server and were NOT included in the static build:");
package/server/index.js CHANGED
@@ -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 services (e.g. _routes.js) at /_vexjs/services/
46
- // Also before the broader /_vexjs mount so the generated _routes.js
47
- // overrides any placeholder that might exist in the framework source.
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,13 +54,13 @@ app.use(
56
54
  })
57
55
  );
58
56
 
59
- // Serve framework client runtime files at /_vexjs/
60
- // (reactive.js, html.js, hydrate.js, navigation/, etc.)
61
- // User imports like `vex/reactive` are marked external by esbuild and resolved
62
- // here at runtime — a single shared instance per page load.
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).
63
61
  app.use(
64
- "/_vexjs",
65
- express.static(CLIENT_DIR, {
62
+ "/_vexjs/user",
63
+ express.static(USER_GENERATED_DIR, {
66
64
  setHeaders(res, filePath) {
67
65
  if (filePath.endsWith(".js")) {
68
66
  res.setHeader("Content-Type", "application/javascript");
@@ -71,15 +69,12 @@ app.use(
71
69
  })
72
70
  );
73
71
 
74
-
75
- // Serve pre-bundled user JS utility files at /_vexjs/user/
76
- // These are @/ and relative imports in <script client> blocks. esbuild bundles
77
- // each file with npm packages inlined; vex/*, @/*, and relative user imports
78
- // stay external so every component shares the same singleton module instance
79
- // via the browser's ES module cache.
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.
80
75
  app.use(
81
- "/_vexjs/user",
82
- express.static(USER_GENERATED_DIR, {
76
+ "/_vexjs",
77
+ express.static(CLIENT_DIR, {
83
78
  setHeaders(res, filePath) {
84
79
  if (filePath.endsWith(".js")) {
85
80
  res.setHeader("Content-Type", "application/javascript");
@@ -564,6 +564,7 @@ async function renderLayouts(pagePath, pageContent, pageHead = {}) {
564
564
  // so users don't need to reference framework scripts in their root.html
565
565
  const devMode = process.env.NODE_ENV !== "production";
566
566
  const frameworkScripts = [
567
+ `<style>vex-root { display: contents; }</style>`,
567
568
  `<script type="module" src="/_vexjs/services/index.js"></script>`,
568
569
  `<script src="/_vexjs/services/hydrate-client-components.js"></script>`,
569
570
  `<script src="/_vexjs/services/hydrate.js" id="hydrate-script"></script>`,
@@ -832,6 +833,11 @@ export async function generateClientComponentModule({
832
833
  // ── 5. Assemble the esbuild entry source ────────────────────────────────────
833
834
  // This is a valid ESM module that esbuild will bundle. Imports at the top,
834
835
  // hydrateClientComponent exported as a named function.
836
+ // Add persistent wrapper element anchors the component in the DOM so that
837
+ // re-renders always have a stable target to replace children into.
838
+ // Using a plain Element (never a DocumentFragment) avoids the fragment-drain
839
+ // problem: after marker.replaceWith(fragment) the fragment empties and
840
+ // disconnects, making subsequent root.replaceWith() calls silently no-op.
835
841
  const entrySource = `
836
842
  ${[...importLines].join("\n")}
837
843
 
@@ -840,20 +846,16 @@ export const metadata = ${JSON.stringify(metadata)};
840
846
  export function hydrateClientComponent(marker, incomingProps = {}) {
841
847
  ${cleanClientCode}
842
848
 
843
- let root = null;
849
+ const wrapper = document.createElement("vex-root");
850
+ marker.replaceWith(wrapper);
851
+
844
852
  function render() {
845
853
  const node = html\`${processedHtml}\`;
846
- if (!root) {
847
- root = node;
848
- marker.replaceWith(node);
849
- } else {
850
- root.replaceWith(node);
851
- root = node;
852
- }
854
+ wrapper.replaceChildren(node);
853
855
  }
854
856
 
855
857
  effect(() => render());
856
- return root;
858
+ return wrapper;
857
859
  }
858
860
  `.trim();
859
861
 
@@ -112,7 +112,6 @@ 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
114
  export const USER_GENERATED_DIR = path.join(GENERATED_DIR, "user");
115
- const SERVER_UTILS_DIR = path.join(GENERATED_DIR);
116
115
  const ROOT_HTML_USER = path.join(PROJECT_ROOT, "root.html");
117
116
  const ROOT_HTML_DEFAULT = path.join(FRAMEWORK_DIR, "server", "root.html");
118
117
  export const ROOT_HTML_DIR = ROOT_HTML_USER;
@@ -134,14 +133,21 @@ export const ROOT_HTML_DIR = ROOT_HTML_USER;
134
133
  */
135
134
  export async function initializeDirectories() {
136
135
  try {
136
+ const servicesDir = path.join(GENERATED_DIR, "services");
137
137
  await Promise.all([
138
138
  fs.mkdir(GENERATED_DIR, { recursive: true }),
139
139
  fs.mkdir(CACHE_DIR, { recursive: true }),
140
140
  fs.mkdir(CLIENT_COMPONENTS_DIR, { recursive: true }),
141
141
  fs.mkdir(USER_GENERATED_DIR, { recursive: true }),
142
- fs.mkdir(path.join(GENERATED_DIR, "services"), { recursive: true }),
142
+ fs.mkdir(servicesDir, { recursive: true }),
143
143
  ]);
144
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
+
145
151
  return true;
146
152
  } catch (err) {
147
153
  console.error("Failed to create cache directory:", err);