@snap-rail/plugin-kit 0.2.1 → 0.2.2

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/lib/build.js CHANGED
@@ -173,7 +173,13 @@ function hostBundle(entry) {
173
173
  * subpath to its module namespace, and the shell seeds
174
174
  * `<package>/<subpath>` ids from that map after requiring the face.
175
175
  *
176
- * @param packageName - the plugin's npm name (the module id it registers as).
176
+ * A package that renders in more than one window calls this once per surface:
177
+ * the main surface keeps the package name as its module id and `client` as its
178
+ * file name, while an auxiliary surface passes `outName` and `moduleId` so its
179
+ * bundle lands beside the main one (`lib-client/ball.js`) and registers as
180
+ * `<package>/ball` — the id that surface's window requires.
181
+ *
182
+ * @param packageName - the plugin's npm name (asset URLs and the default module id).
177
183
  * @param entry - the renderer entry source file.
178
184
  * @param options - see {@link ClientBundleOptions}.
179
185
  */
@@ -191,7 +197,7 @@ function clientBundle(packageName, entry, options = {}) {
191
197
  }
192
198
  };
193
199
  return defineConfig({
194
- entry: { client: entry },
200
+ entry: { [options.outName ?? "client"]: entry },
195
201
  outDir: "lib-client",
196
202
  format: ["cjs"],
197
203
  platform: "browser",
@@ -208,7 +214,7 @@ function clientBundle(packageName, entry, options = {}) {
208
214
  seedImportGuard()
209
215
  ],
210
216
  outputOptions: {
211
- banner: `window.__ModuleLoader__.load({ id: ${JSON.stringify(packageName)}, factory: function (require, module, exports) {`,
217
+ banner: `window.__ModuleLoader__.load({ id: ${JSON.stringify(options.moduleId ?? packageName)}, factory: function (require, module, exports) {`,
212
218
  footer: "return module.exports && module.exports.__esModule === true && \"default\" in module.exports ? module.exports.default : module.exports; } });"
213
219
  }
214
220
  });
package/lib/manifest.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { AUX_SURFACE_IDS } from "@snap-rail/protocol";
2
3
  import { PLUGIN_LAYERS } from "@snap-rail/util/manifest";
3
4
  //#region lib/types/manifest.js
4
5
  /**
@@ -25,11 +26,29 @@ const seedAbiSchema = z.object({
25
26
  /** Base-package versions of the seeds the bundle requires, by seed id. */
26
27
  versions: z.record(z.string().min(1), z.string().min(1))
27
28
  }).strict();
29
+ /**
30
+ * The auxiliary-surface entry map: one client bundle per extra surface the
31
+ * package renders in. Keys are the auxiliary surface ids (`ball`) —
32
+ * `main` is deliberately absent, since the main window's bundle is
33
+ * `client.entry` and a package gets exactly one entry per window. Every
34
+ * declared path is a CJS factory bundle inside the package, packed and
35
+ * validated like the main entry; the surfaces a package does not declare
36
+ * simply have no face and show the kernel's plain fallback.
37
+ */
38
+ const clientSurfacesSchema = z.partialRecord(z.enum(AUX_SURFACE_IDS), z.string().min(1));
28
39
  /** The renderer face declaration: where the client bundle lives. */
