@junejs/server 0.0.31 → 0.0.32

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junejs/server",
3
- "version": "0.0.31",
3
+ "version": "0.0.32",
4
4
  "description": "June host adapters + dev server. Host-coupled; depends on the pure @junejs/core contract layer.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,10 +30,11 @@
30
30
  "typecheck": "tsc --noEmit"
31
31
  },
32
32
  "dependencies": {
33
- "@junejs/core": "0.0.32",
33
+ "@junejs/core": "0.0.33",
34
34
  "@junejs/db": "0.0.28",
35
35
  "lightningcss": "^1.27.0",
36
36
  "marked": "^18.0.5",
37
+ "oxc-parser": "^0.137.0",
37
38
  "rolldown": "^1.1.0"
38
39
  },
39
40
  "peerDependencies": {
@@ -23,6 +23,8 @@
23
23
  import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
24
24
  import { join, relative } from "node:path";
25
25
 
26
+ import { parseSync } from "oxc-parser";
27
+
26
28
  export const ISLAND_REGISTRY_FILE = "_islands.gen.ts";
27
29
 
28
30
  // Shared with rsc-manifest.ts (the RSC client-reference codegen).
@@ -62,27 +64,83 @@ export function exportNames(src: string): string[] {
62
64
  return names;
63
65
  }
64
66
 
67
+ // The island NAME an `island(...)` call registers under — the SAME value the
68
+ // runtime derives (options.name ?? the component's function/identifier name).
69
+ // Keying the loader by THIS (not the export name) is what kills the desync
70
+ // (P1-1): the marker, the registry, and the loader all agree by construction.
71
+ // Returns null for an anonymous component with no { name } — island() throws on
72
+ // that at runtime, so we leave it for the runtime to report.
73
+ function islandNameOfCall(init: unknown): string | null {
74
+ const call = init as { type?: string; callee?: { type?: string; name?: string }; arguments?: any[] };
75
+ if (call?.type !== "CallExpression") return null;
76
+ if (call.callee?.type !== "Identifier" || call.callee.name !== "island") return null;
77
+ const [a0, a1] = call.arguments ?? [];
78
+ if (a1?.type === "ObjectExpression") {
79
+ for (const p of a1.properties ?? []) {
80
+ const key = p.key?.name ?? p.key?.value;
81
+ if (key === "name" && typeof p.value?.value === "string") return p.value.value;
82
+ }
83
+ }
84
+ if (a0?.type === "FunctionExpression") return a0.id?.name ?? null;
85
+ if (a0?.type === "Identifier") return a0.name ?? null;
86
+ return null; // arrow/anonymous without { name } → runtime island() errors
87
+ }
88
+
89
+ // True iff the module imports `island` from @junejs/core/islands (so a local
90
+ // function happening to be named `island` is never mistaken for the API).
91
+ function importsIslandApi(program: { body: any[] }): boolean {
92
+ return program.body.some(
93
+ (n) =>
94
+ n.type === "ImportDeclaration" &&
95
+ n.source?.value === "@junejs/core/islands" &&
96
+ (n.specifiers ?? []).some((s: any) => s.imported?.name === "island"),
97
+ );
98
+ }
99
+
65
100
  // Write app/_islands.gen.ts. Returns the number of island loaders emitted.
101
+ //
102
+ // AST-based (oxc-parser) — robust against multi-line exports / re-exports that a
103
+ // line regex would miss (P1-2), keyed by the island's runtime name (P1-1), and
104
+ // it throws on a duplicate name across modules (P2-1) instead of silently
105
+ // overwriting.
66
106
  export function generateIslandRegistry(appDir: string): number {
67
- const mods = walk(appDir)
68
- .filter((f) => /\.(tsx|ts|jsx)$/.test(f) && !f.endsWith(ISLAND_REGISTRY_FILE))
69
- .map((f) => ({ f, src: readFileSync(f, "utf8") }))
70
- .filter(
71
- ({ src }) =>
72
- firstStatementIsDirective(src, "use client") &&
73
- /@junejs\/core\/islands/.test(src) &&
74
- /\bisland\s*\(/.test(src),
75
- );
76
-
107
+ const seen = new Map<string, string>(); // island name → absolute file (dup guard)
77
108
  const entries: string[] = [];
78
- for (const { f, src } of mods) {
109
+
110
+ for (const f of walk(appDir)) {
111
+ if (!/\.(tsx|ts|jsx)$/.test(f) || f.endsWith(ISLAND_REGISTRY_FILE)) continue;
112
+ const src = readFileSync(f, "utf8");
113
+ if (!firstStatementIsDirective(src, "use client")) continue;
114
+ const { program } = parseSync(f, src);
115
+ if (!importsIslandApi(program as { body: any[] })) continue;
116
+
79
117
  const rel = "./" + relative(appDir, f).replace(/\.(tsx|ts|jsx)$/, "");
80
- for (const name of exportNames(src)) {
81
- if (name === "default") continue; // islands are named exports
82
- entries.push(` ${JSON.stringify(name)}: () => import(${JSON.stringify(rel)}),`);
118
+ for (const node of (program as { body: any[] }).body) {
119
+ const varDecl =
120
+ node.type === "ExportNamedDeclaration" && node.declaration?.type === "VariableDeclaration"
121
+ ? node.declaration
122
+ : node.type === "VariableDeclaration"
123
+ ? node
124
+ : null;
125
+ if (!varDecl) continue;
126
+ for (const d of varDecl.declarations ?? []) {
127
+ const name = islandNameOfCall(d.init);
128
+ if (!name) continue;
129
+ const prev = seen.get(name);
130
+ if (prev && prev !== f) {
131
+ throw new Error(
132
+ `[june] duplicate island name "${name}": defined in ${rel} and ` +
133
+ `./${relative(appDir, prev).replace(/\.(tsx|ts|jsx)$/, "")}. Island names must be unique.`,
134
+ );
135
+ }
136
+ if (seen.has(name)) continue;
137
+ seen.set(name, f);
138
+ entries.push(` ${JSON.stringify(name)}: () => import(${JSON.stringify(rel)}),`);
139
+ }
83
140
  }
84
141
  }
85
142
 
143
+ entries.sort(); // deterministic output
86
144
  const body = entries.length ? entries.join("\n") + "\n" : "";
87
145
  const out =
88
146
  "// AUTO-GENERATED by June — do not edit. Lazy loaders for island() modules.\n" +