@milaboratories/pl-middle-layer 1.66.19 → 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 +18 -18
  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,193 @@
1
+ import { TemplateApplyProblem } from "./template_apply.js";
2
+ import { BlockPackId, BlockPackSpec } from "@milaboratories/pl-model-middle-layer";
3
+ import { BlockKindSelectorReference, BlockPackLocationReference, ProjectTemplateV1 } from "@milaboratories/pl-model-common";
4
+
5
+ //#region src/model/template_resolve.d.ts
6
+ /**
7
+ * Where a template entry's block comes from.
8
+ *
9
+ * All three of an entry's paths to an implementation go through this one interface, and
10
+ * all three come back as the same `BlockPackSpec` — the shape the existing add-block
11
+ * path already consumes — plus the block's published title. That convergence is the
12
+ * point: everything downstream of resolution treats a kind-resolved entry, a
13
+ * version-pinned one and a filesystem-located one identically, which is why a locally
14
+ * built block travels the same preparation and construction path as a published one.
15
+ *
16
+ * Registry knowledge sits deliberately on the far side of this interface. A template
17
+ * file names no registry, so choosing which one (or ones) to consult is a property
18
+ * of the environment applying the file, not of the file — and keeping it out here is
19
+ * what lets resolution be tested with no registry at all.
20
+ *
21
+ * The title is required rather than optional for the same reason: an implementation
22
+ * always has one to hand — a registry adapter reads it off the block's manifest, and
23
+ * anything else names the blocks it serves — while a caller downstream has no source
24
+ * for it at all, and the fallbacks it could invent are all wrong. See
25
+ * {@link ResolvedEntry}'s `title`.
26
+ */
27
+ type BlockPackProvider = {
28
+ /**
29
+ * Find the block implementing a kind selector.
30
+ *
31
+ * @param kind The entry's `{name}@{selector}` reference
32
+ * @param options `allowUnstable` widens the search from stable implementations to
33
+ * every published one, for the whole apply
34
+ */
35
+ byKind: (kind: BlockKindSelectorReference, options: {
36
+ allowUnstable: boolean;
37
+ }) => Promise<KindResolution>;
38
+ /**
39
+ * Find one exact block package version, named directly.
40
+ *
41
+ * This is the `block` override's path, and it bypasses kinds entirely — the
42
+ * override exists precisely to pin an implementation that resolution would not
43
+ * have chosen.
44
+ */
45
+ byExactVersion: (id: BlockPackId) => Promise<ExactResolution>;
46
+ /**
47
+ * Read the implementation sitting at one location.
48
+ *
49
+ * The `location` override's path, for a block that no registry knows because it was
50
+ * built and never published. Nothing is searched: the entry says where, so either
51
+ * that place holds a block or the entry is wrong.
52
+ *
53
+ * Answers "is there a block here", not "is it the right one". Whether the block found
54
+ * implements the kind the entry asks for is settled once it has been prepared, from the
55
+ * compiled model — the one place all three routes have it. This reader could answer it
56
+ * too, since it reads the block's description anyway, and answering it here as well would
57
+ * make two homes for one invariant while leaving the pinned-version route uncovered.
58
+ */
59
+ byLocation: (location: BlockPackLocationReference) => Promise<LocationResolution>;
60
+ };
61
+ /**
62
+ * The outcome of resolving one kind selector.
63
+ *
64
+ * The three failure reasons are distinct because each has a different way out, and
65
+ * a caller that collapsed them would leave the reader guessing which:
66
+ *
67
+ * - `no-matching-kind-version` — the selector matches no published version of the
68
+ * kind at all. The file, or the registry, is wrong.
69
+ * - `no-implementation` — the kind version exists, but nothing implements it. The
70
+ * kind was published ahead of any block.
71
+ * - `no-stable-implementation` — implementations exist but none is stable, and the
72
+ * apply did not allow unstable ones. This is the only reason the user can act on
73
+ * without editing anything.
74
+ */
75
+ type KindResolution = {
76
+ readonly ok: true;
77
+ readonly spec: BlockPackSpec;
78
+ readonly title: string;
79
+ } | {
80
+ readonly ok: false;
81
+ readonly reason: "no-matching-kind-version" | "no-implementation" | "no-stable-implementation";
82
+ };
83
+ /** The outcome of locating one exact block version. */
84
+ type ExactResolution = {
85
+ readonly ok: true;
86
+ readonly spec: BlockPackSpec;
87
+ readonly title: string;
88
+ } | {
89
+ readonly ok: false;
90
+ readonly reason: "no-such-block-version";
91
+ };
92
+ /**
93
+ * The outcome of reading the implementation at one location.
94
+ *
95
+ * The three failure reasons separate three different mistakes, and each is fixed
96
+ * differently:
97
+ *
98
+ * - `unsupported-scheme` — the URI is well-formed but names a way of fetching that
99
+ * this environment has no reader for. A property of the environment, not the file.
100
+ * - `not-found` — nothing is at that location. Usually a template carried to another
101
+ * machine, or a folder that moved.
102
+ * - `not-a-block` — something is there, but it is not a block. Usually a path pointing
103
+ * one directory off, which is why the message has to say what was looked for.
104
+ */
105
+ type LocationResolution = {
106
+ readonly ok: true;
107
+ readonly spec: BlockPackSpec;
108
+ readonly title: string;
109
+ } | {
110
+ readonly ok: false;
111
+ readonly reason: "unsupported-scheme" | "not-found" | "not-a-block";
112
+ };
113
+ /** Where one entry's block will come from. */
114
+ type ResolvedEntry = {
115
+ /** The entry's template-local id. */readonly entryId: string;
116
+ readonly spec: BlockPackSpec;
117
+ /**
118
+ * The block package's own title, as its author published it — `meta.title` for a
119
+ * registry block.
120
+ *
121
+ * Carried here because resolution is the only stage that talks to a registry, and a
122
+ * registry is the only thing that knows it. Nothing downstream can recover it: a
123
+ * prepared block pack holds the model, the workflow and the frontend, none of which
124
+ * names the block.
125
+ *
126
+ * It becomes the created block's label, which is what the user sees for any block
127
+ * whose model derives no title of its own — `graph-maker`, `table` and seven other
128
+ * shipped blocks. Deriving it from the entry instead is specifically wrong: an
129
+ * exported template names its entries by the source project's block ids, so a
130
+ * round-tripped project would show UUIDs in the sidebar.
131
+ */
132
+ readonly title: string;
133
+ /**
134
+ * True when the entry named its own implementation — an exact version or a location —
135
+ * rather than letting its kind choose one.
136
+ *
137
+ * Both overrides count, because what this records is that the choice was the file's
138
+ * and not the registry's, which is the same fact for either of them.
139
+ */
140
+ readonly pinned: boolean;
141
+ };
142
+ /**
143
+ * What resolution found for a whole document.
144
+ *
145
+ * `resolved` is in file order and holds only the entries that resolved, so
146
+ * `problems.length > 0` means it is incomplete. A caller must not apply a partial
147
+ * resolution: the point of resolving before construction is that nothing is created
148
+ * until every entry has an implementation.
149
+ */
150
+ type TemplateResolveOutcome = {
151
+ readonly resolved: readonly ResolvedEntry[];
152
+ readonly problems: readonly TemplateApplyProblem[];
153
+ };
154
+ /**
155
+ * Resolve every entry in a template document to a concrete block pack.
156
+ *
157
+ * The first stage of an apply, and the only one that touches the network. It runs
158
+ * before the project exists, which is what makes "no block for this entry" a message
159
+ * about a file rather than a half-built project — and it is also why the whole
160
+ * construction API downstream can be synchronous.
161
+ *
162
+ * Every entry is attempted, and every failure collected: an unapplicable file should
163
+ * take one pass to fix, not one pass per bad entry. The one exception is that a
164
+ * problem short-circuits nothing, so `resolved` may be shorter than the document.
165
+ *
166
+ * @param document A parsed template document
167
+ * @param provider Where blocks come from
168
+ * @param options `allowUnstable` applies to the whole document, not per entry
169
+ */
170
+ declare function resolveTemplateEntries(document: ProjectTemplateV1, provider: BlockPackProvider, options: {
171
+ allowUnstable: boolean;
172
+ }): Promise<TemplateResolveOutcome>;
173
+ /**
174
+ * Split a block package's npm name into the organization and name a registry knows
175
+ * it by: `@npm-scope/organization.name` → `{ organization, name }`.
176
+ *
177
+ * The npm scope and the organization are different things, and only the second one
178
+ * reaches the registry — `@milaboratories/milaboratories.test-download-file` is
179
+ * published as organization `milaboratories`, name `test-download-file`. The split is
180
+ * on the FIRST dot, so a name may contain further dots.
181
+ *
182
+ * Kind names follow the same convention, which is why a template can carry both in
183
+ * the same npm-ish form.
184
+ *
185
+ * @throws if the name has no npm scope or no organization segment
186
+ */
187
+ declare function parseBlockPackName(npmName: string): {
188
+ organization: string;
189
+ name: string;
190
+ };
191
+ //#endregion
192
+ export { BlockPackProvider, ExactResolution, KindResolution, LocationResolution, ResolvedEntry, TemplateResolveOutcome, parseBlockPackName, resolveTemplateEntries };
193
+ //# sourceMappingURL=template_resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template_resolve.d.ts","names":[],"sources":["../../src/model/template_resolve.ts"],"mappings":";;;;;;;AAkCA;;;;;;;;;;;;;;;;;;;KAAY,iBAAA;EAWG;;;;;;;EAHb,MAAA,GACE,IAAA,EAAM,0BAAA,EACN,OAAA;IAAW,aAAA;EAAA,MACR,OAAA,CAAQ,cAAA;EAwBiD;;AAAkB;AAiBlF;;;;EAhCE,cAAA,GAAiB,EAAA,EAAI,WAAA,KAAgB,OAAA,CAAQ,eAAA;EAiCb;;;;;;AAGb;AAOrB;;;;;;EA5BE,UAAA,GAAa,QAAA,EAAU,0BAAA,KAA+B,OAAA,CAAQ,kBAAA;AAAA;;;;AA8BvB;AAezC;;;;;;;;;;KA5BY,cAAA;EAAA,SACG,EAAA;EAAA,SAAmB,IAAA,EAAM,aAAa;EAAA,SAAW,KAAA;AAAA;EAAA,SAEjD,EAAA;EAAA,SACA,MAAA;AAAA;;KAOH,eAAA;EAAA,SACG,EAAA;EAAA,SAAmB,IAAA,EAAM,aAAa;EAAA,SAAW,KAAA;AAAA;EAAA,SACjD,EAAA;EAAA,SAAoB,MAAA;AAAA;;;;;;AAmEe;AAmBlD;;;;;;;KAvEY,kBAAA;EAAA,SAEG,EAAA;EAAA,SACA,IAAA,EAAM,aAAa;EAAA,SACnB,KAAA;AAAA;EAAA,SAGA,EAAA;EAAA,SACA,MAAA;AAAA;;KAIH,aAAA;EA+DqB,8CA7DtB,OAAA;EAAA,SACA,IAAA,EAAM,aAAa;EAqMI;;;;;;;AAA+C;;;;;;;;EAA/C,SArLvB,KAAA;;;;;;;;WAQA,MAAA;AAAA;;;;;;;;;KAWC,sBAAA;EAAA,SACD,QAAA,WAAmB,aAAA;EAAA,SACnB,QAAA,WAAmB,oBAAoB;AAAA;;;;;;;;;;;;;;;;;iBAmB5B,sBAAA,CACpB,QAAA,EAAU,iBAAA,EACV,QAAA,EAAU,iBAAA,EACV,OAAA;EAAW,aAAA;AAAA,IACV,OAAA,CAAQ,sBAAA;;;;;;;;;;;;;;;iBAyIK,kBAAA,CAAmB,OAAA;EAAoB,YAAA;EAAsB,IAAA;AAAA"}
@@ -0,0 +1,133 @@
1
+ import { parseBlockPackReference, parseKindSelectorReference } from "@milaboratories/pl-model-common";
2
+ //#region src/model/template_resolve.ts
3
+ /**
4
+ * Resolve every entry in a template document to a concrete block pack.
5
+ *
6
+ * The first stage of an apply, and the only one that touches the network. It runs
7
+ * before the project exists, which is what makes "no block for this entry" a message
8
+ * about a file rather than a half-built project — and it is also why the whole
9
+ * construction API downstream can be synchronous.
10
+ *
11
+ * Every entry is attempted, and every failure collected: an unapplicable file should
12
+ * take one pass to fix, not one pass per bad entry. The one exception is that a
13
+ * problem short-circuits nothing, so `resolved` may be shorter than the document.
14
+ *
15
+ * @param document A parsed template document
16
+ * @param provider Where blocks come from
17
+ * @param options `allowUnstable` applies to the whole document, not per entry
18
+ */
19
+ async function resolveTemplateEntries(document, provider, options) {
20
+ const resolved = [];
21
+ const problems = [];
22
+ for (const entry of document.blocks) {
23
+ const outcome = await resolveEntry(entry, provider, options);
24
+ if (outcome.ok) resolved.push(outcome.entry);
25
+ else problems.push(outcome.problem);
26
+ }
27
+ return {
28
+ resolved,
29
+ problems
30
+ };
31
+ }
32
+ async function resolveEntry(entry, provider, options) {
33
+ const problem = (error) => ({
34
+ ok: false,
35
+ problem: {
36
+ entryId: entry.id,
37
+ error
38
+ }
39
+ });
40
+ if (entry.location !== void 0) {
41
+ const outcome = await provider.byLocation(entry.location);
42
+ if (!outcome.ok) switch (outcome.reason) {
43
+ case "unsupported-scheme": return problem(`This entry says its block is at '${entry.location}', which this application cannot read. Only a local path ('file:') is supported here.`);
44
+ case "not-found": return problem(`Nothing is at '${entry.location}', where this entry says its block is. A template that pins a folder only works on the machine that folder is on.`);
45
+ case "not-a-block": return problem(`'${entry.location}' is not a block: it holds neither a built block package nor a block's manifest. Point the entry at the folder holding the block's package.json.`);
46
+ }
47
+ return {
48
+ ok: true,
49
+ entry: {
50
+ entryId: entry.id,
51
+ spec: outcome.spec,
52
+ title: outcome.title,
53
+ pinned: true
54
+ }
55
+ };
56
+ }
57
+ if (entry.block !== void 0) {
58
+ let id;
59
+ try {
60
+ const { name, version } = parseBlockPackReference(entry.block);
61
+ id = {
62
+ ...parseBlockPackName(name),
63
+ version
64
+ };
65
+ } catch (e) {
66
+ return problem(`Pinned block version is not readable: ${messageOf(e)}`);
67
+ }
68
+ const outcome = await provider.byExactVersion(id);
69
+ if (outcome.ok) return {
70
+ ok: true,
71
+ entry: {
72
+ entryId: entry.id,
73
+ spec: outcome.spec,
74
+ title: outcome.title,
75
+ pinned: true
76
+ }
77
+ };
78
+ return problem(`Block '${id.organization}/${id.name}' version ${id.version} was not found. Correct the pinned version in this entry, or remove it so a version is chosen automatically.`);
79
+ }
80
+ let kindName;
81
+ try {
82
+ kindName = parseKindSelectorReference(entry.kind).name;
83
+ } catch (e) {
84
+ return problem(`Block kind is not readable: ${messageOf(e)}`);
85
+ }
86
+ const outcome = await provider.byKind(entry.kind, options);
87
+ if (outcome.ok) return {
88
+ ok: true,
89
+ entry: {
90
+ entryId: entry.id,
91
+ spec: outcome.spec,
92
+ title: outcome.title,
93
+ pinned: false
94
+ }
95
+ };
96
+ switch (outcome.reason) {
97
+ case "no-matching-kind-version": return problem(`No published version of '${kindName}' matches what this entry asks for (${entry.kind}). Check the version, or update the block registry.`);
98
+ case "no-implementation": return problem(`'${kindName}' has no block implementing the version this entry asks for (${entry.kind}). Nothing can be installed for it yet.`);
99
+ case "no-stable-implementation": return problem(`The only blocks implementing '${kindName}' for this entry (${entry.kind}) are pre-release versions. Import again with unstable versions allowed to use them.`);
100
+ }
101
+ }
102
+ /**
103
+ * Split a block package's npm name into the organization and name a registry knows
104
+ * it by: `@npm-scope/organization.name` → `{ organization, name }`.
105
+ *
106
+ * The npm scope and the organization are different things, and only the second one
107
+ * reaches the registry — `@milaboratories/milaboratories.test-download-file` is
108
+ * published as organization `milaboratories`, name `test-download-file`. The split is
109
+ * on the FIRST dot, so a name may contain further dots.
110
+ *
111
+ * Kind names follow the same convention, which is why a template can carry both in
112
+ * the same npm-ish form.
113
+ *
114
+ * @throws if the name has no npm scope or no organization segment
115
+ */
116
+ function parseBlockPackName(npmName) {
117
+ const slash = npmName.indexOf("/");
118
+ if (!npmName.startsWith("@") || slash < 0) throw new Error(`expected '@npm-scope/organization.name', got '${npmName}'`);
119
+ const base = npmName.slice(slash + 1);
120
+ const dot = base.indexOf(".");
121
+ if (dot <= 0 || dot === base.length - 1) throw new Error(`'${base}' does not separate an organization from a name with '.' in '${npmName}'`);
122
+ return {
123
+ organization: base.slice(0, dot),
124
+ name: base.slice(dot + 1)
125
+ };
126
+ }
127
+ function messageOf(e) {
128
+ return e instanceof Error ? e.message : String(e);
129
+ }
130
+ //#endregion
131
+ export { parseBlockPackName, resolveTemplateEntries };
132
+
133
+ //# sourceMappingURL=template_resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template_resolve.js","names":[],"sources":["../../src/model/template_resolve.ts"],"sourcesContent":["import type {\n BlockKindSelectorReference,\n BlockPackLocationReference,\n ProjectTemplateV1,\n ProjectTemplateV1Entry,\n} from \"@milaboratories/pl-model-common\";\nimport {\n parseBlockPackReference,\n parseKindSelectorReference,\n} from \"@milaboratories/pl-model-common\";\nimport type { BlockPackId, BlockPackSpec } from \"@milaboratories/pl-model-middle-layer\";\nimport type { TemplateApplyProblem } from \"./template_apply\";\n\n/**\n * Where a template entry's block comes from.\n *\n * All three of an entry's paths to an implementation go through this one interface, and\n * all three come back as the same `BlockPackSpec` — the shape the existing add-block\n * path already consumes — plus the block's published title. That convergence is the\n * point: everything downstream of resolution treats a kind-resolved entry, a\n * version-pinned one and a filesystem-located one identically, which is why a locally\n * built block travels the same preparation and construction path as a published one.\n *\n * Registry knowledge sits deliberately on the far side of this interface. A template\n * file names no registry, so choosing which one (or ones) to consult is a property\n * of the environment applying the file, not of the file — and keeping it out here is\n * what lets resolution be tested with no registry at all.\n *\n * The title is required rather than optional for the same reason: an implementation\n * always has one to hand — a registry adapter reads it off the block's manifest, and\n * anything else names the blocks it serves — while a caller downstream has no source\n * for it at all, and the fallbacks it could invent are all wrong. See\n * {@link ResolvedEntry}'s `title`.\n */\nexport type BlockPackProvider = {\n /**\n * Find the block implementing a kind selector.\n *\n * @param kind The entry's `{name}@{selector}` reference\n * @param options `allowUnstable` widens the search from stable implementations to\n * every published one, for the whole apply\n */\n byKind: (\n kind: BlockKindSelectorReference,\n options: { allowUnstable: boolean },\n ) => Promise<KindResolution>;\n\n /**\n * Find one exact block package version, named directly.\n *\n * This is the `block` override's path, and it bypasses kinds entirely — the\n * override exists precisely to pin an implementation that resolution would not\n * have chosen.\n */\n byExactVersion: (id: BlockPackId) => Promise<ExactResolution>;\n\n /**\n * Read the implementation sitting at one location.\n *\n * The `location` override's path, for a block that no registry knows because it was\n * built and never published. Nothing is searched: the entry says where, so either\n * that place holds a block or the entry is wrong.\n *\n * Answers \"is there a block here\", not \"is it the right one\". Whether the block found\n * implements the kind the entry asks for is settled once it has been prepared, from the\n * compiled model — the one place all three routes have it. This reader could answer it\n * too, since it reads the block's description anyway, and answering it here as well would\n * make two homes for one invariant while leaving the pinned-version route uncovered.\n */\n byLocation: (location: BlockPackLocationReference) => Promise<LocationResolution>;\n};\n\n/**\n * The outcome of resolving one kind selector.\n *\n * The three failure reasons are distinct because each has a different way out, and\n * a caller that collapsed them would leave the reader guessing which:\n *\n * - `no-matching-kind-version` — the selector matches no published version of the\n * kind at all. The file, or the registry, is wrong.\n * - `no-implementation` — the kind version exists, but nothing implements it. The\n * kind was published ahead of any block.\n * - `no-stable-implementation` — implementations exist but none is stable, and the\n * apply did not allow unstable ones. This is the only reason the user can act on\n * without editing anything.\n */\nexport type KindResolution =\n | { readonly ok: true; readonly spec: BlockPackSpec; readonly title: string }\n | {\n readonly ok: false;\n readonly reason:\n | \"no-matching-kind-version\"\n | \"no-implementation\"\n | \"no-stable-implementation\";\n };\n\n/** The outcome of locating one exact block version. */\nexport type ExactResolution =\n | { readonly ok: true; readonly spec: BlockPackSpec; readonly title: string }\n | { readonly ok: false; readonly reason: \"no-such-block-version\" };\n\n/**\n * The outcome of reading the implementation at one location.\n *\n * The three failure reasons separate three different mistakes, and each is fixed\n * differently:\n *\n * - `unsupported-scheme` — the URI is well-formed but names a way of fetching that\n * this environment has no reader for. A property of the environment, not the file.\n * - `not-found` — nothing is at that location. Usually a template carried to another\n * machine, or a folder that moved.\n * - `not-a-block` — something is there, but it is not a block. Usually a path pointing\n * one directory off, which is why the message has to say what was looked for.\n */\nexport type LocationResolution =\n | {\n readonly ok: true;\n readonly spec: BlockPackSpec;\n readonly title: string;\n }\n | {\n readonly ok: false;\n readonly reason: \"unsupported-scheme\" | \"not-found\" | \"not-a-block\";\n };\n\n/** Where one entry's block will come from. */\nexport type ResolvedEntry = {\n /** The entry's template-local id. */\n readonly entryId: string;\n readonly spec: BlockPackSpec;\n /**\n * The block package's own title, as its author published it — `meta.title` for a\n * registry block.\n *\n * Carried here because resolution is the only stage that talks to a registry, and a\n * registry is the only thing that knows it. Nothing downstream can recover it: a\n * prepared block pack holds the model, the workflow and the frontend, none of which\n * names the block.\n *\n * It becomes the created block's label, which is what the user sees for any block\n * whose model derives no title of its own — `graph-maker`, `table` and seven other\n * shipped blocks. Deriving it from the entry instead is specifically wrong: an\n * exported template names its entries by the source project's block ids, so a\n * round-tripped project would show UUIDs in the sidebar.\n */\n readonly title: string;\n /**\n * True when the entry named its own implementation — an exact version or a location —\n * rather than letting its kind choose one.\n *\n * Both overrides count, because what this records is that the choice was the file's\n * and not the registry's, which is the same fact for either of them.\n */\n readonly pinned: boolean;\n};\n\n/**\n * What resolution found for a whole document.\n *\n * `resolved` is in file order and holds only the entries that resolved, so\n * `problems.length > 0` means it is incomplete. A caller must not apply a partial\n * resolution: the point of resolving before construction is that nothing is created\n * until every entry has an implementation.\n */\nexport type TemplateResolveOutcome = {\n readonly resolved: readonly ResolvedEntry[];\n readonly problems: readonly TemplateApplyProblem[];\n};\n\n/**\n * Resolve every entry in a template document to a concrete block pack.\n *\n * The first stage of an apply, and the only one that touches the network. It runs\n * before the project exists, which is what makes \"no block for this entry\" a message\n * about a file rather than a half-built project — and it is also why the whole\n * construction API downstream can be synchronous.\n *\n * Every entry is attempted, and every failure collected: an unapplicable file should\n * take one pass to fix, not one pass per bad entry. The one exception is that a\n * problem short-circuits nothing, so `resolved` may be shorter than the document.\n *\n * @param document A parsed template document\n * @param provider Where blocks come from\n * @param options `allowUnstable` applies to the whole document, not per entry\n */\nexport async function resolveTemplateEntries(\n document: ProjectTemplateV1,\n provider: BlockPackProvider,\n options: { allowUnstable: boolean },\n): Promise<TemplateResolveOutcome> {\n const resolved: ResolvedEntry[] = [];\n const problems: TemplateApplyProblem[] = [];\n\n // Sequential, not concurrent. Entries commonly share a kind, and a provider\n // caching its reads can only dedupe them if the second request happens after the\n // first has landed. A template holds a handful of entries, so there is nothing to\n // win by overlapping the reads.\n for (const entry of document.blocks) {\n const outcome = await resolveEntry(entry, provider, options);\n if (outcome.ok) resolved.push(outcome.entry);\n else problems.push(outcome.problem);\n }\n\n return { resolved, problems };\n}\n\ntype EntryOutcome =\n | { ok: true; entry: ResolvedEntry }\n | { ok: false; problem: TemplateApplyProblem };\n\nasync function resolveEntry(\n entry: ProjectTemplateV1Entry,\n provider: BlockPackProvider,\n options: { allowUnstable: boolean },\n): Promise<EntryOutcome> {\n const problem = (error: string): EntryOutcome => ({\n ok: false,\n problem: { entryId: entry.id, error },\n });\n\n if (entry.location !== undefined) {\n const outcome = await provider.byLocation(entry.location);\n\n if (!outcome.ok) {\n switch (outcome.reason) {\n case \"unsupported-scheme\":\n return problem(\n `This entry says its block is at '${entry.location}', which this application ` +\n `cannot read. Only a local path ('file:') is supported here.`,\n );\n case \"not-found\":\n return problem(\n `Nothing is at '${entry.location}', where this entry says its block is. A ` +\n `template that pins a folder only works on the machine that folder is on.`,\n );\n case \"not-a-block\":\n return problem(\n `'${entry.location}' is not a block: it holds neither a built block package ` +\n `nor a block's manifest. Point the entry at the folder holding the block's ` +\n `package.json.`,\n );\n }\n }\n\n return {\n ok: true,\n entry: { entryId: entry.id, spec: outcome.spec, title: outcome.title, pinned: true },\n };\n }\n\n if (entry.block !== undefined) {\n let id: BlockPackId;\n try {\n const { name, version } = parseBlockPackReference(entry.block);\n id = { ...parseBlockPackName(name), version };\n } catch (e) {\n // The document's parser already checked the `{name}@X.Y.Z` grammar, but not\n // that the name carries an organization, so this is reachable from a\n // hand-written file. Reported rather than thrown, so one malformed entry cannot\n // hide the state of the rest.\n return problem(`Pinned block version is not readable: ${messageOf(e)}`);\n }\n\n const outcome = await provider.byExactVersion(id);\n if (outcome.ok)\n return {\n ok: true,\n entry: { entryId: entry.id, spec: outcome.spec, title: outcome.title, pinned: true },\n };\n\n return problem(\n `Block '${id.organization}/${id.name}' version ${id.version} was not found. Correct ` +\n `the pinned version in this entry, or remove it so a version is chosen ` +\n `automatically.`,\n );\n }\n\n let kindName: string;\n try {\n kindName = parseKindSelectorReference(entry.kind).name;\n } catch (e) {\n return problem(`Block kind is not readable: ${messageOf(e)}`);\n }\n\n const outcome = await provider.byKind(entry.kind, options);\n if (outcome.ok)\n return {\n ok: true,\n entry: { entryId: entry.id, spec: outcome.spec, title: outcome.title, pinned: false },\n };\n\n switch (outcome.reason) {\n case \"no-matching-kind-version\":\n return problem(\n `No published version of '${kindName}' matches what this entry asks for ` +\n `(${entry.kind}). Check the version, or update the block registry.`,\n );\n case \"no-implementation\":\n return problem(\n `'${kindName}' has no block implementing the version this entry asks for ` +\n `(${entry.kind}). Nothing can be installed for it yet.`,\n );\n case \"no-stable-implementation\":\n // The one failure with a way out that changes nothing in the file, so the\n // message names it.\n return problem(\n `The only blocks implementing '${kindName}' for this entry (${entry.kind}) are ` +\n `pre-release versions. Import again with unstable versions allowed to use them.`,\n );\n }\n}\n\n/**\n * Split a block package's npm name into the organization and name a registry knows\n * it by: `@npm-scope/organization.name` → `{ organization, name }`.\n *\n * The npm scope and the organization are different things, and only the second one\n * reaches the registry — `@milaboratories/milaboratories.test-download-file` is\n * published as organization `milaboratories`, name `test-download-file`. The split is\n * on the FIRST dot, so a name may contain further dots.\n *\n * Kind names follow the same convention, which is why a template can carry both in\n * the same npm-ish form.\n *\n * @throws if the name has no npm scope or no organization segment\n */\nexport function parseBlockPackName(npmName: string): { organization: string; name: string } {\n const slash = npmName.indexOf(\"/\");\n if (!npmName.startsWith(\"@\") || slash < 0) {\n throw new Error(`expected '@npm-scope/organization.name', got '${npmName}'`);\n }\n\n const base = npmName.slice(slash + 1);\n const dot = base.indexOf(\".\");\n if (dot <= 0 || dot === base.length - 1) {\n throw new Error(\n `'${base}' does not separate an organization from a name with '.' in '${npmName}'`,\n );\n }\n\n return { organization: base.slice(0, dot), name: base.slice(dot + 1) };\n}\n\nfunction messageOf(e: unknown): string {\n return e instanceof Error ? e.message : String(e);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAyLA,eAAsB,uBACpB,UACA,UACA,SACiC;CACjC,MAAM,WAA4B,CAAC;CACnC,MAAM,WAAmC,CAAC;CAM1C,KAAK,MAAM,SAAS,SAAS,QAAQ;EACnC,MAAM,UAAU,MAAM,aAAa,OAAO,UAAU,OAAO;EAC3D,IAAI,QAAQ,IAAI,SAAS,KAAK,QAAQ,KAAK;OACtC,SAAS,KAAK,QAAQ,OAAO;CACpC;CAEA,OAAO;EAAE;EAAU;CAAS;AAC9B;AAMA,eAAe,aACb,OACA,UACA,SACuB;CACvB,MAAM,WAAW,WAAiC;EAChD,IAAI;EACJ,SAAS;GAAE,SAAS,MAAM;GAAI;EAAM;CACtC;CAEA,IAAI,MAAM,aAAa,KAAA,GAAW;EAChC,MAAM,UAAU,MAAM,SAAS,WAAW,MAAM,QAAQ;EAExD,IAAI,CAAC,QAAQ,IACX,QAAQ,QAAQ,QAAhB;GACE,KAAK,sBACH,OAAO,QACL,oCAAoC,MAAM,SAAS,sFAErD;GACF,KAAK,aACH,OAAO,QACL,kBAAkB,MAAM,SAAS,kHAEnC;GACF,KAAK,eACH,OAAO,QACL,IAAI,MAAM,SAAS,iJAGrB;EACJ;EAGF,OAAO;GACL,IAAI;GACJ,OAAO;IAAE,SAAS,MAAM;IAAI,MAAM,QAAQ;IAAM,OAAO,QAAQ;IAAO,QAAQ;GAAK;EACrF;CACF;CAEA,IAAI,MAAM,UAAU,KAAA,GAAW;EAC7B,IAAI;EACJ,IAAI;GACF,MAAM,EAAE,MAAM,YAAY,wBAAwB,MAAM,KAAK;GAC7D,KAAK;IAAE,GAAG,mBAAmB,IAAI;IAAG;GAAQ;EAC9C,SAAS,GAAG;GAKV,OAAO,QAAQ,yCAAyC,UAAU,CAAC,GAAG;EACxE;EAEA,MAAM,UAAU,MAAM,SAAS,eAAe,EAAE;EAChD,IAAI,QAAQ,IACV,OAAO;GACL,IAAI;GACJ,OAAO;IAAE,SAAS,MAAM;IAAI,MAAM,QAAQ;IAAM,OAAO,QAAQ;IAAO,QAAQ;GAAK;EACrF;EAEF,OAAO,QACL,UAAU,GAAG,aAAa,GAAG,GAAG,KAAK,YAAY,GAAG,QAAQ,6GAG9D;CACF;CAEA,IAAI;CACJ,IAAI;EACF,WAAW,2BAA2B,MAAM,IAAI,CAAC,CAAC;CACpD,SAAS,GAAG;EACV,OAAO,QAAQ,+BAA+B,UAAU,CAAC,GAAG;CAC9D;CAEA,MAAM,UAAU,MAAM,SAAS,OAAO,MAAM,MAAM,OAAO;CACzD,IAAI,QAAQ,IACV,OAAO;EACL,IAAI;EACJ,OAAO;GAAE,SAAS,MAAM;GAAI,MAAM,QAAQ;GAAM,OAAO,QAAQ;GAAO,QAAQ;EAAM;CACtF;CAEF,QAAQ,QAAQ,QAAhB;EACE,KAAK,4BACH,OAAO,QACL,4BAA4B,SAAS,sCAC/B,MAAM,KAAK,oDACnB;EACF,KAAK,qBACH,OAAO,QACL,IAAI,SAAS,+DACP,MAAM,KAAK,wCACnB;EACF,KAAK,4BAGH,OAAO,QACL,iCAAiC,SAAS,oBAAoB,MAAM,KAAK,qFAE3E;CACJ;AACF;;;;;;;;;;;;;;;AAgBA,SAAgB,mBAAmB,SAAyD;CAC1F,MAAM,QAAQ,QAAQ,QAAQ,GAAG;CACjC,IAAI,CAAC,QAAQ,WAAW,GAAG,KAAK,QAAQ,GACtC,MAAM,IAAI,MAAM,iDAAiD,QAAQ,EAAE;CAG7E,MAAM,OAAO,QAAQ,MAAM,QAAQ,CAAC;CACpC,MAAM,MAAM,KAAK,QAAQ,GAAG;CAC5B,IAAI,OAAO,KAAK,QAAQ,KAAK,SAAS,GACpC,MAAM,IAAI,MACR,IAAI,KAAK,+DAA+D,QAAQ,EAClF;CAGF,OAAO;EAAE,cAAc,KAAK,MAAM,GAAG,GAAG;EAAG,MAAM,KAAK,MAAM,MAAM,CAAC;CAAE;AACvE;AAEA,SAAS,UAAU,GAAoB;CACrC,OAAO,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;AAClD"}
@@ -0,0 +1,162 @@
1
+ const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
2
+ const require_template_export = require("./template_export.cjs");
3
+ let yaml = require("yaml");
4
+ yaml = require_runtime.__toESM(yaml, 1);
5
+ let node_url = require("node:url");
6
+ let _milaboratories_pl_model_common = require("@milaboratories/pl-model-common");
7
+ //#region src/model/template_serializer.ts
8
+ /**
9
+ * The `location` to write for a block installed from the filesystem, or `undefined`
10
+ * for one that came from a registry and therefore needs no locator.
11
+ *
12
+ * Both filesystem spec shapes are emitted, and they anchor at different directories
13
+ * — a dev block at its facade package, an npm-consumed one at its block-pack folder.
14
+ * The document does not distinguish them: one URI is written either way, and telling
15
+ * the two layouts apart is done by looking at what is actually there, by the side
16
+ * that has the filesystem anyway. Encoding the layout in the file instead would
17
+ * freeze today's two shapes into the format.
18
+ *
19
+ * A dev spec carries an OS path and is converted here, which also percent-encodes a
20
+ * path containing spaces. An npm-consumed spec already carries a `file:` URL and is
21
+ * passed through: it is the locator the block itself emitted, and reconstructing one
22
+ * from it could only lose information.
23
+ */
24
+ function locationOf(spec) {
25
+ switch (spec.type) {
26
+ case "dev-v2": return (0, node_url.pathToFileURL)(spec.folder).href;
27
+ case "from-pack-v2": return spec.packUrl;
28
+ case "dev-v1":
29
+ case "from-registry-v1":
30
+ case "from-registry-v2": return;
31
+ }
32
+ }
33
+ /**
34
+ * Turn a project into a template document.
35
+ *
36
+ * Assembly is deliberately dull — the entry is the block's id, its widened kind
37
+ * reference, and the params the walk already collected. The interesting decisions
38
+ * were made upstream; what is left here is the two things only this layer can
39
+ * check, both of which produce problems rather than a broken file:
40
+ *
41
+ * - **A block with no kind cannot be written.** An entry's `kind` is required — it
42
+ * is the params contract the entry is typed against — while a block's kind is
43
+ * optional, so a block that predates kinds, or that uses the deprecated
44
+ * kind-less model overload, has no legal entry. Reported per block. This is not
45
+ * an edge case today: it is what most existing projects will hit until their
46
+ * blocks are republished.
47
+ * - **References must point at an entry declared earlier.** Verbatim id reuse
48
+ * means a reference to a deleted block survives into the file naming nothing:
49
+ * deleting a block only removes it from the structure and does not rewrite
50
+ * downstream args, so a live project holds such references routinely.
51
+ *
52
+ * `block` is never emitted. That override exists to pin an implementation against
53
+ * a kind's version range, and export always writes the exact version the block
54
+ * implements, so there is nothing left for it to pin.
55
+ *
56
+ * `location` IS emitted, for every block that was installed from the filesystem. Such
57
+ * a block is not in any registry, so the kind reference alone names nothing the
58
+ * importer could find, and a file that omitted the one usable answer would describe a
59
+ * project that cannot be recreated. It costs portability, and nothing says so: such a
60
+ * file is the debugging path, read by the developer who wrote it on the machine that
61
+ * wrote it.
62
+ *
63
+ * Problems from `walk` are carried through, so a caller can hand a walk straight
64
+ * in and get one combined list.
65
+ */
66
+ function assembleProjectTemplateV1(walk, kindProvider, specProvider) {
67
+ const problems = [...walk.problems];
68
+ const blocks = [];
69
+ for (const entry of walk.entries) {
70
+ const kind = kindProvider(entry.blockId);
71
+ if (kind === void 0) {
72
+ problems.push({
73
+ blockId: entry.blockId,
74
+ error: "Block declares no kind, so it cannot be written to a template: an entry's kind carries the params contract the entry is typed against"
75
+ });
76
+ continue;
77
+ }
78
+ let selector;
79
+ try {
80
+ selector = (0, _milaboratories_pl_model_common.kindReferenceToSelectorReference)(kind);
81
+ } catch (e) {
82
+ problems.push({
83
+ blockId: entry.blockId,
84
+ error: `Block's stored kind reference is malformed: ${e instanceof Error ? e.message : String(e)}`
85
+ });
86
+ continue;
87
+ }
88
+ const spec = specProvider(entry.blockId);
89
+ const location = spec === void 0 ? void 0 : locationOf(spec);
90
+ blocks.push({
91
+ id: entry.blockId,
92
+ kind: selector,
93
+ params: entry.params,
94
+ ...location !== void 0 ? { location } : {}
95
+ });
96
+ }
97
+ return {
98
+ document: {
99
+ schema: _milaboratories_pl_model_common.PROJECT_TEMPLATE_SCHEMA_V1,
100
+ blocks
101
+ },
102
+ problems
103
+ };
104
+ }
105
+ /**
106
+ * Render a template document to YAML text.
107
+ *
108
+ * Two non-default emitter settings, both about the file being read by someone
109
+ * else's code:
110
+ *
111
+ * - **No line folding.** A wrapped scalar still parses, but it makes a diff between
112
+ * two exported templates unreadable, which is most of the reason to prefer YAML
113
+ * over JSON here.
114
+ * - **Quote as if the reader were YAML 1.1**, while still parsing as 1.2. YAML 1.2
115
+ * dropped `yes`/`no`/`on`/`off`/`y`/`n` as booleans and dropped sexagesimal
116
+ * integers, so a 1.2 emitter leaves a params value of `"yes"` or `"1:30"` bare —
117
+ * which a 1.1 reader (PyYAML's default, and Go's yaml.v2) turns into `true` and
118
+ * `90`. A template is a contract for a second implementation, so the safe
119
+ * combination is to quote against the stricter ruleset and read with the looser
120
+ * one: a quoted scalar means the same thing under both. This adds no `%YAML`
121
+ * directive — it only changes which scalars get quotes.
122
+ */
123
+ function stringifyProjectTemplateV1(document) {
124
+ return yaml.default.stringify(document, {
125
+ lineWidth: 0,
126
+ version: "1.1"
127
+ });
128
+ }
129
+ /**
130
+ * Export a project as `template-v1` YAML, or report every reason it cannot be.
131
+ *
132
+ * All-or-nothing on purpose. A partial template silently drops blocks and the
133
+ * surviving entries may reference the dropped ones, so what looks like a
134
+ * successful export would produce a project missing pieces the user never chose
135
+ * to leave out. Reporting everything at once instead of failing on the first
136
+ * problem is the other half of that: fixing an export should take one pass.
137
+ *
138
+ * @param structure The project structure, which supplies both membership and order
139
+ * @param paramsProvider A block's derived template params, in live form
140
+ * @param kindProvider A block's exact kind reference, read from its stored config
141
+ * @param specProvider A block's origin spec, read from the same stored container
142
+ */
143
+ function exportProjectAsTemplateV1(structure, paramsProvider, kindProvider, specProvider) {
144
+ const { document, problems } = assembleProjectTemplateV1(require_template_export.walkProjectForTemplateExport(structure, paramsProvider), kindProvider, specProvider);
145
+ if (problems.length > 0) return {
146
+ ok: false,
147
+ problems
148
+ };
149
+ (0, _milaboratories_pl_model_common.parseProjectTemplateV1)(document);
150
+ return {
151
+ ok: true,
152
+ yaml: stringifyProjectTemplateV1(document),
153
+ document
154
+ };
155
+ }
156
+ //#endregion
157
+ exports.assembleProjectTemplateV1 = assembleProjectTemplateV1;
158
+ exports.exportProjectAsTemplateV1 = exportProjectAsTemplateV1;
159
+ exports.locationOf = locationOf;
160
+ exports.stringifyProjectTemplateV1 = stringifyProjectTemplateV1;
161
+
162
+ //# sourceMappingURL=template_serializer.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template_serializer.cjs","names":["PROJECT_TEMPLATE_SCHEMA_V1","YAML","walkProjectForTemplateExport"],"sources":["../../src/model/template_serializer.ts"],"sourcesContent":["import YAML from \"yaml\";\nimport { pathToFileURL } from \"node:url\";\nimport type {\n BlockKindReference,\n BlockKindSelectorReference,\n BlockPackLocationReference,\n ProjectTemplateV1,\n ProjectTemplateV1Entry,\n} from \"@milaboratories/pl-model-common\";\nimport {\n PROJECT_TEMPLATE_SCHEMA_V1,\n kindReferenceToSelectorReference,\n parseProjectTemplateV1,\n} from \"@milaboratories/pl-model-common\";\nimport type { BlockPackSpec } from \"@milaboratories/pl-model-middle-layer\";\nimport type { ProjectStructure } from \"./project_model\";\nimport type {\n TemplateExportProblem,\n TemplateExportWalk,\n TemplateParamsResult,\n} from \"./template_export\";\nimport { walkProjectForTemplateExport } from \"./template_export\";\n\n/** A block's exact kind reference, or `undefined` for a block that declares no kind. */\nexport type BlockKindProvider = (blockId: string) => BlockKindReference | undefined;\n\n/**\n * A block's origin spec — where the installed block came from — or `undefined` when\n * it is not known for that block.\n *\n * The project stores this next to the kind reference, so both are read from the same\n * place and neither costs an extra round-trip.\n */\nexport type BlockPackSpecProvider = (blockId: string) => BlockPackSpec | undefined;\n\n/** What the caller gets back for a whole project. */\nexport type ProjectTemplateExportOutcome =\n | {\n readonly ok: true;\n readonly yaml: string;\n /** The document the YAML was rendered from, already validated. */\n readonly document: ProjectTemplateV1;\n }\n | {\n readonly ok: false;\n /** Every block that stands in the way, not just the first. */\n readonly problems: readonly TemplateExportProblem[];\n };\n\n/**\n * The `location` to write for a block installed from the filesystem, or `undefined`\n * for one that came from a registry and therefore needs no locator.\n *\n * Both filesystem spec shapes are emitted, and they anchor at different directories\n * — a dev block at its facade package, an npm-consumed one at its block-pack folder.\n * The document does not distinguish them: one URI is written either way, and telling\n * the two layouts apart is done by looking at what is actually there, by the side\n * that has the filesystem anyway. Encoding the layout in the file instead would\n * freeze today's two shapes into the format.\n *\n * A dev spec carries an OS path and is converted here, which also percent-encodes a\n * path containing spaces. An npm-consumed spec already carries a `file:` URL and is\n * passed through: it is the locator the block itself emitted, and reconstructing one\n * from it could only lose information.\n */\nexport function locationOf(spec: BlockPackSpec): BlockPackLocationReference | undefined {\n switch (spec.type) {\n case \"dev-v2\":\n return pathToFileURL(spec.folder).href as BlockPackLocationReference;\n case \"from-pack-v2\":\n return spec.packUrl as BlockPackLocationReference;\n // A registry block is found by name, which is what makes the entry portable —\n // writing where this machine happened to cache it would take that away. `dev-v1`\n // predates kinds entirely, so such a block has no kind and never reaches here.\n case \"dev-v1\":\n case \"from-registry-v1\":\n case \"from-registry-v2\":\n return undefined;\n }\n}\n\n/**\n * Turn a project into a template document.\n *\n * Assembly is deliberately dull — the entry is the block's id, its widened kind\n * reference, and the params the walk already collected. The interesting decisions\n * were made upstream; what is left here is the two things only this layer can\n * check, both of which produce problems rather than a broken file:\n *\n * - **A block with no kind cannot be written.** An entry's `kind` is required — it\n * is the params contract the entry is typed against — while a block's kind is\n * optional, so a block that predates kinds, or that uses the deprecated\n * kind-less model overload, has no legal entry. Reported per block. This is not\n * an edge case today: it is what most existing projects will hit until their\n * blocks are republished.\n * - **References must point at an entry declared earlier.** Verbatim id reuse\n * means a reference to a deleted block survives into the file naming nothing:\n * deleting a block only removes it from the structure and does not rewrite\n * downstream args, so a live project holds such references routinely.\n *\n * `block` is never emitted. That override exists to pin an implementation against\n * a kind's version range, and export always writes the exact version the block\n * implements, so there is nothing left for it to pin.\n *\n * `location` IS emitted, for every block that was installed from the filesystem. Such\n * a block is not in any registry, so the kind reference alone names nothing the\n * importer could find, and a file that omitted the one usable answer would describe a\n * project that cannot be recreated. It costs portability, and nothing says so: such a\n * file is the debugging path, read by the developer who wrote it on the machine that\n * wrote it.\n *\n * Problems from `walk` are carried through, so a caller can hand a walk straight\n * in and get one combined list.\n */\nexport function assembleProjectTemplateV1(\n walk: TemplateExportWalk,\n kindProvider: BlockKindProvider,\n specProvider: BlockPackSpecProvider,\n): { document: ProjectTemplateV1; problems: readonly TemplateExportProblem[] } {\n const problems: TemplateExportProblem[] = [...walk.problems];\n const blocks: ProjectTemplateV1Entry[] = [];\n\n for (const entry of walk.entries) {\n const kind = kindProvider(entry.blockId);\n\n if (kind === undefined) {\n problems.push({\n blockId: entry.blockId,\n error:\n \"Block declares no kind, so it cannot be written to a template: an entry's kind \" +\n \"carries the params contract the entry is typed against\",\n });\n continue;\n }\n\n let selector: BlockKindSelectorReference;\n try {\n // Widening validates, and therefore throws — which is why it happens here\n // and not where the reference is read: every read site sits inside a\n // recomputed project overview, where one malformed stored reference must not\n // be able to break unrelated blocks.\n selector = kindReferenceToSelectorReference(kind);\n } catch (e) {\n problems.push({\n blockId: entry.blockId,\n error: `Block's stored kind reference is malformed: ${e instanceof Error ? e.message : String(e)}`,\n });\n continue;\n }\n\n const spec = specProvider(entry.blockId);\n const location = spec === undefined ? undefined : locationOf(spec);\n\n blocks.push({\n id: entry.blockId,\n kind: selector,\n params: entry.params,\n ...(location !== undefined ? { location } : {}),\n });\n }\n\n // References are not examined. A project's structure is topological by construction, so an\n // entry cannot legally reference one below it — and checking would mean reading the params,\n // which only the block that wrote them can do.\n const document: ProjectTemplateV1 = { schema: PROJECT_TEMPLATE_SCHEMA_V1, blocks };\n\n return { document, problems };\n}\n\n/**\n * Render a template document to YAML text.\n *\n * Two non-default emitter settings, both about the file being read by someone\n * else's code:\n *\n * - **No line folding.** A wrapped scalar still parses, but it makes a diff between\n * two exported templates unreadable, which is most of the reason to prefer YAML\n * over JSON here.\n * - **Quote as if the reader were YAML 1.1**, while still parsing as 1.2. YAML 1.2\n * dropped `yes`/`no`/`on`/`off`/`y`/`n` as booleans and dropped sexagesimal\n * integers, so a 1.2 emitter leaves a params value of `\"yes\"` or `\"1:30\"` bare —\n * which a 1.1 reader (PyYAML's default, and Go's yaml.v2) turns into `true` and\n * `90`. A template is a contract for a second implementation, so the safe\n * combination is to quote against the stricter ruleset and read with the looser\n * one: a quoted scalar means the same thing under both. This adds no `%YAML`\n * directive — it only changes which scalars get quotes.\n */\nexport function stringifyProjectTemplateV1(document: ProjectTemplateV1): string {\n return YAML.stringify(document, { lineWidth: 0, version: \"1.1\" });\n}\n\n/**\n * Export a project as `template-v1` YAML, or report every reason it cannot be.\n *\n * All-or-nothing on purpose. A partial template silently drops blocks and the\n * surviving entries may reference the dropped ones, so what looks like a\n * successful export would produce a project missing pieces the user never chose\n * to leave out. Reporting everything at once instead of failing on the first\n * problem is the other half of that: fixing an export should take one pass.\n *\n * @param structure The project structure, which supplies both membership and order\n * @param paramsProvider A block's derived template params, in live form\n * @param kindProvider A block's exact kind reference, read from its stored config\n * @param specProvider A block's origin spec, read from the same stored container\n */\nexport function exportProjectAsTemplateV1(\n structure: ProjectStructure,\n paramsProvider: (blockId: string) => TemplateParamsResult | undefined,\n kindProvider: BlockKindProvider,\n specProvider: BlockPackSpecProvider,\n): ProjectTemplateExportOutcome {\n const walk = walkProjectForTemplateExport(structure, paramsProvider);\n const { document, problems } = assembleProjectTemplateV1(walk, kindProvider, specProvider);\n\n if (problems.length > 0) return { ok: false, problems };\n\n // Export must emit exactly what import parses, so that is asserted on every\n // export rather than only in tests — running the import-side parser over the\n // document we are about to write is the cheapest possible proof of it. Nothing\n // user-facing is expected to fail here: the kind grammar was checked by the\n // widening above, params were checked to be a mapping by the walk, and the\n // reference rules by the assembler. A throw means a bug in the assembler, with\n // one known exception: a project structure holding two blocks with the same id,\n // which is reachable through the mutator and produces duplicate entry ids.\n parseProjectTemplateV1(document);\n\n return {\n ok: true,\n yaml: stringifyProjectTemplateV1(document),\n document,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAiEA,SAAgB,WAAW,MAA6D;CACtF,QAAQ,KAAK,MAAb;EACE,KAAK,UACH,QAAA,GAAA,SAAA,cAAA,CAAqB,KAAK,MAAM,CAAC,CAAC;EACpC,KAAK,gBACH,OAAO,KAAK;EAId,KAAK;EACL,KAAK;EACL,KAAK,oBACH;CACJ;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,SAAgB,0BACd,MACA,cACA,cAC6E;CAC7E,MAAM,WAAoC,CAAC,GAAG,KAAK,QAAQ;CAC3D,MAAM,SAAmC,CAAC;CAE1C,KAAK,MAAM,SAAS,KAAK,SAAS;EAChC,MAAM,OAAO,aAAa,MAAM,OAAO;EAEvC,IAAI,SAAS,KAAA,GAAW;GACtB,SAAS,KAAK;IACZ,SAAS,MAAM;IACf,OACE;GAEJ,CAAC;GACD;EACF;EAEA,IAAI;EACJ,IAAI;GAKF,YAAA,GAAA,gCAAA,iCAAA,CAA4C,IAAI;EAClD,SAAS,GAAG;GACV,SAAS,KAAK;IACZ,SAAS,MAAM;IACf,OAAO,+CAA+C,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;GACjG,CAAC;GACD;EACF;EAEA,MAAM,OAAO,aAAa,MAAM,OAAO;EACvC,MAAM,WAAW,SAAS,KAAA,IAAY,KAAA,IAAY,WAAW,IAAI;EAEjE,OAAO,KAAK;GACV,IAAI,MAAM;GACV,MAAM;GACN,QAAQ,MAAM;GACd,GAAI,aAAa,KAAA,IAAY,EAAE,SAAS,IAAI,CAAC;EAC/C,CAAC;CACH;CAOA,OAAO;EAAE,UAAA;GAF6B,QAAQA,gCAAAA;GAA4B;EAE1D;EAAG;CAAS;AAC9B;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,2BAA2B,UAAqC;CAC9E,OAAOC,KAAAA,QAAK,UAAU,UAAU;EAAE,WAAW;EAAG,SAAS;CAAM,CAAC;AAClE;;;;;;;;;;;;;;;AAgBA,SAAgB,0BACd,WACA,gBACA,cACA,cAC8B;CAE9B,MAAM,EAAE,UAAU,aAAa,0BADlBC,wBAAAA,6BAA6B,WAAW,cACO,GAAG,cAAc,YAAY;CAEzF,IAAI,SAAS,SAAS,GAAG,OAAO;EAAE,IAAI;EAAO;CAAS;CAUtD,CAAA,GAAA,gCAAA,uBAAA,CAAuB,QAAQ;CAE/B,OAAO;EACL,IAAI;EACJ,MAAM,2BAA2B,QAAQ;EACzC;CACF;AACF"}
@@ -0,0 +1,17 @@
1
+ import { TemplateExportProblem } from "./template_export.js";
2
+ import { BlockPackSpec } from "@milaboratories/pl-model-middle-layer";
3
+ import { BlockKindReference, BlockPackLocationReference, ProjectTemplateV1 } from "@milaboratories/pl-model-common";
4
+
5
+ //#region src/model/template_serializer.d.ts
6
+ /** What the caller gets back for a whole project. */
7
+ type ProjectTemplateExportOutcome = {
8
+ readonly ok: true;
9
+ readonly yaml: string; /** The document the YAML was rendered from, already validated. */
10
+ readonly document: ProjectTemplateV1;
11
+ } | {
12
+ readonly ok: false; /** Every block that stands in the way, not just the first. */
13
+ readonly problems: readonly TemplateExportProblem[];
14
+ };
15
+ //#endregion
16
+ export { ProjectTemplateExportOutcome };
17
+ //# sourceMappingURL=template_serializer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template_serializer.d.ts","names":[],"sources":["../../src/model/template_serializer.ts"],"mappings":";;;;;;KAoCY,4BAAA;EAAA,SAEG,EAAA;EAAA,SACA,IAAA,UAOwC;EAAA,SALxC,QAAA,EAAU,iBAAA;AAAA;EAAA,SAGV,EAAA;WAEA,QAAA,WAAmB,qBAAqB;AAAA"}