@valentinkolb/cloud 0.1.2 → 0.2.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/package.json +1 -1
- package/scripts/build.ts +45 -22
- package/src/_internal/define-app.ts +27 -13
package/package.json
CHANGED
package/scripts/build.ts
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Production build for a single app.
|
|
2
|
+
* Production build for a single cloud app.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Two consumer shapes:
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Monorepo (this repo):
|
|
7
|
+
* APP_ID=<id> bun run packages/cloud/scripts/build.ts
|
|
8
|
+
* # appDir defaults to packages/<APP_ID>, run from workspace root.
|
|
9
|
+
*
|
|
10
|
+
* Standalone (npm consumer, see cloud-template):
|
|
11
|
+
* APP_ID=<id> APP_DIR=src bun run node_modules/@valentinkolb/cloud/scripts/build.ts
|
|
12
|
+
* # appDir = APP_DIR (resolved against cwd).
|
|
13
|
+
*
|
|
14
|
+
* Output goes to <cwd>/dist:
|
|
7
15
|
* server.js bundled Bun entry
|
|
8
16
|
* _ssr/<island>.js hydration bundles (auto-emitted by the SSR plugin)
|
|
9
17
|
* public/<id>/app.css Tailwind, if the app has src/styles/app.css
|
|
10
|
-
* public/<id>/... anything from
|
|
18
|
+
* public/<id>/... anything from <appDir>/public/
|
|
11
19
|
*
|
|
12
20
|
* If the app needs additional artefacts (core's global.css, logo, katex),
|
|
13
21
|
* it ships a `scripts/build-extras.ts` that this script runs at the end.
|
|
@@ -19,21 +27,23 @@ import { fileURLToPath } from "node:url";
|
|
|
19
27
|
import tailwind from "bun-plugin-tailwind";
|
|
20
28
|
import { Glob, CryptoHasher } from "bun";
|
|
21
29
|
|
|
22
|
-
// Mirrors @valentinkolb/ssr's island-id (md5 of POSIX path relative to the
|
|
23
|
-
// SSR plugin's rootDir, truncated to 12 chars). define-app sets rootDir to
|
|
24
|
-
// the `packages` directory.
|
|
25
|
-
const ssrRootDir = "packages";
|
|
26
|
-
const islandId = (file: string): string => {
|
|
27
|
-
const rel = file.slice(root.length + 1 + ssrRootDir.length + 1).replace(/\\/g, "/");
|
|
28
|
-
return new CryptoHasher("md5").update(rel).digest("hex").slice(0, 12);
|
|
29
|
-
};
|
|
30
|
-
|
|
31
30
|
const appId = process.env.APP_ID;
|
|
32
31
|
if (!appId) throw new Error("APP_ID env var required");
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
// `root` = wherever the user is building from. SSR plugin's rootDir is
|
|
34
|
+
// process.cwd() (set in defineApp), so we use the same here for hash parity.
|
|
35
|
+
const root = process.cwd();
|
|
36
|
+
|
|
37
|
+
// Framework dir — works whether this script is in packages/cloud/scripts/
|
|
38
|
+
// (monorepo) or node_modules/@valentinkolb/cloud/scripts/ (npm install).
|
|
39
|
+
const frameworkDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
40
|
+
|
|
41
|
+
// App dir — APP_DIR override for standalone consumers, defaults to monorepo
|
|
42
|
+
// convention. Resolved against cwd if relative.
|
|
43
|
+
const appDir = process.env.APP_DIR
|
|
44
|
+
? resolve(root, process.env.APP_DIR)
|
|
45
|
+
: resolve(root, "packages", appId);
|
|
46
|
+
if (!existsSync(appDir)) throw new Error(`Unknown app dir: ${appDir} (set APP_DIR or check APP_ID)`);
|
|
37
47
|
|
|
38
48
|
const dist = resolve(root, "dist");
|
|
39
49
|
const distPublic = resolve(dist, "public");
|
|
@@ -41,8 +51,20 @@ const distPublic = resolve(dist, "public");
|
|
|
41
51
|
await rm(dist, { recursive: true, force: true });
|
|
42
52
|
await mkdir(distPublic, { recursive: true });
|
|
43
53
|
|
|
54
|
+
// Mirrors @valentinkolb/ssr's island-id (md5 of POSIX path relative to the
|
|
55
|
+
// SSR plugin's rootDir, truncated to 12 chars). Both this script and the
|
|
56
|
+
// plugin use process.cwd() as the rootDir, so hashes match.
|
|
57
|
+
const islandId = (file: string): string => {
|
|
58
|
+
const rel = file.slice(root.length + 1).replace(/\\/g, "/");
|
|
59
|
+
return new CryptoHasher("md5").update(rel).digest("hex").slice(0, 12);
|
|
60
|
+
};
|
|
61
|
+
|
|
44
62
|
// Register the app's SSR plugin (Solid JSX transform + island bundler).
|
|
45
|
-
|
|
63
|
+
// In the monorepo this resolves via `packages/<id>/src/config`; in standalone
|
|
64
|
+
// it resolves via the appDir path (because the script's relative imports
|
|
65
|
+
// only work for monorepo, we use absolute file:// for standalone).
|
|
66
|
+
const configPath = resolve(appDir, "src/config");
|
|
67
|
+
const { plugin } = await import(configPath);
|
|
46
68
|
|
|
47
69
|
// 1. Server entry — also emits dist/_ssr/<island>.js via the SSR plugin.
|
|
48
70
|
const server = await Bun.build({
|
|
@@ -58,13 +80,14 @@ if (!server.success) {
|
|
|
58
80
|
throw new Error("Server bundle failed");
|
|
59
81
|
}
|
|
60
82
|
|
|
61
|
-
// 1b. The SSR plugin scans the
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
83
|
+
// 1b. The SSR plugin scans the project root and emits one chunk per island
|
|
84
|
+
// across every reachable package (including any island-shaped files in
|
|
85
|
+
// other workspace packages or in node_modules). Keep only this app's own
|
|
86
|
+
// islands plus the framework's; drop the rest. chunk-* files are shared
|
|
87
|
+
// splits and always kept.
|
|
65
88
|
const ssrDir = resolve(dist, "_ssr");
|
|
66
89
|
if (existsSync(ssrDir)) {
|
|
67
|
-
const allowedDirs = [
|
|
90
|
+
const allowedDirs = [frameworkDir, appDir];
|
|
68
91
|
const allowedIds = new Set<string>();
|
|
69
92
|
for (const dir of allowedDirs) {
|
|
70
93
|
for await (const file of new Glob("**/*.{island,client}.tsx").scan({ cwd: dir, absolute: true })) {
|
|
@@ -9,8 +9,6 @@ import { createSSRHandler, routes } from "@valentinkolb/ssr/hono";
|
|
|
9
9
|
import { Hono } from "hono";
|
|
10
10
|
import { serveStatic } from "hono/bun";
|
|
11
11
|
import type { SsrConfig } from "@valentinkolb/ssr";
|
|
12
|
-
import { resolve, dirname } from "node:path";
|
|
13
|
-
import { fileURLToPath } from "node:url";
|
|
14
12
|
import type {
|
|
15
13
|
AppMeta,
|
|
16
14
|
AppLifecycle,
|
|
@@ -111,13 +109,26 @@ export type AppOptions<S extends AppSettingsMap = {}> = {
|
|
|
111
109
|
* prefix-trie from these strings.
|
|
112
110
|
*/
|
|
113
111
|
routes: readonly string[];
|
|
112
|
+
/**
|
|
113
|
+
* Project root used by the SSR plugin to discover island/client files.
|
|
114
|
+
* Defaults to `process.cwd()`. Override only if you run the entrypoint
|
|
115
|
+
* from a directory other than the project root.
|
|
116
|
+
*/
|
|
117
|
+
appRoot?: string;
|
|
114
118
|
};
|
|
115
119
|
|
|
116
120
|
export type StartOptions = {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Single Hono instance mounted at `/` of the app's container. The app owns
|
|
123
|
+
* its full URL space — routes are written with their absolute paths
|
|
124
|
+
* (`/api/<id>`, `/app/<id>`, `/admin/<id>`, …), matching what the app
|
|
125
|
+
* declared in `defineApp({ routes: [...] })`.
|
|
126
|
+
*
|
|
127
|
+
* Framework-owned mounts (`/_ssr/*`, `/public/*`, `/api/_internal/search`)
|
|
128
|
+
* register before this router so they take precedence — apps can ignore
|
|
129
|
+
* those paths.
|
|
130
|
+
*/
|
|
131
|
+
router: Hono<any>;
|
|
121
132
|
lifecycle?: AppLifecycle;
|
|
122
133
|
capabilities?: AppCapabilities;
|
|
123
134
|
port?: number;
|
|
@@ -193,7 +204,7 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
193
204
|
const { config, plugin, html } = createSsrConfig<PageOptions>({
|
|
194
205
|
dev: process.env.NODE_ENV !== "production",
|
|
195
206
|
verbose: true,
|
|
196
|
-
rootDir:
|
|
207
|
+
rootDir: opts.appRoot ?? process.cwd(),
|
|
197
208
|
basePath: opts.basePath,
|
|
198
209
|
template: ({ body, scripts, title, description, theme }) => {
|
|
199
210
|
const themeFixed = theme !== undefined;
|
|
@@ -332,13 +343,14 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
332
343
|
onFound: (_path, c) => {
|
|
333
344
|
c.header("Cache-Control", "public, max-age=31536000, immutable");
|
|
334
345
|
},
|
|
335
|
-
}))
|
|
346
|
+
}))
|
|
347
|
+
// serveStatic calls next() on miss — terminate /public/* here so a
|
|
348
|
+
// missing asset is a clean 404 instead of falling through to the app
|
|
349
|
+
// router (which might render an HTML page for the missing path).
|
|
350
|
+
.all("/public/*", (c) => c.notFound());
|
|
336
351
|
|
|
337
|
-
//
|
|
338
|
-
|
|
339
|
-
if (startOpts.routes.pages) server.route("/", startOpts.routes.pages);
|
|
340
|
-
|
|
341
|
-
// Internal search endpoint
|
|
352
|
+
// Framework-internal endpoints register BEFORE the app router so they
|
|
353
|
+
// take precedence over any catch-all the app might mount.
|
|
342
354
|
if (startOpts.capabilities?.search) {
|
|
343
355
|
const searchRun = startOpts.capabilities.search.run;
|
|
344
356
|
server.post("/api/_internal/search", auth.requireRole("authenticated"), async (c) => {
|
|
@@ -349,6 +361,8 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
349
361
|
});
|
|
350
362
|
}
|
|
351
363
|
|
|
364
|
+
server.route("/", startOpts.router);
|
|
365
|
+
|
|
352
366
|
// Lifecycle
|
|
353
367
|
const cloudCtx: CloudContext = {
|
|
354
368
|
logger,
|