@k2b/ssr 0.12.0 → 0.14.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.
- package/README.md +23 -3
- package/package.json +2 -2
- package/src/adapter/bun.ts +5 -3
- package/src/adapter/elysia.ts +3 -1
- package/src/adapter/hono.ts +4 -3
- package/src/adapter/utils.ts +42 -4
- package/src/build.ts +39 -14
- package/src/index.ts +6 -18
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ SPA routing.
|
|
|
55
55
|
- Monorepo support via `rootDir`
|
|
56
56
|
- Public path mounting via `basePath` for microfrontends
|
|
57
57
|
- Stable file-path-based island IDs (collision-safe across workspace packages)
|
|
58
|
-
- Production
|
|
58
|
+
- Production module cache busting (`/_ssr/<buildTimestamp>/*.js`)
|
|
59
59
|
- Linked development source maps and validator-aware asset delivery
|
|
60
60
|
- Stale generated island assets removed after successful builds
|
|
61
61
|
- Visibility-aware development reload with cross-tab SSE coordination
|
|
@@ -309,6 +309,7 @@ createConfig({
|
|
|
309
309
|
dev?: boolean; // default: false
|
|
310
310
|
verbose?: boolean; // default: !dev
|
|
311
311
|
rootDir?: string; // default: process.cwd()
|
|
312
|
+
componentRoots?: readonly string[]; // explicit island/client discovery directories
|
|
312
313
|
basePath?: string; // default: "", example: "/docs"
|
|
313
314
|
external?: string[]; // passed to Bun.build for island bundle
|
|
314
315
|
devSourcemap?: "none" | "linked" | "inline"; // default: "linked"
|
|
@@ -319,10 +320,23 @@ createConfig({
|
|
|
319
320
|
### Notes
|
|
320
321
|
|
|
321
322
|
- `rootDir` is important in monorepos where server entrypoint and island files live in different packages.
|
|
323
|
+
- `componentRoots` replaces the discovery scan with explicit directories. Relative paths resolve against `rootDir`; absolute paths support installed framework packages. Omit it to scan `rootDir`, or pass `[]` to scan nothing. Missing directories fail the build. Symlinks and overlapping roots are deduplicated by canonical path; nested `node_modules` and `.git` directories are skipped. Select an installed package directory explicitly to scan its components.
|
|
324
|
+
- Solid core, web, and store imports use the app's dependency and the selected build mode consistently, including components imported from installed packages.
|
|
325
|
+
- `rootDir` still controls component IDs and the development asset directory. All selected components share one browser build. Ordinary component libraries with browser/SSR exports (such as `@k2b/ui`) are resolved through imports and do not need discovery roots. Do not scan their examples or test fixtures.
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
createConfig({
|
|
329
|
+
rootDir: workspaceRoot,
|
|
330
|
+
componentRoots: ["packages/my-app/src", frameworkSourceDir],
|
|
331
|
+
});
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Use the same configuration in development and production. Files outside `rootDir` retain the existing canonical absolute-path ID fallback; moving an external package can change its IDs. SSR wrappers and browser assets must come from the same build.
|
|
322
335
|
- `basePath` moves SSR assets and dev endpoints under that prefix, e.g. `/docs/_ssr`.
|
|
323
336
|
- Development builds emit linked source maps by default. Use `"inline"` only when a tool requires embedded maps, or `"none"` to disable them.
|
|
324
|
-
- In production,
|
|
325
|
-
- All adapters stream island assets from `Bun.file`. Production assets and content-hashed development chunks are immutable; stable development entries and source maps use validators for inexpensive freshness checks.
|
|
337
|
+
- In production, all modules share a build timestamp directory (`/_ssr/<version>/<id>.js`). Relative lazy imports inherit that directory, so each module has one URL. Files stay flat on disk; adapters serve only the current build version.
|
|
338
|
+
- All adapters stream island assets from `Bun.file`. Production assets under the versioned path and content-hashed development chunks are immutable; stable development entries and source maps use validators for inexpensive freshness checks.
|
|
339
|
+
- Production adapters serve adjacent `.br` or `.gz` files when accepted by the request, preserving the original MIME type and varying caches by `Accept-Encoding`. Generate these siblings in the application build; the adapter does not compress responses at runtime. Development always serves the original file to avoid stale compressed copies.
|
|
326
340
|
|
|
327
341
|
## Microfrontend mount example
|
|
328
342
|
|
|
@@ -347,6 +361,12 @@ With this setup, hydration chunks and dev endpoints are served from `/docs/_ssr/
|
|
|
347
361
|
|
|
348
362
|
## Build for production
|
|
349
363
|
|
|
364
|
+
Set the environment before starting Bun so both the build configuration and bundled code use production mode:
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
NODE_ENV=production bun scripts/build.ts
|
|
368
|
+
```
|
|
369
|
+
|
|
350
370
|
```ts
|
|
351
371
|
// scripts/build.ts
|
|
352
372
|
import { plugin } from "./config";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@k2b/ssr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Minimal SSR framework for SolidJS and Bun",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@happy-dom/global-registrator": "^20.10.6",
|
|
40
|
-
"@types/bun": "
|
|
40
|
+
"@types/bun": "1.4.2",
|
|
41
41
|
"elysia": "^1.4.29",
|
|
42
42
|
"file-type": "^21.3.1",
|
|
43
43
|
"hono": "^4.12.30",
|
package/src/adapter/bun.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import type { SsrConfig } from "../index";
|
|
6
6
|
import {
|
|
7
7
|
createAssetResponse,
|
|
8
|
+
getAssetPrefix,
|
|
8
9
|
createPingResponse,
|
|
9
10
|
getSsrDir,
|
|
10
11
|
createReloadResponse,
|
|
@@ -30,6 +31,7 @@ type Routes = Record<string, RouteHandler>;
|
|
|
30
31
|
export const routes = (config: SsrConfig): Routes => {
|
|
31
32
|
const { dev, ssrPath } = config;
|
|
32
33
|
const ssrDir = getSsrDir(config);
|
|
34
|
+
const assetPath = ssrPath + getAssetPrefix(dev);
|
|
33
35
|
|
|
34
36
|
const devRoutes: Routes = dev
|
|
35
37
|
? {
|
|
@@ -39,13 +41,13 @@ export const routes = (config: SsrConfig): Routes => {
|
|
|
39
41
|
: {};
|
|
40
42
|
|
|
41
43
|
const serveAsset: RouteHandler = (req) => {
|
|
42
|
-
const filename = new URL(req.url).pathname.
|
|
44
|
+
const filename = new URL(req.url).pathname.slice(assetPath.length + 1);
|
|
43
45
|
return createAssetResponse(req, ssrDir, filename, dev);
|
|
44
46
|
};
|
|
45
47
|
|
|
46
48
|
return {
|
|
47
49
|
...devRoutes,
|
|
48
|
-
[`${
|
|
49
|
-
[`${
|
|
50
|
+
[`${assetPath}/*.js`]: serveAsset,
|
|
51
|
+
[`${assetPath}/*.js.map`]: serveAsset,
|
|
50
52
|
};
|
|
51
53
|
};
|
package/src/adapter/elysia.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { Elysia } from "elysia";
|
|
|
6
6
|
import type { SsrConfig } from "../index";
|
|
7
7
|
import {
|
|
8
8
|
createAssetResponse,
|
|
9
|
+
getAssetPrefix,
|
|
9
10
|
createPingResponse,
|
|
10
11
|
getSsrDir,
|
|
11
12
|
createReloadResponse,
|
|
@@ -27,13 +28,14 @@ import {
|
|
|
27
28
|
export const routes = (config: SsrConfig) => {
|
|
28
29
|
const { dev, ssrPath } = config;
|
|
29
30
|
const ssrDir = getSsrDir(config);
|
|
31
|
+
const assetPath = ssrPath + getAssetPrefix(dev);
|
|
30
32
|
|
|
31
33
|
return new Elysia({ name: "ssr" })
|
|
32
34
|
.get(`${ssrPath}/_reload`, ({ request }) =>
|
|
33
35
|
dev ? createReloadResponse(request.signal) : notFound(),
|
|
34
36
|
)
|
|
35
37
|
.get(`${ssrPath}/_ping`, () => (dev ? createPingResponse() : notFound()))
|
|
36
|
-
.get(`${
|
|
38
|
+
.get(`${assetPath}/*`, ({ request, params }) =>
|
|
37
39
|
createAssetResponse(request, ssrDir, params["*"], dev),
|
|
38
40
|
);
|
|
39
41
|
};
|
package/src/adapter/hono.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { Hono } from "hono";
|
|
|
6
6
|
import { createFactory } from "hono/factory";
|
|
7
7
|
import type { Context, Env, Handler, MiddlewareHandler, TypedResponse } from "hono";
|
|
8
8
|
import type { SsrConfig, HtmlFn, RenderFn } from "../index";
|
|
9
|
-
import { createAssetResponse, createPingResponse, getSsrDir, createReloadResponse } from "./utils";
|
|
9
|
+
import { createAssetResponse, getAssetPrefix, createPingResponse, getSsrDir, createReloadResponse } from "./utils";
|
|
10
10
|
|
|
11
11
|
// ============================================================================
|
|
12
12
|
// Types
|
|
@@ -183,8 +183,9 @@ export const routes = (config: SsrConfig) => {
|
|
|
183
183
|
return createAssetResponse(c.req.raw, ssrDir, filename, dev);
|
|
184
184
|
};
|
|
185
185
|
|
|
186
|
-
|
|
187
|
-
app.get(
|
|
186
|
+
const prefix = getAssetPrefix(dev);
|
|
187
|
+
app.get(`${prefix}/:filename{.+\\.js$}`, serveAsset);
|
|
188
|
+
app.get(`${prefix}/:filename{.+\\.js\\.map$}`, serveAsset);
|
|
188
189
|
|
|
189
190
|
return app;
|
|
190
191
|
};
|
package/src/adapter/utils.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* SSE live reload, and security utilities.
|
|
4
4
|
*/
|
|
5
5
|
import { dirname, join, resolve } from "path";
|
|
6
|
+
import { statSync } from "fs";
|
|
6
7
|
import type { SsrConfig } from "../index";
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -34,9 +35,36 @@ export const toSsrPath = (basePath: string): string =>
|
|
|
34
35
|
export const getSsrDir = (config: SsrConfig): string =>
|
|
35
36
|
join(config.dev ? config.rootDir ?? process.cwd() : dirname(Bun.main), "_ssr");
|
|
36
37
|
|
|
38
|
+
// Keep one version for this server process, including when Bun.main has no mtime.
|
|
39
|
+
const buildVersion = (() => {
|
|
40
|
+
try {
|
|
41
|
+
return String(Math.floor(statSync(Bun.main).mtimeMs));
|
|
42
|
+
} catch {
|
|
43
|
+
return String(Date.now());
|
|
44
|
+
}
|
|
45
|
+
})();
|
|
46
|
+
|
|
47
|
+
/** Relative imports inherit a versioned directory, unlike a query string. */
|
|
48
|
+
export const getAssetPrefix = (dev: boolean): string => dev ? "" : `/${buildVersion}`;
|
|
49
|
+
|
|
37
50
|
const HASHED_CHUNK = /^chunk-[a-z0-9]+\.js$/i;
|
|
38
51
|
const ASSET_FILE = /^[a-z0-9._-]+\.js(?:\.map)?$/i;
|
|
39
52
|
|
|
53
|
+
const acceptedEncodings = (header: string | null): Array<"br" | "gzip" | "identity"> => {
|
|
54
|
+
const qualities = new Map<string, number>();
|
|
55
|
+
for (const part of header?.split(",") ?? []) {
|
|
56
|
+
const [name, ...parameters] = part.trim().toLowerCase().split(";");
|
|
57
|
+
if (!name) continue;
|
|
58
|
+
const q = parameters.map((parameter) => parameter.trim()).find((parameter) => parameter.startsWith("q="));
|
|
59
|
+
const quality = q === undefined ? 1 : Number(q.slice(2));
|
|
60
|
+
qualities.set(name, Number.isFinite(quality) && quality >= 0 && quality <= 1 ? quality : 0);
|
|
61
|
+
}
|
|
62
|
+
const quality = (encoding: string) => qualities.get(encoding) ??
|
|
63
|
+
(encoding === "identity" ? (qualities.get("*") === 0 ? 0 : 1) : qualities.get("*") ?? 0);
|
|
64
|
+
return (["br", "gzip", "identity"] as const).filter((encoding) => quality(encoding) > 0)
|
|
65
|
+
.sort((left, right) => quality(right) - quality(left));
|
|
66
|
+
};
|
|
67
|
+
|
|
40
68
|
/**
|
|
41
69
|
* Stable entry names can change during development. Content-hashed chunks
|
|
42
70
|
* cannot, so the browser may retain them across page navigations.
|
|
@@ -85,9 +113,19 @@ export const createAssetResponse = async (
|
|
|
85
113
|
|
|
86
114
|
const cacheControl = getCacheHeaders(dev, filename);
|
|
87
115
|
if (!dev) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
116
|
+
for (const encoding of acceptedEncodings(request.headers.get("Accept-Encoding"))) {
|
|
117
|
+
const selected = encoding === "identity" ? file : Bun.file(`${path}${encoding === "br" ? ".br" : ".gz"}`);
|
|
118
|
+
if (!(await selected.exists())) continue;
|
|
119
|
+
const headers = new Headers({
|
|
120
|
+
"Content-Type": contentType,
|
|
121
|
+
"Content-Length": String(selected.size),
|
|
122
|
+
"Cache-Control": cacheControl,
|
|
123
|
+
Vary: "Accept-Encoding",
|
|
124
|
+
});
|
|
125
|
+
if (encoding !== "identity") headers.set("Content-Encoding", encoding);
|
|
126
|
+
return new Response(request.method === "HEAD" ? null : selected, { headers });
|
|
127
|
+
}
|
|
128
|
+
return new Response(null, { status: 406, headers: { Vary: "Accept-Encoding" } });
|
|
91
129
|
}
|
|
92
130
|
|
|
93
131
|
const lastModified = file.lastModified;
|
|
@@ -102,7 +140,7 @@ export const createAssetResponse = async (
|
|
|
102
140
|
return new Response(null, { status: 304, headers: validatorHeaders });
|
|
103
141
|
}
|
|
104
142
|
|
|
105
|
-
return new Response(file, {
|
|
143
|
+
return new Response(request.method === "HEAD" ? null : file, {
|
|
106
144
|
headers: { "Content-Type": contentType, ...validatorHeaders },
|
|
107
145
|
});
|
|
108
146
|
};
|
package/src/build.ts
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* Island bundler - discovers *.island.tsx and *.client.tsx files,
|
|
3
3
|
* transforms them for the browser, and outputs chunks to _ssr directory.
|
|
4
4
|
*/
|
|
5
|
-
import { relative, resolve } from "path";
|
|
5
|
+
import { join, relative, resolve } from "path";
|
|
6
6
|
import { Glob } from "bun";
|
|
7
|
-
import { unlink } from "fs/promises";
|
|
7
|
+
import { readdir, stat, unlink } from "fs/promises";
|
|
8
|
+
import { existsSync } from "fs";
|
|
8
9
|
import { transform } from "./transform";
|
|
9
|
-
import { ISLAND_ID_LENGTH, islandIdFromFile, toStableKey } from "./island-id";
|
|
10
|
+
import { ISLAND_ID_LENGTH, canonicalFilePath, islandIdFromFile, toStableKey } from "./island-id";
|
|
10
11
|
|
|
11
12
|
type ComponentType = "island" | "client";
|
|
12
13
|
|
|
@@ -89,34 +90,48 @@ export const buildIslands = async (options: {
|
|
|
89
90
|
pattern: string;
|
|
90
91
|
outdir: string;
|
|
91
92
|
cwd: string;
|
|
93
|
+
componentRoots?: readonly string[];
|
|
92
94
|
verbose: boolean;
|
|
93
95
|
dev?: boolean;
|
|
94
96
|
devSourcemap?: DevSourcemap;
|
|
95
97
|
external?: string[];
|
|
96
98
|
}): Promise<void> => {
|
|
97
|
-
const { pattern, outdir, cwd, verbose, dev = false, devSourcemap = "linked", external } = options;
|
|
99
|
+
const { pattern, outdir, cwd, componentRoots, verbose, dev = false, devSourcemap = "linked", external } = options;
|
|
98
100
|
const resolvedCwd = resolve(cwd);
|
|
99
101
|
|
|
100
102
|
const totalStart = performance.now();
|
|
101
103
|
|
|
102
|
-
const files
|
|
104
|
+
const files = new Set<string>();
|
|
103
105
|
|
|
104
106
|
const scanStart = performance.now();
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
const visited = new Set<string>();
|
|
108
|
+
const matcher = new Glob(pattern);
|
|
109
|
+
const scan = async (directory: string, scanRoot: string): Promise<void> => {
|
|
110
|
+
const canonical = canonicalFilePath(directory);
|
|
111
|
+
if (visited.has(canonical)) return;
|
|
112
|
+
visited.add(canonical);
|
|
113
|
+
for (const entry of await readdir(canonical, { withFileTypes: true })) {
|
|
114
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
115
|
+
const path = join(canonical, entry.name);
|
|
116
|
+
const info = entry.isSymbolicLink() ? await stat(path) : entry;
|
|
117
|
+
if (info.isDirectory()) await scan(path, scanRoot);
|
|
118
|
+
else if (info.isFile() && matcher.match(relative(scanRoot, path))) files.add(canonicalFilePath(path));
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
for (const root of componentRoots ?? [resolvedCwd]) {
|
|
122
|
+
const scanRoot = canonicalFilePath(resolve(resolvedCwd, root));
|
|
123
|
+
await scan(scanRoot, scanRoot);
|
|
110
124
|
}
|
|
111
|
-
if (verbose) console.log(`Scan: found ${files.
|
|
125
|
+
if (verbose) console.log(`Scan: found ${files.size} file(s) in ${fmt(performance.now() - scanStart)}`);
|
|
112
126
|
|
|
113
|
-
if (!files.
|
|
127
|
+
if (!files.size) {
|
|
128
|
+
if (existsSync(outdir)) await removeStaleBuildAssets(outdir, []);
|
|
114
129
|
if (verbose) console.log("No island/client files found.");
|
|
115
130
|
return;
|
|
116
131
|
}
|
|
117
132
|
|
|
118
133
|
// Build component metadata
|
|
119
|
-
const components = files.map((componentPath) => {
|
|
134
|
+
const components = [...files].map((componentPath) => {
|
|
120
135
|
const id = islandIdFromFile(componentPath, resolvedCwd);
|
|
121
136
|
const key = toStableKey(componentPath, resolvedCwd);
|
|
122
137
|
const type = getComponentType(componentPath);
|
|
@@ -153,6 +168,7 @@ export const buildIslands = async (options: {
|
|
|
153
168
|
outdir,
|
|
154
169
|
naming: { entry: "[name].js", chunk: "chunk-[hash].js" },
|
|
155
170
|
target: "browser",
|
|
171
|
+
conditions: ["browser", dev ? "development" : "production"],
|
|
156
172
|
external,
|
|
157
173
|
minify: !dev,
|
|
158
174
|
splitting: true,
|
|
@@ -161,6 +177,15 @@ export const buildIslands = async (options: {
|
|
|
161
177
|
{
|
|
162
178
|
name: "solid-islands",
|
|
163
179
|
setup(build) {
|
|
180
|
+
// Core, DOM and store must use the app's Solid copy and the same mode.
|
|
181
|
+
// Explicit exported entries avoid mixing a production core with
|
|
182
|
+
// development store/web modules from the host process conditions.
|
|
183
|
+
build.onResolve({ filter: /^solid-js(?:\/(?:web|store))?$/ }, (args) => {
|
|
184
|
+
const module = args.path === "solid-js" ? "solid" : args.path.slice("solid-js/".length);
|
|
185
|
+
const entry = `${args.path}/dist/${dev ? "dev" : module}.js`;
|
|
186
|
+
return { path: Bun.resolveSync(entry, resolvedCwd) };
|
|
187
|
+
});
|
|
188
|
+
|
|
164
189
|
// Resolve component IDs as virtual entrypoints
|
|
165
190
|
build.onResolve({ filter: new RegExp(`^[a-f0-9]{${ISLAND_ID_LENGTH}}$`) }, (args) => ({
|
|
166
191
|
path: args.path,
|
|
@@ -220,7 +245,7 @@ export const buildIslands = async (options: {
|
|
|
220
245
|
}
|
|
221
246
|
}
|
|
222
247
|
console.log(
|
|
223
|
-
`Built ${files.
|
|
248
|
+
`Built ${files.size} component(s) to ${outdir}/ in ${fmt(performance.now() - totalStart)}${verbose ? " (total)" : ""}`,
|
|
224
249
|
);
|
|
225
250
|
|
|
226
251
|
if (!result.success) {
|
package/src/index.ts
CHANGED
|
@@ -7,12 +7,11 @@
|
|
|
7
7
|
import { renderToString } from "solid-js/web";
|
|
8
8
|
import type { JSX } from "solid-js";
|
|
9
9
|
import type { BunPlugin } from "bun";
|
|
10
|
-
import { statSync } from "fs";
|
|
11
10
|
import { transform } from "./transform";
|
|
12
11
|
import { buildIslands, type DevSourcemap } from "./build";
|
|
13
12
|
import { join, dirname, resolve } from "path";
|
|
14
13
|
import { resolveIslandImport } from "./island-resolve";
|
|
15
|
-
import { getReloadId, normalizeBasePath, toSsrPath } from "./adapter/utils";
|
|
14
|
+
import { getAssetPrefix, getReloadId, normalizeBasePath, toSsrPath } from "./adapter/utils";
|
|
16
15
|
// @ts-ignore - Bun text import
|
|
17
16
|
import devClientCode from "./adapter/client.js" with { type: "text" };
|
|
18
17
|
|
|
@@ -23,19 +22,6 @@ import devClientCode from "./adapter/client.js" with { type: "text" };
|
|
|
23
22
|
/** Glob pattern for island/client component files */
|
|
24
23
|
const COMPONENT_PATTERN = "**/*.{island,client}.tsx";
|
|
25
24
|
|
|
26
|
-
/**
|
|
27
|
-
* Build version used for cache busting island script imports in production.
|
|
28
|
-
* Uses server entrypoint mtime as a stable per-build version value.
|
|
29
|
-
*/
|
|
30
|
-
const getBuildVersion = (dev: boolean): string => {
|
|
31
|
-
if (dev) return "";
|
|
32
|
-
try {
|
|
33
|
-
return String(Math.floor(statSync(Bun.main).mtimeMs));
|
|
34
|
-
} catch {
|
|
35
|
-
return String(Date.now());
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
|
|
39
25
|
// ============================================================================
|
|
40
26
|
// Types
|
|
41
27
|
// ============================================================================
|
|
@@ -47,6 +33,8 @@ export type SsrOptions<T extends object = object> = {
|
|
|
47
33
|
verbose?: boolean;
|
|
48
34
|
/** Project root for island discovery and dev _ssr assets (default: process.cwd()) */
|
|
49
35
|
rootDir?: string;
|
|
36
|
+
/** Discovery roots relative to rootDir (absolute paths allowed). Replaces the default rootDir scan. */
|
|
37
|
+
componentRoots?: readonly string[];
|
|
50
38
|
/** Public app mount path for SSR assets and dev endpoints (default: "") */
|
|
51
39
|
basePath?: string;
|
|
52
40
|
/** Modules to exclude from the island bundle (passed to Bun.build) */
|
|
@@ -121,6 +109,7 @@ export const createConfig = <T extends object = object>(options: SsrOptions<T> =
|
|
|
121
109
|
devSourcemap = "linked",
|
|
122
110
|
template,
|
|
123
111
|
rootDir: rootDirOption,
|
|
112
|
+
componentRoots,
|
|
124
113
|
basePath: basePathOption,
|
|
125
114
|
} = options;
|
|
126
115
|
const rootDir = resolve(rootDirOption ?? process.cwd());
|
|
@@ -153,13 +142,11 @@ export const createConfig = <T extends object = object>(options: SsrOptions<T> =
|
|
|
153
142
|
ssrPath,
|
|
154
143
|
};
|
|
155
144
|
|
|
156
|
-
const buildVersion = getBuildVersion(dev);
|
|
157
|
-
|
|
158
145
|
const islandDisplayStyle =
|
|
159
146
|
"<style>solid-client,solid-island{display:contents}</style>";
|
|
160
147
|
|
|
161
148
|
// Hydration script - dynamically loads island/client bundles based on DOM
|
|
162
|
-
const hydrationScript = `<script type="module">const p=${JSON.stringify(ssrPath
|
|
149
|
+
const hydrationScript = `<script type="module">const p=${JSON.stringify(ssrPath + getAssetPrefix(dev))};document.querySelectorAll('solid-island,solid-client').forEach(e=>import(p+'/'+e.dataset.id+'.js'));</script>`;
|
|
163
150
|
const devConfigScript = `<script>globalThis.__SSR_CONFIG=${JSON.stringify({ ssrPath, reloadId: getReloadId() })}</script>`;
|
|
164
151
|
|
|
165
152
|
// HTML renderer
|
|
@@ -208,6 +195,7 @@ export const createConfig = <T extends object = object>(options: SsrOptions<T> =
|
|
|
208
195
|
pattern: COMPONENT_PATTERN,
|
|
209
196
|
outdir: islandsOutdir,
|
|
210
197
|
cwd: rootDir,
|
|
198
|
+
componentRoots,
|
|
211
199
|
verbose: verbose ?? !dev,
|
|
212
200
|
dev,
|
|
213
201
|
devSourcemap,
|