@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.
- package/dist/block_registry/index.cjs +3 -0
- package/dist/block_registry/index.d.ts +2 -1
- package/dist/block_registry/index.js +2 -1
- package/dist/block_registry/location_provider.cjs +117 -0
- package/dist/block_registry/location_provider.cjs.map +1 -0
- package/dist/block_registry/location_provider.js +114 -0
- package/dist/block_registry/location_provider.js.map +1 -0
- package/dist/block_registry/registry.cjs +13 -0
- package/dist/block_registry/registry.cjs.map +1 -1
- package/dist/block_registry/registry.d.ts +14 -1
- package/dist/block_registry/registry.d.ts.map +1 -1
- package/dist/block_registry/registry.js +13 -0
- package/dist/block_registry/registry.js.map +1 -1
- package/dist/block_registry/template_provider.cjs +104 -0
- package/dist/block_registry/template_provider.cjs.map +1 -0
- package/dist/block_registry/template_provider.d.ts +62 -0
- package/dist/block_registry/template_provider.d.ts.map +1 -0
- package/dist/block_registry/template_provider.js +103 -0
- package/dist/block_registry/template_provider.js.map +1 -0
- package/dist/block_registry/watcher.cjs +3 -3
- package/dist/block_registry/watcher.js +2 -2
- package/dist/dev_env/util.js +2 -2
- package/dist/dev_env/util.js.map +1 -1
- package/dist/index.cjs +10 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/js_render/computable_context.cjs +1 -1
- package/dist/js_render/computable_context.js +1 -1
- package/dist/middle_layer/driver_kit.d.ts +1 -1
- package/dist/middle_layer/middle_layer.cjs +144 -2
- package/dist/middle_layer/middle_layer.cjs.map +1 -1
- package/dist/middle_layer/middle_layer.d.ts +67 -1
- package/dist/middle_layer/middle_layer.d.ts.map +1 -1
- package/dist/middle_layer/middle_layer.js +146 -4
- package/dist/middle_layer/middle_layer.js.map +1 -1
- package/dist/middle_layer/project.cjs +1 -0
- package/dist/middle_layer/project.cjs.map +1 -1
- package/dist/middle_layer/project.d.ts +2 -2
- package/dist/middle_layer/project.js +4 -4
- package/dist/middle_layer/project.js.map +1 -1
- package/dist/middle_layer/sharing_list.d.ts +1 -1
- package/dist/model/args.cjs +9 -28
- package/dist/model/args.cjs.map +1 -1
- package/dist/model/args.js +9 -28
- package/dist/model/args.js.map +1 -1
- package/dist/model/index.cjs +7 -0
- package/dist/model/index.d.ts +4 -1
- package/dist/model/index.js +4 -1
- package/dist/model/project_helper.cjs +111 -0
- package/dist/model/project_helper.cjs.map +1 -1
- package/dist/model/project_helper.d.ts +76 -1
- package/dist/model/project_helper.d.ts.map +1 -1
- package/dist/model/project_helper.js +111 -0
- package/dist/model/project_helper.js.map +1 -1
- package/dist/model/project_model.d.ts +1 -1
- package/dist/model/sharing_model.d.ts +1 -1
- package/dist/model/template_apply.cjs +67 -0
- package/dist/model/template_apply.cjs.map +1 -0
- package/dist/model/template_apply.d.ts +50 -0
- package/dist/model/template_apply.d.ts.map +1 -0
- package/dist/model/template_apply.js +64 -0
- package/dist/model/template_apply.js.map +1 -0
- package/dist/model/template_export.cjs +82 -0
- package/dist/model/template_export.cjs.map +1 -0
- package/dist/model/template_export.d.ts +9 -0
- package/dist/model/template_export.d.ts.map +1 -0
- package/dist/model/template_export.js +82 -0
- package/dist/model/template_export.js.map +1 -0
- package/dist/model/template_parser.cjs +74 -0
- package/dist/model/template_parser.cjs.map +1 -0
- package/dist/model/template_parser.d.ts +43 -0
- package/dist/model/template_parser.d.ts.map +1 -0
- package/dist/model/template_parser.js +72 -0
- package/dist/model/template_parser.js.map +1 -0
- package/dist/model/template_resolve.cjs +134 -0
- package/dist/model/template_resolve.cjs.map +1 -0
- package/dist/model/template_resolve.d.ts +193 -0
- package/dist/model/template_resolve.d.ts.map +1 -0
- package/dist/model/template_resolve.js +133 -0
- package/dist/model/template_resolve.js.map +1 -0
- package/dist/model/template_serializer.cjs +162 -0
- package/dist/model/template_serializer.cjs.map +1 -0
- package/dist/model/template_serializer.d.ts +17 -0
- package/dist/model/template_serializer.d.ts.map +1 -0
- package/dist/model/template_serializer.js +157 -0
- package/dist/model/template_serializer.js.map +1 -0
- package/dist/mutator/block-pack/block_pack.cjs +1 -1
- package/dist/mutator/block-pack/block_pack.js +1 -1
- package/dist/mutator/migration.cjs +1 -1
- package/dist/mutator/migration.js +1 -1
- package/dist/mutator/project.cjs +35 -5
- package/dist/mutator/project.cjs.map +1 -1
- package/dist/mutator/project.d.ts.map +1 -1
- package/dist/mutator/project.js +35 -5
- package/dist/mutator/project.js.map +1 -1
- package/dist/mutator/template_construct.cjs +77 -0
- package/dist/mutator/template_construct.cjs.map +1 -0
- package/dist/mutator/template_construct.js +77 -0
- package/dist/mutator/template_construct.js.map +1 -0
- package/dist/network_check/template.js +3 -3
- package/dist/network_check/template.js.map +1 -1
- package/package.json +17 -17
- package/src/block_registry/index.ts +1 -0
- package/src/block_registry/location_provider.test.ts +236 -0
- package/src/block_registry/location_provider.ts +124 -0
- package/src/block_registry/registry.ts +22 -0
- package/src/block_registry/template_provider.test.ts +275 -0
- package/src/block_registry/template_provider.ts +168 -0
- package/src/middle_layer/middle_layer.ts +192 -1
- package/src/middle_layer/project.ts +1 -1
- package/src/model/args.test.ts +26 -1
- package/src/model/args.ts +16 -31
- package/src/model/index.ts +21 -0
- package/src/model/project_helper.ts +238 -0
- package/src/model/project_helper_params_init.test.ts +249 -0
- package/src/model/template_apply.test.ts +65 -0
- package/src/model/template_apply.ts +128 -0
- package/src/model/template_export.test.ts +255 -0
- package/src/model/template_export.ts +136 -0
- package/src/model/template_parser.test.ts +264 -0
- package/src/model/template_parser.ts +108 -0
- package/src/model/template_resolve.test.ts +397 -0
- package/src/model/template_resolve.ts +346 -0
- package/src/model/template_serializer.test.ts +398 -0
- package/src/model/template_serializer.ts +232 -0
- package/src/model/template_serializer_fixtures.test.ts +274 -0
- package/src/mutator/project-v3.test.ts +11 -11
- package/src/mutator/project.ts +92 -5
- package/src/mutator/template_construct.test.ts +456 -0
- package/src/mutator/template_construct.ts +150 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import YAML from "yaml";
|
|
2
|
+
import type { ProjectTemplateV1, TemplateParseIssue } from "@milaboratories/pl-model-common";
|
|
3
|
+
import {
|
|
4
|
+
PROJECT_TEMPLATE_SCHEMA_V1,
|
|
5
|
+
formatTemplateParseIssue,
|
|
6
|
+
readProjectTemplateV1,
|
|
7
|
+
} from "@milaboratories/pl-model-common";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A template file read, or why it could not be.
|
|
11
|
+
*
|
|
12
|
+
* A single message rather than a list of per-entry problems: until the document
|
|
13
|
+
* parses there are no entries to attach anything to, and a file that does not parse
|
|
14
|
+
* has one problem — it is not a template. Per-entry reporting starts at the stage
|
|
15
|
+
* after this one. The message may span several lines when the file has several
|
|
16
|
+
* fixable issues, so that fixing it takes one pass.
|
|
17
|
+
*/
|
|
18
|
+
export type TemplateParseOutcome =
|
|
19
|
+
| { readonly ok: true; readonly document: ProjectTemplateV1 }
|
|
20
|
+
| { readonly ok: false; readonly error: string };
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read `template-v1` YAML text into a document.
|
|
24
|
+
*
|
|
25
|
+
* The text half of the import direction, mirroring `stringifyProjectTemplateV1` on the
|
|
26
|
+
* export side — and, like it, living here rather than in `pl-model-common`, which ships
|
|
27
|
+
* in every block-model and UI bundle and takes no `yaml` dependency. The document half
|
|
28
|
+
* is `parseProjectTemplateV1`, shared with export so that what export writes is by
|
|
29
|
+
* construction what import reads.
|
|
30
|
+
*
|
|
31
|
+
* JSON is accepted for free: every JSON document is also YAML.
|
|
32
|
+
*
|
|
33
|
+
* **Read as YAML 1.2**, while the emitter quotes as if for 1.1. That asymmetry is
|
|
34
|
+
* deliberate on both ends: quoting against the stricter ruleset makes a file we write
|
|
35
|
+
* mean the same thing to any reader, and reading with the looser one means a bare
|
|
36
|
+
* `yes` or `1:30` in a hand-written file stays the string it looks like instead of
|
|
37
|
+
* silently becoming `true` or `90`.
|
|
38
|
+
*
|
|
39
|
+
* Nothing here checks that references point anywhere or that a kind can be resolved —
|
|
40
|
+
* those are later stages, and both need more than the file to answer.
|
|
41
|
+
*/
|
|
42
|
+
export function parseProjectTemplateV1Yaml(text: string): TemplateParseOutcome {
|
|
43
|
+
let value: unknown;
|
|
44
|
+
try {
|
|
45
|
+
value = YAML.parse(text);
|
|
46
|
+
} catch (e) {
|
|
47
|
+
// The library's message already carries "at line L, column C"; what follows it is
|
|
48
|
+
// a code frame of the offending lines, dropped here because whoever is fixing the
|
|
49
|
+
// file has it open and the position is what they need.
|
|
50
|
+
const first = e instanceof Error ? e.message.split("\n")[0] : String(e);
|
|
51
|
+
return { ok: false, error: `The file is not valid YAML: ${first}` };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Empty input, blank lines and a file of nothing but comments all parse to null.
|
|
55
|
+
// Left to the schema, this reads as "expected object, received null", which says
|
|
56
|
+
// nothing about the actual mistake.
|
|
57
|
+
if (value === null || value === undefined) {
|
|
58
|
+
return { ok: false, error: "The file is empty." };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
62
|
+
return {
|
|
63
|
+
ok: false,
|
|
64
|
+
error:
|
|
65
|
+
"The file does not describe a template: expected a mapping with 'schema' and " +
|
|
66
|
+
"'blocks' at the top level.",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Checked ahead of the document reader because it is the likeliest mistake by far — the
|
|
71
|
+
// wrong file was picked — and deserves a sentence about that rather than one about a field
|
|
72
|
+
// whose value is not what was expected.
|
|
73
|
+
const marker = (value as { schema?: unknown }).schema;
|
|
74
|
+
if (marker !== PROJECT_TEMPLATE_SCHEMA_V1) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
error:
|
|
78
|
+
marker === undefined
|
|
79
|
+
? `This file has no 'schema' field, so it is not a ${PROJECT_TEMPLATE_SCHEMA_V1} template.`
|
|
80
|
+
: `This file says it is '${String(marker)}', not ${PROJECT_TEMPLATE_SCHEMA_V1}.`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const outcome = readProjectTemplateV1(value);
|
|
85
|
+
return outcome.ok
|
|
86
|
+
? { ok: true, document: outcome.document }
|
|
87
|
+
: { ok: false, error: describeIssues(outcome.issues) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Turn the reader's issues into something a person can act on, all of them at once.
|
|
92
|
+
*
|
|
93
|
+
* The per-issue line comes from `formatTemplateParseIssue`, shared with the error the reader
|
|
94
|
+
* throws, so a problem reads the same whether it reached a person through this outcome or
|
|
95
|
+
* through a log. What is added here is the headline, which is the one part that belongs to
|
|
96
|
+
* this stage: it says the FILE is at fault, which a caller that has one can say and the
|
|
97
|
+
* document reader cannot.
|
|
98
|
+
*/
|
|
99
|
+
function describeIssues(issues: readonly TemplateParseIssue[]): string {
|
|
100
|
+
const lines = issues.map((issue) => `- ${formatTemplateParseIssue(issue)}`);
|
|
101
|
+
|
|
102
|
+
const headline =
|
|
103
|
+
lines.length === 1
|
|
104
|
+
? "The template file has a problem:"
|
|
105
|
+
: `The template file has ${lines.length} problems:`;
|
|
106
|
+
|
|
107
|
+
return [headline, ...lines].join("\n");
|
|
108
|
+
}
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import type {
|
|
3
|
+
BlockKindSelectorReference,
|
|
4
|
+
BlockPackLocationReference,
|
|
5
|
+
BlockPackLocatorOverride,
|
|
6
|
+
BlockPackReference,
|
|
7
|
+
ProjectTemplateV1,
|
|
8
|
+
ProjectTemplateV1Entry,
|
|
9
|
+
} from "@milaboratories/pl-model-common";
|
|
10
|
+
import { PROJECT_TEMPLATE_SCHEMA_V1 } from "@milaboratories/pl-model-common";
|
|
11
|
+
import type { BlockPackId, BlockPackSpec } from "@milaboratories/pl-model-middle-layer";
|
|
12
|
+
import type {
|
|
13
|
+
BlockPackProvider,
|
|
14
|
+
ExactResolution,
|
|
15
|
+
KindResolution,
|
|
16
|
+
LocationResolution,
|
|
17
|
+
} from "./template_resolve";
|
|
18
|
+
import { parseBlockPackName, resolveTemplateEntries } from "./template_resolve";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolution stage, driven against a fake provider.
|
|
22
|
+
*
|
|
23
|
+
* No registry, no network, no project: what registry to read and how is on the far
|
|
24
|
+
* side of `BlockPackProvider`, and everything these tests cover — which path an entry
|
|
25
|
+
* takes, what each failure tells the reader, that every entry is attempted — is on
|
|
26
|
+
* this side.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const KIND = "@platforma-open/milaboratories.demo.kind@^1.0.0" as BlockKindSelectorReference;
|
|
30
|
+
|
|
31
|
+
const specFor = (name: string): BlockPackSpec => ({
|
|
32
|
+
type: "from-registry-v2",
|
|
33
|
+
registryUrl: "https://block.registry.platforma.bio/releases",
|
|
34
|
+
id: { organization: "milaboratories", name, version: "1.2.3" },
|
|
35
|
+
channel: "stable",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/** A successful resolution of `name`, titled the way a registry would title it. */
|
|
39
|
+
const foundBlock = (name: string) => ({
|
|
40
|
+
ok: true as const,
|
|
41
|
+
spec: specFor(name),
|
|
42
|
+
title: `The ${name} Block`,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* An entry as the parser hands it over, with the locator override left to the caller.
|
|
47
|
+
*
|
|
48
|
+
* `extra` takes {@link BlockPackLocatorOverride} rather than a `Partial<Pick<…>>` of the
|
|
49
|
+
* entry: the latter would let a test hand over both `block` and `location`, which is a
|
|
50
|
+
* document the type forbids and the parser rejects.
|
|
51
|
+
*/
|
|
52
|
+
const entry = (
|
|
53
|
+
id: string,
|
|
54
|
+
extra: { kind?: BlockKindSelectorReference } & BlockPackLocatorOverride = {},
|
|
55
|
+
): ProjectTemplateV1Entry => ({ id, kind: KIND, params: {}, ...extra });
|
|
56
|
+
|
|
57
|
+
const documentOf = (...blocks: ProjectTemplateV1Entry[]): ProjectTemplateV1 => ({
|
|
58
|
+
schema: PROJECT_TEMPLATE_SCHEMA_V1,
|
|
59
|
+
blocks,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/** A provider whose three answers are fixed, recording what it was asked. */
|
|
63
|
+
function fakeProvider(answers: {
|
|
64
|
+
byKind?: KindResolution;
|
|
65
|
+
byExactVersion?: ExactResolution;
|
|
66
|
+
byLocation?: LocationResolution;
|
|
67
|
+
}): {
|
|
68
|
+
provider: BlockPackProvider;
|
|
69
|
+
kindCalls: { kind: string; allowUnstable: boolean }[];
|
|
70
|
+
exactCalls: BlockPackId[];
|
|
71
|
+
locationCalls: string[];
|
|
72
|
+
} {
|
|
73
|
+
const kindCalls: { kind: string; allowUnstable: boolean }[] = [];
|
|
74
|
+
const exactCalls: BlockPackId[] = [];
|
|
75
|
+
const locationCalls: string[] = [];
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
kindCalls,
|
|
79
|
+
exactCalls,
|
|
80
|
+
locationCalls,
|
|
81
|
+
provider: {
|
|
82
|
+
byKind: (kind, options) => {
|
|
83
|
+
kindCalls.push({ kind, allowUnstable: options.allowUnstable });
|
|
84
|
+
return Promise.resolve(answers.byKind ?? foundBlock("resolved"));
|
|
85
|
+
},
|
|
86
|
+
byExactVersion: (id) => {
|
|
87
|
+
exactCalls.push(id);
|
|
88
|
+
return Promise.resolve(answers.byExactVersion ?? foundBlock("pinned"));
|
|
89
|
+
},
|
|
90
|
+
byLocation: (location) => {
|
|
91
|
+
locationCalls.push(location);
|
|
92
|
+
return Promise.resolve(answers.byLocation ?? foundBlock("located"));
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The concrete kind the located block declares, matching {@link KIND}'s selector. */
|
|
99
|
+
|
|
100
|
+
const LOCATION = "file:///Users/dev/blocks/demo/block" as BlockPackLocationReference;
|
|
101
|
+
|
|
102
|
+
const resolve = (document: ProjectTemplateV1, provider: BlockPackProvider, allowUnstable = false) =>
|
|
103
|
+
resolveTemplateEntries(document, provider, { allowUnstable });
|
|
104
|
+
|
|
105
|
+
describe("resolveTemplateEntries", () => {
|
|
106
|
+
test("resolves each entry's kind, in file order", async () => {
|
|
107
|
+
const { provider, kindCalls } = fakeProvider({});
|
|
108
|
+
|
|
109
|
+
const outcome = await resolve(documentOf(entry("a"), entry("b")), provider);
|
|
110
|
+
|
|
111
|
+
expect(kindCalls).toHaveLength(2);
|
|
112
|
+
expect(outcome.problems).toEqual([]);
|
|
113
|
+
expect(outcome.resolved.map((r) => r.entryId)).toEqual(["a", "b"]);
|
|
114
|
+
expect(outcome.resolved[0].spec).toEqual(specFor("resolved"));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("the apply-wide unstable flag reaches every entry", async () => {
|
|
118
|
+
// One decision for the whole file, not per entry: a template that resolved some
|
|
119
|
+
// entries to stable blocks and others to pre-releases would be unreproducible in
|
|
120
|
+
// a way the file itself does not record.
|
|
121
|
+
const { provider, kindCalls } = fakeProvider({});
|
|
122
|
+
|
|
123
|
+
await resolve(documentOf(entry("a"), entry("b")), provider, true);
|
|
124
|
+
|
|
125
|
+
expect(kindCalls.map((c) => c.allowUnstable)).toEqual([true, true]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("a pinned version bypasses kind resolution entirely", async () => {
|
|
129
|
+
const { provider, kindCalls, exactCalls } = fakeProvider({});
|
|
130
|
+
const pinned = entry("a", {
|
|
131
|
+
block: "@platforma-open/milaboratories.demo@2.0.1" as BlockPackReference,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const outcome = await resolve(documentOf(pinned), provider);
|
|
135
|
+
|
|
136
|
+
expect(kindCalls).toEqual([]);
|
|
137
|
+
expect(exactCalls).toEqual([
|
|
138
|
+
{ organization: "milaboratories", name: "demo", version: "2.0.1" },
|
|
139
|
+
]);
|
|
140
|
+
expect(outcome.resolved[0]).toEqual({
|
|
141
|
+
entryId: "a",
|
|
142
|
+
spec: specFor("pinned"),
|
|
143
|
+
title: "The pinned Block",
|
|
144
|
+
pinned: true,
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("both paths produce the same kind of result", async () => {
|
|
149
|
+
// Everything downstream of resolution — preparation, construction — must not care
|
|
150
|
+
// which way an entry got its block.
|
|
151
|
+
const { provider } = fakeProvider({});
|
|
152
|
+
|
|
153
|
+
const outcome = await resolve(
|
|
154
|
+
documentOf(
|
|
155
|
+
entry("a"),
|
|
156
|
+
entry("b", { block: "@platforma-open/milaboratories.demo@2.0.1" as BlockPackReference }),
|
|
157
|
+
),
|
|
158
|
+
provider,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
expect(outcome.resolved.map((r) => Object.keys(r).sort())).toEqual([
|
|
162
|
+
["entryId", "pinned", "spec", "title"],
|
|
163
|
+
["entryId", "pinned", "spec", "title"],
|
|
164
|
+
]);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("the block's published title comes back with it, whichever route it took", async () => {
|
|
168
|
+
// It becomes the created block's label, and this is the only stage that can know it:
|
|
169
|
+
// a prepared block pack carries the model, the workflow and the frontend, none of
|
|
170
|
+
// which names the block. Deriving it from the entry would put a UUID there, since an
|
|
171
|
+
// exported template names its entries by the source project's block ids.
|
|
172
|
+
const { provider } = fakeProvider({});
|
|
173
|
+
|
|
174
|
+
const outcome = await resolve(
|
|
175
|
+
documentOf(
|
|
176
|
+
entry("aaaaaaaa-0000-4000-8000-000000000001"),
|
|
177
|
+
entry("bbbbbbbb-0000-4000-8000-000000000002", {
|
|
178
|
+
block: "@platforma-open/milaboratories.demo@2.0.1" as BlockPackReference,
|
|
179
|
+
}),
|
|
180
|
+
),
|
|
181
|
+
provider,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
expect(outcome.resolved.map((r) => r.title)).toEqual([
|
|
185
|
+
"The resolved Block",
|
|
186
|
+
"The pinned Block",
|
|
187
|
+
]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("every entry is attempted, and every failure collected", async () => {
|
|
191
|
+
// Fixing an unapplicable file should take one pass, not one pass per bad entry.
|
|
192
|
+
const { provider } = fakeProvider({ byKind: { ok: false, reason: "no-implementation" } });
|
|
193
|
+
|
|
194
|
+
const outcome = await resolve(documentOf(entry("a"), entry("b"), entry("c")), provider);
|
|
195
|
+
|
|
196
|
+
expect(outcome.problems.map((p) => p.entryId)).toEqual(["a", "b", "c"]);
|
|
197
|
+
expect(outcome.resolved).toEqual([]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("a document that half resolves reports both halves", async () => {
|
|
201
|
+
// `resolved` being shorter than the document is the signal that it must not be
|
|
202
|
+
// applied — resolution is all-or-nothing for the caller, not per entry.
|
|
203
|
+
const provider: BlockPackProvider = {
|
|
204
|
+
byKind: () => Promise.resolve(foundBlock("resolved")),
|
|
205
|
+
byExactVersion: () => Promise.resolve({ ok: false, reason: "no-such-block-version" }),
|
|
206
|
+
byLocation: () => Promise.resolve({ ok: false, reason: "not-found" }),
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const outcome = await resolve(
|
|
210
|
+
documentOf(
|
|
211
|
+
entry("a"),
|
|
212
|
+
entry("b", { block: "@platforma-open/milaboratories.gone@9.9.9" as BlockPackReference }),
|
|
213
|
+
),
|
|
214
|
+
provider,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
expect(outcome.resolved.map((r) => r.entryId)).toEqual(["a"]);
|
|
218
|
+
expect(outcome.problems.map((p) => p.entryId)).toEqual(["b"]);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("an empty document resolves to nothing at all", async () => {
|
|
222
|
+
const { provider, kindCalls } = fakeProvider({});
|
|
223
|
+
|
|
224
|
+
expect(await resolve(documentOf(), provider)).toEqual({ resolved: [], problems: [] });
|
|
225
|
+
expect(kindCalls).toEqual([]);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe("an entry that says where its block is", () => {
|
|
230
|
+
test("reads that place, and consults no registry at all", async () => {
|
|
231
|
+
const { provider, kindCalls, exactCalls, locationCalls } = fakeProvider({});
|
|
232
|
+
|
|
233
|
+
const outcome = await resolve(documentOf(entry("a", { location: LOCATION })), provider);
|
|
234
|
+
|
|
235
|
+
expect(locationCalls).toEqual([LOCATION]);
|
|
236
|
+
expect(kindCalls).toEqual([]);
|
|
237
|
+
expect(exactCalls).toEqual([]);
|
|
238
|
+
expect(outcome.problems).toEqual([]);
|
|
239
|
+
expect(outcome.resolved[0]).toEqual({
|
|
240
|
+
entryId: "a",
|
|
241
|
+
spec: specFor("located"),
|
|
242
|
+
title: "The located Block",
|
|
243
|
+
pinned: true,
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("a located entry looks like any other downstream", async () => {
|
|
248
|
+
// Preparation and construction must not be able to tell how an entry found its
|
|
249
|
+
// block — that convergence is what lets an unpublished block travel the same path.
|
|
250
|
+
const { provider } = fakeProvider({});
|
|
251
|
+
|
|
252
|
+
const outcome = await resolve(
|
|
253
|
+
documentOf(entry("a"), entry("b", { location: LOCATION })),
|
|
254
|
+
provider,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
expect(outcome.resolved.map((r) => Object.keys(r).sort())).toEqual([
|
|
258
|
+
["entryId", "pinned", "spec", "title"],
|
|
259
|
+
["entryId", "pinned", "spec", "title"],
|
|
260
|
+
]);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("the location wins over a kind, which is the point of naming a place", async () => {
|
|
264
|
+
const { provider, kindCalls, locationCalls } = fakeProvider({});
|
|
265
|
+
|
|
266
|
+
await resolve(documentOf(entry("a", { location: LOCATION })), provider);
|
|
267
|
+
|
|
268
|
+
expect(locationCalls).toHaveLength(1);
|
|
269
|
+
expect(kindCalls).toEqual([]);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe("what each failure tells the reader", () => {
|
|
273
|
+
const messageFor = async (answer: LocationResolution) => {
|
|
274
|
+
const { provider } = fakeProvider({ byLocation: answer });
|
|
275
|
+
const outcome = await resolve(documentOf(entry("a", { location: LOCATION })), provider);
|
|
276
|
+
return outcome.problems[0].error;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
test("a scheme this application cannot read says so, and names what it can", async () => {
|
|
280
|
+
// Distinct from "nothing there": the file may be perfectly correct and simply
|
|
281
|
+
// written for a consumer that fetches more than this one does.
|
|
282
|
+
const message = await messageFor({ ok: false, reason: "unsupported-scheme" });
|
|
283
|
+
|
|
284
|
+
expect(message).toMatch(/cannot read/);
|
|
285
|
+
expect(message).toContain("file:");
|
|
286
|
+
expect(message).toContain(LOCATION);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("a missing folder names the reason a pinned template travels badly", async () => {
|
|
290
|
+
const message = await messageFor({ ok: false, reason: "not-found" });
|
|
291
|
+
|
|
292
|
+
expect(message).toMatch(/Nothing is at/);
|
|
293
|
+
expect(message).toMatch(/only works on the machine/);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("a folder that is not a block says what was looked for", async () => {
|
|
297
|
+
// Reachable by pointing one directory off, so the message has to be actionable
|
|
298
|
+
// rather than just negative.
|
|
299
|
+
const message = await messageFor({ ok: false, reason: "not-a-block" });
|
|
300
|
+
|
|
301
|
+
expect(message).toMatch(/is not a block/);
|
|
302
|
+
expect(message).toMatch(/package\.json/);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe("what each failure tells the reader", () => {
|
|
308
|
+
const messageFor = async (answer: KindResolution) => {
|
|
309
|
+
const { provider } = fakeProvider({ byKind: answer });
|
|
310
|
+
const outcome = await resolve(documentOf(entry("a")), provider);
|
|
311
|
+
return outcome.problems[0].error;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
test("no matching kind version points at the file or the registry", async () => {
|
|
315
|
+
const message = await messageFor({ ok: false, reason: "no-matching-kind-version" });
|
|
316
|
+
|
|
317
|
+
// The kind's own name, not just the raw reference: the reference is what they
|
|
318
|
+
// wrote, the name is what they look for in a registry.
|
|
319
|
+
expect(message).toContain("@platforma-open/milaboratories.demo.kind");
|
|
320
|
+
expect(message).toContain(KIND);
|
|
321
|
+
expect(message).toMatch(/Check the version, or update the block registry/);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("no implementation says nothing can be installed yet", async () => {
|
|
325
|
+
const message = await messageFor({ ok: false, reason: "no-implementation" });
|
|
326
|
+
|
|
327
|
+
expect(message).toMatch(/no block implementing/);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("only unstable implementations names the one action that needs no edit", async () => {
|
|
331
|
+
// This is the reason the allow-unstable checkbox exists, and the only failure a
|
|
332
|
+
// user can clear without touching the file.
|
|
333
|
+
const message = await messageFor({ ok: false, reason: "no-stable-implementation" });
|
|
334
|
+
|
|
335
|
+
expect(message).toMatch(/pre-release/);
|
|
336
|
+
expect(message).toMatch(/Import again with unstable versions allowed/);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("a missing pinned version offers both ways out", async () => {
|
|
340
|
+
// Correct the pin, or drop it — the second one is easy to miss, and is usually
|
|
341
|
+
// what the reader wants.
|
|
342
|
+
const { provider } = fakeProvider({
|
|
343
|
+
byExactVersion: { ok: false, reason: "no-such-block-version" },
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const outcome = await resolve(
|
|
347
|
+
documentOf(
|
|
348
|
+
entry("a", { block: "@platforma-open/milaboratories.demo@2.0.1" as BlockPackReference }),
|
|
349
|
+
),
|
|
350
|
+
provider,
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
expect(outcome.problems[0].error).toContain("milaboratories/demo");
|
|
354
|
+
expect(outcome.problems[0].error).toContain("2.0.1");
|
|
355
|
+
expect(outcome.problems[0].error).toMatch(/remove it so a version is chosen automatically/);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("an unreadable pinned name is a problem, not a throw", async () => {
|
|
359
|
+
// Reachable from a hand-written file: the document parser checks the
|
|
360
|
+
// `{name}@X.Y.Z` grammar but not that the name carries an organization.
|
|
361
|
+
const { provider } = fakeProvider({});
|
|
362
|
+
|
|
363
|
+
const outcome = await resolve(
|
|
364
|
+
documentOf(entry("a", { block: "@platforma-open/demo@2.0.1" as BlockPackReference })),
|
|
365
|
+
provider,
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
expect(outcome.problems[0].entryId).toBe("a");
|
|
369
|
+
expect(outcome.problems[0].error).toMatch(/Pinned block version is not readable/);
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
describe("parseBlockPackName", () => {
|
|
374
|
+
test("splits the organization out of the npm name", () => {
|
|
375
|
+
// The npm scope and the organization are different, and only the organization
|
|
376
|
+
// reaches the registry.
|
|
377
|
+
expect(parseBlockPackName("@milaboratories/milaboratories.test-download-file")).toEqual({
|
|
378
|
+
organization: "milaboratories",
|
|
379
|
+
name: "test-download-file",
|
|
380
|
+
});
|
|
381
|
+
expect(parseBlockPackName("@platforma-open/milaboratories.mixcr-clonotyping")).toEqual({
|
|
382
|
+
organization: "milaboratories",
|
|
383
|
+
name: "mixcr-clonotyping",
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("splits on the first dot, so a name may contain more", () => {
|
|
388
|
+
expect(parseBlockPackName("@scope/org.a.b")).toEqual({ organization: "org", name: "a.b" });
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test("rejects a name that cannot address a registry", () => {
|
|
392
|
+
expect(() => parseBlockPackName("no-scope.name")).toThrow(/expected '@npm-scope/);
|
|
393
|
+
expect(() => parseBlockPackName("@scope/nodot")).toThrow(/does not separate an organization/);
|
|
394
|
+
expect(() => parseBlockPackName("@scope/.name")).toThrow(/does not separate an organization/);
|
|
395
|
+
expect(() => parseBlockPackName("@scope/org.")).toThrow(/does not separate an organization/);
|
|
396
|
+
});
|
|
397
|
+
});
|