@farm.js/islx 0.0.4 → 0.0.5
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/CHANGELOG.md +13 -0
- package/README.md +36 -1
- package/dist/_chunks/{actions-CtLpJPEB.mjs → actions-BuS5jDi0.mjs} +3 -144
- package/dist/_chunks/island-DVOr-OUw.mjs +262 -0
- package/dist/_chunks/{tree-opIpaCke.mjs → tree-BUkGP4AR.mjs} +3 -261
- package/dist/_chunks/values-37UIzxM0.mjs +144 -0
- package/dist/_chunks/vite-DG7hBn8A.mjs +503 -0
- package/dist/react/client.mjs +2 -1
- package/dist/react/server.mjs +4 -2
- package/dist/react.mjs +3 -1
- package/dist/server.mjs +2 -1
- package/dist/tanstack-start/server.d.mts +11 -0
- package/dist/tanstack-start/server.mjs +54 -0
- package/dist/tanstack-start.d.mts +8 -0
- package/dist/tanstack-start.mjs +88 -0
- package/dist/vite.d.mts +21 -2
- package/dist/vite.mjs +1 -497
- package/package.json +19 -1
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
import { i as transformActions, r as hasModuleDirective, t as ActionCompileError } from "./actions-Eutk6UzT.mjs";
|
|
2
|
+
import { t as ISLAND_STRATEGIES } from "./schedule-CjoybS3b.mjs";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { walk } from "estree-walker";
|
|
6
|
+
import MagicString from "magic-string";
|
|
7
|
+
import { analyze } from "periscopic";
|
|
8
|
+
import { readFile, readdir, writeFile } from "node:fs/promises";
|
|
9
|
+
// src/compiler/client.ts
|
|
10
|
+
/**
|
|
11
|
+
* The `"use client"` compiler. In the server build, every component a `"use client"` module
|
|
12
|
+
* exports renders as an island: its server HTML, and its props for the browser. The browser build
|
|
13
|
+
* keeps the module as it is.
|
|
14
|
+
*
|
|
15
|
+
* A component is an export named in PascalCase (`Chart`, not `COLORS` or `useChart`) that is not
|
|
16
|
+
* a literal value, or the default export. Other exports (hooks, constants, helpers) stay as they
|
|
17
|
+
* are on both sides.
|
|
18
|
+
*
|
|
19
|
+
* When an island hydrates comes from the module's `hydrate` export:
|
|
20
|
+
*
|
|
21
|
+
* export const hydrate = "visible"; // every component in the file
|
|
22
|
+
* export const hydrate = { Chart: "visible", Legend: "idle" }; // per component; others "load"
|
|
23
|
+
*/
|
|
24
|
+
const FUNCTION_TYPES = /* @__PURE__ */ new Set([
|
|
25
|
+
"FunctionDeclaration",
|
|
26
|
+
"FunctionExpression",
|
|
27
|
+
"ArrowFunctionExpression"
|
|
28
|
+
]);
|
|
29
|
+
function isComponentName(name) {
|
|
30
|
+
return /^[A-Z]/.test(name) && /[a-z]/.test(name);
|
|
31
|
+
}
|
|
32
|
+
const VALUE_TYPES = /* @__PURE__ */ new Set([
|
|
33
|
+
"Literal",
|
|
34
|
+
"ArrayExpression",
|
|
35
|
+
"ObjectExpression",
|
|
36
|
+
"TemplateLiteral",
|
|
37
|
+
"UnaryExpression",
|
|
38
|
+
"BinaryExpression"
|
|
39
|
+
]);
|
|
40
|
+
/** Reads the `hydrate` export: a strategy for every component, or one per component. */
|
|
41
|
+
function readStrategies(program, fail) {
|
|
42
|
+
const byName = /* @__PURE__ */ new Map();
|
|
43
|
+
const check = (value, node) => {
|
|
44
|
+
if (typeof value !== "string" || !ISLAND_STRATEGIES.includes(value)) fail(`hydrate must be one of ${ISLAND_STRATEGIES.map((s) => `"${s}"`).join(", ")}`, node);
|
|
45
|
+
return value;
|
|
46
|
+
};
|
|
47
|
+
for (const node of program.body ?? []) {
|
|
48
|
+
if (node.type !== "ExportNamedDeclaration" || node.declaration?.type !== "VariableDeclaration") continue;
|
|
49
|
+
for (const declarator of node.declaration.declarations ?? []) {
|
|
50
|
+
if (declarator.id?.type !== "Identifier" || declarator.id.name !== "hydrate") continue;
|
|
51
|
+
let init = declarator.init;
|
|
52
|
+
while (init && (init.type === "TSAsExpression" || init.type === "TSSatisfiesExpression")) init = init.expression;
|
|
53
|
+
if (init?.type === "Literal") return {
|
|
54
|
+
all: check(init.value, init),
|
|
55
|
+
byName
|
|
56
|
+
};
|
|
57
|
+
if (init?.type === "ObjectExpression") {
|
|
58
|
+
for (const property of init.properties ?? []) {
|
|
59
|
+
const key = property.key?.type === "Identifier" ? property.key.name : property.key?.value;
|
|
60
|
+
if (property.type !== "Property" || property.computed || typeof key !== "string" || property.value?.type !== "Literal") fail("hydrate must be a string, or an object of strings: { Chart: \"visible\" }", property);
|
|
61
|
+
byName.set(key, check(property.value.value, property.value));
|
|
62
|
+
}
|
|
63
|
+
return { byName };
|
|
64
|
+
}
|
|
65
|
+
fail("hydrate must be written as a literal: \"visible\", or { Chart: \"visible\" }", declarator);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { byName };
|
|
69
|
+
}
|
|
70
|
+
/** Wraps a `"use client"` module's components as islands, or returns null for any other module. */
|
|
71
|
+
function transformClientModule(options) {
|
|
72
|
+
const { code, id, root } = options;
|
|
73
|
+
if (!hasModuleDirective(code, options.directive ?? "use client")) return null;
|
|
74
|
+
const file = id.split("?", 1)[0];
|
|
75
|
+
const relative = path.relative(root, file).split(path.sep).join("/");
|
|
76
|
+
const program = options.parse(code);
|
|
77
|
+
const fail = (message, node) => {
|
|
78
|
+
throw new ActionCompileError(message, file, code, node.start);
|
|
79
|
+
};
|
|
80
|
+
const strategies = readStrategies(program, fail);
|
|
81
|
+
const strategyOf = (name) => {
|
|
82
|
+
const configured = options.strategy?.(name);
|
|
83
|
+
if (configured !== void 0 && !ISLAND_STRATEGIES.includes(configured)) throw new Error(`islx: strategy for ${name} must be one of ${ISLAND_STRATEGIES.join(", ")}`);
|
|
84
|
+
return configured ?? strategies.byName.get(name) ?? strategies.all ?? "load";
|
|
85
|
+
};
|
|
86
|
+
const output = new MagicString(code);
|
|
87
|
+
const wrapped = [];
|
|
88
|
+
for (const node of program.body ?? []) if (node.type === "ExportNamedDeclaration" && !node.source && node.exportKind !== "type") {
|
|
89
|
+
const declaration = node.declaration;
|
|
90
|
+
if (declaration) {
|
|
91
|
+
const names = declaration.type === "VariableDeclaration" ? declaration.declarations.flatMap((d) => d.id?.type === "Identifier" && !(d.init && VALUE_TYPES.has(d.init.type)) ? [d.id.name] : []) : declaration.id?.name ? [declaration.id.name] : [];
|
|
92
|
+
const declared = declaration.type === "VariableDeclaration" ? declaration.declarations.length : names.length;
|
|
93
|
+
const components = names.filter(isComponentName);
|
|
94
|
+
if (components.length === 0 || String(declaration.type).startsWith("TS")) continue;
|
|
95
|
+
if (components.length !== declared) fail("Export components and other values in separate statements", declaration);
|
|
96
|
+
output.remove(node.start, declaration.start);
|
|
97
|
+
for (const name of components) wrapped.push({
|
|
98
|
+
local: name,
|
|
99
|
+
exportName: name
|
|
100
|
+
});
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const kept = [];
|
|
104
|
+
for (const specifier of node.specifiers ?? []) {
|
|
105
|
+
const exported = specifier.exported?.name ?? specifier.exported?.value;
|
|
106
|
+
const local = specifier.local?.name ?? specifier.local?.value;
|
|
107
|
+
if (specifier.exportKind !== "type" && (isComponentName(exported) || exported === "default")) wrapped.push({
|
|
108
|
+
local,
|
|
109
|
+
exportName: exported
|
|
110
|
+
});
|
|
111
|
+
else kept.push(output.slice(specifier.start, specifier.end));
|
|
112
|
+
}
|
|
113
|
+
output.overwrite(node.start, node.end, kept.length > 0 ? `export { ${kept.join(", ")} };` : "");
|
|
114
|
+
} else if (node.type === "ExportDefaultDeclaration") {
|
|
115
|
+
const declaration = node.declaration;
|
|
116
|
+
if (FUNCTION_TYPES.has(declaration.type) || declaration.type === "ClassDeclaration" || declaration.type === "ClassExpression") {
|
|
117
|
+
if (declaration.id?.name) {
|
|
118
|
+
output.remove(node.start, declaration.start);
|
|
119
|
+
wrapped.push({
|
|
120
|
+
local: declaration.id.name,
|
|
121
|
+
exportName: "default"
|
|
122
|
+
});
|
|
123
|
+
} else {
|
|
124
|
+
output.overwrite(node.start, declaration.start, "const $$islx_default = ");
|
|
125
|
+
wrapped.push({
|
|
126
|
+
local: "$$islx_default",
|
|
127
|
+
exportName: "default"
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
} else if (declaration.type === "Identifier" || declaration.type.endsWith("Expression")) {
|
|
131
|
+
output.overwrite(node.start, declaration.start, "const $$islx_default = ");
|
|
132
|
+
if (!code.slice(declaration.end, node.end).includes(";")) output.appendLeft(declaration.end, ";");
|
|
133
|
+
wrapped.push({
|
|
134
|
+
local: "$$islx_default",
|
|
135
|
+
exportName: "default"
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (wrapped.length === 0) return null;
|
|
140
|
+
const runtime = options.runtime ?? "@farm.js/islx/react";
|
|
141
|
+
const src = options.reference(relative);
|
|
142
|
+
const lines = [...wrapped.map(({ local, exportName }, index) => {
|
|
143
|
+
return `const $$islx_island_${index} = $$islx_island(${local}, ${src}, ${JSON.stringify(exportName)}, ${JSON.stringify(strategyOf(exportName === "default" ? "default" : exportName))});`;
|
|
144
|
+
}), `export { ${wrapped.map(({ exportName }, index) => `$$islx_island_${index} as ${exportName}`).join(", ")} };`];
|
|
145
|
+
const imports = [`import { createIsland as $$islx_island } from ${JSON.stringify(runtime)};`, ...(options.sideEffects ?? []).map((module) => `import ${JSON.stringify(module)};`)];
|
|
146
|
+
output.prepend(`${imports.join("\n")}\n`);
|
|
147
|
+
output.append(`\n${lines.join("\n")}\n`);
|
|
148
|
+
return {
|
|
149
|
+
code: output.toString(),
|
|
150
|
+
map: output.generateMap({
|
|
151
|
+
hires: true,
|
|
152
|
+
source: file
|
|
153
|
+
}),
|
|
154
|
+
islands: wrapped.map(({ exportName }) => ({
|
|
155
|
+
exportName,
|
|
156
|
+
strategy: strategyOf(exportName)
|
|
157
|
+
}))
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
// src/vite.ts
|
|
161
|
+
/**
|
|
162
|
+
* @farm.js/islx/vite: islx for Vite 6 and later (the Environment API).
|
|
163
|
+
*
|
|
164
|
+
* import react from "@vitejs/plugin-react";
|
|
165
|
+
* import { islx } from "@farm.js/islx/vite";
|
|
166
|
+
* export default defineConfig({ plugins: [react(), islx()] });
|
|
167
|
+
*
|
|
168
|
+
* - Server environments: `"use client"` components render as islands, `"use server"` exports
|
|
169
|
+
* and inline actions register.
|
|
170
|
+
* - The client environment: `"use server"` modules become references; inline actions are
|
|
171
|
+
* refused; the browser entry hydrates islands.
|
|
172
|
+
* - `vite build` builds the server first, which finds the client components pages use, then the
|
|
173
|
+
* browser with exactly those (plus `clientComponents.include`), each in a chunk of its own that
|
|
174
|
+
* the browser entry imports when an island needs it. An island names its module by its path
|
|
175
|
+
* from the root; the server build learns the entry's URL once the browser build has named it.
|
|
176
|
+
*/
|
|
177
|
+
const RUNTIME_ID = "virtual:islx/runtime";
|
|
178
|
+
/** @internal The browser entry, for a framework whose own entry imports it. */
|
|
179
|
+
const ENTRY_ID = "virtual:islx/client-entry";
|
|
180
|
+
const CLIENT_HOOKS_ID = "virtual:islx/hooks-client";
|
|
181
|
+
const SERVER_HOOKS_ID = "virtual:islx/hooks-server";
|
|
182
|
+
const BROWSER_GLOBALS = /* @__PURE__ */ new Set([
|
|
183
|
+
"window",
|
|
184
|
+
"document",
|
|
185
|
+
"location",
|
|
186
|
+
"history",
|
|
187
|
+
"navigator",
|
|
188
|
+
"screen",
|
|
189
|
+
"localStorage",
|
|
190
|
+
"sessionStorage",
|
|
191
|
+
"alert",
|
|
192
|
+
"confirm",
|
|
193
|
+
"prompt",
|
|
194
|
+
"requestAnimationFrame",
|
|
195
|
+
"cancelAnimationFrame",
|
|
196
|
+
"requestIdleCallback",
|
|
197
|
+
"cancelIdleCallback",
|
|
198
|
+
"IntersectionObserver",
|
|
199
|
+
"MutationObserver",
|
|
200
|
+
"ResizeObserver",
|
|
201
|
+
"HTMLElement",
|
|
202
|
+
"Element",
|
|
203
|
+
"Node",
|
|
204
|
+
"CustomEvent",
|
|
205
|
+
"Event",
|
|
206
|
+
"matchMedia",
|
|
207
|
+
"getComputedStyle",
|
|
208
|
+
"innerWidth",
|
|
209
|
+
"innerHeight",
|
|
210
|
+
"scrollX",
|
|
211
|
+
"scrollY",
|
|
212
|
+
"self"
|
|
213
|
+
]);
|
|
214
|
+
/**
|
|
215
|
+
* The source of a hooks module made from callbacks. Each callback is copied as written, so it
|
|
216
|
+
* must not use anything from where it was written: an outside variable, an import.
|
|
217
|
+
*/
|
|
218
|
+
function serializeHooks(hooks, where, parse) {
|
|
219
|
+
const entries = [];
|
|
220
|
+
for (const [name, value] of Object.entries(hooks)) {
|
|
221
|
+
if (value === void 0) continue;
|
|
222
|
+
if (typeof value !== "function") throw new Error(`islx: hooks.${where}.${name} must be a function`);
|
|
223
|
+
const source = Function.prototype.toString.call(value);
|
|
224
|
+
if (source.includes("[native code]")) throw new Error(`islx: hooks.${where}.${name} must be written in the config, not a built-in or bound function`);
|
|
225
|
+
const expression = parse("hook.js", `(${source});`).errors.length === 0;
|
|
226
|
+
entries.push(expression ? `${JSON.stringify(name)}: ${source}` : source);
|
|
227
|
+
}
|
|
228
|
+
const code = `export default {\n${entries.join(",\n")}\n};\n`;
|
|
229
|
+
const program = parse("hooks.js", code);
|
|
230
|
+
const { globals } = analyze(program);
|
|
231
|
+
const outside = [...globals.keys()].filter((name) => where === "client" ? !(name in globalThis) && !BROWSER_GLOBALS.has(name) : BROWSER_GLOBALS.has(name) || !(name in globalThis));
|
|
232
|
+
let imports = false;
|
|
233
|
+
walk(program, { enter(node) {
|
|
234
|
+
if (node.type === "ImportExpression") imports = true;
|
|
235
|
+
} });
|
|
236
|
+
if (outside.length > 0 || imports) {
|
|
237
|
+
const what = imports ? "`import()`" : outside.map((name) => `\`${name}\``).join(", ");
|
|
238
|
+
throw new Error(`islx: hooks.${where} uses ${what}, which does not exist where the hook runs: hooks written in the config are copied into the ${where === "client" ? "browser" : "server"} build as source. Use a global${where === "client" ? " (window.analytics)" : ""}, or put the hooks in a module and pass its path: hooks: { ${where}: "./src/islx.${where}.ts" }.`);
|
|
239
|
+
}
|
|
240
|
+
return code;
|
|
241
|
+
}
|
|
242
|
+
/** Stand for the browser entry's URL, and the files each client module needs, in the server
|
|
243
|
+
* build until the browser build names them. */
|
|
244
|
+
const ENTRY_PLACEHOLDER = "__islx_client_entry_url__";
|
|
245
|
+
const MODULES_PLACEHOLDER = "__islx_client_modules__";
|
|
246
|
+
const SCRIPT = /\.[cm]?[jt]sx?$/;
|
|
247
|
+
async function createParser() {
|
|
248
|
+
const withErrors = (result) => Object.assign(result.program, { errors: result.errors ?? [] });
|
|
249
|
+
const vite = await import("vite");
|
|
250
|
+
if (typeof vite.parseSync === "function") return (file, code) => withErrors(vite.parseSync(file, code));
|
|
251
|
+
try {
|
|
252
|
+
const oxc = await import("oxc-parser");
|
|
253
|
+
return (file, code) => withErrors(oxc.parseSync(file, code));
|
|
254
|
+
} catch {
|
|
255
|
+
throw new Error("@farm.js/islx/vite needs Vite 8, or oxc-parser installed with an older Vite");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/** A tiny glob matcher: `**`, `*` and `?`, on paths from the root. */
|
|
259
|
+
function globToRegExp(glob) {
|
|
260
|
+
let pattern = "";
|
|
261
|
+
for (let index = 0; index < glob.length; index++) {
|
|
262
|
+
const char = glob[index];
|
|
263
|
+
if (char === "*" && glob[index + 1] === "*") {
|
|
264
|
+
pattern += glob[index + 2] === "/" ? "(?:.*/)?" : ".*";
|
|
265
|
+
index += glob[index + 2] === "/" ? 2 : 1;
|
|
266
|
+
} else if (char === "*") pattern += "[^/]*";
|
|
267
|
+
else if (char === "?") pattern += "[^/]";
|
|
268
|
+
else pattern += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
269
|
+
}
|
|
270
|
+
return new RegExp(`^${pattern.replace(/^\.\//, "")}$`);
|
|
271
|
+
}
|
|
272
|
+
async function listFiles(root, directory = root) {
|
|
273
|
+
const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
274
|
+
const files = [];
|
|
275
|
+
for (const entry of entries) {
|
|
276
|
+
if (entry.name === "node_modules" || entry.name.startsWith(".") || entry.name === "dist") continue;
|
|
277
|
+
const full = path.join(directory, entry.name);
|
|
278
|
+
if (entry.isDirectory()) files.push(...await listFiles(root, full));
|
|
279
|
+
else if (SCRIPT.test(entry.name)) files.push(full);
|
|
280
|
+
}
|
|
281
|
+
return files;
|
|
282
|
+
}
|
|
283
|
+
async function listOutput(directory) {
|
|
284
|
+
const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
285
|
+
const files = [];
|
|
286
|
+
for (const entry of entries) {
|
|
287
|
+
const full = path.join(directory, entry.name);
|
|
288
|
+
if (entry.isDirectory()) files.push(...await listOutput(full));
|
|
289
|
+
else if (/\.[cm]?js$/.test(entry.name)) files.push(full);
|
|
290
|
+
}
|
|
291
|
+
return files;
|
|
292
|
+
}
|
|
293
|
+
const toPosix = (value) => value.split(path.sep).join("/");
|
|
294
|
+
const RESERVED_DIRECTIVES = /* @__PURE__ */ new Set(["use strict", "use asm"]);
|
|
295
|
+
/** The directives to use, refusing ones that would make a file mean two things. */
|
|
296
|
+
function resolveDirectives(directives = {}) {
|
|
297
|
+
const client = directives.client ?? "use client";
|
|
298
|
+
const server = directives.server ?? "use server";
|
|
299
|
+
for (const [kind, value] of [["client", client], ["server", server]]) if (typeof value !== "string" || !/^[\w-]+( [\w-]+)*$/.test(value) || RESERVED_DIRECTIVES.has(value)) throw new Error(`islx: directives.${kind} must be a plain directive such as "use island", not ${JSON.stringify(value)}`);
|
|
300
|
+
if (client === server) throw new Error(`islx: directives.client and directives.server are both ${JSON.stringify(client)}`);
|
|
301
|
+
return {
|
|
302
|
+
client,
|
|
303
|
+
server
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function islx(options = {}, host = {}) {
|
|
307
|
+
const directives = resolveDirectives(options.directives);
|
|
308
|
+
let config;
|
|
309
|
+
let parse;
|
|
310
|
+
let preamble = false;
|
|
311
|
+
const include = (options.clientComponents?.include ?? []).map(globToRegExp);
|
|
312
|
+
const exclude = (options.clientComponents?.exclude ?? []).map(globToRegExp);
|
|
313
|
+
/** Client modules the server build rendered, by path from the root. */
|
|
314
|
+
const discovered = /* @__PURE__ */ new Set();
|
|
315
|
+
/** The browser entry's URL, once the browser build named it. */
|
|
316
|
+
let entryUrl;
|
|
317
|
+
/** The files each client module needs, once the browser build named them. */
|
|
318
|
+
const moduleFiles = {};
|
|
319
|
+
/** Whether the browser build ran first (a framework that orders the build). */
|
|
320
|
+
let clientBuilt = false;
|
|
321
|
+
const buildKey = randomBytes(32).toString("base64url");
|
|
322
|
+
const isServer = (name) => options.serverEnvironments ? options.serverEnvironments.includes(name) : name !== "client";
|
|
323
|
+
const relativeOf = (id) => toPosix(path.relative(config.root, id.split("?", 1)[0]));
|
|
324
|
+
const excluded = (relative) => exclude.some((pattern) => pattern.test(relative));
|
|
325
|
+
const isBuild = () => config.command === "build";
|
|
326
|
+
const clientInputs = async () => {
|
|
327
|
+
const inputs = new Set(discovered);
|
|
328
|
+
if (include.length > 0) for (const file of await listFiles(config.root)) {
|
|
329
|
+
const relative = relativeOf(file);
|
|
330
|
+
if (!include.some((pattern) => pattern.test(relative)) || excluded(relative)) continue;
|
|
331
|
+
if ((await readFile(file, "utf8")).includes("use client")) inputs.add(relative);
|
|
332
|
+
}
|
|
333
|
+
return [...inputs].sort();
|
|
334
|
+
};
|
|
335
|
+
return [
|
|
336
|
+
{
|
|
337
|
+
name: "islx",
|
|
338
|
+
enforce: "pre",
|
|
339
|
+
sharedDuringBuild: true,
|
|
340
|
+
config(user) {
|
|
341
|
+
const clientBuild = user.environments?.client?.build;
|
|
342
|
+
const input = clientBuild?.rollupOptions?.input;
|
|
343
|
+
return {
|
|
344
|
+
builder: user.builder ?? {},
|
|
345
|
+
environments: { client: {
|
|
346
|
+
optimizeDeps: { include: [
|
|
347
|
+
"react",
|
|
348
|
+
"react/jsx-runtime",
|
|
349
|
+
"react/jsx-dev-runtime",
|
|
350
|
+
"react-dom",
|
|
351
|
+
"react-dom/client"
|
|
352
|
+
] },
|
|
353
|
+
build: {
|
|
354
|
+
outDir: clientBuild?.outDir ?? "dist/client",
|
|
355
|
+
rollupOptions: { input: host.frameworkEntry ? input : {
|
|
356
|
+
...typeof input === "string" ? { index: input } : Array.isArray(input) ? Object.fromEntries(input.map((entry, index) => [`input${index}`, entry])) : input,
|
|
357
|
+
"islx-entry": ENTRY_ID
|
|
358
|
+
} }
|
|
359
|
+
}
|
|
360
|
+
} }
|
|
361
|
+
};
|
|
362
|
+
},
|
|
363
|
+
async configResolved(resolved) {
|
|
364
|
+
config = resolved;
|
|
365
|
+
parse = await createParser();
|
|
366
|
+
preamble = resolved.plugins.some((plugin) => plugin.name === "vite:react-virtual-preamble");
|
|
367
|
+
},
|
|
368
|
+
resolveId(id) {
|
|
369
|
+
if (id === RUNTIME_ID || id === "virtual:islx/client-entry" || id === CLIENT_HOOKS_ID || id === SERVER_HOOKS_ID) return `\0${id}`;
|
|
370
|
+
},
|
|
371
|
+
async load(id) {
|
|
372
|
+
if (id === `\0${RUNTIME_ID}`) {
|
|
373
|
+
const entry = host.frameworkEntry ? void 0 : !isBuild() ? `${config.base}@id/__x00__${ENTRY_ID}` : clientBuilt ? entryUrl : ENTRY_PLACEHOLDER;
|
|
374
|
+
const modules = !isBuild() ? "{}" : clientBuilt ? JSON.stringify(moduleFiles) : `JSON.parse(${JSON.stringify(MODULES_PLACEHOLDER)})`;
|
|
375
|
+
return [
|
|
376
|
+
...host.frameworkRender ? [`import "@farm.js/islx/react/server";`] : [],
|
|
377
|
+
`import { setBuildDefaults, setServerHooks } from "@farm.js/islx/server";`,
|
|
378
|
+
`import hooks from ${JSON.stringify(SERVER_HOOKS_ID)};`,
|
|
379
|
+
`setBuildDefaults({ entry: ${JSON.stringify(entry)}, key: ${JSON.stringify(buildKey)}, modules: ${modules} });`,
|
|
380
|
+
`setServerHooks(hooks);`
|
|
381
|
+
].join("\n");
|
|
382
|
+
}
|
|
383
|
+
if (id === `\0${CLIENT_HOOKS_ID}` || id === `\0${SERVER_HOOKS_ID}`) {
|
|
384
|
+
const where = id === `\0${CLIENT_HOOKS_ID}` ? "client" : "server";
|
|
385
|
+
const hooks = options.hooks?.[where];
|
|
386
|
+
if (hooks === void 0) return "export default undefined;";
|
|
387
|
+
if (typeof hooks === "string") return `export { default } from ${JSON.stringify(path.resolve(config.root, hooks))};`;
|
|
388
|
+
return serializeHooks(hooks, where, parse);
|
|
389
|
+
}
|
|
390
|
+
if (id === `\0virtual:islx/client-entry`) {
|
|
391
|
+
const load = isBuild() ? `const modules = {\n${(await clientInputs()).map((relative) => ` ${JSON.stringify(relative)}: () => import(${JSON.stringify(path.resolve(config.root, relative))}),`).join("\n")}\n};\nconst load = (src) => modules[src] ? modules[src]() : Promise.reject(new Error("islx: no client module " + src));` : `const root = new URL(${JSON.stringify(`file://${toPosix(config.root)}/`)});\nconst load = (src) => import(/* @vite-ignore */ src.startsWith("../") ? "/@fs" + new URL(src, root).pathname : ${JSON.stringify(config.base)} + src);`;
|
|
392
|
+
return [
|
|
393
|
+
...preamble && !isBuild() ? [`import "@vitejs/plugin-react/preamble";`] : [],
|
|
394
|
+
`import { createHydrationRuntime, refreshDocument, scheduleHydration, swapDocument } from "@farm.js/islx/react/client";`,
|
|
395
|
+
`import { enhanceForms, setActionNavigator, setClientHooks, startNavigation, navigate } from "@farm.js/islx/client";`,
|
|
396
|
+
`import hooks from ${JSON.stringify(CLIENT_HOOKS_ID)};`,
|
|
397
|
+
`setClientHooks(hooks);`,
|
|
398
|
+
load,
|
|
399
|
+
`const runtime = createHydrationRuntime({ load, schedule: scheduleHydration });`,
|
|
400
|
+
`setActionNavigator({ refresh: () => refreshDocument(runtime), navigate: (href) => navigate(href) });`,
|
|
401
|
+
`enhanceForms();`,
|
|
402
|
+
...options.navigation === false ? [] : [`startNavigation({ swap: (next) => swapDocument(runtime, next), viewTransition: ${typeof options.navigation === "object" && options.navigation.viewTransition === true} });`],
|
|
403
|
+
`runtime.hydrate(document);`,
|
|
404
|
+
`export { runtime };`
|
|
405
|
+
].join("\n");
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
transform(code, id) {
|
|
409
|
+
if (id.startsWith("\0") || !SCRIPT.test(id.split("?", 1)[0])) return null;
|
|
410
|
+
const library = id.includes("/node_modules/");
|
|
411
|
+
const clientDirective = library ? "use client" : directives.client;
|
|
412
|
+
if (!code.includes(clientDirective) && (library || !code.includes(directives.server))) return null;
|
|
413
|
+
const file = id.split("?", 1)[0];
|
|
414
|
+
const server = isServer(this.environment.name);
|
|
415
|
+
const relative = relativeOf(file);
|
|
416
|
+
const parseFile = (source) => parse(file, source);
|
|
417
|
+
if (server && !excluded(relative)) {
|
|
418
|
+
const island = transformClientModule({
|
|
419
|
+
code,
|
|
420
|
+
id: file,
|
|
421
|
+
root: config.root,
|
|
422
|
+
parse: parseFile,
|
|
423
|
+
reference: (relative) => {
|
|
424
|
+
discovered.add(relative);
|
|
425
|
+
return JSON.stringify(relative);
|
|
426
|
+
},
|
|
427
|
+
sideEffects: [RUNTIME_ID],
|
|
428
|
+
directive: clientDirective
|
|
429
|
+
});
|
|
430
|
+
if (island) {
|
|
431
|
+
if (!library) transformActions({
|
|
432
|
+
code,
|
|
433
|
+
id: file,
|
|
434
|
+
root: config.root,
|
|
435
|
+
environment: "client",
|
|
436
|
+
parse: parseFile,
|
|
437
|
+
directive: directives.server
|
|
438
|
+
});
|
|
439
|
+
return {
|
|
440
|
+
code: island.code,
|
|
441
|
+
map: island.map
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (library) return null;
|
|
446
|
+
const actions = transformActions({
|
|
447
|
+
code,
|
|
448
|
+
id: file,
|
|
449
|
+
root: config.root,
|
|
450
|
+
environment: server ? "server" : "client",
|
|
451
|
+
parse: parseFile,
|
|
452
|
+
directive: directives.server
|
|
453
|
+
});
|
|
454
|
+
if (!actions) return null;
|
|
455
|
+
return {
|
|
456
|
+
code: server ? `import ${JSON.stringify(RUNTIME_ID)};\n${actions.code}` : actions.code,
|
|
457
|
+
map: server ? null : actions.map
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
name: "islx:reload",
|
|
463
|
+
apply: "serve",
|
|
464
|
+
hotUpdate({ file, modules, server }) {
|
|
465
|
+
if (!isServer(this.environment.name) || modules.length === 0) return;
|
|
466
|
+
const client = server.environments.client;
|
|
467
|
+
if (client?.moduleGraph.getModulesByFile(file)?.size) return;
|
|
468
|
+
client?.hot.send({ type: "full-reload" });
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
name: "islx:build",
|
|
473
|
+
sharedDuringBuild: true,
|
|
474
|
+
async buildApp(builder) {
|
|
475
|
+
if (host.frameworkBuild) return;
|
|
476
|
+
const servers = Object.values(builder.environments).filter((environment) => isServer(environment.name));
|
|
477
|
+
for (const environment of servers) await builder.build(environment);
|
|
478
|
+
await builder.build(builder.environments.client);
|
|
479
|
+
if (!entryUrl) throw new Error("islx: the browser build did not emit the islx entry");
|
|
480
|
+
for (const environment of servers) {
|
|
481
|
+
const outDir = path.resolve(config.root, environment.config.build.outDir);
|
|
482
|
+
for (const file of await listOutput(outDir)) {
|
|
483
|
+
const source = await readFile(file, "utf8");
|
|
484
|
+
if (!source.includes(ENTRY_PLACEHOLDER) && !source.includes(MODULES_PLACEHOLDER)) continue;
|
|
485
|
+
const modules = JSON.stringify(JSON.stringify(moduleFiles)).slice(1, -1);
|
|
486
|
+
await writeFile(file, source.replaceAll(ENTRY_PLACEHOLDER, entryUrl).replaceAll(MODULES_PLACEHOLDER, modules));
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
generateBundle(_options, bundle) {
|
|
491
|
+
if (this.environment.name !== "client") return;
|
|
492
|
+
const url = (fileName) => `${config.base}${fileName}`;
|
|
493
|
+
for (const chunk of Object.values(bundle)) {
|
|
494
|
+
if (chunk.type !== "chunk" || !chunk.facadeModuleId) continue;
|
|
495
|
+
if (chunk.isEntry && chunk.facadeModuleId === `\0virtual:islx/client-entry`) entryUrl = url(chunk.fileName);
|
|
496
|
+
else if (chunk.isDynamicEntry && !chunk.facadeModuleId.startsWith("\0")) moduleFiles[relativeOf(chunk.facadeModuleId)] = [chunk.fileName, ...chunk.imports].map(url);
|
|
497
|
+
}
|
|
498
|
+
clientBuilt = true;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
];
|
|
502
|
+
}
|
|
503
|
+
export { serializeHooks as i, islx as n, resolveDirectives as r, ENTRY_ID as t };
|
package/dist/react/client.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { a as emit, l as getValueStore, t as createServerReference, u as valueHeaders } from "../_chunks/actions-8RQfrM-p.mjs";
|
|
2
2
|
import { i as decodeTagged, n as NOT_A_VALUE, r as decode, u as setOwn } from "../_chunks/codec-C8to-RQW.mjs";
|
|
3
3
|
import { F as getTag, f as FORM_STATE_SCRIPT_TYPE, h as ISLAND_ELEMENT, m as HYDRATED_ATTRIBUTE, w as SLOT_ELEMENT, x as PROPS_SCRIPT_TYPE, y as PROMISE_SCRIPT_TYPE } from "../_chunks/wire-QEQWBr5s.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { i as getContexts, o as isolateIslands, u as withinClientRoot } from "../_chunks/island-DVOr-OUw.mjs";
|
|
5
|
+
import { i as createServerTreeLayout, n as PAYLOAD_ONLY_ATTRIBUTE, o as updateServerTrees, r as createServerTreeElement } from "../_chunks/tree-BUkGP4AR.mjs";
|
|
5
6
|
import { n as isIslandStrategy, r as scheduleHydration, t as ISLAND_STRATEGIES } from "../_chunks/schedule-CjoybS3b.mjs";
|
|
6
7
|
import * as React from "react";
|
|
7
8
|
import { hydrateRoot } from "react-dom/client";
|
package/dist/react/server.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { a as encode } from "../_chunks/codec-C8to-RQW.mjs";
|
|
2
2
|
import { f as FORM_STATE_SCRIPT_TYPE } from "../_chunks/wire-QEQWBr5s.mjs";
|
|
3
3
|
import { r as PREHYDRATION_SCRIPT } from "../_chunks/prehydration-B1GLTLdk.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { i as encodeActionReference } from "../_chunks/actions-BuS5jDi0.mjs";
|
|
5
5
|
import { t as getBuildDefaults } from "../_chunks/build-COIcs7wm.mjs";
|
|
6
|
-
import { a as
|
|
6
|
+
import { a as parseHeldValues, n as addressValues, o as selectValuesToSend, r as escapeScriptJson } from "../_chunks/values-37UIzxM0.mjs";
|
|
7
|
+
import { c as setIslandServerHook, i as getContexts, l as setRenderStateResolver, n as createIsland, o as isolateIslands, r as getClientReference, s as runtimeTagged, t as IslandsProvider, u as withinClientRoot } from "../_chunks/island-DVOr-OUw.mjs";
|
|
8
|
+
import { a as renderServerTree, n as PAYLOAD_ONLY_ATTRIBUTE, t as Outlet } from "../_chunks/tree-BUkGP4AR.mjs";
|
|
7
9
|
import * as React from "react";
|
|
8
10
|
// src/react/server.ts
|
|
9
11
|
/**
|
package/dist/react.mjs
CHANGED
|
@@ -1 +1,3 @@
|
|
|
1
|
-
|
|
1
|
+
import { n as createIsland, o as isolateIslands, t as IslandsProvider, u as withinClientRoot } from "./_chunks/island-DVOr-OUw.mjs";
|
|
2
|
+
import { a as renderServerTree, t as Outlet } from "./_chunks/tree-BUkGP4AR.mjs";
|
|
3
|
+
export { IslandsProvider, Outlet, createIsland, isolateIslands, renderServerTree, withinClientRoot };
|
package/dist/server.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as parseHeldValues, i as hashValue, n as addressValues, o as selectValuesToSend, r as escapeScriptJson, t as VALUE_MIN_LENGTH } from "./_chunks/values-37UIzxM0.mjs";
|
|
2
|
+
import { a as getActionReference, c as resolveAction, d as generateActionKey, f as sealBound, i as encodeActionReference, l as SealError, n as configureActionKeys, o as registerInlineAction, p as unsealBound, r as decodeActionReference, s as registerServerReference, t as bindInlineAction, u as createActionKeys } from "./_chunks/actions-BuS5jDi0.mjs";
|
|
2
3
|
import { a as encode, r as decode } from "./_chunks/codec-C8to-RQW.mjs";
|
|
3
4
|
import { I as readPostedAction, L as stripRuntimeFields, N as attachmentField, n as ACTION_KEY_FIELD, o as BOUND_FIELD, r as ACTION_STALE_HEADER, s as CALL_FIELD, t as ACTION_HEADER } from "./_chunks/wire-QEQWBr5s.mjs";
|
|
4
5
|
import { n as setBuildDefaults, t as getBuildDefaults } from "./_chunks/build-COIcs7wm.mjs";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { HandlerCallback } from "@tanstack/react-router/ssr/server";
|
|
2
|
+
import { AnyRouter } from "@tanstack/react-router";
|
|
3
|
+
|
|
4
|
+
// src/tanstack-start/server.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Renders the page with its islands: values the browser holds stay out, and a navigation (an
|
|
8
|
+
* `F-Render` request) gets the whole page in one piece, as islx's browser runtime swaps it in.
|
|
9
|
+
*/
|
|
10
|
+
declare const islandsHandler: HandlerCallback<AnyRouter>;
|
|
11
|
+
export { islandsHandler };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { t as IslandsProvider } from "../_chunks/island-DVOr-OUw.mjs";
|
|
2
|
+
import { A as VALUES_HEADER, C as RENDER_HEADER } from "../_chunks/wire-QEQWBr5s.mjs";
|
|
3
|
+
import { a as parseHeldValues } from "../_chunks/values-37UIzxM0.mjs";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { StartServer } from "@tanstack/react-start/server";
|
|
6
|
+
import { createSsrStreamResponse, defineHandlerCallback, getSsrStatus, renderRouterToStream, transformReadableStreamWithRouter } from "@tanstack/react-router/ssr/server";
|
|
7
|
+
// src/tanstack-start/server.ts
|
|
8
|
+
/**
|
|
9
|
+
* @farm.js/islx/tanstack-start/server: the render handler of a TanStack Start app as islands.
|
|
10
|
+
* `islxTanstackStart()` uses it for Start's default server entry; an app with its own
|
|
11
|
+
* `src/server.ts` passes it to `createStartHandler`:
|
|
12
|
+
*
|
|
13
|
+
* import { createServerEntry, createStartHandler } from "@tanstack/react-start/server";
|
|
14
|
+
* import { islandsHandler } from "@farm.js/islx/tanstack-start/server";
|
|
15
|
+
* export default createServerEntry({ fetch: createStartHandler(islandsHandler) });
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Renders the page with its islands: values the browser holds stay out, and a navigation (an
|
|
19
|
+
* `F-Render` request) gets the whole page in one piece, as islx's browser runtime swaps it in.
|
|
20
|
+
*/
|
|
21
|
+
const islandsHandler = defineHandlerCallback(({ request, router, responseHeaders }) => {
|
|
22
|
+
const held = parseHeldValues(request);
|
|
23
|
+
responseHeaders.append("Vary", `${VALUES_HEADER}, ${RENDER_HEADER}`);
|
|
24
|
+
if (held.size > 0) responseHeaders.set("Cache-Control", "private, no-store");
|
|
25
|
+
const children = React.createElement(IslandsProvider, { held }, React.createElement(StartServer, { router }));
|
|
26
|
+
if (!request.headers.has("F-Render")) return renderRouterToStream({
|
|
27
|
+
request,
|
|
28
|
+
router,
|
|
29
|
+
responseHeaders,
|
|
30
|
+
children
|
|
31
|
+
});
|
|
32
|
+
return renderWhole(request, router, responseHeaders, children);
|
|
33
|
+
});
|
|
34
|
+
/** Waits for every Suspense boundary, so React writes the page with no streaming scripts. */
|
|
35
|
+
async function renderWhole(request, router, responseHeaders, children) {
|
|
36
|
+
const { renderToReadableStream } = await import("react-dom/server");
|
|
37
|
+
const stream = await renderToReadableStream(children, {
|
|
38
|
+
signal: request.signal,
|
|
39
|
+
nonce: router.options.ssr?.nonce,
|
|
40
|
+
onError(error) {
|
|
41
|
+
if (!request.signal.aborted) console.error("Error in renderToReadableStream:", error);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
await stream.allReady;
|
|
45
|
+
const body = transformReadableStreamWithRouter(router, stream, {
|
|
46
|
+
rendererSafePoint: "script-close",
|
|
47
|
+
signal: request.signal
|
|
48
|
+
});
|
|
49
|
+
return createSsrStreamResponse(router, new Response(body, {
|
|
50
|
+
status: getSsrStatus(router),
|
|
51
|
+
headers: responseHeaders
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
export { islandsHandler };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { IslxOptions } from "./vite.mjs";
|
|
2
|
+
import { Plugin } from "vite";
|
|
3
|
+
|
|
4
|
+
// src/tanstack-start/index.d.ts
|
|
5
|
+
|
|
6
|
+
type IslxTanstackStartOptions = IslxOptions;
|
|
7
|
+
declare function islxTanstackStart(options?: IslxTanstackStartOptions): Plugin[];
|
|
8
|
+
export { IslxTanstackStartOptions, islxTanstackStart as default, islxTanstackStart };
|