@junejs/server 0.0.31 → 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 +3 -2
- package/src/build.ts +6 -1
- package/src/client-bundle.ts +4 -1
- package/src/island-registry.ts +97 -41
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@junejs/server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.33",
|
|
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.
|
|
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": {
|
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,18 @@
|
|
|
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";
|
|
14
|
+
|
|
15
|
+
import { parseSync } from "oxc-parser";
|
|
25
16
|
|
|
26
17
|
export const ISLAND_REGISTRY_FILE = "_islands.gen.ts";
|
|
27
18
|
|
|
@@ -34,7 +25,7 @@ export function walk(dir: string): string[] {
|
|
|
34
25
|
}
|
|
35
26
|
|
|
36
27
|
// True iff the first real statement (skipping blanks + line comments) is the
|
|
37
|
-
// directive —
|
|
28
|
+
// directive — used by the RSC manifest codegen.
|
|
38
29
|
export function firstStatementIsDirective(src: string, directive: string): boolean {
|
|
39
30
|
for (const line of src.split("\n")) {
|
|
40
31
|
const t = line.trim();
|
|
@@ -44,14 +35,14 @@ export function firstStatementIsDirective(src: string, directive: string): boole
|
|
|
44
35
|
return false;
|
|
45
36
|
}
|
|
46
37
|
|
|
47
|
-
// The named exports of a module
|
|
38
|
+
// The named exports of a module — used by the RSC manifest codegen.
|
|
48
39
|
export function exportNames(src: string): string[] {
|
|
49
40
|
const names: string[] = [];
|
|
50
41
|
for (const line of src.split("\n")) {
|
|
51
42
|
const t = line.trim();
|
|
52
43
|
const decl = t.match(/^export\s+(?:async\s+)?(?:function|const|let|var|class)\s+([A-Za-z0-9_$]+)/);
|
|
53
44
|
if (decl) names.push(decl[1]!);
|
|
54
|
-
const named = t.match(/^export\s*\{([^}]*)\}/);
|
|
45
|
+
const named = t.match(/^export\s*\{([^}]*)\}/);
|
|
55
46
|
if (named) {
|
|
56
47
|
for (const part of named[1]!.split(",")) {
|
|
57
48
|
const e = part.split(" as ").pop()?.trim();
|
|
@@ -62,31 +53,96 @@ export function exportNames(src: string): string[] {
|
|
|
62
53
|
return names;
|
|
63
54
|
}
|
|
64
55
|
|
|
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" });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return map;
|
|
71
|
+
}
|
|
72
|
+
|
|
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",
|
|
77
|
+
);
|
|
78
|
+
}
|
|
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
|
+
|
|
65
94
|
// Write app/_islands.gen.ts. Returns the number of island loaders emitted.
|
|
66
95
|
export function generateIslandRegistry(appDir: string): number {
|
|
67
|
-
const
|
|
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
|
-
|
|
96
|
+
const seen = new Map<string, string>(); // island name → loader specifier (dup guard)
|
|
77
97
|
const entries: string[] = [];
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
98
|
+
|
|
99
|
+
for (const f of walk(appDir)) {
|
|
100
|
+
if (!/\.(tsx|jsx)$/.test(f) || f.endsWith(ISLAND_REGISTRY_FILE) || f.endsWith(".gen.ts")) continue;
|
|
101
|
+
const src = readFileSync(f, "utf8");
|
|
102
|
+
if (!src.includes("client:")) continue; // cheap pre-filter
|
|
103
|
+
const { program } = parseSync(f, src);
|
|
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;
|
|
126
|
+
|
|
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
|
+
);
|
|
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
|
+
);
|
|
83
138
|
}
|
|
84
139
|
}
|
|
85
140
|
|
|
141
|
+
entries.sort(); // deterministic output
|
|
86
142
|
const body = entries.length ? entries.join("\n") + "\n" : "";
|
|
87
143
|
const out =
|
|
88
|
-
"// AUTO-GENERATED by June — do not edit. Lazy loaders for
|
|
89
|
-
"// 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" +
|
|
90
146
|
"export const ISLAND_LOADERS: Record<string, () => Promise<unknown>> = {\n" +
|
|
91
147
|
body +
|
|
92
148
|
"};\n";
|