@milaboratories/pl-middle-layer 1.66.18 → 1.67.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/dist/block_registry/index.cjs +3 -0
  2. package/dist/block_registry/index.d.ts +2 -1
  3. package/dist/block_registry/index.js +2 -1
  4. package/dist/block_registry/location_provider.cjs +117 -0
  5. package/dist/block_registry/location_provider.cjs.map +1 -0
  6. package/dist/block_registry/location_provider.js +114 -0
  7. package/dist/block_registry/location_provider.js.map +1 -0
  8. package/dist/block_registry/registry.cjs +13 -0
  9. package/dist/block_registry/registry.cjs.map +1 -1
  10. package/dist/block_registry/registry.d.ts +14 -1
  11. package/dist/block_registry/registry.d.ts.map +1 -1
  12. package/dist/block_registry/registry.js +13 -0
  13. package/dist/block_registry/registry.js.map +1 -1
  14. package/dist/block_registry/template_provider.cjs +104 -0
  15. package/dist/block_registry/template_provider.cjs.map +1 -0
  16. package/dist/block_registry/template_provider.d.ts +62 -0
  17. package/dist/block_registry/template_provider.d.ts.map +1 -0
  18. package/dist/block_registry/template_provider.js +103 -0
  19. package/dist/block_registry/template_provider.js.map +1 -0
  20. package/dist/block_registry/watcher.cjs +3 -3
  21. package/dist/block_registry/watcher.js +2 -2
  22. package/dist/dev_env/util.js +2 -2
  23. package/dist/dev_env/util.js.map +1 -1
  24. package/dist/index.cjs +10 -0
  25. package/dist/index.d.ts +5 -1
  26. package/dist/index.js +5 -1
  27. package/dist/js_render/computable_context.cjs +1 -1
  28. package/dist/js_render/computable_context.js +1 -1
  29. package/dist/middle_layer/driver_kit.d.ts +1 -1
  30. package/dist/middle_layer/middle_layer.cjs +144 -2
  31. package/dist/middle_layer/middle_layer.cjs.map +1 -1
  32. package/dist/middle_layer/middle_layer.d.ts +67 -1
  33. package/dist/middle_layer/middle_layer.d.ts.map +1 -1
  34. package/dist/middle_layer/middle_layer.js +146 -4
  35. package/dist/middle_layer/middle_layer.js.map +1 -1
  36. package/dist/middle_layer/project.cjs +1 -0
  37. package/dist/middle_layer/project.cjs.map +1 -1
  38. package/dist/middle_layer/project.d.ts +2 -2
  39. package/dist/middle_layer/project.js +4 -4
  40. package/dist/middle_layer/project.js.map +1 -1
  41. package/dist/middle_layer/sharing_list.d.ts +1 -1
  42. package/dist/model/args.cjs +9 -28
  43. package/dist/model/args.cjs.map +1 -1
  44. package/dist/model/args.js +9 -28
  45. package/dist/model/args.js.map +1 -1
  46. package/dist/model/index.cjs +7 -0
  47. package/dist/model/index.d.ts +4 -1
  48. package/dist/model/index.js +4 -1
  49. package/dist/model/project_helper.cjs +111 -0
  50. package/dist/model/project_helper.cjs.map +1 -1
  51. package/dist/model/project_helper.d.ts +76 -1
  52. package/dist/model/project_helper.d.ts.map +1 -1
  53. package/dist/model/project_helper.js +111 -0
  54. package/dist/model/project_helper.js.map +1 -1
  55. package/dist/model/project_model.d.ts +1 -1
  56. package/dist/model/sharing_model.d.ts +1 -1
  57. package/dist/model/template_apply.cjs +67 -0
  58. package/dist/model/template_apply.cjs.map +1 -0
  59. package/dist/model/template_apply.d.ts +50 -0
  60. package/dist/model/template_apply.d.ts.map +1 -0
  61. package/dist/model/template_apply.js +64 -0
  62. package/dist/model/template_apply.js.map +1 -0
  63. package/dist/model/template_export.cjs +82 -0
  64. package/dist/model/template_export.cjs.map +1 -0
  65. package/dist/model/template_export.d.ts +9 -0
  66. package/dist/model/template_export.d.ts.map +1 -0
  67. package/dist/model/template_export.js +82 -0
  68. package/dist/model/template_export.js.map +1 -0
  69. package/dist/model/template_parser.cjs +74 -0
  70. package/dist/model/template_parser.cjs.map +1 -0
  71. package/dist/model/template_parser.d.ts +43 -0
  72. package/dist/model/template_parser.d.ts.map +1 -0
  73. package/dist/model/template_parser.js +72 -0
  74. package/dist/model/template_parser.js.map +1 -0
  75. package/dist/model/template_resolve.cjs +134 -0
  76. package/dist/model/template_resolve.cjs.map +1 -0
  77. package/dist/model/template_resolve.d.ts +193 -0
  78. package/dist/model/template_resolve.d.ts.map +1 -0
  79. package/dist/model/template_resolve.js +133 -0
  80. package/dist/model/template_resolve.js.map +1 -0
  81. package/dist/model/template_serializer.cjs +162 -0
  82. package/dist/model/template_serializer.cjs.map +1 -0
  83. package/dist/model/template_serializer.d.ts +17 -0
  84. package/dist/model/template_serializer.d.ts.map +1 -0
  85. package/dist/model/template_serializer.js +157 -0
  86. package/dist/model/template_serializer.js.map +1 -0
  87. package/dist/mutator/block-pack/block_pack.cjs +1 -1
  88. package/dist/mutator/block-pack/block_pack.js +1 -1
  89. package/dist/mutator/migration.cjs +1 -1
  90. package/dist/mutator/migration.js +1 -1
  91. package/dist/mutator/project.cjs +35 -5
  92. package/dist/mutator/project.cjs.map +1 -1
  93. package/dist/mutator/project.d.ts.map +1 -1
  94. package/dist/mutator/project.js +35 -5
  95. package/dist/mutator/project.js.map +1 -1
  96. package/dist/mutator/template_construct.cjs +77 -0
  97. package/dist/mutator/template_construct.cjs.map +1 -0
  98. package/dist/mutator/template_construct.js +77 -0
  99. package/dist/mutator/template_construct.js.map +1 -0
  100. package/dist/network_check/template.js +3 -3
  101. package/dist/network_check/template.js.map +1 -1
  102. package/package.json +17 -17
  103. package/src/block_registry/index.ts +1 -0
  104. package/src/block_registry/location_provider.test.ts +236 -0
  105. package/src/block_registry/location_provider.ts +124 -0
  106. package/src/block_registry/registry.ts +22 -0
  107. package/src/block_registry/template_provider.test.ts +275 -0
  108. package/src/block_registry/template_provider.ts +168 -0
  109. package/src/middle_layer/middle_layer.ts +192 -1
  110. package/src/middle_layer/project.ts +1 -1
  111. package/src/model/args.test.ts +26 -1
  112. package/src/model/args.ts +16 -31
  113. package/src/model/index.ts +21 -0
  114. package/src/model/project_helper.ts +238 -0
  115. package/src/model/project_helper_params_init.test.ts +249 -0
  116. package/src/model/template_apply.test.ts +65 -0
  117. package/src/model/template_apply.ts +128 -0
  118. package/src/model/template_export.test.ts +255 -0
  119. package/src/model/template_export.ts +136 -0
  120. package/src/model/template_parser.test.ts +264 -0
  121. package/src/model/template_parser.ts +108 -0
  122. package/src/model/template_resolve.test.ts +397 -0
  123. package/src/model/template_resolve.ts +346 -0
  124. package/src/model/template_serializer.test.ts +398 -0
  125. package/src/model/template_serializer.ts +232 -0
  126. package/src/model/template_serializer_fixtures.test.ts +274 -0
  127. package/src/mutator/project-v3.test.ts +11 -11
  128. package/src/mutator/project.ts +92 -5
  129. package/src/mutator/template_construct.test.ts +456 -0
  130. package/src/mutator/template_construct.ts +150 -0
