@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/surfaces.ts
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// An app's file tree → one merged realm.ts source, with esbuild alone.
|
|
2
|
+
//
|
|
3
|
+
// The merge does two jobs, and an app can need either or both: attach the
|
|
4
|
+
// file-based UI surfaces, and inline the entry's relative-import closure (an app
|
|
5
|
+
// can have no UI at all — just realm.ts + relative helpers — and still has to
|
|
6
|
+
// arrive as one module, since `bundle()` blocks relative specifiers).
|
|
7
|
+
//
|
|
8
|
+
// Authoring is Next.js style: each pages/<key>.tsx / widgets/<key>.tsx exports
|
|
9
|
+
// `title`, `icon`, optional `loaders` / `actions` (and `cols` / `rows` for
|
|
10
|
+
// widgets) plus a default render fn; the author's realm.tsx never references
|
|
11
|
+
// them. The merge generates a glue entry that imports realm.tsx's app + each
|
|
12
|
+
// surface namespace, calls `app.attachSurfaces({ pages, widgets })`, and
|
|
13
|
+
// re-exports the app. esbuild inlines the relatives into one module; every
|
|
14
|
+
// package import — @yourrealm/sdk*, preact, zod — stays external. No TypeScript
|
|
15
|
+
// compiler is involved (surface exports are read off the namespace at runtime),
|
|
16
|
+
// which is what lets Home run this at install.
|
|
17
|
+
//
|
|
18
|
+
// Home consumes a packed artifact via `mergePackedSource` (unpack → confined tmp
|
|
19
|
+
// tree → merge); `realm-sdk dev` calls `mergeSurfaces` on the live app dir. The
|
|
20
|
+
// only confinement is esbuild's `--metafile` guard in `esbuildMerge`: refuse the
|
|
21
|
+
// merge if esbuild read any file outside the app root.
|
|
22
|
+
|
|
23
|
+
import { dirname, join } from "@std/path";
|
|
24
|
+
import { REALM_ENTRIES, unpackFiles } from "./pack.ts";
|
|
25
|
+
import { pkgRoot } from "./pkg-root.ts";
|
|
26
|
+
|
|
27
|
+
const ID_RE = /^[A-Za-z_$][\w$]*$/;
|
|
28
|
+
|
|
29
|
+
const SDK_DIR = pkgRoot(import.meta.url);
|
|
30
|
+
|
|
31
|
+
interface SurfaceFile {
|
|
32
|
+
kind: "page" | "widget";
|
|
33
|
+
key: string;
|
|
34
|
+
dir: "pages" | "widgets";
|
|
35
|
+
file: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function globSurfaces(
|
|
39
|
+
appDir: string,
|
|
40
|
+
kind: "page" | "widget",
|
|
41
|
+
): Promise<SurfaceFile[]> {
|
|
42
|
+
const dir = kind === "page" ? "pages" : "widgets";
|
|
43
|
+
const out: SurfaceFile[] = [];
|
|
44
|
+
try {
|
|
45
|
+
for await (const entry of Deno.readDir(join(appDir, dir))) {
|
|
46
|
+
if (!entry.isFile || !entry.name.endsWith(".tsx")) continue;
|
|
47
|
+
const key = entry.name.slice(0, -".tsx".length);
|
|
48
|
+
if (!ID_RE.test(key)) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`${dir}/${entry.name}: surface key "${key}" must be a valid identifier — rename the file.`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
out.push({ kind, key, dir, file: entry.name });
|
|
54
|
+
}
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (!(err instanceof Deno.errors.NotFound)) throw err; // no pages/ or widgets/ dir is fine
|
|
57
|
+
}
|
|
58
|
+
return out.sort((a, b) => a.key.localeCompare(b.key));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const surfaceNs = (f: SurfaceFile): string => `__realm_${f.kind}_${f.key}`;
|
|
62
|
+
|
|
63
|
+
// `page()`/`widget()` call reading every field off the surface's namespace —
|
|
64
|
+
// missing optionals read as `undefined`, which the helpers + createApp tolerate.
|
|
65
|
+
function surfaceCall(f: SurfaceFile): string {
|
|
66
|
+
const ns = surfaceNs(f);
|
|
67
|
+
const factory = f.kind === "page" ? "__realmPage" : "__realmWidget";
|
|
68
|
+
const fields = [
|
|
69
|
+
`title: ${ns}.title`,
|
|
70
|
+
`icon: ${ns}.icon`,
|
|
71
|
+
`render: ${ns}.default`,
|
|
72
|
+
`stateSchema: ${ns}.stateSchema`,
|
|
73
|
+
`configSchema: ${ns}.configSchema`,
|
|
74
|
+
`loaders: ${ns}.loaders`,
|
|
75
|
+
`actions: ${ns}.actions`,
|
|
76
|
+
];
|
|
77
|
+
if (f.kind === "widget") fields.push(`cols: ${ns}.cols`, `rows: ${ns}.rows`);
|
|
78
|
+
return `${factory}({ ${fields.join(", ")} })`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// The glue entry: import the app + each surface namespace, attach the assembled
|
|
82
|
+
// ui, re-export the app. esbuild inlines the relatives; the app runs first
|
|
83
|
+
// (createApp during its import), then attachSurfaces in the glue body.
|
|
84
|
+
function surfaceGlue(entry: string, files: SurfaceFile[]): string {
|
|
85
|
+
// Surface-less app: the glue exists only to inline the entry's
|
|
86
|
+
// relative-import closure, so keep it to a bare re-export. Importing
|
|
87
|
+
// `@yourrealm/sdk/ui` with nothing to attach would pull a UI barrel into an
|
|
88
|
+
// app that has no UI — a QuickJS load-safety risk for no gain.
|
|
89
|
+
if (files.length === 0)
|
|
90
|
+
return `import __realmApp from "./${entry}";\nexport default __realmApp;\n`;
|
|
91
|
+
const lines = [
|
|
92
|
+
`import __realmApp from "./${entry}";`,
|
|
93
|
+
`import { page as __realmPage, widget as __realmWidget } from "@yourrealm/sdk/ui";`,
|
|
94
|
+
];
|
|
95
|
+
for (const f of files) lines.push(`import * as ${surfaceNs(f)} from "./${f.dir}/${f.file}";`);
|
|
96
|
+
lines.push("__realmApp.attachSurfaces({", " pages: {");
|
|
97
|
+
for (const f of files) if (f.kind === "page") lines.push(` ${f.key}: ${surfaceCall(f)},`);
|
|
98
|
+
lines.push(" },", " widgets: {");
|
|
99
|
+
for (const f of files) if (f.kind === "widget") lines.push(` ${f.key}: ${surfaceCall(f)},`);
|
|
100
|
+
lines.push(" },", "});", "export default __realmApp;", "");
|
|
101
|
+
return lines.join("\n");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// esbuild / oxfmt are devDeps in node_modules/.bin, not necessarily on PATH.
|
|
105
|
+
// Walk up from the app dir to find them.
|
|
106
|
+
function findBin(startDir: string, name: string): string | null {
|
|
107
|
+
let dir = startDir;
|
|
108
|
+
while (true) {
|
|
109
|
+
const candidate = join(dir, "node_modules", ".bin", name);
|
|
110
|
+
try {
|
|
111
|
+
Deno.statSync(candidate);
|
|
112
|
+
return candidate;
|
|
113
|
+
} catch {
|
|
114
|
+
/* keep walking */
|
|
115
|
+
}
|
|
116
|
+
const parent = dirname(dir);
|
|
117
|
+
if (parent === dir) return null;
|
|
118
|
+
dir = parent;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// esbuild lives in node_modules/.bin. The author's app dir has one up the tree;
|
|
123
|
+
// Home's install-time tmp tree doesn't, so fall back to the SDK's own location,
|
|
124
|
+
// then to PATH (Home stages the binary there).
|
|
125
|
+
function findEsbuild(appDir: string): string {
|
|
126
|
+
return (
|
|
127
|
+
findBin(appDir, "esbuild") ??
|
|
128
|
+
(SDK_DIR === null ? null : findBin(SDK_DIR, "esbuild")) ??
|
|
129
|
+
"esbuild"
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Bundle the generated glue through esbuild via stdin, with the process cwd set
|
|
134
|
+
// to the app dir so the glue's relative imports (./realm.tsx, ./pages/*) resolve
|
|
135
|
+
// against the real tree. The app dir itself is never mutated — output + metafile
|
|
136
|
+
// go to a scratch dir outside it.
|
|
137
|
+
//
|
|
138
|
+
// esbuild follows every relative/absolute import straight to disk
|
|
139
|
+
// (`--packages=external` externalizes only bare specifiers), so a hostile
|
|
140
|
+
// `import "../../secret"` in a packed file would otherwise inline a
|
|
141
|
+
// control-plane file. The sole confinement is esbuild's own `--metafile`: refuse
|
|
142
|
+
// the merge if it read ANY input outside the app root. It keys off what esbuild
|
|
143
|
+
// actually resolved (static, dynamic, require, css) rather than a source scan,
|
|
144
|
+
// so no import form or line layout slips past it.
|
|
145
|
+
async function esbuildMerge(glue: string, appDir: string): Promise<string> {
|
|
146
|
+
const esbuild = findEsbuild(appDir);
|
|
147
|
+
const outDir = await Deno.makeTempDir({ prefix: "realm-merge-out-" });
|
|
148
|
+
const outFile = join(outDir, "merged.js");
|
|
149
|
+
const metaFile = join(outDir, "meta.json");
|
|
150
|
+
try {
|
|
151
|
+
const child = new Deno.Command(esbuild, {
|
|
152
|
+
args: [
|
|
153
|
+
"--bundle",
|
|
154
|
+
"--loader=tsx", // stdin has no filename to infer from
|
|
155
|
+
"--packages=external", // every bare import stays external; only relatives inline
|
|
156
|
+
// Normalize raw `zod` to the SDK re-export. A file shared between an
|
|
157
|
+
// app's container code and a UI surface imports `z` from raw `zod`, but
|
|
158
|
+
// Home's pruned image resolves only `@yourrealm/sdk`, which re-exports
|
|
159
|
+
// the same `z`. Alias fires before the external check, so the specifier
|
|
160
|
+
// is rewritten and left external, never resolved here.
|
|
161
|
+
"--alias:zod=@yourrealm/sdk",
|
|
162
|
+
"--format=esm",
|
|
163
|
+
"--platform=neutral",
|
|
164
|
+
"--target=esnext", // keep output close to source — no needless downleveling
|
|
165
|
+
"--jsx=preserve", // Home's bundle() does the JSX transform; keep <Page> readable
|
|
166
|
+
"--charset=utf8", // keep `…`/emoji literal instead of \u escapes
|
|
167
|
+
`--outfile=${outFile}`, // esbuild requires an output path to emit a metafile
|
|
168
|
+
`--metafile=${metaFile}`,
|
|
169
|
+
],
|
|
170
|
+
cwd: appDir,
|
|
171
|
+
stdin: "piped",
|
|
172
|
+
stdout: "piped",
|
|
173
|
+
stderr: "piped",
|
|
174
|
+
}).spawn();
|
|
175
|
+
const writer = child.stdin.getWriter();
|
|
176
|
+
await writer.write(new TextEncoder().encode(glue));
|
|
177
|
+
await writer.close();
|
|
178
|
+
const { code, stderr } = await child.output();
|
|
179
|
+
if (code !== 0) throw new Error(new TextDecoder().decode(stderr));
|
|
180
|
+
|
|
181
|
+
// esbuild reports every input it read, keyed by a path relative to its cwd
|
|
182
|
+
// (the app dir). An in-app file never starts with `../`; anything that does
|
|
183
|
+
// (or is absolute) was read from outside the app — refuse it. `<stdin>` is
|
|
184
|
+
// the glue itself.
|
|
185
|
+
const meta = JSON.parse(await Deno.readTextFile(metaFile)) as {
|
|
186
|
+
inputs: Record<string, unknown>;
|
|
187
|
+
};
|
|
188
|
+
for (const input of Object.keys(meta.inputs)) {
|
|
189
|
+
if (input === "<stdin>") continue;
|
|
190
|
+
if (input.startsWith("/") || input.startsWith("../")) {
|
|
191
|
+
throw new Error(`merge refused: an import resolves outside the app root ("${input}")`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return await Deno.readTextFile(outFile);
|
|
195
|
+
} finally {
|
|
196
|
+
await Deno.remove(outDir, { recursive: true }).catch(() => {});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function findRealmEntry(appDir: string): string {
|
|
201
|
+
for (const name of REALM_ENTRIES) {
|
|
202
|
+
try {
|
|
203
|
+
Deno.statSync(join(appDir, name));
|
|
204
|
+
return name;
|
|
205
|
+
} catch {
|
|
206
|
+
/* try next */
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
throw new Error(`no ${REALM_ENTRIES.join(" or ")} in ${appDir}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Merge an app's tree into the one source `bundle()` ingests, using esbuild
|
|
214
|
+
* alone: attach the file-based surfaces AND inline the entry's relative-import
|
|
215
|
+
* closure. Runs at install (Home) and in dev.
|
|
216
|
+
*
|
|
217
|
+
* Every app goes through the merge, surfaces or not — a surface-less app can
|
|
218
|
+
* still be multi-file (`realm.ts` + plain relative helpers), and its relatives
|
|
219
|
+
* have to be inlined all the same or `bundle()` rejects them as blocked
|
|
220
|
+
* specifiers. Nothing is lost by merging the single-file case too: `bundle()`
|
|
221
|
+
* esbuilds the source into the QuickJS artifact immediately afterward either
|
|
222
|
+
* way.
|
|
223
|
+
*/
|
|
224
|
+
export async function mergeSurfaces(
|
|
225
|
+
appDir: string,
|
|
226
|
+
// The entry's filename, when the caller already knows it. Defaults to the
|
|
227
|
+
// canonical realm.tsx/realm.ts an app dir holds — but a test fixture dir can
|
|
228
|
+
// hold several apps under arbitrary names (`sample.realm.ts`), and the test
|
|
229
|
+
// harness knows exactly which one it's merging.
|
|
230
|
+
entryName?: string,
|
|
231
|
+
): Promise<{ source: string; surfaceCount: number }> {
|
|
232
|
+
const entry = entryName ?? findRealmEntry(appDir);
|
|
233
|
+
const files = [
|
|
234
|
+
...(await globSurfaces(appDir, "page")),
|
|
235
|
+
...(await globSurfaces(appDir, "widget")),
|
|
236
|
+
];
|
|
237
|
+
return {
|
|
238
|
+
source: await esbuildMerge(surfaceGlue(entry, files), appDir),
|
|
239
|
+
surfaceCount: files.length,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Install-time entry point: unpack a packed artifact into a confined tmp tree
|
|
245
|
+
* and merge it into the single source `bundle()` ingests. Folded into `bundle()`
|
|
246
|
+
* / `bundleUiSurfaces()`, so Home handles a packed artifact at every call site.
|
|
247
|
+
* A hostile import that reaches outside the tmp tree is refused by esbuild's
|
|
248
|
+
* `--metafile` guard in `esbuildMerge`. No TypeScript compiler — safe in Home's
|
|
249
|
+
* least-privilege runtime.
|
|
250
|
+
*/
|
|
251
|
+
export async function mergePackedSource(packed: string): Promise<string> {
|
|
252
|
+
const files = unpackFiles(packed);
|
|
253
|
+
const tmpDir = await Deno.makeTempDir({ prefix: "realm-unpack-" });
|
|
254
|
+
try {
|
|
255
|
+
for (const [path, content] of files) {
|
|
256
|
+
const dest = join(tmpDir, path);
|
|
257
|
+
await Deno.mkdir(dirname(dest), { recursive: true });
|
|
258
|
+
await Deno.writeTextFile(dest, content);
|
|
259
|
+
}
|
|
260
|
+
return (await mergeSurfaces(tmpDir)).source;
|
|
261
|
+
} finally {
|
|
262
|
+
await Deno.remove(tmpDir, { recursive: true }).catch(() => {});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Resolve the `tailwindcss` binary by walking up from a starting dir
|
|
2
|
+
// looking for `node_modules/.bin/tailwindcss`.
|
|
3
|
+
//
|
|
4
|
+
// Under byonm (root `nodeModulesDir: "manual"`) pnpm writes a `.bin` into
|
|
5
|
+
// each workspace package's node_modules, so walking up from any caller
|
|
6
|
+
// finds the nearest one.
|
|
7
|
+
|
|
8
|
+
import { dirname, join } from "@std/path";
|
|
9
|
+
|
|
10
|
+
export function findTailwindBin(startDir: string): string {
|
|
11
|
+
let dir = startDir;
|
|
12
|
+
while (true) {
|
|
13
|
+
const candidate = join(dir, "node_modules", ".bin", "tailwindcss");
|
|
14
|
+
try {
|
|
15
|
+
Deno.statSync(candidate);
|
|
16
|
+
return candidate;
|
|
17
|
+
} catch {
|
|
18
|
+
// keep walking
|
|
19
|
+
}
|
|
20
|
+
const parent = dirname(dir);
|
|
21
|
+
if (parent === dir) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`tailwindcss binary not found by walking up from ${startDir}. ` +
|
|
24
|
+
`Add @tailwindcss/cli to a workspace package's devDependencies and run pnpm install.`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
dir = parent;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Collect every `node_modules/.bin` directory from `startDir` up to
|
|
33
|
+
* the filesystem root. Useful for `realm-sdk dev`, which spawns esbuild
|
|
34
|
+
* + tailwindcss as bare-name commands and needs PATH primed when
|
|
35
|
+
* invoked directly via `deno run` (pnpm's bin-shimming only activates
|
|
36
|
+
* inside `pnpm <script>`). pnpm hoists most devDeps to the workspace
|
|
37
|
+
* root's `.bin`, but a workspace package's local `.bin` can shadow
|
|
38
|
+
* them — return all matches in walked order so callers prepend them
|
|
39
|
+
* to PATH from nearest to farthest.
|
|
40
|
+
*/
|
|
41
|
+
export function findBinDirs(startDir: string): string[] {
|
|
42
|
+
const out: string[] = [];
|
|
43
|
+
let dir = startDir;
|
|
44
|
+
while (true) {
|
|
45
|
+
const candidate = join(dir, "node_modules", ".bin");
|
|
46
|
+
try {
|
|
47
|
+
const stat = Deno.statSync(candidate);
|
|
48
|
+
if (stat.isDirectory) out.push(candidate);
|
|
49
|
+
} catch {
|
|
50
|
+
// keep walking
|
|
51
|
+
}
|
|
52
|
+
const parent = dirname(dir);
|
|
53
|
+
if (parent === dir) return out;
|
|
54
|
+
dir = parent;
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/test/boot.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// `t.boot` — render compile output to docker-compose, splice a
|
|
2
|
+
// Playwright sidecar, and run `docker compose up -d --wait` against a
|
|
3
|
+
// unique project name. The harness returns a handle whose `page` is a
|
|
4
|
+
// browser context running *inside* the same compose network, so a
|
|
5
|
+
// realm.ts test can drive `http://serviceName:port` URLs the way a
|
|
6
|
+
// sibling container would.
|
|
7
|
+
//
|
|
8
|
+
// Boot reuses `compile`'s output. A failed compile is fatal here — the
|
|
9
|
+
// thrown error carries the full issues list so a test's catch site can
|
|
10
|
+
// still inspect it.
|
|
11
|
+
//
|
|
12
|
+
// Cleanup has two layers, both automatic:
|
|
13
|
+
// 1. The describe.ts wrapper tears down every stack a test booted
|
|
14
|
+
// after the body resolves or throws.
|
|
15
|
+
// 2. A module-level set of active handles is drained on
|
|
16
|
+
// `beforeunload`, so a test that crashes the process still tears
|
|
17
|
+
// down before exit.
|
|
18
|
+
|
|
19
|
+
import { chromium, type Page } from "playwright-core";
|
|
20
|
+
import { expect } from "@playwright/test";
|
|
21
|
+
import { convertToDockerCompose, type ComposeService } from "../runtime/docker/compose/index.ts";
|
|
22
|
+
import type { WorkerApp, WorkerState } from "../worker-protocol/index.ts";
|
|
23
|
+
import { compile as runCompile } from "./compile.ts";
|
|
24
|
+
import { RealmCompileError } from "../errors.ts";
|
|
25
|
+
import { bringUpStack } from "../dev/stack.ts";
|
|
26
|
+
import { fixtureIngressInput } from "./ingress-fixture.ts";
|
|
27
|
+
import type { CompileFixture } from "./types.ts";
|
|
28
|
+
|
|
29
|
+
// Pin this in lockstep with the playwright-core / @playwright/test
|
|
30
|
+
// versions in package.json AND the tag built from
|
|
31
|
+
// platform/playwright/Dockerfile. The run-server protocol is version-coupled.
|
|
32
|
+
const PLAYWRIGHT_IMAGE = "benjick/realm-playwright:1.58.0";
|
|
33
|
+
const PLAYWRIGHT_PORT = 14000;
|
|
34
|
+
|
|
35
|
+
interface BootInputs<App> {
|
|
36
|
+
bundledCode: string;
|
|
37
|
+
appName: string;
|
|
38
|
+
fixture: CompileFixture<App>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RealmBootHandle {
|
|
42
|
+
page: Page;
|
|
43
|
+
/**
|
|
44
|
+
* Map of service name → compose hostname. Indexable by any string;
|
|
45
|
+
* the runtime always returns `${appId}_${name}` (the renderer's compose
|
|
46
|
+
* key, which is also the in-network DNS name).
|
|
47
|
+
*/
|
|
48
|
+
services: Record<string, string>;
|
|
49
|
+
expect: typeof expect;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Internal: what `boot()` actually returns to the describe.ts wrapper.
|
|
54
|
+
* The wrapper registers `cleanup` against the test body and returns
|
|
55
|
+
* the bare `handle` to the test author.
|
|
56
|
+
*/
|
|
57
|
+
interface BootResult {
|
|
58
|
+
handle: RealmBootHandle;
|
|
59
|
+
cleanup: () => Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function boot<App>(inputs: BootInputs<App>): Promise<BootResult> {
|
|
63
|
+
const { bundledCode, appName, fixture } = inputs;
|
|
64
|
+
const result = await runCompile<App>({ bundledCode, appName, fixture });
|
|
65
|
+
if (result.error) {
|
|
66
|
+
throw new RealmCompileError([...result.issues]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const appId = fixture.appId ?? appName;
|
|
70
|
+
|
|
71
|
+
// The harness renders in-process and never serializes to the wire, and the
|
|
72
|
+
// renderer reads only the fields it knows, so the resolve model's extra
|
|
73
|
+
// Home-only fields are simply ignored — cast straight to the wire shape.
|
|
74
|
+
const workerApp: WorkerApp = {
|
|
75
|
+
appId,
|
|
76
|
+
services: result.app.services as unknown as WorkerApp["services"],
|
|
77
|
+
...(result.app.egress ? { egress: true } : {}),
|
|
78
|
+
externalNetworks: [],
|
|
79
|
+
dependsOn: {},
|
|
80
|
+
inlineFiles: {},
|
|
81
|
+
allowedAbsoluteHostPaths: [],
|
|
82
|
+
};
|
|
83
|
+
const wireState: WorkerState = {
|
|
84
|
+
apps: [workerApp],
|
|
85
|
+
artifactHashes: [],
|
|
86
|
+
workerNetworks: [],
|
|
87
|
+
// Nothing in the harness renders ingress, but the wire has no absent
|
|
88
|
+
// state for it - so state what this box's ingress would be, read off the
|
|
89
|
+
// same fixture the eval resolved `host.management()` from.
|
|
90
|
+
ingress: fixtureIngressInput(fixture.management),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Single-app harness: one project per app, and boot only has one.
|
|
94
|
+
const compose = convertToDockerCompose(wireState)[0]!.compose;
|
|
95
|
+
|
|
96
|
+
// Mark every realm service for `realm-sdk down`. The Playwright
|
|
97
|
+
// sidecar gets the same label below.
|
|
98
|
+
for (const svc of Object.values(compose.services)) {
|
|
99
|
+
svc.labels = { ...svc.labels, "com.realm.test": "true" };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const hostPort = await pickFreePort();
|
|
103
|
+
const sidecarName = `${appId}_realm_playwright`;
|
|
104
|
+
compose.services[sidecarName] = playwrightSidecar({ appId, hostPort });
|
|
105
|
+
|
|
106
|
+
const project = `realm-test-${appId}-${nano(8)}`;
|
|
107
|
+
const tmpDir = await Deno.makeTempDir({ prefix: `${project}-` });
|
|
108
|
+
|
|
109
|
+
let stack;
|
|
110
|
+
try {
|
|
111
|
+
stack = await bringUpStack({ compose, project, tmpDir });
|
|
112
|
+
} catch (err) {
|
|
113
|
+
await Deno.remove(tmpDir, { recursive: true }).catch(() => {});
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const browser = await chromium.connect(`ws://localhost:${hostPort}/`);
|
|
118
|
+
const ctx = await browser.newContext();
|
|
119
|
+
const page = await ctx.newPage();
|
|
120
|
+
|
|
121
|
+
const services = new Proxy<Record<string, string>>(
|
|
122
|
+
{},
|
|
123
|
+
{
|
|
124
|
+
get: (_t, prop) => {
|
|
125
|
+
if (typeof prop !== "string") return undefined;
|
|
126
|
+
return `${appId}_${prop}`;
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const cleanup = async () => {
|
|
132
|
+
await ctx.close().catch(() => {});
|
|
133
|
+
await browser.close().catch(() => {});
|
|
134
|
+
await stack.cleanup();
|
|
135
|
+
await Deno.remove(tmpDir, { recursive: true }).catch(() => {});
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return { handle: { page, services, expect }, cleanup };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function playwrightSidecar(opts: { appId: string; hostPort: number }): ComposeService {
|
|
142
|
+
return {
|
|
143
|
+
image: PLAYWRIGHT_IMAGE,
|
|
144
|
+
labels: { "com.realm.test": "true" },
|
|
145
|
+
ports: [{ target: PLAYWRIGHT_PORT, published: opts.hostPort, protocol: "tcp" }],
|
|
146
|
+
networks: { [`realm_${opts.appId}`]: { aliases: ["realm-playwright"] } },
|
|
147
|
+
// `--wait` only waits for healthy containers. Without a check
|
|
148
|
+
// here, compose returns as soon as the container starts, which
|
|
149
|
+
// races chromium.connect against playwright's own boot. The check
|
|
150
|
+
// is the same TCP-probe e2e uses.
|
|
151
|
+
healthcheck: {
|
|
152
|
+
test: [
|
|
153
|
+
"CMD",
|
|
154
|
+
"node",
|
|
155
|
+
"-e",
|
|
156
|
+
`require('net').createConnection(${PLAYWRIGHT_PORT}, 'localhost').on('connect', () => process.exit(0)).on('error', () => process.exit(1))`,
|
|
157
|
+
],
|
|
158
|
+
interval: "2s",
|
|
159
|
+
timeout: "3s",
|
|
160
|
+
retries: 60,
|
|
161
|
+
start_period: "5s",
|
|
162
|
+
},
|
|
163
|
+
cap_drop: ["ALL"],
|
|
164
|
+
cap_add: [],
|
|
165
|
+
security_opt: ["no-new-privileges:true"],
|
|
166
|
+
pids_limit: 4096,
|
|
167
|
+
ulimits: { nofile: 65536, nproc: 4096 },
|
|
168
|
+
restart: "no",
|
|
169
|
+
logging: { driver: "json-file", options: { "max-size": "10m", "max-file": "3" } },
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function pickFreePort(): Promise<number> {
|
|
174
|
+
// Another process could grab the port between close() and `docker compose
|
|
175
|
+
// up`; the bind failure surfaces at the docker-compose layer. The window is
|
|
176
|
+
// small enough not to matter across a test suite.
|
|
177
|
+
const listener = Deno.listen({ port: 0 });
|
|
178
|
+
const port = (listener.addr as Deno.NetAddr).port;
|
|
179
|
+
listener.close();
|
|
180
|
+
return port;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function nano(n: number): string {
|
|
184
|
+
// Project names are docker-compose-validated; a-z 0-9 only. Cryptographic
|
|
185
|
+
// uniqueness is not needed — collision odds with 8 lowercase hex chars are
|
|
186
|
+
// negligible across a single test run.
|
|
187
|
+
const bytes = new Uint8Array(Math.ceil(n / 2));
|
|
188
|
+
crypto.getRandomValues(bytes);
|
|
189
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0"))
|
|
190
|
+
.join("")
|
|
191
|
+
.slice(0, n);
|
|
192
|
+
}
|