@cfdez11/vex 0.5.0 → 0.6.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.
@@ -78,12 +78,12 @@
78
78
  // Start observing the document for new nodes
79
79
  observer.observe(document, { childList: true, subtree: true });
80
80
 
81
- // Hydrate existing components on DOMContentLoaded or immediately if already interactive
81
+ // Hydrate existing components on DOMContentLoaded or immediately if already interactive.
82
+ // The observer is intentionally NOT disconnected here — it must stay active to catch
83
+ // components inserted after DOMContentLoaded (nested CSR components, Suspense streaming,
84
+ // SPA navigations). The `data-hydrated` guard in hydrateMarker prevents double-hydration.
82
85
  if (document.readyState === "loading") {
83
- document.addEventListener("DOMContentLoaded", () => {
84
- hydrateComponents();
85
- observer.disconnect();
86
- });
86
+ document.addEventListener("DOMContentLoaded", () => hydrateComponents());
87
87
  } else {
88
88
  hydrateComponents();
89
89
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfdez11/vex",
3
- "version": "0.5.0",
3
+ "version": "0.6.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",
@@ -22,6 +22,7 @@
22
22
  "license": "MIT",
23
23
  "dependencies": {
24
24
  "dom-serializer": "^2.0.0",
25
+ "dotenv": "^16.0.0",
25
26
  "esbuild": "^0.25.0",
26
27
  "express": "^5.2.1",
27
28
  "htmlparser2": "^10.0.0"
@@ -1,3 +1,4 @@
1
+ import "dotenv/config";
1
2
  import fs from "fs/promises";
2
3
  import path from "path";
3
4
  import { build } from "./utils/component-processor.js";
package/server/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import "dotenv/config";
1
2
  import express from "express";
2
3
  import path from "path";
3
4
  import { pathToFileURL } from "url";
@@ -115,7 +116,7 @@ app.use(async (req, res) => {
115
116
  res.status(404).send("Page not found");
116
117
  });
117
118
 
118
- const PORT = process.env.PORT || 3001;
119
+ const PORT = process.env.VEX_PORT || process.env.PORT || 3001;
119
120
  app.listen(PORT, () => {
120
121
  console.log(`Server running on port ${PORT}`);
121
122
  });
@@ -1,3 +1,4 @@
1
+ import "dotenv/config";
1
2
  import { build } from "./utils/component-processor.js";
2
3
  import { initializeDirectories } from "./utils/files.js";
3
4
 
@@ -177,7 +177,7 @@ const getScriptImports = async (script, isClientSide = false, filePath = null) =
177
177
  while ((match = importRegex.exec(script)) !== null) {
178
178
  const [importStatement, defaultImport, namedImports, modulePath] = match;
179
179
 
180
- const { path, fileUrl } = await getImportData(modulePath);
180
+ const { path, fileUrl } = await getImportData(modulePath, filePath);
181
181
 
182
182
  if (path.endsWith(".vex")) {
183
183
  // Recursively process HTML component
@@ -989,7 +989,7 @@ async function generateServerComponentHTML(componentPath) {
989
989
  * and runtime interpolations (e.g., `${variable}`).
990
990
  *
991
991
  * @param {string} componentName - The logical name of the component.
992
- * @param {string} originalPath - The original file path of the component.
992
+ * @param {string} componentAbsPath - The absolute file path of the component (resolved by getImportData).
993
993
  * @param {Record<string, any>} [props={}] - An object of props to pass to the component.
994
994
  * Values can be literals or template
995
995
  * interpolations (`${…}`) for dynamic evaluation.
@@ -997,10 +997,13 @@ async function generateServerComponentHTML(componentPath) {
997
997
  * @returns {Promise<string>} A promise that resolves to a string containing
998
998
  * the `<template>` HTML for hydration.
999
999
  */
1000
- export async function processClientComponent(componentName, originalPath, props = {}) {
1000
+ export async function processClientComponent(componentName, componentAbsPath, props = {}) {
1001
1001
  const targetId = `client-${componentName}-${Date.now()}`;
1002
1002
 
1003
- const componentImport = generateComponentId(originalPath)
1003
+ // componentAbsPath is the absolute resolved path — generateComponentId strips ROOT_DIR
1004
+ // internally, so this produces the same hash as the bundle filename written by
1005
+ // generateComponentAndFillCache (which also calls generateComponentId with the abs path).
1006
+ const componentImport = generateComponentId(componentAbsPath);
1004
1007
  const propsJson = serializeClientComponentProps(props);
1005
1008
  const html = `<template id="${targetId}" data-client:component="${componentImport}" data-client:props='${propsJson}'></template>`;
1006
1009
 
@@ -811,7 +811,7 @@ export async function saveClientComponentModule(componentName, jsModuleCode) {
811
811
  * importPath: string
812
812
  * }}
813
813
  */
814
- export async function getImportData(importPath) {
814
+ export async function getImportData(importPath, callerFilePath = null) {
815
815
  let resolvedPath;
816
816
  if (importPath.startsWith("vex/server/")) {
817
817
  resolvedPath = path.resolve(FRAMEWORK_DIR, importPath.replace("vex/server/", "server/"));
@@ -821,6 +821,11 @@ export async function getImportData(importPath) {
821
821
  resolvedPath = path.resolve(FRAMEWORK_DIR, importPath.replace(".app/server/", "server/"));
822
822
  } else if (importPath.startsWith(".app/")) {
823
823
  resolvedPath = path.resolve(FRAMEWORK_DIR, importPath.replace(".app/", ""));
824
+ } else if ((importPath.startsWith("./") || importPath.startsWith("../")) && callerFilePath) {
825
+ // Relative import — resolve against the caller component's directory, not ROOT_DIR.
826
+ // Without this, `import Foo from './foo.vex'` inside a nested component would be
827
+ // resolved from the project root instead of from the file that contains the import.
828
+ resolvedPath = path.resolve(path.dirname(callerFilePath), importPath);
824
829
  } else {
825
830
  resolvedPath = path.resolve(ROOT_DIR, importPath);
826
831
  }
@@ -128,7 +128,7 @@ async function renderClientComponents(html, clientComponents) {
128
128
  let processedHtml = html;
129
129
  const allScripts = [];
130
130
 
131
- for (const [componentName, { originalPath }] of clientComponents.entries()) {
131
+ for (const [componentName, { path: componentAbsPath }] of clientComponents.entries()) {
132
132
  const escapedName = componentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
133
133
 
134
134
  const componentRegex = new RegExp(
@@ -156,7 +156,7 @@ async function renderClientComponents(html, clientComponents) {
156
156
  for (let i = replacements.length - 1; i >= 0; i--) {
157
157
  const { start, end, attrs } = replacements[i];
158
158
 
159
- const htmlComponent = await processClientComponent(componentName, originalPath, attrs);
159
+ const htmlComponent = await processClientComponent(componentName, componentAbsPath, attrs);
160
160
 
161
161
  processedHtml =
162
162
  processedHtml.slice(0, start) +