@@ -0,0 +1,346 @@
1
+ import type {
2
+ BlockKindSelectorReference,
3
+ BlockPackLocationReference,
4
+ ProjectTemplateV1,
5
+ ProjectTemplateV1Entry,
6
+ } from "@milaboratories/pl-model-common";
7
+ import {
8
+ parseBlockPackReference,
9
+ parseKindSelectorReference,
10
+ } from "@milaboratories/pl-model-common";
11
+ import type { BlockPackId, BlockPackSpec } from "@milaboratories/pl-model-middle-layer";
12
+ import type { TemplateApplyProblem } from "./template_apply";
13
+
14
+ /**
15
+ * Where a template entry's block comes from.
16
+ *
17
+ * All three of an entry's paths to an implementation go through this one interface, and
18
+ * all three come back as the same `BlockPackSpec` — the shape the existing add-block
19
+ * path already consumes — plus the block's published title. That convergence is the
20
+ * point: everything downstream of resolution treats a kind-resolved entry, a
21
+ * version-pinned one and a filesystem-located one identically, which is why a locally
22
+ * built block travels the same preparation and construction path as a published one.
23
+ *
24
+ * Registry knowledge sits deliberately on the far side of this interface. A template
25
+ * file names no registry, so choosing which one (or ones) to consult is a property
26
+ * of the environment applying the file, not of the file — and keeping it out here is
27
+ * what lets resolution be tested with no registry at all.
28
+ *
29
+ * The title is required rather than optional for the same reason: an implementation
30
+ * always has one to hand — a registry adapter reads it off the block's manifest, and
31
+ * anything else names the blocks it serves — while a caller downstream has no source
32
+ * for it at all, and the fallbacks it could invent are all wrong. See
33
+ * {@link ResolvedEntry}'s `title`.
34
+ */
35
+ export type BlockPackProvider = {
36
+ /**
37
+ * Find the block implementing a kind selector.
38
+ *
39
+ * @param kind The entry's `{name}@{selector}` reference
40
+ * @param options `allowUnstable` widens the search from stable implementations to
41
+ * every published one, for the whole apply
42
+ */
43
+ byKind: (
44
+ kind: BlockKindSelectorReference,
45
+ options: { allowUnstable: boolean },
46
+ ) => Promise<KindResolution>;
47
+
48
+ /**
49
+ * Find one exact block package version, named directly.
50
+ *
51
+ * This is the `block` override's path, and it bypasses kinds entirely — the
52
+ * override exists precisely to pin an implementation that resolution would not
53
+ * have chosen.
54
+ */
55
+ byExactVersion: (id: BlockPackId) => Promise<ExactResolution>;
56
+
57
+ /**
58
+ * Read the implementation sitting at one location.
59
+ *
60
+ * The `location` override's path, for a block that no registry knows because it was
61
+ * built and never published. Nothing is searched: the entry says where, so either
62
+ * that place holds a block or the entry is wrong.
63
+ *
64
+ * Answers "is there a block here", not "is it the right one". Whether the block found
65
+ * implements the kind the entry asks for is settled once it has been prepared, from the
66
+ * compiled model — the one place all three routes have it. This reader could answer it
67
+ * too, since it reads the block's description anyway, and answering it here as well would
68
+ * make two homes for one invariant while leaving the pinned-version route uncovered.
69
+ */
70
+ byLocation: (location: BlockPackLocationReference) => Promise<LocationResolution>;
71
+ };
72
+
73
+ /**
74
+ * The outcome of resolving one kind selector.
75
+ *
76
+ * The three failure reasons are distinct because each has a different way out, and
77
+ * a caller that collapsed them would leave the reader guessing which:
78
+ *
79
+ * - `no-matching-kind-version` — the selector matches no published version of the
80
+ * kind at all. The file, or the registry, is wrong.
81
+ * - `no-implementation` — the kind version exists, but nothing implements it. The
82
+ * kind was published ahead of any block.
83
+ * - `no-stable-implementation` — implementations exist but none is stable, and the
84
+ * apply did not allow unstable ones. This is the only reason the user can act on
85
+ * without editing anything.
86
+ */
87
+ export type KindResolution =
88
+ | { readonly ok: true; readonly spec: BlockPackSpec; readonly title: string }
89
+ | {
90
+ readonly ok: false;
91
+ readonly reason:
92
+ | "no-matching-kind-version"
93
+ | "no-implementation"
94
+ | "no-stable-implementation";
95
+ };
96
+
97
+ /** The outcome of locating one exact block version. */
98
+ export type ExactResolution =
99
+ | { readonly ok: true; readonly spec: BlockPackSpec; readonly title: string }
100
+ | { readonly ok: false; readonly reason: "no-such-block-version" };
101
+
102
+ /**
103
+ * The outcome of reading the implementation at one location.
104
+ *
105
+ * The three failure reasons separate three different mistakes, and each is fixed
106
+ * differently:
107
+ *
108
+ * - `unsupported-scheme` — the URI is well-formed but names a way of fetching that
109
+ * this environment has no reader for. A property of the environment, not the file.
110
+ * - `not-found` — nothing is at that location. Usually a template carried to another
111
+ * machine, or a folder that moved.
112
+ * - `not-a-block` — something is there, but it is not a block. Usually a path pointing
113
+ * one directory off, which is why the message has to say what was looked for.
114
+ */
115
+ export type LocationResolution =
116
+ | {
117
+ readonly ok: true;
118
+ readonly spec: BlockPackSpec;
119
+ readonly title: string;
120
+ }
121
+ | {
122
+ readonly ok: false;
123
+ readonly reason: "unsupported-scheme" | "not-found" | "not-a-block";
124
+ };
125
+
126
+ /** Where one entry's block will come from. */
127
+ export type ResolvedEntry = {
128
+ /** The entry's template-local id. */
129
+ readonly entryId: string;
130
+ readonly spec: BlockPackSpec;
131
+ /**
132
+ * The block package's own title, as its author published it — `meta.title` for a
133
+ * registry block.
134
+ *
135
+ * Carried here because resolution is the only stage that talks to a registry, and a
136
+ * registry is the only thing that knows it. Nothing downstream can recover it: a
137
+ * prepared block pack holds the model, the workflow and the frontend, none of which
138
+ * names the block.
139
+ *
140
+ * It becomes the created block's label, which is what the user sees for any block
141
+ * whose model derives no title of its own — `graph-maker`, `table` and seven other
142
+ * shipped blocks. Deriving it from the entry instead is specifically wrong: an
143
+ * exported template names its entries by the source project's block ids, so a
144
+ * round-tripped project would show UUIDs in the sidebar.
145
+ */
146
+ readonly title: string;
147
+ /**
148
+ * True when the entry named its own implementation — an exact version or a location —
149
+ * rather than letting its kind choose one.
150
+ *
151
+ * Both overrides count, because what this records is that the choice was the file's
152
+ * and not the registry's, which is the same fact for either of them.
153
+ */
154
+ readonly pinned: boolean;
155
+ };
156
+
157
+ /**
158
+ * What resolution found for a whole document.
159
+ *
160
+ * `resolved` is in file order and holds only the entries that resolved, so
161
+ * `problems.length > 0` means it is incomplete. A caller must not apply a partial
162
+ * resolution: the point of resolving before construction is that nothing is created
163
+ * until every entry has an implementation.
164
+ */
165
+ export type TemplateResolveOutcome = {
166
+ readonly resolved: readonly ResolvedEntry[];
167
+ readonly problems: readonly TemplateApplyProblem[];
168
+ };
169
+
170
+ /**
171
+ * Resolve every entry in a template document to a concrete block pack.
172
+ *
173
+ * The first stage of an apply, and the only one that touches the network. It runs
174
+ * before the project exists, which is what makes "no block for this entry" a message
175
+ * about a file rather than a half-built project — and it is also why the whole
176
+ * construction API downstream can be synchronous.
177
+ *
178
+ * Every entry is attempted, and every failure collected: an unapplicable file should
179
+ * take one pass to fix, not one pass per bad entry. The one exception is that a
180
+ * problem short-circuits nothing, so `resolved` may be shorter than the document.
181
+ *
182
+ * @param document A parsed template document
183
+ * @param provider Where blocks come from
184
+ * @param options `allowUnstable` applies to the whole document, not per entry
185
+ */
186
+ export async function resolveTemplateEntries(
187
+ document: ProjectTemplateV1,
188
+ provider: BlockPackProvider,
189
+ options: { allowUnstable: boolean },
190
+ ): Promise<TemplateResolveOutcome> {
191
+ const resolved: ResolvedEntry[] = [];
192
+ const problems: TemplateApplyProblem[] = [];
193
+
194
+ // Sequential, not concurrent. Entries commonly share a kind, and a provider
195
+ // caching its reads can only dedupe them if the second request happens after the
196
+ // first has landed. A template holds a handful of entries, so there is nothing to
197
+ // win by overlapping the reads.
198
+ for (const entry of document.blocks) {
199
+ const outcome = await resolveEntry(entry, provider, options);
200
+ if (outcome.ok) resolved.push(outcome.entry);
201
+ else problems.push(outcome.problem);
202
+ }
203
+
204
+ return { resolved, problems };
205
+ }
206
+
207
+ type EntryOutcome =
208
+ | { ok: true; entry: ResolvedEntry }
209
+ | { ok: false; problem: TemplateApplyProblem };
210
+
211
+ async function resolveEntry(
212
+ entry: ProjectTemplateV1Entry,
213
+ provider: BlockPackProvider,
214
+ options: { allowUnstable: boolean },
215
+ ): Promise<EntryOutcome> {
216
+ const problem = (error: string): EntryOutcome => ({
217
+ ok: false,
218
+ problem: { entryId: entry.id, error },
219
+ });
220
+
221
+ if (entry.location !== undefined) {
222
+ const outcome = await provider.byLocation(entry.location);
223
+
224
+ if (!outcome.ok) {
225
+ switch (outcome.reason) {
226
+ case "unsupported-scheme":
227
+ return problem(
228
+ `This entry says its block is at '${entry.location}', which this application ` +
229
+ `cannot read. Only a local path ('file:') is supported here.`,
230
+ );
231
+ case "not-found":
232
+ return problem(
233
+ `Nothing is at '${entry.location}', where this entry says its block is. A ` +
234
+ `template that pins a folder only works on the machine that folder is on.`,
235
+ );
236
+ case "not-a-block":
237
+ return problem(
238
+ `'${entry.location}' is not a block: it holds neither a built block package ` +
239
+ `nor a block's manifest. Point the entry at the folder holding the block's ` +
240
+ `package.json.`,
241
+ );
242
+ }
243
+ }
244
+
245
+ return {
246
+ ok: true,
247
+ entry: { entryId: entry.id, spec: outcome.spec, title: outcome.title, pinned: true },
248
+ };
249
+ }
250
+
251
+ if (entry.block !== undefined) {
252
+ let id: BlockPackId;
253
+ try {
254
+ const { name, version } = parseBlockPackReference(entry.block);
255
+ id = { ...parseBlockPackName(name), version };
256
+ } catch (e) {
257
+ // The document's parser already checked the `{name}@X.Y.Z` grammar, but not
258
+ // that the name carries an organization, so this is reachable from a
259
+ // hand-written file. Reported rather than thrown, so one malformed entry cannot
260
+ // hide the state of the rest.
261
+ return problem(`Pinned block version is not readable: ${messageOf(e)}`);
262
+ }
263
+
264
+ const outcome = await provider.byExactVersion(id);
265
+ if (outcome.ok)
266
+ return {
267
+ ok: true,
268
+ entry: { entryId: entry.id, spec: outcome.spec, title: outcome.title, pinned: true },
269
+ };
270
+
271
+ return problem(
272
+ `Block '${id.organization}/${id.name}' version ${id.version} was not found. Correct ` +
273
+ `the pinned version in this entry, or remove it so a version is chosen ` +
274
+ `automatically.`,
275
+ );
276
+ }
277
+
278
+ let kindName: string;
279
+ try {
280
+ kindName = parseKindSelectorReference(entry.kind).name;
281
+ } catch (e) {
282
+ return problem(`Block kind is not readable: ${messageOf(e)}`);
283
+ }
284
+
285
+ const outcome = await provider.byKind(entry.kind, options);
286
+ if (outcome.ok)
287
+ return {
288
+ ok: true,
289
+ entry: { entryId: entry.id, spec: outcome.spec, title: outcome.title, pinned: false },
290
+ };
291
+
292
+ switch (outcome.reason) {
293
+ case "no-matching-kind-version":
294
+ return problem(
295
+ `No published version of '${kindName}' matches what this entry asks for ` +
296
+ `(${entry.kind}). Check the version, or update the block registry.`,
297
+ );
298
+ case "no-implementation":
299
+ return problem(
300
+ `'${kindName}' has no block implementing the version this entry asks for ` +
301
+ `(${entry.kind}). Nothing can be installed for it yet.`,
302
+ );
303
+ case "no-stable-implementation":
304
+ // The one failure with a way out that changes nothing in the file, so the
305
+ // message names it.
306
+ return problem(
307
+ `The only blocks implementing '${kindName}' for this entry (${entry.kind}) are ` +
308
+ `pre-release versions. Import again with unstable versions allowed to use them.`,
309
+ );
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Split a block package's npm name into the organization and name a registry knows
315
+ * it by: `@npm-scope/organization.name` → `{ organization, name }`.
316
+ *
317
+ * The npm scope and the organization are different things, and only the second one
318
+ * reaches the registry — `@milaboratories/milaboratories.test-download-file` is
319
+ * published as organization `milaboratories`, name `test-download-file`. The split is
320
+ * on the FIRST dot, so a name may contain further dots.
321
+ *
322
+ * Kind names follow the same convention, which is why a template can carry both in
323
+ * the same npm-ish form.
324
+ *
325
+ * @throws if the name has no npm scope or no organization segment
326
+ */
327
+ export function parseBlockPackName(npmName: string): { organization: string; name: string } {
328
+ const slash = npmName.indexOf("/");
329
+ if (!npmName.startsWith("@") || slash < 0) {
330
+ throw new Error(`expected '@npm-scope/organization.name', got '${npmName}'`);
331
+ }
332
+
333
+ const base = npmName.slice(slash + 1);
334
+ const dot = base.indexOf(".");
335
+ if (dot <= 0 || dot === base.length - 1) {
336
+ throw new Error(
337
+ `'${base}' does not separate an organization from a name with '.' in '${npmName}'`,
338
+ );
339
+ }
340
+
341
+ return { organization: base.slice(0, dot), name: base.slice(dot + 1) };
342
+ }
343
+
344
+ function messageOf(e: unknown): string {
345
+ return e instanceof Error ? e.message : String(e);
346
+ }