@happyvertical/smrt-core 0.55.10 → 0.55.12

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.
Files changed (87) hide show
  1. package/AGENTS.md +2 -0
  2. package/agents/build-knowledge.md +40 -0
  3. package/agents/cookbook-engine.md +102 -0
  4. package/agents/cookbook.md +74 -0
  5. package/dist/consumer-plugin/artifact-publication.js +3 -3
  6. package/dist/consumer-plugin/artifact-publication.js.map +1 -1
  7. package/dist/consumer-plugin/hosted-sti-collections.d.ts +10 -0
  8. package/dist/consumer-plugin/hosted-sti-collections.d.ts.map +1 -0
  9. package/dist/consumer-plugin/hosted-sti-collections.js +96 -0
  10. package/dist/consumer-plugin/hosted-sti-collections.js.map +1 -0
  11. package/dist/consumer-plugin/index.d.ts +1 -1
  12. package/dist/consumer-plugin/index.d.ts.map +1 -1
  13. package/dist/consumer-plugin/index.js +76 -51
  14. package/dist/consumer-plugin/index.js.map +1 -1
  15. package/dist/cookbook/engine/commands.d.ts +37 -0
  16. package/dist/cookbook/engine/commands.d.ts.map +1 -0
  17. package/dist/cookbook/engine/commands.js +275 -0
  18. package/dist/cookbook/engine/commands.js.map +1 -0
  19. package/dist/cookbook/engine/defaults.d.ts +10 -0
  20. package/dist/cookbook/engine/defaults.d.ts.map +1 -0
  21. package/dist/cookbook/engine/defaults.js +49 -0
  22. package/dist/cookbook/engine/defaults.js.map +1 -0
  23. package/dist/cookbook/engine/document.d.ts +66 -0
  24. package/dist/cookbook/engine/document.d.ts.map +1 -0
  25. package/dist/cookbook/engine/document.js +357 -0
  26. package/dist/cookbook/engine/document.js.map +1 -0
  27. package/dist/cookbook/engine/engine.d.ts +49 -0
  28. package/dist/cookbook/engine/engine.d.ts.map +1 -0
  29. package/dist/cookbook/engine/engine.js +399 -0
  30. package/dist/cookbook/engine/engine.js.map +1 -0
  31. package/dist/cookbook/engine/index.d.ts +14 -0
  32. package/dist/cookbook/engine/index.d.ts.map +1 -0
  33. package/dist/cookbook/engine/index.js +8 -0
  34. package/dist/cookbook/engine/nav.d.ts +45 -0
  35. package/dist/cookbook/engine/nav.d.ts.map +1 -0
  36. package/dist/cookbook/engine/nav.js +191 -0
  37. package/dist/cookbook/engine/nav.js.map +1 -0
  38. package/dist/cookbook/engine/schemas.d.ts +29 -0
  39. package/dist/cookbook/engine/schemas.d.ts.map +1 -0
  40. package/dist/cookbook/engine/schemas.js +363 -0
  41. package/dist/cookbook/engine/schemas.js.map +1 -0
  42. package/dist/cookbook/engine/types.d.ts +331 -0
  43. package/dist/cookbook/engine/types.d.ts.map +1 -0
  44. package/dist/cookbook/engine/types.js +10 -0
  45. package/dist/cookbook/engine/types.js.map +1 -0
  46. package/dist/cookbook/engine.d.ts +2 -0
  47. package/dist/cookbook/engine.d.ts.map +1 -0
  48. package/dist/cookbook/engine.js +9 -0
  49. package/dist/cookbook/index.d.ts +6 -0
  50. package/dist/cookbook/index.d.ts.map +1 -0
  51. package/dist/cookbook/v1.schema.json +131 -0
  52. package/dist/cookbook/validate.d.ts +37 -0
  53. package/dist/cookbook/validate.d.ts.map +1 -0
  54. package/dist/cookbook/validate.js +172 -0
  55. package/dist/cookbook/validate.js.map +1 -0
  56. package/dist/cookbook.d.ts +7 -0
  57. package/dist/cookbook.d.ts.map +1 -0
  58. package/dist/cookbook.js +2 -0
  59. package/dist/manifest/static-manifest.js +1 -1
  60. package/dist/manifest/static-manifest.js.map +1 -1
  61. package/dist/manifest/store.js +1 -1
  62. package/dist/manifest.json +1 -1
  63. package/dist/prebuild/index.js +13 -13
  64. package/dist/prebuild/index.js.map +1 -1
  65. package/dist/scanner/types.d.ts +8 -0
  66. package/dist/scanner/types.d.ts.map +1 -1
  67. package/dist/scanner/types.js.map +1 -1
  68. package/dist/smrt-knowledge.json +23 -7
  69. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  70. package/dist/vite-plugin/sveltekit-generator.js +4 -1
  71. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  72. package/dist/vite-plugin/sync-apply-route.d.ts.map +1 -1
  73. package/dist/vite-plugin/sync-apply-route.js +1 -0
  74. package/dist/vite-plugin/sync-apply-route.js.map +1 -1
  75. package/dist/vite-plugin/web-collections.d.ts +5 -0
  76. package/dist/vite-plugin/web-collections.d.ts.map +1 -1
  77. package/dist/vite-plugin/web-collections.js +1 -1
  78. package/dist/vite-plugin/web-collections.js.map +1 -1
  79. package/dist/vite-preset/cookbook-exposure.d.ts +11 -0
  80. package/dist/vite-preset/cookbook-exposure.d.ts.map +1 -0
  81. package/dist/vite-preset/cookbook-exposure.js +95 -0
  82. package/dist/vite-preset/cookbook-exposure.js.map +1 -0
  83. package/dist/vite-preset/index.d.ts +9 -0
  84. package/dist/vite-preset/index.d.ts.map +1 -1
  85. package/dist/vite-preset/index.js +17 -7
  86. package/dist/vite-preset/index.js.map +1 -1
  87. package/package.json +18 -6
