@yourrealm/sdk 0.0.0-alpha.1378.ce03c8dc
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/LICENSE-EXCEPTION +26 -0
- package/README.md +34 -0
- package/dist/chunk-3R6KEFOI.js +22 -0
- package/dist/chunk-67BDAMYX.js +376 -0
- package/dist/chunk-AITGS27R.js +139 -0
- package/dist/chunk-CMAEHQ4F.js +6 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-O3UIK34B.js +1430 -0
- package/dist/chunk-VHTUSZUX.js +176 -0
- package/dist/cli.js +9006 -0
- package/dist/docker.js +54 -0
- package/dist/index.js +1053 -0
- package/dist/ingress.js +436 -0
- package/dist/jsx-runtime.js +2 -0
- package/dist/preact.js +22 -0
- package/dist/service-url.js +11 -0
- package/dist/services.js +213 -0
- package/dist/test.js +5069 -0
- package/dist/ui.js +254 -0
- package/dist/worker-protocol.js +89 -0
- package/package.json +112 -0
- package/src/app-icon.ts +74 -0
- package/src/apply.ts +52 -0
- package/src/bundle-ui.ts +211 -0
- package/src/bundle.ts +546 -0
- package/src/cli/build-dev-landing.ts +84 -0
- package/src/cli/build-install-panel.ts +90 -0
- package/src/cli/build-runtime.ts +133 -0
- package/src/cli/build.ts +63 -0
- package/src/cli/dev.ts +107 -0
- package/src/cli/down.ts +60 -0
- package/src/cli/main.ts +56 -0
- package/src/cli/test.ts +61 -0
- package/src/cli/validate.ts +37 -0
- package/src/compile.ts +100 -0
- package/src/create-app.ts +982 -0
- package/src/deep-merge.ts +33 -0
- package/src/dev/fixture.ts +48 -0
- package/src/dev/install-handler.ts +383 -0
- package/src/dev/server.ts +968 -0
- package/src/dev/stack.ts +108 -0
- package/src/dev/state.ts +159 -0
- package/src/docker.ts +96 -0
- package/src/errors.ts +61 -0
- package/src/factory.ts +481 -0
- package/src/garage.ts +56 -0
- package/src/generator/hash.ts +6 -0
- package/src/generator/resolve-refs.ts +207 -0
- package/src/grants-builder.ts +100 -0
- package/src/grants.ts +260 -0
- package/src/home-api.ts +172 -0
- package/src/home-cookie.ts +10 -0
- package/src/home-registry.ts +120 -0
- package/src/home.ts +187 -0
- package/src/host-registry.ts +143 -0
- package/src/http-header.ts +20 -0
- package/src/identifiers.ts +49 -0
- package/src/index.ts +66 -0
- package/src/info.ts +64 -0
- package/src/ini.ts +50 -0
- package/src/json.ts +14 -0
- package/src/jsx-runtime/index.ts +5 -0
- package/src/keys.ts +29 -0
- package/src/mysql.ts +56 -0
- package/src/pack-build.ts +108 -0
- package/src/pack.ts +115 -0
- package/src/peer-registry.ts +121 -0
- package/src/peer-world.ts +117 -0
- package/src/permission-meta.ts +116 -0
- package/src/permissions.ts +108 -0
- package/src/pkg-root.ts +34 -0
- package/src/postgres.ts +79 -0
- package/src/preact/index.ts +27 -0
- package/src/redis.ts +43 -0
- package/src/ref.ts +271 -0
- package/src/remote-registry.ts +33 -0
- package/src/required-host-kinds.ts +110 -0
- package/src/resolve-context.ts +111 -0
- package/src/resolve.ts +471 -0
- package/src/runtime/docker/compose/compose-schema.ts +118 -0
- package/src/runtime/docker/compose/convert-to-docker-compose.ts +278 -0
- package/src/runtime/docker/compose/default-hardening.ts +21 -0
- package/src/runtime/docker/compose/effects.ts +107 -0
- package/src/runtime/docker/compose/index.ts +17 -0
- package/src/runtime/docker/compose/labels.ts +39 -0
- package/src/runtime/docker/compose/types.ts +78 -0
- package/src/runtime/docker/home-proxy-server/index.ts +177 -0
- package/src/runtime/docker/home-proxy-server/protocol.ts +53 -0
- package/src/runtime/docker/ingress/build-config.ts +819 -0
- package/src/runtime/docker/ingress/index.ts +16 -0
- package/src/runtime/docker/ingress/types.ts +182 -0
- package/src/runtime/docker/log-frames.ts +121 -0
- package/src/runtime/docker/logs-server/index.ts +366 -0
- package/src/runtime/docker/spawn-server/context-key.ts +82 -0
- package/src/runtime/docker/spawn-server/index.ts +492 -0
- package/src/runtime/docker/spawn-server/protocol.ts +70 -0
- package/src/runtime/docker/spawn-server/spec.ts +122 -0
- package/src/runtime/docker/stats-server/disks.ts +161 -0
- package/src/runtime/docker/stats-server/index.ts +322 -0
- package/src/runtime/home-client/index.ts +71 -0
- package/src/runtime/log-line.ts +24 -0
- package/src/runtime/logs-client/index.ts +175 -0
- package/src/runtime/spawn-client/index.ts +93 -0
- package/src/runtime/spawn-client/kaniko.ts +79 -0
- package/src/runtime/stats-client/index.ts +65 -0
- package/src/runtime/stats-sample.ts +95 -0
- package/src/safe-json.ts +37 -0
- package/src/sandbox.ts +1209 -0
- package/src/schema/build-apps.ts +38 -0
- package/src/schema/home.ts +217 -0
- package/src/schema/host.ts +792 -0
- package/src/schema/inputs.ts +26 -0
- package/src/schema/nodes.ts +75 -0
- package/src/schema/peer.ts +400 -0
- package/src/schema/public-domain.ts +116 -0
- package/src/schema/remote.ts +74 -0
- package/src/security.ts +594 -0
- package/src/serialize.ts +21 -0
- package/src/service-definition.ts +284 -0
- package/src/service-url.ts +64 -0
- package/src/service.ts +39 -0
- package/src/services/index.ts +5 -0
- package/src/shared.ts +65 -0
- package/src/speculative-silo.ts +96 -0
- package/src/stable-secret.ts +14 -0
- package/src/surfaces.ts +264 -0
- package/src/tailwind-bin.ts +56 -0
- package/src/test/boot.ts +192 -0
- package/src/test/compile.ts +501 -0
- package/src/test/describe.ts +117 -0
- package/src/test/index.ts +11 -0
- package/src/test/ingress-fixture.ts +37 -0
- package/src/test/types.ts +318 -0
- package/src/trusted-headers.ts +79 -0
- package/src/types.ts +126 -0
- package/src/ui/components/aside.tsx +96 -0
- package/src/ui/components/badge.tsx +42 -0
- package/src/ui/components/button.tsx +59 -0
- package/src/ui/components/card.tsx +106 -0
- package/src/ui/components/checkbox.tsx +37 -0
- package/src/ui/components/code-block.tsx +29 -0
- package/src/ui/components/combobox.tsx +119 -0
- package/src/ui/components/config-field.tsx +284 -0
- package/src/ui/components/dialog.tsx +137 -0
- package/src/ui/components/domain-editor.tsx +317 -0
- package/src/ui/components/domain-list.ts +205 -0
- package/src/ui/components/dropdown-menu.tsx +73 -0
- package/src/ui/components/empty-state.tsx +51 -0
- package/src/ui/components/highlight.ts +59 -0
- package/src/ui/components/icons.tsx +101 -0
- package/src/ui/components/image-posture.tsx +85 -0
- package/src/ui/components/input.tsx +28 -0
- package/src/ui/components/label.tsx +16 -0
- package/src/ui/components/line-chart.tsx +113 -0
- package/src/ui/components/page.tsx +39 -0
- package/src/ui/components/permission-row.tsx +262 -0
- package/src/ui/components/permission-section.tsx +51 -0
- package/src/ui/components/pie-chart.tsx +245 -0
- package/src/ui/components/schema-block.tsx +60 -0
- package/src/ui/components/separator.tsx +42 -0
- package/src/ui/components/sheet.tsx +130 -0
- package/src/ui/components/sidebar.tsx +650 -0
- package/src/ui/components/silo-picker.tsx +510 -0
- package/src/ui/components/skeleton.tsx +13 -0
- package/src/ui/components/sonner.tsx +33 -0
- package/src/ui/components/switch.tsx +58 -0
- package/src/ui/components/table-view.ts +219 -0
- package/src/ui/components/table.tsx +506 -0
- package/src/ui/components/tabs.tsx +59 -0
- package/src/ui/components/timeago.tsx +84 -0
- package/src/ui/components/tooltip.tsx +56 -0
- package/src/ui/components.json +21 -0
- package/src/ui/dev/dev-shell.tsx +145 -0
- package/src/ui/home/home-grid.tsx +177 -0
- package/src/ui/hooks/use-mobile.ts +19 -0
- package/src/ui/index.ts +189 -0
- package/src/ui/install/adapter.ts +193 -0
- package/src/ui/install/deploy-targets-picker.tsx +185 -0
- package/src/ui/install/install-app-panel.tsx +912 -0
- package/src/ui/install/install-error.tsx +62 -0
- package/src/ui/install/perms-inactive.ts +50 -0
- package/src/ui/install/required-fields.ts +37 -0
- package/src/ui/install/tabs-form.tsx +557 -0
- package/src/ui/tokens.css +279 -0
- package/src/ui/utils.ts +78 -0
- package/src/ui-error.ts +139 -0
- package/src/ui-runtime/dev-landing-entry.tsx +258 -0
- package/src/ui-runtime/index.ts +83 -0
- package/src/ui-runtime/install-entry.tsx +119 -0
- package/src/ui-runtime/mount.ts +367 -0
- package/src/ui-runtime/runtime.css +56 -0
- package/src/ui-surface.ts +451 -0
- package/src/valkey.ts +43 -0
- package/src/worker/extract-tar.ts +125 -0
- package/src/worker/identity.ts +183 -0
- package/src/worker/index.ts +16 -0
- package/src/worker/public-ip.ts +92 -0
- package/src/worker/queue.ts +54 -0
- package/src/worker/reconcile.ts +127 -0
- package/src/worker/start-worker.ts +227 -0
- package/src/worker/ui-proxy.ts +335 -0
- package/src/worker/uplink.ts +523 -0
- package/src/worker/validate.ts +34 -0
- package/src/worker/verify-relay.ts +102 -0
- package/src/worker-protocol/app-dirs.ts +94 -0
- package/src/worker-protocol/home-proxy-paths.ts +76 -0
- package/src/worker-protocol/index.ts +55 -0
- package/src/worker-protocol/ingress.ts +166 -0
- package/src/worker-protocol/logs-socket-path.ts +50 -0
- package/src/worker-protocol/networks.ts +39 -0
- package/src/worker-protocol/node-version.ts +70 -0
- package/src/worker-protocol/silo-paths.ts +114 -0
- package/src/worker-protocol/spawn-paths.ts +165 -0
- package/src/worker-protocol/stats-socket-path.ts +40 -0
- package/src/worker-protocol/update-files.ts +77 -0
- package/src/worker-protocol/uplink/constants.ts +86 -0
- package/src/worker-protocol/uplink/errors.ts +61 -0
- package/src/worker-protocol/uplink/frame.ts +114 -0
- package/src/worker-protocol/uplink/identity.ts +40 -0
- package/src/worker-protocol/uplink/index.ts +63 -0
- package/src/worker-protocol/uplink/methods.ts +462 -0
- package/src/worker-protocol/uplink/mux.ts +646 -0
- package/src/worker-protocol/uplink/protocol.ts +11 -0
- package/src/worker-protocol/uplink/rpc.ts +309 -0
- package/src/worker-protocol/uplink/session.ts +540 -0
- package/src/worker-protocol/uplink/test-support.ts +188 -0
- package/src/worker-protocol/worker-mount.ts +27 -0
- package/src/worker-protocol/worker-state.ts +190 -0
- package/src/xml.ts +49 -0
- package/src/yaml.ts +15 -0
package/src/bundle.ts
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
// Bundles a realm.ts source into a self-contained JS string for QuickJS
|
|
2
|
+
// evaluation. The bundling step is independent of permission shape.
|
|
3
|
+
|
|
4
|
+
import { dirname, join } from "@std/path";
|
|
5
|
+
import { log } from "@local/logger";
|
|
6
|
+
import { pkgRoot } from "./pkg-root.ts";
|
|
7
|
+
import { RealmCompileError } from "./errors.ts";
|
|
8
|
+
import { sha256Hex } from "./generator/hash.ts";
|
|
9
|
+
import { isPacked } from "./pack.ts";
|
|
10
|
+
import { mergePackedSource } from "./surfaces.ts";
|
|
11
|
+
|
|
12
|
+
const EVAL_PKG_DIR = pkgRoot(import.meta.url);
|
|
13
|
+
|
|
14
|
+
// `preact` and `preact/hooks` are allowed because surface render fns import
|
|
15
|
+
// hooks directly from preact, and the static import has to resolve at QuickJS
|
|
16
|
+
// bundle time. The imports never fire inside QuickJS — render fns only run in
|
|
17
|
+
// the iframe — so both specifiers alias to local noop stubs below rather than
|
|
18
|
+
// pulling real preact into the sandbox bundle.
|
|
19
|
+
const ALLOWED_IMPORT_PREFIXES = ["@yourrealm/sdk", "preact"];
|
|
20
|
+
|
|
21
|
+
function validateImports(source: string): void {
|
|
22
|
+
const importPattern = /(?:import|export)\s.*?from\s+["']([^"']+)["']|import\s*["']([^"']+)["']/g;
|
|
23
|
+
let match;
|
|
24
|
+
while ((match = importPattern.exec(source)) !== null) {
|
|
25
|
+
const specifier = match[1] ?? match[2]!;
|
|
26
|
+
if (specifier.startsWith("./") || specifier.startsWith("../") || specifier.startsWith("/")) {
|
|
27
|
+
throw new RealmCompileError({
|
|
28
|
+
kind: "validation",
|
|
29
|
+
path: ["imports"],
|
|
30
|
+
message: `blocked specifier: ${specifier}`,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (!ALLOWED_IMPORT_PREFIXES.some((p) => specifier === p || specifier.startsWith(p + "/"))) {
|
|
34
|
+
throw new RealmCompileError({
|
|
35
|
+
kind: "validation",
|
|
36
|
+
path: ["imports"],
|
|
37
|
+
message: `blocked specifier: ${specifier}`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function findNodeModules(startDir: string): string | null {
|
|
44
|
+
// Walk up looking for a node_modules that exposes `@yourrealm/sdk`. The first
|
|
45
|
+
// match isn't always right: when this file lives inside the SDK itself,
|
|
46
|
+
// `packages/sdk/node_modules/` exists but can't contain `@yourrealm/sdk` (a
|
|
47
|
+
// package can't depend on itself). The caller's node_modules (Home, an app
|
|
48
|
+
// dir, the workspace root) is the one esbuild resolves realm.ts's imports in.
|
|
49
|
+
let dir = startDir;
|
|
50
|
+
while (true) {
|
|
51
|
+
const candidate = join(dir, "node_modules");
|
|
52
|
+
try {
|
|
53
|
+
Deno.statSync(candidate);
|
|
54
|
+
try {
|
|
55
|
+
Deno.statSync(join(candidate, "@yourrealm", "sdk"));
|
|
56
|
+
return candidate;
|
|
57
|
+
} catch {
|
|
58
|
+
// node_modules exists but doesn't expose @yourrealm/sdk - keep walking
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// no node_modules at this level
|
|
62
|
+
}
|
|
63
|
+
const parent = dirname(dir);
|
|
64
|
+
if (parent === dir) return null;
|
|
65
|
+
dir = parent;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The esbuild binary usually sits in the same node_modules bundle() already
|
|
70
|
+
// resolved (`<nodeModules>/.bin/esbuild`). Spawn that path instead of a bare
|
|
71
|
+
// `esbuild`, which only resolves when the *invoking* package depends on esbuild
|
|
72
|
+
// (Home's runtime ENV, the SDK's own tests) — not when `realm-sdk build` runs
|
|
73
|
+
// from an app dir that doesn't.
|
|
74
|
+
//
|
|
75
|
+
// The second candidate is what pnpm's isolated layout requires: only a
|
|
76
|
+
// package's *direct* dependencies' binaries land in its `.bin`, so an app dir
|
|
77
|
+
// gets `.bin/realm-sdk` and nothing else while esbuild (the SDK's dependency,
|
|
78
|
+
// not the app's) stays in `packages/sdk/node_modules/.bin`. A bare `esbuild` is
|
|
79
|
+
// the last resort, and only resolves when something put it on PATH.
|
|
80
|
+
export function esbuildBinIn(nodeModules: string | null): string {
|
|
81
|
+
const candidates = [
|
|
82
|
+
nodeModules === null ? null : join(nodeModules, ".bin", "esbuild"),
|
|
83
|
+
EVAL_PKG_DIR === null ? null : join(EVAL_PKG_DIR, "node_modules", ".bin", "esbuild"),
|
|
84
|
+
];
|
|
85
|
+
for (const candidate of candidates) {
|
|
86
|
+
if (candidate === null) continue;
|
|
87
|
+
try {
|
|
88
|
+
Deno.statSync(candidate);
|
|
89
|
+
return candidate;
|
|
90
|
+
} catch {
|
|
91
|
+
// not here - try the next candidate
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return "esbuild";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// A process-stable dir per resolved node_modules, holding a single
|
|
98
|
+
// `node_modules` symlink. Each bundle runs in a temp *subdir* of it and lets
|
|
99
|
+
// esbuild walk up to resolve `@yourrealm/sdk`, so the symlink is created once
|
|
100
|
+
// rather than per call: per-call create/remove churn races under concurrent
|
|
101
|
+
// bundling (`deno test --parallel`), where esbuild fails EINVAL/ENOENT on the
|
|
102
|
+
// just-created symlink. Roots leak for the process lifetime (one tiny symlink
|
|
103
|
+
// dir each) - the OS reclaims /tmp.
|
|
104
|
+
const bundleRoots = new Map<string, string>();
|
|
105
|
+
function bundleRootFor(nodeModules: string): string {
|
|
106
|
+
const cached = bundleRoots.get(nodeModules);
|
|
107
|
+
if (cached) return cached;
|
|
108
|
+
const root = Deno.makeTempDirSync({ prefix: "realm-bundle-root-" });
|
|
109
|
+
Deno.symlinkSync(nodeModules, join(root, "node_modules"));
|
|
110
|
+
bundleRoots.set(nodeModules, root);
|
|
111
|
+
return root;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---- on-disk bundle cache ---------------------------------------------------
|
|
115
|
+
//
|
|
116
|
+
// Memoize successful bundles. Recompute re-evals every app on every trigger
|
|
117
|
+
// (webhook, boot, deploy) and each eval re-bundles, so without this an unchanged
|
|
118
|
+
// app re-spawns esbuild on every reconcile.
|
|
119
|
+
//
|
|
120
|
+
// The cache lives on disk rather than in the heap because of what it holds: a
|
|
121
|
+
// bundle is ~610KB, ~110ms of esbuild to produce, ~0.4ms to read back. An in-RAM
|
|
122
|
+
// map is ~610MB at 1000 apps, and capping it evicts every entry before reuse,
|
|
123
|
+
// because a recompute touches every installed app. Disk has neither problem.
|
|
124
|
+
//
|
|
125
|
+
// Content-addressed like `ui-bundle-storage.ts`: `<key>.js`, written
|
|
126
|
+
// tmp-then-rename so a partial file is never visible at the real path. Nothing
|
|
127
|
+
// here is durable - the whole directory can be deleted at any moment, at a cost
|
|
128
|
+
// of one rebuild per app.
|
|
129
|
+
|
|
130
|
+
const HASH_RE = /^[0-9a-f]{64}$/;
|
|
131
|
+
|
|
132
|
+
let cacheDirOverride: string | null = null;
|
|
133
|
+
let cacheDirResolved: string | null | undefined;
|
|
134
|
+
let buildId: string | null = null;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Point the bundle cache at a persistent directory. Home calls this at boot with
|
|
138
|
+
* a path under `HOME_DATA_DIR` so the cache survives a restart. Everything else
|
|
139
|
+
* (the CLI, app tests, `realm-sdk dev`) leaves it unset and shares one directory
|
|
140
|
+
* under the temp dir, which a container start wipes and the OS reclaims.
|
|
141
|
+
*
|
|
142
|
+
* `buildId` identifies the shipped build of the SDK, and a persistent directory
|
|
143
|
+
* needs one whenever the SDK isn't on disk as source - see `sdkFingerprint`.
|
|
144
|
+
* Without it the persistent directory is refused and the ephemeral one used
|
|
145
|
+
* instead: slower, never stale.
|
|
146
|
+
*/
|
|
147
|
+
export function setBundleCacheDir(dir: string, opts?: { buildId?: string }): void {
|
|
148
|
+
cacheDirOverride = dir;
|
|
149
|
+
buildId = opts?.buildId ?? null;
|
|
150
|
+
cacheDirResolved = undefined;
|
|
151
|
+
fingerprint = null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Shared by every process that didn't ask for a directory. */
|
|
155
|
+
function ephemeralCacheDir(): string | null {
|
|
156
|
+
const base = Deno.env.get("TMPDIR") ?? "/tmp";
|
|
157
|
+
// One shared directory, not one per process: a per-process temp dir is never
|
|
158
|
+
// reclaimed until the OS gets to it, and `pnpm test` alone spawns dozens.
|
|
159
|
+
// Sharing is safe because entries are content-addressed.
|
|
160
|
+
for (const dir of [join(base, "realm-bundle-cache"), null]) {
|
|
161
|
+
try {
|
|
162
|
+
if (dir === null) return Deno.makeTempDirSync({ prefix: "realm-bundle-cache-" });
|
|
163
|
+
Deno.mkdirSync(dir, { recursive: true });
|
|
164
|
+
return dir;
|
|
165
|
+
} catch {
|
|
166
|
+
// Try the next candidate; a null return disables caching entirely.
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Where bundles land, or `null` when nothing is usable and we bundle uncached. */
|
|
173
|
+
function bundleCacheDir(): string | null {
|
|
174
|
+
if (cacheDirResolved !== undefined) return cacheDirResolved;
|
|
175
|
+
if (cacheDirOverride !== null) {
|
|
176
|
+
try {
|
|
177
|
+
Deno.mkdirSync(cacheDirOverride, { recursive: true });
|
|
178
|
+
cacheDirResolved = cacheDirOverride;
|
|
179
|
+
return cacheDirResolved;
|
|
180
|
+
} catch (err) {
|
|
181
|
+
// A read-only or full data dir costs speed, not correctness. Falling back
|
|
182
|
+
// rather than throwing keeps an unwritable dir from failing every compile,
|
|
183
|
+
// which would turn Home's default-app seeding into a crashloop.
|
|
184
|
+
log.warn(`bundle cache: ${cacheDirOverride} unusable, falling back: ${String(err)}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
cacheDirResolved = ephemeralCacheDir();
|
|
188
|
+
return cacheDirResolved;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// The key has to cover the SDK build as well as the app source: esbuild inlines
|
|
192
|
+
// `@yourrealm/sdk` into every bundle, so one realm.ts compiles to different
|
|
193
|
+
// output after an SDK change, and a cache on disk outlives the process and, in
|
|
194
|
+
// production, the *image* (HOME_DATA_DIR survives an upgrade).
|
|
195
|
+
//
|
|
196
|
+
// Only one of the two layouts can be fingerprinted from disk:
|
|
197
|
+
//
|
|
198
|
+
// dev checkout - `@yourrealm/sdk` resolves to `packages/sdk`, so `src/` holds
|
|
199
|
+
// every input, including this file (the esbuild argv and the preact/jsx stub
|
|
200
|
+
// text inlined into every bundle) and `surfaces.ts` (which rewrites a packed
|
|
201
|
+
// app's source before esbuild sees it). Hashing `src/` is sound.
|
|
202
|
+
//
|
|
203
|
+
// Home image - only the pre-bundled `dist/` is staged, built from the barrel
|
|
204
|
+
// entries alone. This file, `surfaces.ts`, `pack.ts`, `compile.ts` and
|
|
205
|
+
// `sandbox.ts` are NOT in it - they can't be, they touch `Deno` at module
|
|
206
|
+
// scope - so the files that decide bundle output are exactly the ones a
|
|
207
|
+
// `dist/` hash misses.
|
|
208
|
+
//
|
|
209
|
+
// A dist-only layout therefore needs `buildId` from the caller (Home passes the
|
|
210
|
+
// image's GIT_HASH). Without one the persistent directory is refused outright,
|
|
211
|
+
// because a cache that cannot be invalidated is worse than no cache.
|
|
212
|
+
let fingerprint: Promise<string> | null = null;
|
|
213
|
+
|
|
214
|
+
function sdkFingerprint(): Promise<string> {
|
|
215
|
+
if (fingerprint === null) {
|
|
216
|
+
const pending = computeSdkFingerprint();
|
|
217
|
+
// Don't pin a rejection: this tree is known to throw transiently (see the
|
|
218
|
+
// esbuild retry note below), and a memoized failure would fail every
|
|
219
|
+
// compile for the life of the process.
|
|
220
|
+
pending.catch(() => {
|
|
221
|
+
if (fingerprint === pending) fingerprint = null;
|
|
222
|
+
});
|
|
223
|
+
fingerprint = pending;
|
|
224
|
+
}
|
|
225
|
+
return fingerprint;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function collectSdkFiles(root: string, dir: string, out: string[]): Promise<void> {
|
|
229
|
+
const entries = [];
|
|
230
|
+
for await (const entry of Deno.readDir(dir)) entries.push(entry);
|
|
231
|
+
// readDir order isn't stable across filesystems and the hash is order-
|
|
232
|
+
// sensitive: two boxes must agree on the key to reuse a restored cache.
|
|
233
|
+
entries.sort((a, b) => (a.name < b.name ? -1 : 1));
|
|
234
|
+
for (const entry of entries) {
|
|
235
|
+
const path = join(dir, entry.name);
|
|
236
|
+
if (entry.isDirectory) {
|
|
237
|
+
if (entry.name === "node_modules") continue; // a dev checkout has one; esbuild reaches it by resolution, not by path
|
|
238
|
+
await collectSdkFiles(root, path, out);
|
|
239
|
+
} else if (/\.(ts|tsx|js|mjs|json)$/.test(entry.name)) {
|
|
240
|
+
// Relative to the SDK root, so where the checkout happens to live doesn't
|
|
241
|
+
// change the key - a restored HOME_DATA_DIR keeps working.
|
|
242
|
+
out.push(path.slice(root.length), await Deno.readTextFile(path));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function computeSdkFingerprint(): Promise<string> {
|
|
248
|
+
const nodeModules = findNodeModules(
|
|
249
|
+
Deno.env.get("ESBUILD_RESOLVE_DIR") ?? EVAL_PKG_DIR ?? Deno.cwd(),
|
|
250
|
+
);
|
|
251
|
+
const sdkDir = nodeModules === null ? EVAL_PKG_DIR : join(nodeModules, "@yourrealm", "sdk");
|
|
252
|
+
if (sdkDir === null) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
"bundle cache: no node_modules exposes @yourrealm/sdk and the SDK package is not on disk",
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
const parts: string[] = [];
|
|
258
|
+
let hashedSource = false;
|
|
259
|
+
for (const sub of ["src", "dist"]) {
|
|
260
|
+
// Probe the subdir, then walk it unguarded: guarding the whole walk would
|
|
261
|
+
// swallow a NotFound raised *inside* it - a file vanishing mid-walk - and
|
|
262
|
+
// memoize the truncated prefix as if it were the whole tree, exactly the
|
|
263
|
+
// staleness this fingerprint exists to prevent.
|
|
264
|
+
try {
|
|
265
|
+
if (!(await Deno.stat(join(sdkDir, sub))).isDirectory) continue;
|
|
266
|
+
} catch (e) {
|
|
267
|
+
if (e instanceof Deno.errors.NotFound) continue;
|
|
268
|
+
throw e;
|
|
269
|
+
}
|
|
270
|
+
await collectSdkFiles(sdkDir, join(sdkDir, sub), parts);
|
|
271
|
+
if (sub === "src") hashedSource = true;
|
|
272
|
+
}
|
|
273
|
+
if (parts.length === 0) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
`bundle cache: no SDK sources under ${sdkDir}/{src,dist} to fingerprint - ` +
|
|
276
|
+
`a cached bundle could not be invalidated when the SDK changes`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
// `dist/` alone cannot identify the bundler (see the note above), so a caller
|
|
280
|
+
// that wants a persistent directory has to name its build. Refuse the
|
|
281
|
+
// directory rather than hand back a key that can't invalidate.
|
|
282
|
+
if (buildId !== null) parts.push(`buildId:${buildId}`);
|
|
283
|
+
if (!hashedSource && buildId === null && cacheDirOverride !== null) {
|
|
284
|
+
log.warn(
|
|
285
|
+
`bundle cache: ${sdkDir} ships no src/ and no buildId was given, ` +
|
|
286
|
+
`so ${cacheDirOverride} can't be invalidated - using an ephemeral cache`,
|
|
287
|
+
);
|
|
288
|
+
cacheDirOverride = null;
|
|
289
|
+
cacheDirResolved = undefined;
|
|
290
|
+
}
|
|
291
|
+
// package.json too, for its dependency ranges and `exports` map. It is the one
|
|
292
|
+
// input not covered exactly: a `pnpm update` inside an unchanged range swaps
|
|
293
|
+
// an inlined dependency without moving anything hashed here. The image path
|
|
294
|
+
// has no such gap (dist/ is pre-bundled, so a dependency bump rewrites it),
|
|
295
|
+
// and a dev checkout clears the cache with the rest of dev state.
|
|
296
|
+
try {
|
|
297
|
+
parts.push("package.json", await Deno.readTextFile(join(sdkDir, "package.json")));
|
|
298
|
+
} catch (e) {
|
|
299
|
+
if (!(e instanceof Deno.errors.NotFound)) throw e;
|
|
300
|
+
}
|
|
301
|
+
// The esbuild binary is the other input to the output. Stat rather than
|
|
302
|
+
// `--version`: a spawn costs more than the whole tree read above, and a
|
|
303
|
+
// swapped binary moves size and mtime together.
|
|
304
|
+
try {
|
|
305
|
+
const stat = Deno.statSync(esbuildBinIn(nodeModules));
|
|
306
|
+
parts.push(`esbuild:${stat.size}:${stat.mtime?.getTime() ?? 0}`);
|
|
307
|
+
} catch {
|
|
308
|
+
// Not resolvable here - bundling itself will fail next, with a better message.
|
|
309
|
+
}
|
|
310
|
+
return await sha256Hex(parts.join("\0"));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Collapses concurrent callers asking for the same source onto one esbuild run
|
|
314
|
+
// (multi-worker recompute, `deno test --parallel`). Entries are deleted the
|
|
315
|
+
// moment they settle, so nothing is retained here - that is the disk's job - and
|
|
316
|
+
// a transient esbuild flake (see the retry note below) isn't pinned.
|
|
317
|
+
const inFlight = new Map<string, Promise<string>>();
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Bundle realm.ts source into a self-contained JS string.
|
|
321
|
+
* Inlines @yourrealm/sdk and all dependencies (zod, dayjs).
|
|
322
|
+
* Output is an IIFE that assigns the module exports to `__realm_module__`.
|
|
323
|
+
*
|
|
324
|
+
* Uses esbuild CLI binary (not the JS API) so it works when Home
|
|
325
|
+
* is bundled into a single file by Deno.bundle(). Memoized on disk.
|
|
326
|
+
*/
|
|
327
|
+
export async function bundle(source: string): Promise<string> {
|
|
328
|
+
const key = await sha256Hex(`${await sdkFingerprint()}\0${source}`);
|
|
329
|
+
const existing = inFlight.get(key);
|
|
330
|
+
if (existing) return await existing;
|
|
331
|
+
// No await between the call and the set, so two callers can't both miss.
|
|
332
|
+
const pending = bundleThroughDisk(key, source);
|
|
333
|
+
inFlight.set(key, pending);
|
|
334
|
+
try {
|
|
335
|
+
return await pending;
|
|
336
|
+
} finally {
|
|
337
|
+
inFlight.delete(key);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function bundleThroughDisk(key: string, source: string): Promise<string> {
|
|
342
|
+
const dir = bundleCacheDir();
|
|
343
|
+
if (dir === null) return await bundleUncached(source);
|
|
344
|
+
const path = join(dir, `${key}.js`);
|
|
345
|
+
try {
|
|
346
|
+
const hit = await Deno.readTextFile(path);
|
|
347
|
+
// A hit has to look like esbuild's output before it is trusted. `rename`
|
|
348
|
+
// isn't preceded by an fsync, so a crash in that window can leave a torn or
|
|
349
|
+
// empty file at the real path - and since every read below refreshes the
|
|
350
|
+
// mtime, the age sweep would never reap it. Falling through rebuilds and
|
|
351
|
+
// overwrites it instead.
|
|
352
|
+
if (!hit.includes("__realm_module__")) throw new Deno.errors.NotFound(path);
|
|
353
|
+
// Keep entries in use young, so the sweep reaps only what stopped being read
|
|
354
|
+
// (an app's previous source, a discarded install preview) and never an
|
|
355
|
+
// installed app. Awaited rather than floated: an op still in flight when a
|
|
356
|
+
// test ends is what `deno test`'s sanitizer reports as a leak, and one
|
|
357
|
+
// stat-sized write against a ~110ms miss isn't worth that class of flake.
|
|
358
|
+
try {
|
|
359
|
+
const now = new Date();
|
|
360
|
+
await Deno.utime(path, now, now);
|
|
361
|
+
} catch {
|
|
362
|
+
// A read-only cache dir still serves reads; it just can't age entries.
|
|
363
|
+
}
|
|
364
|
+
return hit;
|
|
365
|
+
} catch (e) {
|
|
366
|
+
if (!(e instanceof Deno.errors.NotFound)) throw e;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const code = await bundleUncached(source);
|
|
370
|
+
try {
|
|
371
|
+
const tmp = `${path}.tmp-${crypto.randomUUID()}`;
|
|
372
|
+
await Deno.writeTextFile(tmp, code);
|
|
373
|
+
await Deno.rename(tmp, path);
|
|
374
|
+
} catch {
|
|
375
|
+
// A full or read-only cache dir costs speed, not correctness - the bundle in
|
|
376
|
+
// hand is already good. Swallow so a deploy never fails on a cache write.
|
|
377
|
+
}
|
|
378
|
+
return code;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Delete cached bundles that nothing has read for `maxAgeMs`, plus any
|
|
383
|
+
* `.tmp-*` file a crash left mid-write. Returns the number of files removed.
|
|
384
|
+
*
|
|
385
|
+
* Age, not liveness: the key is a hash of (SDK build + source), so there is no
|
|
386
|
+
* app id to match rows against, and an app that changes its source orphans its
|
|
387
|
+
* old entry with no event to hang a sweep off. Reads keep live entries young,
|
|
388
|
+
* so an installed app is never reaped no matter how long it sits unchanged.
|
|
389
|
+
*
|
|
390
|
+
* Per-file errors are swallowed so one bad file doesn't stop the sweep.
|
|
391
|
+
*/
|
|
392
|
+
export async function sweepBundleCache(maxAgeMs: number): Promise<number> {
|
|
393
|
+
const dir = bundleCacheDir();
|
|
394
|
+
if (dir === null) return 0;
|
|
395
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
396
|
+
let removed = 0;
|
|
397
|
+
try {
|
|
398
|
+
// Deno.readDir is lazy - a missing dir surfaces on first iteration.
|
|
399
|
+
for await (const entry of Deno.readDir(dir)) {
|
|
400
|
+
if (!entry.isFile) continue;
|
|
401
|
+
const stale = /\.tmp-[0-9a-f-]+$/.test(entry.name);
|
|
402
|
+
if (!stale && !(entry.name.endsWith(".js") && HASH_RE.test(entry.name.slice(0, -3))))
|
|
403
|
+
continue;
|
|
404
|
+
const path = join(dir, entry.name);
|
|
405
|
+
try {
|
|
406
|
+
const stat = await Deno.stat(path);
|
|
407
|
+
if ((stat.mtime?.getTime() ?? 0) > cutoff) continue;
|
|
408
|
+
await Deno.remove(path);
|
|
409
|
+
removed++;
|
|
410
|
+
} catch {
|
|
411
|
+
// Best effort - the next sweep picks up survivors.
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
} catch (e) {
|
|
415
|
+
if (e instanceof Deno.errors.NotFound) return 0;
|
|
416
|
+
throw e;
|
|
417
|
+
}
|
|
418
|
+
return removed;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function bundleUncached(source: string): Promise<string> {
|
|
422
|
+
const resolveDir = Deno.env.get("ESBUILD_RESOLVE_DIR") ?? EVAL_PKG_DIR ?? Deno.cwd();
|
|
423
|
+
|
|
424
|
+
// A packed artifact is unpacked and its file-based surfaces merged in here, at
|
|
425
|
+
// install — with esbuild alone, no TypeScript compiler, so it is safe in Home.
|
|
426
|
+
// A plain (unmarked) source passes straight through, and the merge inlines
|
|
427
|
+
// relatives so validateImports below still only sees the package allowlist.
|
|
428
|
+
if (isPacked(source)) source = await mergePackedSource(source);
|
|
429
|
+
|
|
430
|
+
validateImports(source);
|
|
431
|
+
|
|
432
|
+
// Run inside a subdir of the shared root (which holds the one node_modules
|
|
433
|
+
// symlink) so esbuild resolves @yourrealm/sdk by walking up - no per-call
|
|
434
|
+
// symlink. With no node_modules found it falls back to a bare /tmp dir, where
|
|
435
|
+
// esbuild cannot resolve the SDK.
|
|
436
|
+
const nodeModules = findNodeModules(resolveDir);
|
|
437
|
+
const root = nodeModules ? bundleRootFor(nodeModules) : undefined;
|
|
438
|
+
const tmpDir = await Deno.makeTempDir(
|
|
439
|
+
root ? { dir: root, prefix: "b-" } : { prefix: "realm-bundle-" },
|
|
440
|
+
);
|
|
441
|
+
// Always .tsx so esbuild's tsx loader handles both plain TS and JSX. The
|
|
442
|
+
// render fns inside `ui:` surfaces never run inside QuickJS (only their
|
|
443
|
+
// existence is checked), so the JSX compiles to captured `h(...)` calls that
|
|
444
|
+
// fire only when the iframe runtime mounts.
|
|
445
|
+
const tmpFile = join(tmpDir, "realm-eval.tsx");
|
|
446
|
+
// esbuild's automatic JSX transform emits `import { jsx } from
|
|
447
|
+
// "@yourrealm/sdk/jsx-runtime"` for every JSX element, and the SDK's
|
|
448
|
+
// jsx-runtime entry re-exports preact/jsx-runtime — which Home's runtime image
|
|
449
|
+
// does not stage. The QuickJS bundle never invokes render, so these stubs in
|
|
450
|
+
// the same tmpdir let esbuild short-circuit the chain locally.
|
|
451
|
+
const jsxStubPath = join(tmpDir, "jsx-runtime-stub.js");
|
|
452
|
+
const preactStubPath = join(tmpDir, "preact-stub.js");
|
|
453
|
+
const preactHooksStubPath = join(tmpDir, "preact-hooks-stub.js");
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
await Deno.writeTextFile(tmpFile, source);
|
|
457
|
+
await Deno.writeTextFile(
|
|
458
|
+
jsxStubPath,
|
|
459
|
+
// Identity / noop exports. Render fns capture these in their
|
|
460
|
+
// closure but never invoke them inside QuickJS.
|
|
461
|
+
"const noop = () => null;\n" +
|
|
462
|
+
"export const jsx = noop;\n" +
|
|
463
|
+
"export const jsxs = noop;\n" +
|
|
464
|
+
"export const jsxDEV = noop;\n" +
|
|
465
|
+
"export const Fragment = noop;\n",
|
|
466
|
+
);
|
|
467
|
+
await Deno.writeTextFile(
|
|
468
|
+
preactStubPath,
|
|
469
|
+
"const noop = () => null;\n" +
|
|
470
|
+
"export const h = noop;\n" +
|
|
471
|
+
"export const Fragment = noop;\n" +
|
|
472
|
+
"export const render = noop;\n",
|
|
473
|
+
);
|
|
474
|
+
await Deno.writeTextFile(
|
|
475
|
+
preactHooksStubPath,
|
|
476
|
+
// Same shape as preact/hooks at the type level — the render fn captures
|
|
477
|
+
// these names but never calls them inside QuickJS. The iframe runtime
|
|
478
|
+
// supplies the real implementations.
|
|
479
|
+
"const noop = () => {};\n" +
|
|
480
|
+
"export const useState = (init) => [init, noop];\n" +
|
|
481
|
+
"export const useEffect = noop;\n" +
|
|
482
|
+
"export const useMemo = (fn) => fn();\n" +
|
|
483
|
+
"export const useCallback = (fn) => fn;\n" +
|
|
484
|
+
"export const useReducer = (_, init) => [init, noop];\n" +
|
|
485
|
+
"export const useRef = (init) => ({ current: init });\n",
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
const esbuildArgs = [
|
|
489
|
+
tmpFile,
|
|
490
|
+
"--bundle",
|
|
491
|
+
"--format=iife",
|
|
492
|
+
"--global-name=__realm_module__",
|
|
493
|
+
"--platform=neutral",
|
|
494
|
+
"--main-fields=module,main",
|
|
495
|
+
"--target=es2020",
|
|
496
|
+
"--jsx=automatic",
|
|
497
|
+
"--jsx-import-source=@yourrealm/sdk",
|
|
498
|
+
// Short-circuits the jsx-runtime chain to the local stubs above, so
|
|
499
|
+
// preact need not be resolvable inside Home's eval image.
|
|
500
|
+
`--alias:preact/jsx-runtime=${jsxStubPath}`,
|
|
501
|
+
`--alias:preact/hooks=${preactHooksStubPath}`,
|
|
502
|
+
`--alias:preact=${preactStubPath}`,
|
|
503
|
+
];
|
|
504
|
+
|
|
505
|
+
// Retry transient resolution/FS hiccups. Running ~16 esbuild subprocesses at
|
|
506
|
+
// once (`deno test --parallel`) occasionally trips macOS resolving the SDK
|
|
507
|
+
// through pnpm's symlinked node_modules — a spurious "Could not resolve",
|
|
508
|
+
// EINVAL, or ENOENT that clears on a retry. A syntax error doesn't match and
|
|
509
|
+
// fails on the first attempt; a genuinely unresolvable (but allowlisted) SDK
|
|
510
|
+
// subpath is textually identical to the transient case, so it pays the
|
|
511
|
+
// retries and then fails with the same esbuild message.
|
|
512
|
+
const TRANSIENT =
|
|
513
|
+
/Could not resolve|Cannot read file|invalid argument|no such file or directory|operation not permitted/;
|
|
514
|
+
for (let attempt = 0; ; attempt++) {
|
|
515
|
+
// Re-resolve the binary on each attempt. Under `nodeModulesDir: "auto"`
|
|
516
|
+
// Deno rebuilds the workspace-member link (node_modules/@yourrealm/sdk) on
|
|
517
|
+
// every invocation as a remove-then-symlink, leaving a window where the
|
|
518
|
+
// link is gone; a bundle landing in another process's window resolves no
|
|
519
|
+
// node_modules, falls back to a bare "esbuild" that isn't on PATH, and
|
|
520
|
+
// dies. Re-resolving makes that window cost a retry, not the whole eval.
|
|
521
|
+
let output;
|
|
522
|
+
try {
|
|
523
|
+
output = await new Deno.Command(esbuildBinIn(findNodeModules(resolveDir)), {
|
|
524
|
+
args: esbuildArgs,
|
|
525
|
+
stdout: "piped",
|
|
526
|
+
stderr: "piped",
|
|
527
|
+
}).output();
|
|
528
|
+
} catch (e) {
|
|
529
|
+
// A spawn failure throws rather than returning a code, so it would
|
|
530
|
+
// otherwise skip the retry below entirely.
|
|
531
|
+
if (attempt >= 2) throw e;
|
|
532
|
+
await new Promise((r) => setTimeout(r, 20 * (attempt + 1)));
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
const { code, stdout, stderr } = output;
|
|
536
|
+
if (code === 0) return new TextDecoder().decode(stdout);
|
|
537
|
+
const err = new TextDecoder().decode(stderr);
|
|
538
|
+
if (attempt >= 2 || !TRANSIENT.test(err)) {
|
|
539
|
+
throw new RealmCompileError({ kind: "eval", message: err });
|
|
540
|
+
}
|
|
541
|
+
await new Promise((r) => setTimeout(r, 20 * (attempt + 1)));
|
|
542
|
+
}
|
|
543
|
+
} finally {
|
|
544
|
+
await Deno.remove(tmpDir, { recursive: true }).catch(() => {});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// `realm-sdk build-dev-landing --out <dir>` — esbuild the dev-landing preact
|
|
2
|
+
// entry into `dev-landing.js`, next to `runtime.js`. CSS is shared: runtime.css's
|
|
3
|
+
// Tailwind scan already covers `packages/sdk/src/ui-runtime/dev-landing-entry.tsx`.
|
|
4
|
+
// `realm-sdk dev` builds it alongside runtime.js + install-panel.js on first boot
|
|
5
|
+
// and serves it at `/dev-landing.js`, mounted by a thin HTML shell.
|
|
6
|
+
|
|
7
|
+
import { resolve } from "@std/path";
|
|
8
|
+
import { sdkDir } from "../pkg-root.ts";
|
|
9
|
+
|
|
10
|
+
const ENTRY = resolve(sdkDir(import.meta.url), "src/ui-runtime/dev-landing-entry.tsx");
|
|
11
|
+
|
|
12
|
+
interface BuildOpts {
|
|
13
|
+
outDir: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function buildJs(outDir: string): Promise<void> {
|
|
17
|
+
const cmd = new Deno.Command("esbuild", {
|
|
18
|
+
args: [
|
|
19
|
+
ENTRY,
|
|
20
|
+
"--bundle",
|
|
21
|
+
`--outfile=${outDir}/dev-landing.js`,
|
|
22
|
+
"--format=esm",
|
|
23
|
+
"--platform=browser",
|
|
24
|
+
"--target=es2020",
|
|
25
|
+
"--main-fields=module,main",
|
|
26
|
+
"--jsx=automatic",
|
|
27
|
+
"--jsx-import-source=preact",
|
|
28
|
+
"--alias:react=preact/compat",
|
|
29
|
+
"--alias:react-dom=preact/compat",
|
|
30
|
+
"--alias:react/jsx-runtime=preact/jsx-runtime",
|
|
31
|
+
'--define:process.env.NODE_ENV="production"',
|
|
32
|
+
"--minify",
|
|
33
|
+
],
|
|
34
|
+
stdout: "piped",
|
|
35
|
+
stderr: "piped",
|
|
36
|
+
});
|
|
37
|
+
const { code, stderr } = await cmd.output();
|
|
38
|
+
if (code !== 0) {
|
|
39
|
+
const err = new TextDecoder().decode(stderr);
|
|
40
|
+
throw new Error(`esbuild dev-landing failed:\n${err}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function buildDevLanding(opts: BuildOpts): Promise<void> {
|
|
45
|
+
const outDir = resolve(Deno.cwd(), opts.outDir);
|
|
46
|
+
await Deno.mkdir(outDir, { recursive: true });
|
|
47
|
+
await buildJs(outDir);
|
|
48
|
+
const jsStat = await Deno.stat(`${outDir}/dev-landing.js`);
|
|
49
|
+
console.log(`dev-landing → ${outDir}: dev-landing.js (${(jsStat.size / 1024).toFixed(1)} KB)`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function runBuildDevLanding(args: string[]): Promise<number> {
|
|
53
|
+
let outDir = "./ui-runtime";
|
|
54
|
+
for (let i = 0; i < args.length; i++) {
|
|
55
|
+
const a = args[i]!;
|
|
56
|
+
if (a === "--out" || a === "-o") {
|
|
57
|
+
const next = args[i + 1];
|
|
58
|
+
if (!next) {
|
|
59
|
+
console.error("--out requires a directory argument");
|
|
60
|
+
return 2;
|
|
61
|
+
}
|
|
62
|
+
outDir = next;
|
|
63
|
+
i++;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (a === "--help" || a === "-h") {
|
|
67
|
+
console.log(`Usage: realm-sdk build-dev-landing [--out DIR]
|
|
68
|
+
|
|
69
|
+
Builds the dev landing entry (dev-landing.js) into DIR.
|
|
70
|
+
DIR defaults to ./ui-runtime (relative to the current directory).
|
|
71
|
+
`);
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
console.error(`Unknown flag: ${a}`);
|
|
75
|
+
return 2;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
await buildDevLanding({ outDir });
|
|
79
|
+
return 0;
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.error((err as Error).message);
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
}
|