@junejs/server 0.0.32 → 0.0.33
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 +1 -1
- package/src/build.ts +6 -1
- package/src/client-bundle.ts +4 -1
- package/src/island-registry.ts +81 -83
package/package.json
CHANGED
package/src/build.ts
CHANGED
|
@@ -532,7 +532,12 @@ ${adapterEntry.wrap("pipeline")}
|
|
|
532
532
|
// the dev-only code tree-shakes. Build-time on purpose: runtime process.env.NODE_ENV
|
|
533
533
|
// differs by target (Vercel sets it; workerd may not), so baking it makes the output
|
|
534
534
|
// deterministic and target-agnostic. `june dev` doesn't use this path.
|
|
535
|
-
transform: {
|
|
535
|
+
transform: {
|
|
536
|
+
define: { "process.env.NODE_ENV": JSON.stringify("production") },
|
|
537
|
+
// Route JSX through June's runtime so `<X client:*/>` in pages emits island
|
|
538
|
+
// markers at SSR (rolldown ignores the tsconfig/pragma; set it explicitly).
|
|
539
|
+
jsx: { runtime: "automatic", importSource: "@junejs/core" },
|
|
540
|
+
},
|
|
536
541
|
plugins: [rolldownCssModulesPlugin(cssModuleMaps)], // .module.css → scoped class map
|
|
537
542
|
external: (id: string) => {
|
|
538
543
|
// Bun built-ins exist only at Bun runtime, never in the workerd graph (see isBunSpecifier).
|
package/src/client-bundle.ts
CHANGED
|
@@ -50,7 +50,10 @@ async function bundleClient(entryFile: string, cwd: string, mode: BundleMode, ma
|
|
|
50
50
|
// branch resolved at build (no `process` in the browser to read it at runtime).
|
|
51
51
|
platform: "browser",
|
|
52
52
|
plugins: [rolldownCssModulesPlugin(maps)], // islands may import .module.css
|
|
53
|
-
transform: {
|
|
53
|
+
transform: {
|
|
54
|
+
define: { "process.env.NODE_ENV": JSON.stringify(mode) },
|
|
55
|
+
jsx: { runtime: "automatic", importSource: "@junejs/core" }, // June's island JSX runtime
|
|
56
|
+
},
|
|
54
57
|
resolve: { conditionNames: ["browser", "import", "default"] },
|
|
55
58
|
});
|
|
56
59
|
return bundle;
|
package/src/island-registry.ts
CHANGED
|
@@ -1,27 +1,16 @@
|
|
|
1
|
-
// Auto-generate the island registry by scanning
|
|
2
|
-
//
|
|
1
|
+
// Auto-generate the island registry by scanning ISLAND USAGE — every
|
|
2
|
+
// `<Counter client:*/>` in the app — and resolving the component's import AT THE
|
|
3
|
+
// USAGE SITE. Writes app/_islands.gen.ts mapping each island name to a lazy loader
|
|
4
|
+
// that dynamic-imports its module and resolves the component export.
|
|
3
5
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
+
// Usage-driven, so a third-party (lib) island is discovered the same way as an app
|
|
7
|
+
// one — the import specifier is right there at the call site (no lib manifest, no
|
|
8
|
+
// scan of node_modules). The KEY is the imported name = the component's runtime name
|
|
9
|
+
// = the marker the JSX runtime stamps, so all three agree by construction.
|
|
6
10
|
//
|
|
7
|
-
//
|
|
8
|
-
// "Counter": () => import("./poc/Counter"),
|
|
9
|
-
// "Tabs": () => import("./poc/Tabs"),
|
|
10
|
-
// };
|
|
11
|
-
//
|
|
12
|
-
// The KEY is the module's EXPORT name; importing the module runs its `island()`
|
|
13
|
-
// call, which self-registers the component under its runtime name. Those must
|
|
14
|
-
// agree — the marker the server stamps (the island's name) is what the client
|
|
15
|
-
// looks up. So the convention is: an island's name == its export name (the React
|
|
16
|
-
// default — `export const Counter = island(function Counter() {…})` satisfies it
|
|
17
|
-
// for free; otherwise pass `island(C, { name })`).
|
|
18
|
-
//
|
|
19
|
-
// Over-inclusion is harmless: a loader whose name no marker references is never
|
|
20
|
-
// called, so no chunk for an unused export is ever fetched.
|
|
21
|
-
//
|
|
22
|
-
// Host-coupled (node:fs), so it lives in @junejs/server, never the contract layer.
|
|
11
|
+
// Host-coupled (node:fs + oxc-parser), so it lives in @junejs/server.
|
|
23
12
|
import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
24
|
-
import { join, relative } from "node:path";
|
|
13
|
+
import { join, relative, dirname, resolve } from "node:path";
|
|
25
14
|
|
|
26
15
|
import { parseSync } from "oxc-parser";
|
|
27
16
|
|
|
@@ -36,7 +25,7 @@ export function walk(dir: string): string[] {
|
|
|
36
25
|
}
|
|
37
26
|
|
|
38
27
|
// True iff the first real statement (skipping blanks + line comments) is the
|
|
39
|
-
// directive —
|
|
28
|
+
// directive — used by the RSC manifest codegen.
|
|
40
29
|
export function firstStatementIsDirective(src: string, directive: string): boolean {
|
|
41
30
|
for (const line of src.split("\n")) {
|
|
42
31
|
const t = line.trim();
|
|
@@ -46,14 +35,14 @@ export function firstStatementIsDirective(src: string, directive: string): boole
|
|
|
46
35
|
return false;
|
|
47
36
|
}
|
|
48
37
|
|
|
49
|
-
// The named exports of a module
|
|
38
|
+
// The named exports of a module — used by the RSC manifest codegen.
|
|
50
39
|
export function exportNames(src: string): string[] {
|
|
51
40
|
const names: string[] = [];
|
|
52
41
|
for (const line of src.split("\n")) {
|
|
53
42
|
const t = line.trim();
|
|
54
43
|
const decl = t.match(/^export\s+(?:async\s+)?(?:function|const|let|var|class)\s+([A-Za-z0-9_$]+)/);
|
|
55
44
|
if (decl) names.push(decl[1]!);
|
|
56
|
-
const named = t.match(/^export\s*\{([^}]*)\}/);
|
|
45
|
+
const named = t.match(/^export\s*\{([^}]*)\}/);
|
|
57
46
|
if (named) {
|
|
58
47
|
for (const part of named[1]!.split(",")) {
|
|
59
48
|
const e = part.split(" as ").pop()?.trim();
|
|
@@ -64,87 +53,96 @@ export function exportNames(src: string): string[] {
|
|
|
64
53
|
return names;
|
|
65
54
|
}
|
|
66
55
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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;
|
|
56
|
+
type Import = { spec: string; imported: string; kind: "named" | "default" | "namespace" };
|
|
57
|
+
|
|
58
|
+
// local binding name → its import, for one module.
|
|
59
|
+
function importMap(program: { body: any[] }): Map<string, Import> {
|
|
60
|
+
const map = new Map<string, Import>();
|
|
61
|
+
for (const n of program.body) {
|
|
62
|
+
if (n.type !== "ImportDeclaration" || typeof n.source?.value !== "string") continue;
|
|
63
|
+
const spec = n.source.value as string;
|
|
64
|
+
for (const s of n.specifiers ?? []) {
|
|
65
|
+
if (s.type === "ImportSpecifier") map.set(s.local.name, { spec, imported: s.imported.name, kind: "named" });
|
|
66
|
+
else if (s.type === "ImportDefaultSpecifier") map.set(s.local.name, { spec, imported: "default", kind: "default" });
|
|
67
|
+
else if (s.type === "ImportNamespaceSpecifier") map.set(s.local.name, { spec, imported: "*", kind: "namespace" });
|
|
82
68
|
}
|
|
83
69
|
}
|
|
84
|
-
|
|
85
|
-
if (a0?.type === "Identifier") return a0.name ?? null;
|
|
86
|
-
return null; // arrow/anonymous without { name } → runtime island() errors
|
|
70
|
+
return map;
|
|
87
71
|
}
|
|
88
72
|
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
(n) =>
|
|
94
|
-
n.type === "ImportDeclaration" &&
|
|
95
|
-
n.source?.value === "@junejs/core/islands" &&
|
|
96
|
-
(n.specifiers ?? []).some((s: any) => s.imported?.name === "island"),
|
|
73
|
+
// Does this JSX element carry a `client:*` directive?
|
|
74
|
+
function hasClientDirective(openingElement: any): boolean {
|
|
75
|
+
return (openingElement.attributes ?? []).some(
|
|
76
|
+
(a: any) => a.type === "JSXAttribute" && a.name?.type === "JSXNamespacedName" && a.name.namespace?.name === "client",
|
|
97
77
|
);
|
|
98
78
|
}
|
|
99
79
|
|
|
80
|
+
// Collect every JSX element tag used with a client:* directive (recursive walk).
|
|
81
|
+
function islandTags(program: { body: any[] }): string[] {
|
|
82
|
+
const tags: string[] = [];
|
|
83
|
+
(function walkNode(node: any) {
|
|
84
|
+
if (!node || typeof node !== "object") return;
|
|
85
|
+
if (Array.isArray(node)) return node.forEach(walkNode);
|
|
86
|
+
if (node.type === "JSXElement" && node.openingElement?.name?.type === "JSXIdentifier" && hasClientDirective(node.openingElement)) {
|
|
87
|
+
tags.push(node.openingElement.name.name);
|
|
88
|
+
}
|
|
89
|
+
for (const k in node) if (k !== "type") walkNode(node[k]);
|
|
90
|
+
})(program);
|
|
91
|
+
return tags;
|
|
92
|
+
}
|
|
93
|
+
|
|
100
94
|
// 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.
|
|
106
95
|
export function generateIslandRegistry(appDir: string): number {
|
|
107
|
-
const seen = new Map<string, string>(); // island name →
|
|
96
|
+
const seen = new Map<string, string>(); // island name → loader specifier (dup guard)
|
|
108
97
|
const entries: string[] = [];
|
|
109
98
|
|
|
110
99
|
for (const f of walk(appDir)) {
|
|
111
|
-
if (!/\.(tsx|
|
|
100
|
+
if (!/\.(tsx|jsx)$/.test(f) || f.endsWith(ISLAND_REGISTRY_FILE) || f.endsWith(".gen.ts")) continue;
|
|
112
101
|
const src = readFileSync(f, "utf8");
|
|
113
|
-
if (!
|
|
102
|
+
if (!src.includes("client:")) continue; // cheap pre-filter
|
|
114
103
|
const { program } = parseSync(f, src);
|
|
115
|
-
|
|
104
|
+
const ast = program as { body: any[] };
|
|
105
|
+
const imports = importMap(ast);
|
|
106
|
+
const rel = "./" + relative(appDir, f);
|
|
107
|
+
|
|
108
|
+
for (const tag of islandTags(ast)) {
|
|
109
|
+
const imp = imports.get(tag);
|
|
110
|
+
if (!imp) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`[june] <${tag} client:*/> in ${rel}: an island must be an IMPORTED component (a named import), not a local definition.`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
if (imp.kind !== "named") {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`[june] <${tag} client:*/> in ${rel}: island imports must be NAMED imports (a named \`{ ${tag} }\` import; default/namespace imports aren't supported as islands).`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const name = imp.imported; // = the component's runtime name = the marker the JSX runtime stamps
|
|
121
|
+
// Re-base a relative specifier from the importing file to appDir (the gen
|
|
122
|
+
// file lives in appDir); a package specifier is used verbatim.
|
|
123
|
+
const loaderSpec = imp.spec.startsWith(".")
|
|
124
|
+
? "./" + relative(appDir, resolve(dirname(f), imp.spec))
|
|
125
|
+
: imp.spec;
|
|
116
126
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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)}),`);
|
|
127
|
+
const prev = seen.get(name);
|
|
128
|
+
if (prev && prev !== loaderSpec) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`[june] duplicate island name "${name}" from different modules ("${prev}" and "${loaderSpec}"). Island names must be unique.`,
|
|
131
|
+
);
|
|
139
132
|
}
|
|
133
|
+
if (seen.has(name)) continue; // same island used on several pages — fine
|
|
134
|
+
seen.set(name, loaderSpec);
|
|
135
|
+
entries.push(
|
|
136
|
+
` ${JSON.stringify(name)}: () => import(${JSON.stringify(loaderSpec)}).then((m) => m.${name}),`,
|
|
137
|
+
);
|
|
140
138
|
}
|
|
141
139
|
}
|
|
142
140
|
|
|
143
141
|
entries.sort(); // deterministic output
|
|
144
142
|
const body = entries.length ? entries.join("\n") + "\n" : "";
|
|
145
143
|
const out =
|
|
146
|
-
"// AUTO-GENERATED by June — do not edit. Lazy loaders for
|
|
147
|
-
"// Maps each island name to a
|
|
144
|
+
"// AUTO-GENERATED by June — do not edit. Lazy loaders for client:* islands.\n" +
|
|
145
|
+
"// Maps each island name to a loader that resolves its component; hydrateIslands uses it.\n" +
|
|
148
146
|
"export const ISLAND_LOADERS: Record<string, () => Promise<unknown>> = {\n" +
|
|
149
147
|
body +
|
|
150
148
|
"};\n";
|