29
40
  const clientFaceSchema = z.object({
30
- /** Path of the CJS factory bundle inside the package (from its root). */
41
+ /** Path of the CJS factory bundle inside the package (from its root) —
42
+ * the **main** surface's entry. */
31
43
  entry: z.string().min(1).default("lib/client.js"),
32
44
  /**
45
+ * Extra surfaces this package renders in: one client bundle per auxiliary
46
+ * window (`ball`). Each bundle is loaded into that surface's own
47
+ * window with the same seed table, and its module id is
48
+ * `<package>/<surface>` (the main entry keeps the plain package name).
49
+ */
50
+ surfaces: clientSurfacesSchema.optional(),
51
+ /**
33
52
  * Subpaths this client face registers as contributed seeds (e.g. framework
34
53
  * primitives like `./ui`): each becomes a `__ModuleLoader__` id other
35
54
  * plugins' client bundles may `require` — same instance, one execution.
@@ -119,4 +138,4 @@ const snapRailManifestSchema = z.object({
119
138
  }).strict().optional()
120
139
  });
121
140
  //#endregion
122
- export { PLUGIN_KINDS, PLUGIN_LAYERS, clientFaceSchema, seedAbiSchema, snapRailManifestSchema };
141
+ export { PLUGIN_KINDS, PLUGIN_LAYERS, clientFaceSchema, clientSurfacesSchema, seedAbiSchema, snapRailManifestSchema };
package/lib/pack.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { CLIENT_EXTERNALS } from "./seeds.js";
2
+ import { clientSurfacesSchema } from "./manifest.js";
2
3
  import { createRequire } from "node:module";
3
4
  import { readFile, readdir } from "node:fs/promises";
4
5
  import { join, relative } from "node:path";
@@ -64,22 +65,25 @@ function basePackageOf(seedId) {
64
65
  return seedId.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
65
66
  }
66
67
  /**
67
- * Stamp the seed ABI a built client face was built against: the seeds its
68
- * bundle actually requires (a `require("…")` literal scan — externals are
69
- * the only bare requires the welded CJS output carries) plus each seed's
70
- * base-package version resolved from the package's own dependency tree.
71
- * Ids whose base package cannot be resolved here are omitted — the runtime
72
- * check compares only the intersection, so an omitted id is unchecked,
73
- * never failed. Returns `undefined` when the bundle requires no seeds or
74
- * none could be versioned (nothing to enforce).
68
+ * Stamp the seed ABI a set of built client faces were built against: the
69
+ * seeds their bundles actually require (a `require("…")` literal scan —
70
+ * externals are the only bare requires the welded CJS output carries) plus
71
+ * each seed's base-package version resolved from the package's own dependency
72
+ * tree. A multi-surface package stamps ONE declaration for all its entries
73
+ * (the union of their requires): every surface shares the shell's seed table,
74
+ * so a per-face stamp would only repeat the same verdict N times. Ids whose
75
+ * base package cannot be resolved here are omitted — the runtime check
76
+ * compares only the intersection, so an omitted id is unchecked, never
77
+ * failed. Returns `undefined` when the bundles require no seeds or none could
78
+ * be versioned (nothing to enforce).
75
79
  *
76
80
  * @param packageDir - the package being packed (version resolution root).
77
- * @param bundleCode - the built client bundle's source text.
81
+ * @param bundleCodes - the built client bundles' source texts, one per surface.
78
82
  */
79
- async function seedAbiStamp(packageDir, bundleCode) {
83
+ async function seedAbiStamp(packageDir, bundleCodes) {
80
84
  const seeds = new Set(CLIENT_EXTERNALS);
81
85
  const required = /* @__PURE__ */ new Set();
82
- for (const match of bundleCode.matchAll(/require\(\s*(['"])([^'"\n]+)\1\s*\)/g)) {
86
+ for (const bundleCode of bundleCodes) for (const match of bundleCode.matchAll(/require\(\s*(['"])([^'"\n]+)\1\s*\)/g)) {
83
87
  const id = match[2];
84
88
  if (id !== void 0 && seeds.has(id)) required.add(id);
85
89
  }
@@ -137,6 +141,12 @@ async function releaseFiles(spec) {
137
141
  if (spec.hostFace !== void 0 && spec.hostDir === void 0) throw new Error(`pack: ${spec.name} declares a host face without a hostDir`);
138
142
  if (spec.hostFace === void 0 && spec.clientFace === void 0) throw new Error(`pack: ${spec.name} ships neither a host face nor a client bundle`);
139
143
  const clientEntry = typeof snapRail?.client?.entry === "string" ? snapRail.client.entry : "lib/client.js";
144
+ let clientSurfaces = {};
145
+ if (snapRail?.client?.surfaces !== void 0) {
146
+ const parsed = clientSurfacesSchema.safeParse(snapRail.client.surfaces);
147
+ if (!parsed.success) throw new Error(`pack: ${spec.name} declares an invalid snapRail.client.surfaces (${parsed.error.issues.map((issue) => issue.message).join("; ")})`);
148
+ clientSurfaces = parsed.data;
149
+ }
140
150
  const main = spec.hostFace ?? clientEntry;
141
151
  const files = {};
142
152
  if (spec.hostDir !== void 0) {
@@ -146,7 +156,13 @@ async function releaseFiles(spec) {
146
156
  if (spec.clientFace !== void 0) Object.assign(files, await collectDir(join(spec.dir, spec.clientFace), spec.dir));
147
157
  if (spec.hostFace !== void 0 && files[spec.hostFace] === void 0) throw new Error(`pack: ${spec.name} is missing its host face ${spec.hostFace} under ${String(spec.hostDir)}`);
148
158
  if (declaredClient && files[clientEntry] === void 0) throw new Error(`pack: ${spec.name} declares client entry ${clientEntry} but the zip does not carry it`);
149
- const seedAbi = declaredClient && files[clientEntry] !== void 0 ? await seedAbiStamp(spec.dir, new TextDecoder().decode(files[clientEntry])) : void 0;
159
+ for (const [surface, entry] of Object.entries(clientSurfaces)) {
160
+ if (entry === void 0) continue;
161
+ if (files[entry] === void 0) throw new Error(`pack: ${spec.name} declares the ${surface} surface entry ${entry} but the zip does not carry it`);
162
+ if (entry === clientEntry) throw new Error(`pack: ${spec.name} points the ${surface} surface at the main client entry ${entry} — each surface needs its own bundle`);
163
+ }
164
+ const clientBundles = [clientEntry, ...Object.values(clientSurfaces)].filter((entry) => typeof entry === "string" && files[entry] !== void 0).map((entry) => new TextDecoder().decode(files[entry]));
165
+ const seedAbi = declaredClient && clientBundles.length > 0 ? await seedAbiStamp(spec.dir, clientBundles) : void 0;
150
166
  files["package.json"] = new TextEncoder().encode(releaseManifest(source, main, spec.exports, seedAbi));
151
167
  for (const [subpath, target] of Object.entries(spec.exports ?? {})) if (files[target] === void 0) throw new Error(`pack: ${spec.name} exports ${subpath} to ${target} but the zip does not carry it`);
152
168
  for (const path of Object.keys(files)) if (path.startsWith("src/")) throw new Error(`pack: ${spec.name} would ship a source file (${path})`);
package/lib/seeds.js CHANGED
@@ -39,6 +39,7 @@ const SEED_MODULES = [
39
39
  "@snap-rail/client-modules",
40
40
  "@snap-rail/connection",
41
41
  "@snap-rail/protocol",
42
+ "@snap-rail/shell/contract",
42
43
  "@snap-rail/station-rpc/contract",
43
44
  "@snap-rail/app-boot/contract",
44
45
  "@snap-rail/equipment/contract",
@@ -50,6 +50,21 @@ export interface ClientBundleOptions {
50
50
  * by the entry, so the bundle carries its own styling.
51
51
  */
52
52
  css?: readonly string[] | undefined;
53
+ /**
54
+ * The emitted file's base name inside `lib-client` (default `client`, so
55
+ * the file is `lib-client/client.js`). Auxiliary surfaces build one bundle
56
+ * per window and name them after the surface (`ball`); the
57
+ * manifest's `client.surfaces` points at those files.
58
+ */
59
+ outName?: string | undefined;
60
+ /**
61
+ * The module id the bundle registers under (default: the package name).
62
+ * A package's **main** surface registers as the package itself, so
63
+ * `createClientRuntime` can require it by row name; an auxiliary surface
64
+ * registers as `<package>/<surface>` — one package, several windows, no
65
+ * two bundles fighting over one id.
66
+ */
67
+ moduleId?: string | undefined;
53
68
  }
54
69
  /**
55
70
  * The client face: a CJS browser bundle welded into the module-loader
@@ -64,7 +79,13 @@ export interface ClientBundleOptions {
64
79
  * subpath to its module namespace, and the shell seeds
65
80
  * `<package>/<subpath>` ids from that map after requiring the face.
66
81
  *
67
- * @param packageName - the plugin's npm name (the module id it registers as).
82
+ * A package that renders in more than one window calls this once per surface:
83
+ * the main surface keeps the package name as its module id and `client` as its
84
+ * file name, while an auxiliary surface passes `outName` and `moduleId` so its
85
+ * bundle lands beside the main one (`lib-client/ball.js`) and registers as
86
+ * `<package>/ball` — the id that surface's window requires.
87
+ *
88
+ * @param packageName - the plugin's npm name (asset URLs and the default module id).
68
89
  * @param entry - the renderer entry source file.
69
90
  * @param options - see {@link ClientBundleOptions}.
70
91
  */
@@ -19,9 +19,24 @@ export declare const seedAbiSchema: z.ZodObject<{
19
19
  revision: z.ZodNumber;
20
20
  versions: z.ZodRecord<z.ZodString, z.ZodString>;
21
21
  }, z.core.$strict>;
22
+ /**
23
+ * The auxiliary-surface entry map: one client bundle per extra surface the
24
+ * package renders in. Keys are the auxiliary surface ids (`ball`) —
25
+ * `main` is deliberately absent, since the main window's bundle is
26
+ * `client.entry` and a package gets exactly one entry per window. Every
27
+ * declared path is a CJS factory bundle inside the package, packed and
28
+ * validated like the main entry; the surfaces a package does not declare
29
+ * simply have no face and show the kernel's plain fallback.
30
+ */
31
+ export declare const clientSurfacesSchema: z.ZodRecord<z.ZodEnum<{
32
+ ball: "ball";
33
+ }> & z.core.$partial, z.ZodString>;
22
34
  /** The renderer face declaration: where the client bundle lives. */
23
35
  export declare const clientFaceSchema: z.ZodObject<{
24
36
  entry: z.ZodDefault<z.ZodString>;
37
+ surfaces: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
38
+ ball: "ball";
39
+ }> & z.core.$partial, z.ZodString>>;
25
40
  seeds: z.ZodOptional<z.ZodArray<z.ZodString>>;
26
41
  externalSeeds: z.ZodOptional<z.ZodArray<z.ZodString>>;
27
42
  seedAbi: z.ZodOptional<z.ZodObject<{
@@ -52,6 +67,9 @@ export declare const snapRailManifestSchema: z.ZodObject<{
52
67
  clientOnly: z.ZodOptional<z.ZodBoolean>;
53
68
  client: z.ZodOptional<z.ZodObject<{
54
69
  entry: z.ZodDefault<z.ZodString>;
70
+ surfaces: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
71
+ ball: "ball";
72
+ }> & z.core.$partial, z.ZodString>>;
55
73
  seeds: z.ZodOptional<z.ZodArray<z.ZodString>>;
56
74
  externalSeeds: z.ZodOptional<z.ZodArray<z.ZodString>>;
57
75
  seedAbi: z.ZodOptional<z.ZodObject<{
@@ -20,6 +20,7 @@ interface SourceManifest {
20
20
  clientOnly?: unknown;
21
21
  client?: {
22
22
  entry?: unknown;
23
+ surfaces?: unknown;
23
24
  } | undefined;
24
25
  } & Record<string, unknown>;
25
26
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@snap-rail/plugin-kit",
3
3
  "description": "The plugin author's kit: snapRail manifest vocabulary, the renderer seed whitelist, tsdown presets for installable host + client faces, release signing, and the final-format packer",
4
- "version": "0.2.1",
4
+ "version": "0.2.2",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -58,6 +58,7 @@
58
58
  "dependencies": {
59
59
  "fflate": "^0.8.3",
60
60
  "zod": "^4.4.3",
61
+ "@snap-rail/protocol": "^0.1.3",
61
62
  "@snap-rail/util": "^0.1.2"
62
63
  },
63
64
  "peerDependencies": {