package/AGENTS.md CHANGED
@@ -27,6 +27,8 @@ and repository rules.
27
27
  | `src/registry/qualified-name-aliases.ts`, `src/migrations/qualified-name-aliases.ts` | Model moves: `previousQualifiedNames` aliases, deprecation, doctor counts, opt-in backfill (#3338) | [agents/model-moves.md](agents/model-moves.md) |
28
28
  | `src/run-once.ts` | Shared idempotency seam: insert-only claim + caller work in one transaction, replay on retry, typed in-flight/unknown-outcome answers (#3080) | [agents/run-once.md](agents/run-once.md) |
29
29
  | `src/recipe.ts`, `../scanner/src/recipes.ts` | `SmrtRecipe`: declared user-facing units (models, nav, `requires`, curation `options`) collected into manifest and knowledge `recipes` (#3590) | [agents/recipes.md](agents/recipes.md) |
30
+ | `src/cookbook/` | Cookbook v1 document: browser-safe validate/migrate, JSON Schema (`./cookbook`, `./cookbook/v1.schema.json`; #3748) | [agents/cookbook.md](agents/cookbook.md) |
31
+ | `src/cookbook/engine/` | Headless cookbook command engine: typed commands, idempotent retries, atomic batches, receipts, tool schemas (`./cookbook/engine`; #3753) | [agents/cookbook-engine.md](agents/cookbook-engine.md) |
30
32
  | `src/browser.ts`, `src/host.ts`, `src/host.browser.ts` | Browser entry that builds and evaluates without Node-only modules: host seam, `package.json#browser` swaps, `pnpm test:browser` (#2838) | [agents/browser-entry.md](agents/browser-entry.md) |
31
33
 
32
34
  ## Cross-module invariants
@@ -183,6 +183,46 @@ also dedupes `@sveltejs/kit` (`smrt:shared-runtime`): SvelteKit matches
183
183
  `redirect()`/`fail()` by class, and a linked/workspace SMRT SvelteKit entry
184
184
  would otherwise import its own copy and turn redirects into 500s.
185
185
 
186
+ **Package models over REST (#3749).** `consumer.packages` registers a package's
187
+ models and gives `@smrt/web` (the consumer's `collectionDefinitions`, one per
188
+ list-capable model, endpoint `/<collection>`) their definitions, but a package
189
+ model gets generated REST routes only when it is on the consumer's hosting
190
+ allowlist (`smrtConsumer({ svelteKit: { objects } })`, #2850: an HTTP exposure
191
+ boundary, each model's own `api` config still applies, so `api: false` and
192
+ empty-include models stay closed, tenancy comes with the generated route). The
193
+ preset fills that allowlist: with `smrt.cookbook.json` present it is the models
194
+ of the cookbook's recipes plus `features`, minus models the cookbook `exposure`
195
+ turns off for `api` or a recipe's `options[model].exposure.api === false`
196
+ (`vite-preset/cookbook-exposure.ts`; recipe `{ exclude }` narrowing is not
197
+ applied by the build yet). `smrt({ expose })` replaces the derived list. No
198
+ cookbook and no `expose` keeps `svelteKit: true` (nothing hosted). The producer's
199
+ `@happyvertical/smrt-virt-web` still covers only the app's own models; read
200
+ package models from `@smrt/web`. `smrt({ objectsDir })` is passed to the hosting
201
+ contribution too: the producer and consumer share one `routesDir`, and the
202
+ generator refuses it when their settings differ.
203
+
204
+ **STI siblings (#3749).** Package STI subclasses share their base's table, and
205
+ the manifest gives them the base's `collection` unless they declare one
206
+ (`consumer-plugin/hosted-sti-collections.ts`). Two hosted siblings would both
207
+ claim `/<base collection>`, so each hosted subclass that inherits its
208
+ collection gets its class-derived one (`Order` -> `orders`,
209
+ `defaultCollectionName` in the scanner), in every manifest view the consumer
210
+ derives: routes, `@smrt/web`, the generated client and registration. This is the
211
+ convention the framework already has for an independent route collection
212
+ (`@smrt({ collection })`), applied by default for hosted subclasses; an explicit
213
+ collection wins. The route therefore reads through the subclass's own
214
+ collection (rows scoped to its `_meta_type`) and the definition for that model
215
+ advertises that route (`objectRef` = the model, `endpoint` = `/orders`). A hosted
216
+ base keeps the shared name and the base-collection semantics (every row of the
217
+ table, any subtype). A subclass left off the allowlist keeps the shared
218
+ collection and folds into the base's definition, as before. A collection class
219
+ that inherits another's item type (`MaterialCollection extends
220
+ ProductCollection`) resolves to the same item and collection, so only one of
221
+ them is hosted (the class named for the item, else the first). Subclasses that
222
+ declare no `api` config get the omitted-config default (full CRUD), not the
223
+ base's `include`; narrow them on the subclass if the base is meant to restrict
224
+ them.
225
+
186
226
  ```typescript
187
227
  // vite.config.ts — required for @smrt() decorators (Vite 8+, oxc transform)
188
228
  export default defineConfig({
@@ -0,0 +1,102 @@
1
+ # Cookbook command engine (#3753)
2
+
3
+ `@happyvertical/smrt-core/cookbook/engine` (`src/cookbook/engine/`) is the one
4
+ place the rules for changing a [cookbook](cookbook.md) live. A planner UI, a
5
+ local Kitchen MCP server, the CLI and a website all call it; each is an adapter.
6
+ It imports only relative modules and `@happyvertical/smrt-types` types, so it
7
+ runs in Node and in a browser. No SvelteKit, storage, singleton or inference.
8
+
9
+ It lives in `smrt-core` because the contract and validator (`./cookbook`) do and
10
+ the package DAG forbids a back edge: types -> core. A separate package would add
11
+ a node for pure functions with no other dependency.
12
+
13
+ ## API
14
+
15
+ ```ts
16
+ import { createCookbookEngine } from '@happyvertical/smrt-core/cookbook/engine';
17
+ const engine = createCookbookEngine({ catalog, cookbook?, state? });
18
+ engine.run({ id?, name, input, expectedRevision? }) // CommandResult, never throws
19
+ engine.batch({ id?, commands, expectedRevision? }) // BatchResult, atomic
20
+ engine.snapshot() / .cookbook() / .state() / .subscribe(fn)
21
+ engine.tools / engine.batchTool // JSON Schema tool definitions
22
+ ```
23
+
24
+ - **Catalog is data** (`EngineCatalog`): `recipes` (manifest `RecipeDefinition`
25
+ fits), `models` (`id, name, packageId, exposed?, fields?`), optional
26
+ `cookbooks` (a library), `themePresets`, `currencies`, `settingTargets`.
27
+ Nothing is bundled. Build it from published manifests or `smrt-knowledge.json`.
28
+ - **Commands** (20, `COOKBOOK_COMMANDS_VERSION = 1`): `apply_cookbook` (select),
29
+ `import_cookbook`, `add_cookbook`, `remove_cookbook`, `add_recipes`,
30
+ `remove_recipes`, `add_features`, `remove_features`, `set_settings`,
31
+ `set_policy`, `set_theme`, `reset_theme`, `set_name`, `rename_section`,
32
+ `rename_item`, `hide`, `show`, `validate`, `export_cookbook`, `undo`. Navigation
33
+ and focus are not commands: they are an adapter concern.
34
+ - **Errors** are typed (`CommandError`: `code`, `message`, `path?`, `details?`):
35
+ `unknown_command`, `invalid_input`, `not_found`, `confirmation_required`,
36
+ `conflict`, `id_reuse`, `batch_failed`, `failed`.
37
+ - **Result** carries the `receipt` (`name, id?, summary, changed, revisionBefore,
38
+ revisionAfter, undoId?, changes?, warnings?`), the compact `snapshot` and
39
+ optional `data` (`validate`, `export_cookbook`).
40
+
41
+ ## Execution contract
42
+
43
+ - **Revisions** count applied changes (a no-op does not move it), so equal
44
+ histories give equal revisions. `expectedRevision` that is not current fails
45
+ `conflict` with `details.{expectedRevision,currentRevision}`: an older model
46
+ action cannot overwrite a newer manual edit. Send it on every model action.
47
+ - **Command ids**: a repeated `id` with the same command returns the first
48
+ result with `replayed: true` and applies nothing (failures replay too: take a
49
+ new id after re-reading). The same id with a different command is `id_reuse`.
50
+ The journal keeps `maxJournal` (500) ids and is part of `state()`.
51
+ - **Atomic batches** run on a working copy; one failure leaves the state
52
+ byte-identical and returns `batch_failed` naming `details.index`. A batch that
53
+ changes n times advances the revision by n, as n runs would, and has one undo.
54
+ `undo` and per-command ids/revisions are not allowed inside a batch.
55
+ - **Undo** restores the document from before the change; it is a `conflict`
56
+ while the plan differs from what that change left, unless `force`.
57
+ - **Recovery**: `state()` is plain JSON (revision, applied library id, document,
58
+ journal); `createCookbookEngine({ catalog, state })` resumes it, retries
59
+ included. Undo entries are not persisted.
60
+ - **Canonical document**: ids sorted, policies sorted, empty sections absent,
61
+ key order fixed, so two engines export identical text. Recipe/feature ids the
62
+ catalog lacks stay in the document and show as `snapshot.unavailable`.
63
+
64
+ ## Deliberately not here
65
+
66
+ Package or starter version pinning (waits on #3604/#3754). Deep overview-widget
67
+ and nav-id checks (smrt-svelte owns them; `import_cookbook` carries `overviews`
68
+ and `layout` through unjudged, `validate` only flags layout ids the menu lacks).
69
+ Typed diagnostics from `validateCookbook` itself (#3754): `validate` wraps its
70
+ strings as `invalid_document` until it returns them.
71
+
72
+ ## Becoming an adapter: what smrt-planner changes (after the smrt release)
73
+
74
+ Parity to hold: the planner's `tests/headless-planner.test.ts` scenario is
75
+ ported in `__tests__/engine.test.ts` (`SCRIPT`) over a data catalog.
76
+
77
+ 1. Depend on the released `@happyvertical/smrt-core`; build `EngineCatalog` from
78
+ its catalog data (`recipes.json`/`catalog.json` -> `recipes`, `models`,
79
+ `cookbooks`: the library wrapper keeps id/icon/keywords planner-side).
80
+ 2. Delete `commands/execute.ts`, `commands/runner.ts`, `commands/snapshot.ts`,
81
+ `commands/schemas.ts` (keep `checkSchema` only if still used), `recipes/
82
+ plan-data.ts`, `recipes/resolve.ts`, `settings/app-settings.ts` rules,
83
+ `assistant/change.ts` apply* helpers, `cookbook/assemble.ts` and the
84
+ `createPlainHost`. `createHeadlessPlanner` becomes `createCookbookEngine`
85
+ behind the same `run/snapshot/subscribe/cookbook` names.
86
+ 3. The browser controller keeps: `focus` and the planner tab/section cells
87
+ (snapshot `focus` is added by the adapter), reactive wrapping of
88
+ `engine.subscribe`, `onReplaced` (regenerate sample data after
89
+ `apply_cookbook`), and the `cookbookStore` <-> `engine.state()` sync
90
+ (`engine.cookbook()` to persist; rebuild with `cookbook`/`state` on load).
91
+ 4. Command differences to absorb: `apply_cookbook`/`import_cookbook` report
92
+ `changed` by content; `rename_*`/`hide`/`show` need the catalog `models`
93
+ `packageId` for item ids; undo is whole-document (the old per-slice undo goes);
94
+ `unsupported` is gone (the engine always has every slice); new `set_name`,
95
+ `validate`; receipts gain `revisionBefore/After`; `expectedRevision` moves
96
+ into the command (the option form still works).
97
+ 5. Overview customisation stays a planner/smrt-svelte concern; hold `overviews`
98
+ in the document via `import_cookbook` and write them with a planner-side
99
+ command until an engine command is agreed.
100
+ 6. MCP/Kitchen (#3752, done): `smrt kitchen mcp` holds one engine per plan id and
101
+ serves `engine.batchTool`'s schema as `run_commands`; the plan reaches disk
102
+ as `smrt.cookbook.json` through `cookbook apply` (`cli/agents/kitchen.md`).
@@ -0,0 +1,74 @@
1
+ # Cookbook v1 contract (#3747, #3748)
2
+
3
+ A cookbook is the portable JSON document that says what an app is made of.
4
+ Defined once here so the CLI (`smrt cookbook validate|apply`) and the cookbook
5
+ app shell read the same thing.
6
+
7
+ | Piece | Where |
8
+ |---|---|
9
+ | Types (type-only) | `@happyvertical/smrt-types`: `Cookbook`, `CookbookPolicyRow`, `CookbookLayout`, `CookbookTheme`, `CookbookOverviewOverride`, `CookbookExposureSurface` |
10
+ | Parse / validate / migrate (browser-safe) | `@happyvertical/smrt-core/cookbook`: `validateCookbook(doc, { recipes? })`, `parseCookbookText`, `COOKBOOK_SCHEMA_URL`, `COOKBOOK_LEGACY_SCHEMA_URL`, `COOKBOOK_VERSION` |
11
+ | JSON Schema | `@happyvertical/smrt-core/cookbook/v1.schema.json` (`$id` is the `$schema` URL; s-m-r-t.dev hosts it later) |
12
+ | App shell | `@happyvertical/smrt-svelte/cookbook` renders a cookbook; `loadCookbookApp` in `/cookbook/server` loads and validates it ([cookbook-shell.md](../../smrt-svelte/agents/cookbook-shell.md)) |
13
+ | Fixtures | `src/cookbook/__fixtures__/` (the planner's bakery, mechanic, welder, yoga-studio documents) |
14
+
15
+ Fields: `$schema`, `version: 1`, optional `name`/`description`, `recipes`,
16
+ `features` (absent reads as `[]`), `policies` (app-scope smrt-fields rows),
17
+ `exposure`, `layout` (smrt-svelte `ShellLayout`), `theme`, `overviews`
18
+ (smrt-svelte `OverviewOverride` per overview id). Sample data is not part of
19
+ the document; the planner's library wrapper (id, icon, keywords, settings) is
20
+ planner-side.
21
+
22
+ ## Rules
23
+
24
+ - Versioning: add optional fields within `version: 1`; readers keep and ignore
25
+ unknown keys (`additionalProperties: true`). A breaking change is `version: 2`.
26
+ - Layout and overview values are typed structurally so smrt-types and core never
27
+ depend on smrt-svelte; smrt-svelte owns deep validation (widgets against page
28
+ definitions, nav ids). `CookbookLayout`/`CookbookOverviewOverride` are
29
+ identical to the smrt-svelte types; a typechecked assertion in smrt-svelte
30
+ (`cookbook/__tests__/contract-types.ts`) fails when either side adds a field.
31
+ - Legacy `.../blueprint/v1.json` is accepted on read and rewritten; never written.
32
+ - Recipe existence, `requires`/`requiresAny` and setting targets need manifests:
33
+ `validateCookbook(doc, { recipes })` checks ids when a catalog is passed;
34
+ the rest is the CLI's job.
35
+ - Keep the JSON Schema and `validate.ts` in step; the test validates the same
36
+ fixtures against both.
37
+
38
+ ## Applying a cookbook
39
+
40
+ `smrt cookbook validate <file|url>` and `smrt cookbook apply <file|url> [dir]`
41
+ (`packages/cli/agents/cookbook-apply.md`) are the consumers. Apply writes
42
+ `smrt.cookbook.json` at the project root, plus the packages the recipes,
43
+ features and policies need, into `package.json`. The file is the cookbook
44
+ verbatim (legacy `$schema` rewritten); apps read it, apply does not interpret it.
45
+ `policies` is required by the validator (use `[]`). The cookbook carries no package
46
+ versions, so apply uses the project's smrt line.
47
+
48
+ ## Hosting package models
49
+
50
+ An app that applies a cookbook lists the recipes' packages in `consumer.packages`
51
+ and nothing else: the `smrt()` Vite preset derives the REST allowlist from the
52
+ cookbook, so the Orders of a `commerce.sales` recipe get `/api/orders` routes and
53
+ an `@smrt/web` definition (see [build-knowledge.md](build-knowledge.md), "Package
54
+ models over REST"). `exposure` listing `api` for a model withdraws its route. Models that share an
55
+ STI table (the bakery's `Order`, `WholesaleOrder`, `PurchaseOrder` and
56
+ `ProductionOrder`; `Product` and `Material`) each get their own route and
57
+ definition, at `/<class collection>` (`/orders`, `/productionorders`), listing
58
+ only their own rows; the build-knowledge section explains why.
59
+
60
+ Permissions do not follow the route. The permission catalog grants one namespace
61
+ per STI table, the base's (`contracts.create`; there is no `orders.create`), so a
62
+ hosted subclass moved to its own route collection keeps authorizing against the
63
+ base's slugs: the hosting rewrite records the original collection as
64
+ `permissionCollection`, which the generated route guard and `sync/apply` read
65
+ before the (rewritten) route collection. It matches the fail-closed `api`
66
+ inheritance and the CLI/MCP operation guards, which already derive the base's
67
+ slug. No role is widened: a caller needs exactly the grants the base requires.
68
+ Reads need no operation slug. A subclass that declares its own `collection` has
69
+ its own namespace and is untouched.
70
+
71
+ ## Editing a cookbook
72
+
73
+ The command engine (`@happyvertical/smrt-core/cookbook/engine`) is the one place
74
+ that changes a cookbook: see [cookbook-engine.md](cookbook-engine.md).
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import * as fs from "node:fs";
2
+ import * as fs$1 from "node:fs";
3
3
  //#region src/consumer-plugin/artifact-publication.ts
4
4
  /**
5
5
  * Stage and publish a related artifact set with synchronous-failure recovery.
@@ -13,7 +13,7 @@ import * as fs from "node:fs";
13
13
  * There is no portable multi-file filesystem transaction: a process crash or a
14
14
  * concurrent reader between individual renames can still observe a mixed set.
15
15
  */
16
- function publishArtifactFiles(files, filesystem = fs) {
16
+ function publishArtifactFiles(files, filesystem = fs$1) {
17
17
  const unique = `${process.pid}-${Date.now()}`;
18
18
  const staged = [];
19
19
  const backed = [];
@@ -76,7 +76,7 @@ function publishArtifactFiles(files, filesystem = fs) {
76
76
  }
77
77
  }
78
78
  /** Atomically replace one artifact without ever removing its reader-visible path. */
79
- function publishAtomicArtifact(file, filesystem = fs) {
79
+ function publishAtomicArtifact(file, filesystem = fs$1) {
80
80
  const stagedPath = `${file.path}.smrt-${randomUUID()}.tmp`;
81
81
  const stats = filesystem.existsSync(file.path) ? filesystem.statSync(file.path) : void 0;
82
82
  if (stats && !stats.isFile()) throw new Error(`Artifact target is not a file: ${file.path}`);
@@ -1 +1 @@
1
- {"version":3,"file":"artifact-publication.js","names":[],"sources":["../../src/consumer-plugin/artifact-publication.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport * as fs from 'node:fs';\n\nexport interface ArtifactFile {\n path: string;\n content: string;\n}\n\nexport interface ArtifactFilesystem {\n existsSync: typeof fs.existsSync;\n renameSync: typeof fs.renameSync;\n statSync: typeof fs.statSync;\n unlinkSync: typeof fs.unlinkSync;\n writeFileSync: typeof fs.writeFileSync;\n chmodSync: typeof fs.chmodSync;\n}\n\ninterface StagedArtifact {\n path: string;\n stagedPath: string;\n backupPath?: string;\n}\n\n/**\n * Stage and publish a related artifact set with synchronous-failure recovery.\n *\n * Every existing target is first renamed to an adjacent backup, preserving its\n * mode, before a staged replacement is renamed into place. If a later rename\n * fails, rollback restores backups using rename only; it never truncates an\n * old target by rewriting it. A rollback failure retains its backup for manual\n * recovery and is included with the original publication failure.\n *\n * There is no portable multi-file filesystem transaction: a process crash or a\n * concurrent reader between individual renames can still observe a mixed set.\n */\nexport function publishArtifactFiles(\n files: ArtifactFile[],\n filesystem: ArtifactFilesystem = fs,\n): void {\n const unique = `${process.pid}-${Date.now()}`;\n const staged: StagedArtifact[] = [];\n const backed: StagedArtifact[] = [];\n const published: StagedArtifact[] = [];\n let retainBackups = false;\n\n try {\n for (const [index, file] of files.entries()) {\n const exists = filesystem.existsSync(file.path);\n const stats = exists ? filesystem.statSync(file.path) : undefined;\n if (stats && !stats.isFile()) {\n throw new Error(`Artifact target is not a file: ${file.path}`);\n }\n const mode = stats ? stats.mode & 0o777 : undefined;\n const stagedFile = {\n path: file.path,\n stagedPath: `${file.path}.smrt-${unique}-${index}.tmp`,\n backupPath: exists\n ? `${file.path}.smrt-${unique}-${index}.bak`\n : undefined,\n };\n // Register before the write: a failed write can still leave a partial\n // temporary file, which the finally cleanup must see.\n staged.push(stagedFile);\n filesystem.writeFileSync(stagedFile.stagedPath, file.content, 'utf-8');\n if (mode !== undefined) filesystem.chmodSync(stagedFile.stagedPath, mode);\n }\n for (const file of staged) {\n if (!file.backupPath) continue;\n filesystem.renameSync(file.path, file.backupPath);\n backed.push(file);\n }\n for (const file of staged) {\n filesystem.renameSync(file.stagedPath, file.path);\n published.push(file);\n }\n } catch (publicationError) {\n const rollbackErrors: unknown[] = [];\n for (const file of backed.reverse()) {\n try {\n if (!file.backupPath) {\n throw new Error(`Missing artifact backup: ${file.path}`);\n }\n // Replacing a published staged file is safe: its authoritative prior\n // contents are held by backupPath until this rename succeeds.\n filesystem.renameSync(file.backupPath, file.path);\n } catch (rollbackError) {\n rollbackErrors.push(rollbackError);\n }\n }\n for (const file of published) {\n if (file.backupPath) continue;\n try {\n if (filesystem.existsSync(file.path)) filesystem.unlinkSync(file.path);\n } catch (rollbackError) {\n rollbackErrors.push(rollbackError);\n }\n }\n if (rollbackErrors.length > 0) {\n retainBackups = true;\n throw new AggregateError(\n [publicationError, ...rollbackErrors],\n 'Artifact publication failed and rollback retained recovery backups',\n );\n }\n throw publicationError;\n } finally {\n for (const file of staged) {\n try {\n if (filesystem.existsSync(file.stagedPath))\n filesystem.unlinkSync(file.stagedPath);\n } catch {\n // A staging file is not an authoritative artifact.\n }\n if (!retainBackups) {\n try {\n if (file.backupPath && filesystem.existsSync(file.backupPath)) {\n filesystem.unlinkSync(file.backupPath);\n }\n } catch {\n // A failed cleanup leaves a recoverable backup behind.\n }\n }\n }\n }\n}\n\n/** Atomically replace one artifact without ever removing its reader-visible path. */\nexport function publishAtomicArtifact(\n file: ArtifactFile,\n filesystem: ArtifactFilesystem = fs,\n): void {\n const stagedPath = `${file.path}.smrt-${randomUUID()}.tmp`;\n const exists = filesystem.existsSync(file.path);\n const stats = exists ? filesystem.statSync(file.path) : undefined;\n if (stats && !stats.isFile()) {\n throw new Error(`Artifact target is not a file: ${file.path}`);\n }\n try {\n filesystem.writeFileSync(stagedPath, file.content, 'utf-8');\n if (stats) filesystem.chmodSync(stagedPath, stats.mode & 0o777);\n // POSIX rename replaces the destination atomically: readers see either\n // the old complete file or the new complete file, never no path/bytes.\n filesystem.renameSync(stagedPath, file.path);\n } finally {\n if (filesystem.existsSync(stagedPath)) filesystem.unlinkSync(stagedPath);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"artifact-publication.js","names":[],"sources":["../../src/consumer-plugin/artifact-publication.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport * as fs from 'node:fs';\n\nexport interface ArtifactFile {\n path: string;\n content: string;\n}\n\nexport interface ArtifactFilesystem {\n existsSync: typeof fs.existsSync;\n renameSync: typeof fs.renameSync;\n statSync: typeof fs.statSync;\n unlinkSync: typeof fs.unlinkSync;\n writeFileSync: typeof fs.writeFileSync;\n chmodSync: typeof fs.chmodSync;\n}\n\ninterface StagedArtifact {\n path: string;\n stagedPath: string;\n backupPath?: string;\n}\n\n/**\n * Stage and publish a related artifact set with synchronous-failure recovery.\n *\n * Every existing target is first renamed to an adjacent backup, preserving its\n * mode, before a staged replacement is renamed into place. If a later rename\n * fails, rollback restores backups using rename only; it never truncates an\n * old target by rewriting it. A rollback failure retains its backup for manual\n * recovery and is included with the original publication failure.\n *\n * There is no portable multi-file filesystem transaction: a process crash or a\n * concurrent reader between individual renames can still observe a mixed set.\n */\nexport function publishArtifactFiles(\n files: ArtifactFile[],\n filesystem: ArtifactFilesystem = fs,\n): void {\n const unique = `${process.pid}-${Date.now()}`;\n const staged: StagedArtifact[] = [];\n const backed: StagedArtifact[] = [];\n const published: StagedArtifact[] = [];\n let retainBackups = false;\n\n try {\n for (const [index, file] of files.entries()) {\n const exists = filesystem.existsSync(file.path);\n const stats = exists ? filesystem.statSync(file.path) : undefined;\n if (stats && !stats.isFile()) {\n throw new Error(`Artifact target is not a file: ${file.path}`);\n }\n const mode = stats ? stats.mode & 0o777 : undefined;\n const stagedFile = {\n path: file.path,\n stagedPath: `${file.path}.smrt-${unique}-${index}.tmp`,\n backupPath: exists\n ? `${file.path}.smrt-${unique}-${index}.bak`\n : undefined,\n };\n // Register before the write: a failed write can still leave a partial\n // temporary file, which the finally cleanup must see.\n staged.push(stagedFile);\n filesystem.writeFileSync(stagedFile.stagedPath, file.content, 'utf-8');\n if (mode !== undefined) filesystem.chmodSync(stagedFile.stagedPath, mode);\n }\n for (const file of staged) {\n if (!file.backupPath) continue;\n filesystem.renameSync(file.path, file.backupPath);\n backed.push(file);\n }\n for (const file of staged) {\n filesystem.renameSync(file.stagedPath, file.path);\n published.push(file);\n }\n } catch (publicationError) {\n const rollbackErrors: unknown[] = [];\n for (const file of backed.reverse()) {\n try {\n if (!file.backupPath) {\n throw new Error(`Missing artifact backup: ${file.path}`);\n }\n // Replacing a published staged file is safe: its authoritative prior\n // contents are held by backupPath until this rename succeeds.\n filesystem.renameSync(file.backupPath, file.path);\n } catch (rollbackError) {\n rollbackErrors.push(rollbackError);\n }\n }\n for (const file of published) {\n if (file.backupPath) continue;\n try {\n if (filesystem.existsSync(file.path)) filesystem.unlinkSync(file.path);\n } catch (rollbackError) {\n rollbackErrors.push(rollbackError);\n }\n }\n if (rollbackErrors.length > 0) {\n retainBackups = true;\n throw new AggregateError(\n [publicationError, ...rollbackErrors],\n 'Artifact publication failed and rollback retained recovery backups',\n );\n }\n throw publicationError;\n } finally {\n for (const file of staged) {\n try {\n if (filesystem.existsSync(file.stagedPath))\n filesystem.unlinkSync(file.stagedPath);\n } catch {\n // A staging file is not an authoritative artifact.\n }\n if (!retainBackups) {\n try {\n if (file.backupPath && filesystem.existsSync(file.backupPath)) {\n filesystem.unlinkSync(file.backupPath);\n }\n } catch {\n // A failed cleanup leaves a recoverable backup behind.\n }\n }\n }\n }\n}\n\n/** Atomically replace one artifact without ever removing its reader-visible path. */\nexport function publishAtomicArtifact(\n file: ArtifactFile,\n filesystem: ArtifactFilesystem = fs,\n): void {\n const stagedPath = `${file.path}.smrt-${randomUUID()}.tmp`;\n const exists = filesystem.existsSync(file.path);\n const stats = exists ? filesystem.statSync(file.path) : undefined;\n if (stats && !stats.isFile()) {\n throw new Error(`Artifact target is not a file: ${file.path}`);\n }\n try {\n filesystem.writeFileSync(stagedPath, file.content, 'utf-8');\n if (stats) filesystem.chmodSync(stagedPath, stats.mode & 0o777);\n // POSIX rename replaces the destination atomically: readers see either\n // the old complete file or the new complete file, never no path/bytes.\n filesystem.renameSync(stagedPath, file.path);\n } finally {\n if (filesystem.existsSync(stagedPath)) filesystem.unlinkSync(stagedPath);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { SmartObjectDefinition } from '../scanner/types.js';
2
+ /**
3
+ * @param manifest - The aggregated manifest of every consumed package.
4
+ * @param hosted - Provider-qualified refs the consumer hosts.
5
+ * @param refOf - The qualified ref of a manifest entry.
6
+ * @returns The manifest with each hosted, collection-inheriting STI subclass
7
+ * moved to its own collection; `manifest` itself when none needs it.
8
+ */
9
+ export declare function separateHostedStiCollections<M extends object>(manifest: M, hosted: readonly string[], refOf: (key: string, def: SmartObjectDefinition) => string | undefined): M;
10
+ //# sourceMappingURL=hosted-sti-collections.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hosted-sti-collections.d.ts","sourceRoot":"","sources":["../../src/consumer-plugin/hosted-sti-collections.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EACV,qBAAqB,EAEtB,MAAM,qBAAqB,CAAC;AAyC7B;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,CAAC,SAAS,MAAM,EAC3D,QAAQ,EAAE,CAAC,EACX,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,qBAAqB,KAAK,MAAM,GAAG,SAAS,GACrE,CAAC,CAqDH"}
@@ -0,0 +1,96 @@
1
+ import { findManifestObjectByName, isCollectionManifestClass, isStiChildModel, resolveCollectionItemObject } from "../vite-plugin/web-collections.js";
2
+ import { defaultCollectionName } from "@happyvertical/smrt-scanner";
3
+ //#region src/consumer-plugin/hosted-sti-collections.ts
4
+ /**
5
+ * Own route collections for hosted STI subclasses (#3749).
6
+ *
7
+ * An STI subclass shares its base's table, and the manifest generator also
8
+ * gives it the base's `collection` unless it declares one
9
+ * (`@smrt({ collection })`, "collection controls routes/permissions
10
+ * independently of shared storage"). Hosting `Order` and `ProductionOrder`
11
+ * beside each other would therefore claim `/contracts` twice, and one handler
12
+ * would silently shadow the other.
13
+ *
14
+ * A hosted subclass that has not declared a collection gets its class-derived
15
+ * one (`orders`), the name it would have had as a plain model. Its generated
16
+ * route then lists and reads through its own collection, which already scopes
17
+ * every read to the subclass `_meta_type`; permissions do not move with it (the
18
+ * catalog grants the STI base's `contracts.*` slugs only, #3770), so the moved
19
+ * definition records the original collection as `permissionCollection`; the base keeps the shared name and
20
+ * the base-collection semantics (every row of the table). The same rewritten
21
+ * manifest feeds the `@smrt/web` definitions and the generated client, so each
22
+ * hosted class advertises exactly the endpoint its route serves.
23
+ */
24
+ /**
25
+ * The `api` config of the nearest STI ancestor that declares one. An omitted
26
+ * `api` means full CRUD, so a hosted subclass that says nothing would expose
27
+ * `DELETE /orders/:id` even though its base (`Contract`) allows only
28
+ * list/get/create/update. Hosting fails closed: the subclass inherits the base's
29
+ * exposure unless it declares its own (hosting path only; the framework-wide
30
+ * default is unchanged).
31
+ */
32
+ function inheritedApi(manifest, obj) {
33
+ const seen = /* @__PURE__ */ new Set();
34
+ let child = obj;
35
+ let parentName = child.extendsQualified || child.extends;
36
+ while (parentName && !seen.has(parentName)) {
37
+ seen.add(parentName);
38
+ const parent = findManifestObjectByName(manifest, parentName, child);
39
+ if (!parent) return void 0;
40
+ if (!(parent.collection === obj.collection || parent.decoratorConfig?.tableStrategy === "sti")) return void 0;
41
+ const api = parent.decoratorConfig?.api;
42
+ if (api !== void 0) return api;
43
+ child = parent;
44
+ parentName = parent.extendsQualified || parent.extends;
45
+ }
46
+ }
47
+ /**
48
+ * @param manifest - The aggregated manifest of every consumed package.
49
+ * @param hosted - Provider-qualified refs the consumer hosts.
50
+ * @param refOf - The qualified ref of a manifest entry.
51
+ * @returns The manifest with each hosted, collection-inheriting STI subclass
52
+ * moved to its own collection; `manifest` itself when none needs it.
53
+ */
54
+ function separateHostedStiCollections(manifest, hosted, refOf) {
55
+ const source = manifest;
56
+ const wanted = new Set(hosted);
57
+ let objects;
58
+ const moved = /* @__PURE__ */ new Map();
59
+ for (const [key, def] of Object.entries(source.objects)) {
60
+ const ref = refOf(key, def);
61
+ if (!ref || !wanted.has(ref)) continue;
62
+ const collection = !def.decoratorConfig?.collection && isStiChildModel(source, def) ? defaultCollectionName(def.className) : def.collection;
63
+ const api = def.decoratorConfig?.api === void 0 ? inheritedApi(source, def) : void 0;
64
+ if (collection === def.collection && api === void 0) continue;
65
+ if (collection !== def.collection) moved.set(def, collection);
66
+ objects ??= { ...source.objects };
67
+ objects[key] = {
68
+ ...def,
69
+ collection,
70
+ ...collection === def.collection ? {} : { permissionCollection: def.collection },
71
+ ...api === void 0 ? {} : { decoratorConfig: {
72
+ ...def.decoratorConfig,
73
+ api
74
+ } }
75
+ };
76
+ }
77
+ if (!objects) return manifest;
78
+ for (const [key, def] of Object.entries(source.objects)) {
79
+ if (!isCollectionManifestClass(source, def)) continue;
80
+ const item = resolveCollectionItemObject(source, def);
81
+ const collection = item ? moved.get(item) : void 0;
82
+ if (collection && def.collection !== collection) objects[key] = {
83
+ ...def,
84
+ collection,
85
+ permissionCollection: def.collection
86
+ };
87
+ }
88
+ return {
89
+ ...manifest,
90
+ objects
91
+ };
92
+ }
93
+ //#endregion
94
+ export { separateHostedStiCollections };
95
+
96
+ //# sourceMappingURL=hosted-sti-collections.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hosted-sti-collections.js","names":[],"sources":["../../src/consumer-plugin/hosted-sti-collections.ts"],"sourcesContent":["/**\n * Own route collections for hosted STI subclasses (#3749).\n *\n * An STI subclass shares its base's table, and the manifest generator also\n * gives it the base's `collection` unless it declares one\n * (`@smrt({ collection })`, \"collection controls routes/permissions\n * independently of shared storage\"). Hosting `Order` and `ProductionOrder`\n * beside each other would therefore claim `/contracts` twice, and one handler\n * would silently shadow the other.\n *\n * A hosted subclass that has not declared a collection gets its class-derived\n * one (`orders`), the name it would have had as a plain model. Its generated\n * route then lists and reads through its own collection, which already scopes\n * every read to the subclass `_meta_type`; permissions do not move with it (the\n * catalog grants the STI base's `contracts.*` slugs only, #3770), so the moved\n * definition records the original collection as `permissionCollection`; the base keeps the shared name and\n * the base-collection semantics (every row of the table). The same rewritten\n * manifest feeds the `@smrt/web` definitions and the generated client, so each\n * hosted class advertises exactly the endpoint its route serves.\n */\nimport { defaultCollectionName } from '@happyvertical/smrt-scanner';\nimport type {\n SmartObjectDefinition,\n SmartObjectManifest,\n} from '../scanner/types.js';\nimport {\n findManifestObjectByName,\n isCollectionManifestClass,\n isStiChildModel,\n resolveCollectionItemObject,\n} from '../vite-plugin/web-collections.js';\n\n/**\n * The `api` config of the nearest STI ancestor that declares one. An omitted\n * `api` means full CRUD, so a hosted subclass that says nothing would expose\n * `DELETE /orders/:id` even though its base (`Contract`) allows only\n * list/get/create/update. Hosting fails closed: the subclass inherits the base's\n * exposure unless it declares its own (hosting path only; the framework-wide\n * default is unchanged).\n */\nfunction inheritedApi(\n manifest: SmartObjectManifest,\n obj: SmartObjectDefinition,\n): unknown {\n const seen = new Set<string>();\n let child = obj;\n let parentName = child.extendsQualified || child.extends;\n while (parentName && !seen.has(parentName)) {\n seen.add(parentName);\n const parent = findManifestObjectByName(manifest, parentName, child);\n if (!parent) return undefined;\n // Only an STI relative lends its exposure: it shares the table (and so the\n // inherited collection) or is the declared STI base.\n const sti =\n parent.collection === obj.collection ||\n parent.decoratorConfig?.tableStrategy === 'sti';\n if (!sti) return undefined;\n const api = parent.decoratorConfig?.api;\n if (api !== undefined) return api;\n child = parent;\n parentName = parent.extendsQualified || parent.extends;\n }\n return undefined;\n}\n\n/**\n * @param manifest - The aggregated manifest of every consumed package.\n * @param hosted - Provider-qualified refs the consumer hosts.\n * @param refOf - The qualified ref of a manifest entry.\n * @returns The manifest with each hosted, collection-inheriting STI subclass\n * moved to its own collection; `manifest` itself when none needs it.\n */\nexport function separateHostedStiCollections<M extends object>(\n manifest: M,\n hosted: readonly string[],\n refOf: (key: string, def: SmartObjectDefinition) => string | undefined,\n): M {\n const source = manifest as unknown as SmartObjectManifest;\n const wanted = new Set(hosted);\n let objects: SmartObjectManifest['objects'] | undefined;\n const moved = new Map<SmartObjectDefinition, string>();\n for (const [key, def] of Object.entries(source.objects)) {\n const ref = refOf(key, def);\n if (!ref || !wanted.has(ref)) continue;\n // An explicit `collection` is already independent; only an inherited one\n // collides.\n const separable =\n !def.decoratorConfig?.collection && isStiChildModel(source, def);\n const collection = separable\n ? defaultCollectionName(def.className)\n : def.collection;\n const api =\n def.decoratorConfig?.api === undefined\n ? inheritedApi(source, def)\n : undefined;\n if (collection === def.collection && api === undefined) continue;\n if (collection !== def.collection) moved.set(def, collection);\n objects ??= { ...source.objects };\n objects[key] = {\n ...def,\n collection,\n // The catalog keeps granting the STI base's slugs; the route must keep\n // requiring them (#3770).\n ...(collection === def.collection\n ? {}\n : { permissionCollection: def.collection }),\n ...(api === undefined\n ? {}\n : { decoratorConfig: { ...def.decoratorConfig, api } }),\n } as SmartObjectDefinition;\n }\n if (!objects) return manifest;\n\n // A collection class follows its item class (the manifest generator gives it\n // the item's collection); left behind it would keep the shared route and\n // collide with its siblings' collection classes on custom-action routes.\n for (const [key, def] of Object.entries(source.objects)) {\n if (!isCollectionManifestClass(source, def)) continue;\n const item = resolveCollectionItemObject(source, def);\n const collection = item ? moved.get(item) : undefined;\n if (collection && def.collection !== collection) {\n objects[key] = {\n ...def,\n collection,\n permissionCollection: def.collection,\n } as SmartObjectDefinition;\n }\n }\n return { ...manifest, objects } as M;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,SAAS,aACP,UACA,KACS;CACT,MAAM,uBAAO,IAAI,IAAY;CAC7B,IAAI,QAAQ;CACZ,IAAI,aAAa,MAAM,oBAAoB,MAAM;CACjD,OAAO,cAAc,CAAC,KAAK,IAAI,UAAU,GAAG;EAC1C,KAAK,IAAI,UAAU;EACnB,MAAM,SAAS,yBAAyB,UAAU,YAAY,KAAK;EACnE,IAAI,CAAC,QAAQ,OAAO,KAAA;EAMpB,IAAI,EAFF,OAAO,eAAe,IAAI,cAC1B,OAAO,iBAAiB,kBAAkB,QAClC,OAAO,KAAA;EACjB,MAAM,MAAM,OAAO,iBAAiB;EACpC,IAAI,QAAQ,KAAA,GAAW,OAAO;EAC9B,QAAQ;EACR,aAAa,OAAO,oBAAoB,OAAO;CACjD;AAEF;;;;;;;;AASA,SAAgB,6BACd,UACA,QACA,OACG;CACH,MAAM,SAAS;CACf,MAAM,SAAS,IAAI,IAAI,MAAM;CAC7B,IAAI;CACJ,MAAM,wBAAQ,IAAI,IAAmC;CACrD,KAAK,MAAM,CAAC,KAAK,QAAQ,OAAO,QAAQ,OAAO,OAAO,GAAG;EACvD,MAAM,MAAM,MAAM,KAAK,GAAG;EAC1B,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,GAAG,GAAG;EAK9B,MAAM,aADJ,CAAC,IAAI,iBAAiB,cAAc,gBAAgB,QAAQ,GAAG,IAE7D,sBAAsB,IAAI,SAAS,IACnC,IAAI;EACR,MAAM,MACJ,IAAI,iBAAiB,QAAQ,KAAA,IACzB,aAAa,QAAQ,GAAG,IACxB,KAAA;EACN,IAAI,eAAe,IAAI,cAAc,QAAQ,KAAA,GAAW;EACxD,IAAI,eAAe,IAAI,YAAY,MAAM,IAAI,KAAK,UAAU;EAC5D,YAAY,EAAE,GAAG,OAAO,QAAQ;EAChC,QAAQ,OAAO;GACb,GAAG;GACH;GAGA,GAAI,eAAe,IAAI,aACnB,CAAC,IACD,EAAE,sBAAsB,IAAI,WAAW;GAC3C,GAAI,QAAQ,KAAA,IACR,CAAC,IACD,EAAE,iBAAiB;IAAE,GAAG,IAAI;IAAiB;GAAI,EAAE;EACzD;CACF;CACA,IAAI,CAAC,SAAS,OAAO;CAKrB,KAAK,MAAM,CAAC,KAAK,QAAQ,OAAO,QAAQ,OAAO,OAAO,GAAG;EACvD,IAAI,CAAC,0BAA0B,QAAQ,GAAG,GAAG;EAC7C,MAAM,OAAO,4BAA4B,QAAQ,GAAG;EACpD,MAAM,aAAa,OAAO,MAAM,IAAI,IAAI,IAAI,KAAA;EAC5C,IAAI,cAAc,IAAI,eAAe,YACnC,QAAQ,OAAO;GACb,GAAG;GACH;GACA,sBAAsB,IAAI;EAC5B;CAEJ;CACA,OAAO;EAAE,GAAG;EAAU;CAAQ;AAChC"}
@@ -11,7 +11,7 @@ export { loadVerifiedSmrtGenerationSnapshot, type SerializeSmrtGenerationSnapsho
11
11
  * `objects` is an HTTP exposure boundary: every entry must be an exact,
12
12
  * provider-qualified manifest key (for example, `@acme/widgets:Widget`).
13
13
  */
14
- export interface SmrtConsumerSvelteKitOptions extends Partial<Pick<SvelteKitOptions, 'routesDir' | 'configPath' | 'configFileName' | 'kebabRoutes' | 'changesRoute' | 'eventsRoute' | 'resourcesRoute'>> {
14
+ export interface SmrtConsumerSvelteKitOptions extends Partial<Pick<SvelteKitOptions, 'routesDir' | 'objectsDir' | 'configPath' | 'configFileName' | 'kebabRoutes' | 'changesRoute' | 'eventsRoute' | 'resourcesRoute'>> {
15
15
  objects: readonly string[];
16
16
  }
17
17
  export interface SmrtConsumerOptions {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/consumer-plugin/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EAAa,MAAM,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,EAEL,KAAK,6BAA6B,EACnC,MAAM,2BAA2B,CAAC;AAYnC,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,uCAAuC,CAAC;AAoB/C,OAAO,EACL,kCAAkC,EAClC,KAAK,sCAAsC,EAC3C,KAAK,8BAA8B,EACnC,KAAK,6BAA6B,EAClC,KAAK,0BAA0B,EAC/B,+BAA+B,EAC/B,4BAA4B,GAC7B,MAAM,2BAA2B,CAAC;AAqDnC;;;;GAIG;AACH,MAAM,WAAW,4BACf,SAAQ,OAAO,CACb,IAAI,CACF,gBAAgB,EACd,WAAW,GACX,YAAY,GACZ,gBAAgB,GAChB,aAAa,GACb,cAAc,GACd,aAAa,GACb,gBAAgB,CACnB,CACF;IACD,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,uCAAuC;IACvC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,6BAA6B,CAAC;IACnD;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,4BAA4B,CAAC;IACnD;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,oDAAoD;IACpD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,4BAA4B;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AA2VD;;GAEG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,MAAM,CA2WtE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/consumer-plugin/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EAAa,MAAM,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,EAEL,KAAK,6BAA6B,EACnC,MAAM,2BAA2B,CAAC;AAYnC,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,uCAAuC,CAAC;AAqB/C,OAAO,EACL,kCAAkC,EAClC,KAAK,sCAAsC,EAC3C,KAAK,8BAA8B,EACnC,KAAK,6BAA6B,EAClC,KAAK,0BAA0B,EAC/B,+BAA+B,EAC/B,4BAA4B,GAC7B,MAAM,2BAA2B,CAAC;AAqDnC;;;;GAIG;AACH,MAAM,WAAW,4BACf,SAAQ,OAAO,CACb,IAAI,CACF,gBAAgB,EACd,WAAW,GACX,YAAY,GACZ,YAAY,GACZ,gBAAgB,GAChB,aAAa,GACb,cAAc,GACd,aAAa,GACb,gBAAgB,CACnB,CACF;IACD,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,uCAAuC;IACvC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,6BAA6B,CAAC;IACnD;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,4BAA4B,CAAC;IACnD;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,oDAAoD;IACpD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,4BAA4B;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAmXD;;GAEG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,MAAM,CAoYtE"}