@canonical/pragma-cli 0.29.0 → 0.30.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/README.md +67 -15
- package/dist/pragma +0 -0
- package/package.json +13 -13
- package/src/config/defaultConfig.test.ts +33 -0
- package/src/config/defaultConfig.ts +59 -0
- package/src/config/defaults.json +17 -0
- package/src/config/findProjectConfigPath.test.ts +53 -0
- package/src/config/findProjectConfigPath.ts +36 -0
- package/src/config/index.ts +17 -2
- package/src/config/mergeConfigFileUpdate.ts +58 -0
- package/src/config/parseConfigValues.ts +215 -0
- package/src/config/readConfig.test.ts +104 -13
- package/src/config/readConfig.ts +12 -153
- package/src/config/readConfigLayers.test.ts +107 -0
- package/src/config/readConfigLayers.ts +115 -0
- package/src/config/resolveGlobalConfigPath.ts +13 -0
- package/src/config/resolveWriteConfigPath.test.ts +53 -0
- package/src/config/resolveWriteConfigPath.ts +31 -0
- package/src/config/types.ts +76 -1
- package/src/config/writeConfig.ts +25 -55
- package/src/domains/block/commands/list.ts +5 -90
- package/src/domains/block/commands/lookup.ts +8 -167
- package/src/domains/block/mcp/specs.ts +16 -196
- package/src/domains/block/orchestration/resolveBlockLookup.ts +2 -1
- package/src/domains/block/stories.test.ts +28 -0
- package/src/domains/block/stories.ts +254 -0
- package/src/domains/config/commands/channel.test.ts +14 -3
- package/src/domains/config/commands/channel.ts +11 -7
- package/src/domains/config/commands/configScope.ts +46 -0
- package/src/domains/config/commands/framework.test.ts +18 -1
- package/src/domains/config/commands/framework.ts +13 -7
- package/src/domains/config/commands/show.ts +5 -45
- package/src/domains/config/commands/tier.test.ts +13 -2
- package/src/domains/config/commands/tier.ts +11 -7
- package/src/domains/config/commands/trace.test.ts +19 -2
- package/src/domains/config/commands/trace.ts +6 -3
- package/src/domains/config/formatters/channel.test.ts +20 -6
- package/src/domains/config/formatters/channel.ts +14 -11
- package/src/domains/config/formatters/framework.ts +14 -11
- package/src/domains/config/formatters/show.test.ts +27 -0
- package/src/domains/config/formatters/show.ts +27 -4
- package/src/domains/config/formatters/tier.test.ts +26 -10
- package/src/domains/config/formatters/tier.ts +19 -11
- package/src/domains/config/formatters/trace.ts +7 -7
- package/src/domains/config/mcp/specs.ts +33 -53
- package/src/domains/config/operations/show.test.ts +18 -0
- package/src/domains/config/operations/show.ts +7 -1
- package/src/domains/config/operations/types.ts +7 -0
- package/src/domains/config/stories.ts +42 -0
- package/src/domains/config/tasks/index.ts +1 -0
- package/src/domains/config/tasks/setChannel.ts +13 -28
- package/src/domains/config/tasks/setFramework.ts +11 -30
- package/src/domains/config/tasks/setTier.ts +10 -29
- package/src/domains/config/tasks/setTrace.ts +10 -28
- package/src/domains/config/tasks/writeConfigField.ts +41 -0
- package/src/domains/create/commands/component.ts +5 -5
- package/src/domains/doctor/operations/checks/checkConfigFile.ts +19 -5
- package/src/domains/doctor/operations/checks/checkMcpCommands.ts +130 -0
- package/src/domains/doctor/operations/checks/checkMcpConfigured.ts +1 -1
- package/src/domains/doctor/operations/checks/checkPackageRefs.ts +2 -14
- package/src/domains/doctor/operations/checks/checkSkillsSymlinked.ts +1 -1
- package/src/domains/doctor/operations/checks/index.ts +1 -0
- package/src/domains/doctor/operations/checks.test.ts +158 -7
- package/src/domains/doctor/operations/runChecks.test.ts +15 -6
- package/src/domains/doctor/operations/runChecks.ts +2 -0
- package/src/domains/graph/helpers/resolveUri.test.ts +50 -0
- package/src/domains/graph/helpers/resolveUri.ts +28 -21
- package/src/domains/graphql/commands/build.test.ts +5 -3
- package/src/domains/graphql/commands/build.ts +8 -16
- package/src/domains/graphql/commands/check.test.ts +5 -3
- package/src/domains/graphql/commands/check.ts +7 -13
- package/src/domains/graphql/commands/index.ts +1 -0
- package/src/domains/graphql/commands/serve.test.ts +40 -0
- package/src/domains/graphql/commands/serve.ts +131 -0
- package/src/domains/graphql/constants.ts +10 -0
- package/src/domains/graphql/helpers/gatherSources.test.ts +92 -0
- package/src/domains/graphql/helpers/gatherSources.ts +58 -0
- package/src/domains/graphql/helpers/index.ts +2 -0
- package/src/domains/graphql/helpers/routeGraphql.test.ts +43 -0
- package/src/domains/graphql/helpers/routeGraphql.ts +28 -0
- package/src/domains/graphql/index.ts +9 -6
- package/src/domains/graphql/operations/compileSchema.test.ts +21 -45
- package/src/domains/graphql/operations/compileSchema.ts +23 -25
- package/src/domains/graphql/operations/createSchemaServer.test.ts +50 -0
- package/src/domains/graphql/operations/createSchemaServer.ts +75 -0
- package/src/domains/graphql/operations/index.ts +5 -0
- package/src/domains/info/operations/collectInfo.test.ts +14 -3
- package/src/domains/info/operations/collectInfo.ts +12 -15
- package/src/domains/llm/capabilities.test.ts +4 -4
- package/src/domains/llm/data/toolCatalog.ts +6 -0
- package/src/domains/modifier/commands/list.ts +5 -38
- package/src/domains/modifier/commands/lookup.ts +8 -79
- package/src/domains/modifier/mcp/specs.ts +15 -94
- package/src/domains/modifier/orchestration/resolveModifierLookup.ts +2 -1
- package/src/domains/modifier/stories.test.ts +20 -0
- package/src/domains/modifier/stories.ts +77 -0
- package/src/domains/ontology/commands/list.ts +5 -58
- package/src/domains/ontology/commands/show.ts +6 -58
- package/src/domains/ontology/formatters/show.ts +7 -1
- package/src/domains/ontology/mcp/specs.ts +7 -84
- package/src/domains/ontology/stories.test.ts +36 -0
- package/src/domains/ontology/stories.ts +108 -0
- package/src/domains/refs/operations/updateRefs.ts +2 -37
- package/src/domains/setup/commands/skills.test.ts +6 -2
- package/src/domains/setup/commands/skills.ts +6 -2
- package/src/domains/setup/helpers/runSetupTask.test.ts +148 -150
- package/src/domains/setup/helpers/runSetupTask.ts +62 -70
- package/src/domains/setup/operations/setupMcp.test.ts +6 -1
- package/src/domains/shared/bootStore.ts +6 -3
- package/src/domains/shared/compactUri.test.ts +11 -0
- package/src/domains/shared/compactUri.ts +10 -5
- package/src/domains/shared/context.ts +2 -2
- package/src/domains/shared/contracts.ts +11 -0
- package/src/domains/shared/createInteractivePromptSession.test.ts +439 -0
- package/src/domains/shared/createInteractivePromptSession.ts +236 -0
- package/src/domains/shared/loaders/bundledLoader.test.ts +248 -0
- package/src/domains/shared/loaders/bundledLoader.ts +110 -12
- package/src/domains/shared/loaders/gitLoader.ts +2 -2
- package/src/domains/shared/loaders/index.ts +4 -1
- package/src/domains/shared/loaders/localLoader.ts +18 -4
- package/src/domains/shared/loaders/readPackageDir.ts +36 -1
- package/src/domains/shared/loaders/storyAssetsPlugin.test.ts +84 -0
- package/src/domains/shared/loaders/storyAssetsPlugin.ts +46 -0
- package/src/domains/shared/lookupMany.test.ts +120 -0
- package/src/domains/shared/lookupMany.ts +34 -2
- package/src/domains/shared/mergeAndParseRefs.test.ts +89 -0
- package/src/domains/shared/mergeAndParseRefs.ts +44 -0
- package/src/domains/shared/packages.ts +10 -17
- package/src/domains/shared/resolveConfiguredGraphs.test.ts +27 -0
- package/src/domains/shared/resolveConfiguredGraphs.ts +41 -0
- package/src/domains/shared/runtime.ts +4 -63
- package/src/domains/shared/semanticPackage.ts +17 -1
- package/src/domains/shared/stories/compileLookupCommand.test.ts +145 -0
- package/src/domains/shared/stories/compileLookupCommand.ts +100 -0
- package/src/domains/shared/stories/compileLookupTool.test.ts +105 -0
- package/src/domains/shared/stories/compileLookupTool.ts +96 -0
- package/src/domains/shared/stories/compileReadCommand.test.ts +121 -0
- package/src/domains/shared/stories/compileReadCommand.ts +48 -0
- package/src/domains/shared/stories/compileReadTool.test.ts +64 -0
- package/src/domains/shared/stories/compileReadTool.ts +38 -0
- package/src/domains/shared/stories/condense.test.ts +18 -0
- package/src/domains/shared/stories/condense.ts +15 -0
- package/src/domains/shared/stories/index.ts +20 -0
- package/src/domains/shared/stories/normalizeNames.test.ts +20 -0
- package/src/domains/shared/stories/normalizeNames.ts +22 -0
- package/src/domains/shared/stories/pack/applyPackFilters.test.ts +102 -0
- package/src/domains/shared/stories/pack/applyPackFilters.ts +76 -0
- package/src/domains/shared/stories/pack/buildLookupQuery.test.ts +51 -0
- package/src/domains/shared/stories/pack/buildLookupQuery.ts +80 -0
- package/src/domains/shared/stories/pack/collectPackStories.test.ts +108 -0
- package/src/domains/shared/stories/pack/collectPackStories.ts +76 -0
- package/src/domains/shared/stories/pack/compilePackCommands.ts +40 -0
- package/src/domains/shared/stories/pack/compilePackStories.test.ts +146 -0
- package/src/domains/shared/stories/pack/compilePackStories.ts +253 -0
- package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +43 -0
- package/src/domains/shared/stories/pack/index.ts +28 -0
- package/src/domains/shared/stories/pack/runSelectQuery.ts +31 -0
- package/src/domains/shared/stories/pack/standardParity.test.ts +308 -0
- package/src/domains/shared/stories/pack/types.ts +104 -0
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +240 -0
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +408 -0
- package/src/domains/shared/stories/projectCliParameters.test.ts +59 -0
- package/src/domains/shared/stories/projectCliParameters.ts +57 -0
- package/src/domains/shared/stories/projectToolParams.test.ts +50 -0
- package/src/domains/shared/stories/projectToolParams.ts +32 -0
- package/src/domains/shared/stories/requirePragmaContext.test.ts +23 -0
- package/src/domains/shared/stories/requirePragmaContext.ts +23 -0
- package/src/domains/shared/stories/resolveLookupDetailed.test.ts +57 -0
- package/src/domains/shared/stories/resolveLookupDetailed.ts +27 -0
- package/src/domains/shared/stories/types.ts +171 -0
- package/src/domains/shared/suggestions/expandGlob.ts +1 -0
- package/src/domains/shared/suggestions/suggestNames.ts +43 -10
- package/src/domains/skill/commands/index.ts +3 -1
- package/src/domains/skill/commands/list.test.ts +6 -2
- package/src/domains/skill/commands/list.ts +5 -63
- package/src/domains/skill/commands/lookup.test.ts +118 -0
- package/src/domains/skill/commands/lookup.ts +14 -0
- package/src/domains/skill/formatters/index.ts +1 -0
- package/src/domains/skill/formatters/lookup.test.ts +46 -0
- package/src/domains/skill/formatters/lookup.ts +57 -0
- package/src/domains/skill/helpers/resolveSkillSources.ts +0 -1
- package/src/domains/skill/index.ts +7 -3
- package/src/domains/skill/mcp/specs.ts +10 -29
- package/src/domains/skill/operations/discover.test.ts +61 -30
- package/src/domains/skill/operations/discover.ts +28 -37
- package/src/domains/skill/operations/index.ts +2 -1
- package/src/domains/skill/operations/list.test.ts +15 -7
- package/src/domains/skill/operations/lookup.test.ts +91 -0
- package/src/domains/skill/operations/lookup.ts +70 -0
- package/src/domains/skill/stories.test.ts +43 -0
- package/src/domains/skill/stories.ts +103 -0
- package/src/domains/skill/types.ts +8 -0
- package/src/domains/standard/commands/list.ts +5 -84
- package/src/domains/standard/commands/lookup.ts +8 -93
- package/src/domains/standard/mcp/specs.ts +16 -171
- package/src/domains/standard/orchestration/resolveStandardLookup.ts +2 -1
- package/src/domains/standard/stories.test.ts +24 -0
- package/src/domains/standard/stories.ts +151 -0
- package/src/domains/tier/commands/list.ts +5 -55
- package/src/domains/tier/mcp/specs.ts +5 -31
- package/src/domains/tier/stories.test.ts +22 -0
- package/src/domains/tier/stories.ts +52 -0
- package/src/domains/token/mcp/specs.ts +5 -1
- package/src/domains/token/orchestration/resolveTokenLookup.ts +2 -1
- package/src/domains/trace/tracePlugin.test.ts +1 -1
- package/src/mcp/registerResources.test.ts +382 -74
- package/src/mcp/registerResources.ts +5 -348
- package/src/mcp/resources/buildGraphIndex.ts +167 -0
- package/src/mcp/resources/buildResourceList.test.ts +97 -0
- package/src/mcp/resources/buildResourceList.ts +176 -0
- package/src/mcp/resources/classifyEntity.test.ts +40 -0
- package/src/mcp/resources/classifyEntity.ts +27 -0
- package/src/mcp/resources/constants.ts +105 -0
- package/src/mcp/resources/displayPredicates.ts +62 -0
- package/src/mcp/resources/pickFirstValue.test.ts +29 -0
- package/src/mcp/resources/pickFirstValue.ts +26 -0
- package/src/mcp/resources/pickPrimaryType.test.ts +41 -0
- package/src/mcp/resources/pickPrimaryType.ts +26 -0
- package/src/mcp/resources/rankUriCompletions.test.ts +52 -0
- package/src/mcp/resources/rankUriCompletions.ts +64 -0
- package/src/mcp/resources/readEntity.ts +169 -0
- package/src/mcp/resources/registerResources.ts +126 -0
- package/src/mcp/resources/types.ts +128 -0
- package/src/mcp/tools/index.ts +9 -0
- package/src/mcp/tools/registerTools.test.ts +65 -8
- package/src/mcp/types.ts +4 -0
- package/src/pipeline/collectCommands.ts +8 -1
- package/src/pipeline/runCli.ts +40 -6
- package/src/testing/createTestMcpClient.ts +4 -1
- package/src/testing/index.ts +10 -0
- package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +1207 -0
- package/src/testing/integration/cross-binary-byte-equality.test.ts +129 -0
- package/src/testing/integration/executor-execution.test.ts +101 -0
- package/src/testing/integration/executor-golden.test.ts +145 -0
- package/src/testing/integration/mcp-surface.test.ts +1 -1
- package/src/testing/integration/parity.test.ts +17 -12
- package/src/testing/regression/0001-skill-discovery-granularity.test.ts +71 -0
- package/src/testing/setupXdgIsolation.ts +15 -0
- package/src/testing/standardParityFixtures.ts +92 -0
- package/src/testing/storyFixtures.ts +63 -0
- package/src/pipeline/runInteractiveCommand.ts +0 -198
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { PackageRef } from "../../refs/operations/parseRef.js";
|
|
3
|
+
import createBundledLoader, {
|
|
4
|
+
resetBundledLoaderCache,
|
|
5
|
+
} from "./bundledLoader.js";
|
|
6
|
+
|
|
7
|
+
const ref: PackageRef = { kind: "npm", pkg: "@canonical/design-system" };
|
|
8
|
+
|
|
9
|
+
/** Minimal stand-in for a Bun embedded-file blob. */
|
|
10
|
+
interface FakeBlob {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
text(): Promise<string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const blob = (name: string, content: string): FakeBlob => ({
|
|
16
|
+
name,
|
|
17
|
+
text: async () => content,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const corruptBlob = (name: string): FakeBlob => ({
|
|
21
|
+
name,
|
|
22
|
+
text: async () => {
|
|
23
|
+
throw new Error("embedded blob unreadable");
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const TTL = "@prefix ds: <https://example.test/ds#> .";
|
|
28
|
+
|
|
29
|
+
function stubEmbeddedFiles(blobs: readonly FakeBlob[]): void {
|
|
30
|
+
(globalThis as { Bun?: unknown }).Bun = { embeddedFiles: blobs };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const hadBun = typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
|
|
34
|
+
const originalBun = (globalThis as { Bun?: unknown }).Bun;
|
|
35
|
+
|
|
36
|
+
let stderrSpy: ReturnType<typeof vi.spyOn>;
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
resetBundledLoaderCache();
|
|
40
|
+
stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
resetBundledLoaderCache();
|
|
45
|
+
if (hadBun) {
|
|
46
|
+
(globalThis as { Bun?: unknown }).Bun = originalBun;
|
|
47
|
+
} else {
|
|
48
|
+
delete (globalThis as { Bun?: unknown }).Bun;
|
|
49
|
+
}
|
|
50
|
+
stderrSpy.mockRestore();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const stderrText = (): string =>
|
|
54
|
+
stderrSpy.mock.calls.map((call) => String(call[0])).join("");
|
|
55
|
+
|
|
56
|
+
describe("createBundledLoader", () => {
|
|
57
|
+
it("returns undefined outside a compiled Bun binary", async () => {
|
|
58
|
+
if (!hadBun) {
|
|
59
|
+
delete (globalThis as { Bun?: unknown }).Bun;
|
|
60
|
+
const resolved = await createBundledLoader().resolve(ref);
|
|
61
|
+
expect(resolved).toBeUndefined();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("bundles all embedded TTL and reads the manifest version", async () => {
|
|
66
|
+
stubEmbeddedFiles([
|
|
67
|
+
blob("button-a1b2c3d4.ttl", TTL),
|
|
68
|
+
blob("package-a1b2c3d4.json", JSON.stringify({ version: "1.2.3" })),
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
const resolved = await createBundledLoader().resolve(ref);
|
|
72
|
+
|
|
73
|
+
expect(resolved).toBeDefined();
|
|
74
|
+
expect(resolved?.source).toBe("bundled");
|
|
75
|
+
expect(resolved?.version).toBe("1.2.3");
|
|
76
|
+
expect(resolved?.graphs).toEqual([
|
|
77
|
+
{
|
|
78
|
+
path: "(bundled)/button-a1b2c3d4.ttl",
|
|
79
|
+
content: TTL,
|
|
80
|
+
format: "turtle",
|
|
81
|
+
},
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("accepts a plain package.json basename", async () => {
|
|
86
|
+
stubEmbeddedFiles([
|
|
87
|
+
blob("button.ttl", TTL),
|
|
88
|
+
blob("package.json", JSON.stringify({ version: "2.0.0" })),
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
const resolved = await createBundledLoader().resolve(ref);
|
|
92
|
+
expect(resolved?.version).toBe("2.0.0");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("warns and skips a corrupt TTL blob instead of silently dropping it", async () => {
|
|
96
|
+
stubEmbeddedFiles([
|
|
97
|
+
blob("good-a1b2c3d4.ttl", TTL),
|
|
98
|
+
corruptBlob("bad-a1b2c3d4.ttl"),
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
const resolved = await createBundledLoader().resolve(ref);
|
|
102
|
+
|
|
103
|
+
expect(resolved?.graphs).toHaveLength(1);
|
|
104
|
+
expect(stderrText()).toContain("bad-a1b2c3d4.ttl");
|
|
105
|
+
expect(stderrText()).toContain("embedded blob unreadable");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("serves embedded story JSON as story-file entries", async () => {
|
|
109
|
+
stubEmbeddedFiles([
|
|
110
|
+
blob("button-a1b2c3d4.ttl", TTL),
|
|
111
|
+
blob("package-a1b2c3d4.json", JSON.stringify({ version: "1.2.3" })),
|
|
112
|
+
blob("colors-fyskc7pf.json", JSON.stringify({ noun: "color" })),
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
const resolved = await createBundledLoader().resolve(ref);
|
|
116
|
+
|
|
117
|
+
expect(resolved?.stories).toEqual([
|
|
118
|
+
{
|
|
119
|
+
path: "(bundled)/colors-fyskc7pf.json",
|
|
120
|
+
definition: { noun: "color" },
|
|
121
|
+
},
|
|
122
|
+
]);
|
|
123
|
+
expect(resolved?.version).toBe("1.2.3");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("does not serve package manifests as stories", async () => {
|
|
127
|
+
stubEmbeddedFiles([
|
|
128
|
+
blob("button.ttl", TTL),
|
|
129
|
+
blob("package.json", JSON.stringify({ version: "2.0.0" })),
|
|
130
|
+
blob("package-deadbeef.json", JSON.stringify({ version: "3.0.0" })),
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
const resolved = await createBundledLoader().resolve(ref);
|
|
134
|
+
|
|
135
|
+
expect(resolved?.stories).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("warns and skips a story blob with invalid JSON instead of crashing", async () => {
|
|
139
|
+
stubEmbeddedFiles([
|
|
140
|
+
blob("button.ttl", TTL),
|
|
141
|
+
blob("good-a1b2c3d4.json", JSON.stringify({ noun: "color" })),
|
|
142
|
+
blob("broken-a1b2c3d4.json", "{ not json"),
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
const resolved = await createBundledLoader().resolve(ref);
|
|
146
|
+
|
|
147
|
+
expect(resolved?.stories).toHaveLength(1);
|
|
148
|
+
expect(resolved?.stories.at(0)?.definition).toEqual({ noun: "color" });
|
|
149
|
+
expect(stderrText()).toContain("broken-a1b2c3d4.json");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("warns and skips an unreadable story blob", async () => {
|
|
153
|
+
stubEmbeddedFiles([
|
|
154
|
+
blob("button.ttl", TTL),
|
|
155
|
+
corruptBlob("story-a1b2c3d4.json"),
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
const resolved = await createBundledLoader().resolve(ref);
|
|
159
|
+
|
|
160
|
+
expect(resolved?.stories).toEqual([]);
|
|
161
|
+
expect(stderrText()).toContain("story-a1b2c3d4.json");
|
|
162
|
+
expect(stderrText()).toContain("embedded blob unreadable");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("resolves a stories-only bundle with no TTL graphs", async () => {
|
|
166
|
+
stubEmbeddedFiles([
|
|
167
|
+
blob("colors-fyskc7pf.json", JSON.stringify({ noun: "color" })),
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
const resolved = await createBundledLoader().resolve(ref);
|
|
171
|
+
|
|
172
|
+
expect(resolved).toBeDefined();
|
|
173
|
+
expect(resolved?.graphs).toEqual([]);
|
|
174
|
+
expect(resolved?.stories).toHaveLength(1);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("returns undefined when no TTL or story content is embedded", async () => {
|
|
178
|
+
stubEmbeddedFiles([
|
|
179
|
+
blob("template-a1b2c3d4.ejs", "<%= name %>"),
|
|
180
|
+
blob("skill-a1b2c3d4.md", "# Skill"),
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
const resolved = await createBundledLoader().resolve(ref);
|
|
184
|
+
|
|
185
|
+
expect(resolved).toBeUndefined();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("warns on a corrupt package manifest and falls back to 0.0.0", async () => {
|
|
189
|
+
stubEmbeddedFiles([
|
|
190
|
+
blob("button.ttl", TTL),
|
|
191
|
+
blob("package-deadbeef.json", "{ not json"),
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
const resolved = await createBundledLoader().resolve(ref);
|
|
195
|
+
|
|
196
|
+
expect(resolved?.version).toBe("0.0.0");
|
|
197
|
+
expect(stderrText()).toContain("package-deadbeef.json");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("rejects JSON blobs whose basename is not a package manifest", async () => {
|
|
201
|
+
stubEmbeddedFiles([
|
|
202
|
+
blob("button.ttl", TTL),
|
|
203
|
+
blob("mypackage.json", JSON.stringify({ version: "9.9.9" })),
|
|
204
|
+
blob("package-lock.json", JSON.stringify({ version: "8.8.8" })),
|
|
205
|
+
blob("packages.json", JSON.stringify({ version: "7.7.7" })),
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
const resolved = await createBundledLoader().resolve(ref);
|
|
209
|
+
expect(resolved?.version).toBe("0.0.0");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("warns on an invalid semver version and keeps looking", async () => {
|
|
213
|
+
stubEmbeddedFiles([
|
|
214
|
+
blob("button.ttl", TTL),
|
|
215
|
+
blob("package-aaaaaaaa.json", JSON.stringify({ version: "not-semver" })),
|
|
216
|
+
blob("package-bbbbbbbb.json", JSON.stringify({ version: "3.4.5" })),
|
|
217
|
+
]);
|
|
218
|
+
|
|
219
|
+
const resolved = await createBundledLoader().resolve(ref);
|
|
220
|
+
|
|
221
|
+
expect(resolved?.version).toBe("3.4.5");
|
|
222
|
+
expect(stderrText()).toContain('invalid semver "not-semver"');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("caches the resolved package until the cache is reset", async () => {
|
|
226
|
+
stubEmbeddedFiles([
|
|
227
|
+
blob("button.ttl", TTL),
|
|
228
|
+
blob("package.json", JSON.stringify({ version: "1.0.0" })),
|
|
229
|
+
]);
|
|
230
|
+
|
|
231
|
+
const first = await createBundledLoader().resolve(ref);
|
|
232
|
+
expect(first?.version).toBe("1.0.0");
|
|
233
|
+
|
|
234
|
+
// New fixture without reset — cached result still returned.
|
|
235
|
+
stubEmbeddedFiles([
|
|
236
|
+
blob("other.ttl", TTL),
|
|
237
|
+
blob("package.json", JSON.stringify({ version: "5.0.0" })),
|
|
238
|
+
]);
|
|
239
|
+
const second = await createBundledLoader().resolve(ref);
|
|
240
|
+
expect(second).toBe(first);
|
|
241
|
+
|
|
242
|
+
// After reset the loader re-reads the embedded files.
|
|
243
|
+
resetBundledLoaderCache();
|
|
244
|
+
const third = await createBundledLoader().resolve(ref);
|
|
245
|
+
expect(third?.version).toBe("5.0.0");
|
|
246
|
+
expect(third).not.toBe(first);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bundled package loader for compiled binaries.
|
|
3
3
|
*
|
|
4
|
-
* Resolves semantic content from `Bun.embeddedFiles` — TTL
|
|
5
|
-
*
|
|
4
|
+
* Resolves semantic content from `Bun.embeddedFiles` — the TTL graphs
|
|
5
|
+
* and story-pack JSON embedded into the compiled binary via the build
|
|
6
|
+
* command. Skills are not embedded; this loader reports `skills: []`.
|
|
6
7
|
*
|
|
7
8
|
* Blob names are hashed by bun (e.g. `button-a1b2c3d4.ttl`), losing
|
|
8
9
|
* directory and package association. The loader therefore returns a
|
|
9
|
-
* single SemanticPackage containing ALL embedded TTL content
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* single SemanticPackage containing ALL embedded TTL and story content,
|
|
11
|
+
* and the same cached package is returned for every ref this loader
|
|
12
|
+
* resolves.
|
|
13
|
+
* Note that repeated loads are NOT fully deduplicated by the store:
|
|
14
|
+
* Oxigraph applies set semantics to ground triples, but blank nodes are
|
|
15
|
+
* re-minted on every parse, so content loaded both here and by a
|
|
16
|
+
* higher-precedence loader can produce duplicated blank-node subgraphs.
|
|
17
|
+
* The orchestrator's first-loader-wins precedence (local > git >
|
|
18
|
+
* bundled) is what keeps individual packages from double-loading.
|
|
12
19
|
*
|
|
13
20
|
* @note Impure — reads embedded blobs.
|
|
14
21
|
*/
|
|
15
22
|
|
|
23
|
+
import { basename } from "node:path";
|
|
16
24
|
import type { PackageRef } from "../../refs/operations/parseRef.js";
|
|
17
25
|
import type {
|
|
18
26
|
GraphContent,
|
|
19
27
|
PackageLoader,
|
|
20
28
|
SemanticPackage,
|
|
29
|
+
StoryFileEntry,
|
|
21
30
|
} from "../semanticPackage.js";
|
|
22
31
|
|
|
23
32
|
// ---------------------------------------------------------------------------
|
|
@@ -27,6 +36,45 @@ import type {
|
|
|
27
36
|
/** Cached result — embedded content is immutable, no need to re-read. */
|
|
28
37
|
let cached: SemanticPackage | undefined;
|
|
29
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Reset the module-level cache. Test-only: lets suites exercise the loader
|
|
41
|
+
* against different `Bun.embeddedFiles` fixtures without module isolation.
|
|
42
|
+
*/
|
|
43
|
+
export function resetBundledLoaderCache(): void {
|
|
44
|
+
cached = undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Validation
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Embedded package manifest basename: exactly `package.json`, or
|
|
53
|
+
* `package-<hash>.json` where `<hash>` is bun's content hash suffix.
|
|
54
|
+
*/
|
|
55
|
+
const PACKAGE_MANIFEST_PATTERN = /^package(?:-[0-9a-f]{6,64})?\.json$/i;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Semver 2.0.0 validation (semver.org's suggested pattern).
|
|
59
|
+
*/
|
|
60
|
+
const SEMVER_PATTERN =
|
|
61
|
+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether a blob name is an embedded package manifest — strict basename
|
|
65
|
+
* match, so unrelated JSON blobs (e.g. `mypackage.json`) are not misread.
|
|
66
|
+
*/
|
|
67
|
+
function isPackageManifest(name: string): boolean {
|
|
68
|
+
return PACKAGE_MANIFEST_PATTERN.test(basename(name));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Emit a loader warning on stderr (safe for both CLI and MCP stdio use).
|
|
73
|
+
*/
|
|
74
|
+
function warn(message: string): void {
|
|
75
|
+
process.stderr.write(`Warning: bundled loader: ${message}\n`);
|
|
76
|
+
}
|
|
77
|
+
|
|
30
78
|
// ---------------------------------------------------------------------------
|
|
31
79
|
// Public API
|
|
32
80
|
// ---------------------------------------------------------------------------
|
|
@@ -44,7 +92,8 @@ export default function createBundledLoader(): PackageLoader {
|
|
|
44
92
|
if (!blobs?.length) return undefined;
|
|
45
93
|
|
|
46
94
|
const graphs = await readEmbeddedGraphs(blobs);
|
|
47
|
-
|
|
95
|
+
const stories = await readEmbeddedStories(blobs);
|
|
96
|
+
if (graphs.length === 0 && stories.length === 0) return undefined;
|
|
48
97
|
|
|
49
98
|
const version = await readEmbeddedVersion(blobs);
|
|
50
99
|
|
|
@@ -54,6 +103,7 @@ export default function createBundledLoader(): PackageLoader {
|
|
|
54
103
|
source: "bundled",
|
|
55
104
|
graphs,
|
|
56
105
|
skills: [],
|
|
106
|
+
stories,
|
|
57
107
|
};
|
|
58
108
|
|
|
59
109
|
return cached;
|
|
@@ -80,12 +130,51 @@ async function readEmbeddedGraphs(
|
|
|
80
130
|
content,
|
|
81
131
|
format: "turtle",
|
|
82
132
|
});
|
|
83
|
-
} catch {
|
|
133
|
+
} catch (error) {
|
|
134
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
135
|
+
warn(`skipping unreadable embedded graph "${blob.name}": ${reason}`);
|
|
136
|
+
}
|
|
84
137
|
}
|
|
85
138
|
|
|
86
139
|
return graphs;
|
|
87
140
|
}
|
|
88
141
|
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Story reading
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Read story-pack definitions from embedded `.json` blobs.
|
|
148
|
+
*
|
|
149
|
+
* The build's story-assets plugin makes `stories/*.json` the only JSON
|
|
150
|
+
* embedded as file assets (see storyAssetsPlugin.ts), so every `.json`
|
|
151
|
+
* blob not named like a package manifest is a story file. Mirrors the
|
|
152
|
+
* local loader: a blob that cannot be read or parsed warns on stderr
|
|
153
|
+
* and is skipped — one bad file cannot break boot. Definitions are
|
|
154
|
+
* shape-validated later by the story-pack compiler.
|
|
155
|
+
*/
|
|
156
|
+
async function readEmbeddedStories(
|
|
157
|
+
blobs: ReadonlyArray<Blob & { name: string }>,
|
|
158
|
+
): Promise<StoryFileEntry[]> {
|
|
159
|
+
const stories: StoryFileEntry[] = [];
|
|
160
|
+
|
|
161
|
+
for (const blob of blobs) {
|
|
162
|
+
if (!blob.name.endsWith(".json") || isPackageManifest(blob.name)) continue;
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
stories.push({
|
|
166
|
+
path: `(bundled)/${blob.name}`,
|
|
167
|
+
definition: JSON.parse(await blob.text()),
|
|
168
|
+
});
|
|
169
|
+
} catch (error) {
|
|
170
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
171
|
+
warn(`skipping invalid embedded story "${blob.name}": ${reason}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return stories;
|
|
176
|
+
}
|
|
177
|
+
|
|
89
178
|
// ---------------------------------------------------------------------------
|
|
90
179
|
// Version reading
|
|
91
180
|
// ---------------------------------------------------------------------------
|
|
@@ -94,14 +183,23 @@ async function readEmbeddedVersion(
|
|
|
94
183
|
blobs: ReadonlyArray<Blob & { name: string }>,
|
|
95
184
|
): Promise<string> {
|
|
96
185
|
for (const blob of blobs) {
|
|
97
|
-
if (!
|
|
98
|
-
continue;
|
|
186
|
+
if (!isPackageManifest(blob.name)) continue;
|
|
99
187
|
|
|
100
188
|
try {
|
|
101
189
|
const text = await blob.text();
|
|
102
|
-
const parsed = JSON.parse(text) as { version?:
|
|
103
|
-
if (parsed.version)
|
|
104
|
-
|
|
190
|
+
const parsed = JSON.parse(text) as { version?: unknown };
|
|
191
|
+
if (typeof parsed.version !== "string") continue;
|
|
192
|
+
if (!SEMVER_PATTERN.test(parsed.version)) {
|
|
193
|
+
warn(
|
|
194
|
+
`ignoring invalid semver "${parsed.version}" in embedded manifest "${blob.name}"`,
|
|
195
|
+
);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
return parsed.version;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
201
|
+
warn(`skipping corrupt embedded manifest "${blob.name}": ${reason}`);
|
|
202
|
+
}
|
|
105
203
|
}
|
|
106
204
|
|
|
107
205
|
return "0.0.0";
|
|
@@ -24,8 +24,8 @@ export default function createGitLoader(): PackageLoader {
|
|
|
24
24
|
const dir = gitCacheDir(ref.pkg, ref.ref);
|
|
25
25
|
if (!existsSync(dir)) return undefined;
|
|
26
26
|
|
|
27
|
-
const { version, graphs, skills } = readPackageDir(dir);
|
|
28
|
-
return { name: ref.pkg, version, source: "git", graphs, skills };
|
|
27
|
+
const { version, graphs, skills, stories } = readPackageDir(dir);
|
|
28
|
+
return { name: ref.pkg, version, source: "git", graphs, skills, stories };
|
|
29
29
|
},
|
|
30
30
|
};
|
|
31
31
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/** @module Semantic package loaders — local, git, and bundled resolution. */
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export {
|
|
4
|
+
default as createBundledLoader,
|
|
5
|
+
resetBundledLoaderCache,
|
|
6
|
+
} from "./bundledLoader.js";
|
|
4
7
|
export { default as createGitLoader } from "./gitLoader.js";
|
|
5
8
|
export { default as createLocalLoader } from "./localLoader.js";
|
|
6
9
|
export { default as readPackageDir } from "./readPackageDir.js";
|
|
@@ -28,16 +28,30 @@ export default function createLocalLoader(): PackageLoader {
|
|
|
28
28
|
// file:// refs → direct path check first
|
|
29
29
|
if (ref.kind === "file") {
|
|
30
30
|
if (!existsSync(ref.path)) return undefined;
|
|
31
|
-
const { version, graphs, skills } = readPackageDir(ref.path);
|
|
32
|
-
return {
|
|
31
|
+
const { version, graphs, skills, stories } = readPackageDir(ref.path);
|
|
32
|
+
return {
|
|
33
|
+
name: ref.pkg,
|
|
34
|
+
version,
|
|
35
|
+
source: "local",
|
|
36
|
+
graphs,
|
|
37
|
+
skills,
|
|
38
|
+
stories,
|
|
39
|
+
};
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
// All ref kinds → try node_modules walk
|
|
36
43
|
const dir = findPackageDir(ref.pkg);
|
|
37
44
|
if (!dir) return undefined;
|
|
38
45
|
|
|
39
|
-
const { version, graphs, skills } = readPackageDir(dir);
|
|
40
|
-
return {
|
|
46
|
+
const { version, graphs, skills, stories } = readPackageDir(dir);
|
|
47
|
+
return {
|
|
48
|
+
name: ref.pkg,
|
|
49
|
+
version,
|
|
50
|
+
source: "local",
|
|
51
|
+
graphs,
|
|
52
|
+
skills,
|
|
53
|
+
stories,
|
|
54
|
+
};
|
|
41
55
|
},
|
|
42
56
|
};
|
|
43
57
|
}
|
|
@@ -16,7 +16,11 @@ import {
|
|
|
16
16
|
statSync,
|
|
17
17
|
} from "node:fs";
|
|
18
18
|
import { join } from "node:path";
|
|
19
|
-
import type {
|
|
19
|
+
import type {
|
|
20
|
+
GraphContent,
|
|
21
|
+
SkillEntry,
|
|
22
|
+
StoryFileEntry,
|
|
23
|
+
} from "../semanticPackage.js";
|
|
20
24
|
|
|
21
25
|
// ---------------------------------------------------------------------------
|
|
22
26
|
// TTL discovery convention
|
|
@@ -35,6 +39,7 @@ export interface PackageDirContents {
|
|
|
35
39
|
readonly version: string;
|
|
36
40
|
readonly graphs: GraphContent[];
|
|
37
41
|
readonly skills: SkillEntry[];
|
|
42
|
+
readonly stories: StoryFileEntry[];
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
/**
|
|
@@ -48,6 +53,7 @@ export default function readPackageDir(dir: string): PackageDirContents {
|
|
|
48
53
|
version: readVersion(dir),
|
|
49
54
|
graphs: readGraphs(dir),
|
|
50
55
|
skills: readSkills(dir),
|
|
56
|
+
stories: readStories(dir),
|
|
51
57
|
};
|
|
52
58
|
}
|
|
53
59
|
|
|
@@ -87,6 +93,35 @@ function readGraphs(dir: string): GraphContent[] {
|
|
|
87
93
|
return graphs;
|
|
88
94
|
}
|
|
89
95
|
|
|
96
|
+
function readStories(dir: string): StoryFileEntry[] {
|
|
97
|
+
const storiesDir = join(dir, "stories");
|
|
98
|
+
if (!existsSync(storiesDir)) return [];
|
|
99
|
+
|
|
100
|
+
const stories: StoryFileEntry[] = [];
|
|
101
|
+
try {
|
|
102
|
+
// Sorted: readdir order is filesystem-dependent, and story collision
|
|
103
|
+
// resolution must be deterministic across platforms.
|
|
104
|
+
for (const entry of [...readdirSync(storiesDir)].sort()) {
|
|
105
|
+
if (!entry.endsWith(".json")) continue;
|
|
106
|
+
const entryPath = join(storiesDir, entry);
|
|
107
|
+
try {
|
|
108
|
+
stories.push({
|
|
109
|
+
path: entryPath,
|
|
110
|
+
definition: JSON.parse(readFileSync(entryPath, "utf-8")),
|
|
111
|
+
});
|
|
112
|
+
} catch {
|
|
113
|
+
process.stderr.write(
|
|
114
|
+
`Warning: invalid JSON in story file ${entryPath}\n`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
// readdir failed — no stories
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return stories;
|
|
123
|
+
}
|
|
124
|
+
|
|
90
125
|
function readSkills(dir: string): SkillEntry[] {
|
|
91
126
|
const skillsDir = join(dir, "skills");
|
|
92
127
|
if (!existsSync(skillsDir)) return [];
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { BunPlugin } from "bun";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
import createStoryAssetsPlugin, {
|
|
7
|
+
STORY_ASSET_PATH_PATTERN,
|
|
8
|
+
} from "./storyAssetsPlugin.js";
|
|
9
|
+
|
|
10
|
+
interface OnLoadRegistration {
|
|
11
|
+
readonly filter: RegExp;
|
|
12
|
+
readonly callback: (args: { path: string }) => unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Run the plugin's setup against a stub builder and capture its onLoad. */
|
|
16
|
+
function captureOnLoadRegistration(): OnLoadRegistration {
|
|
17
|
+
let captured: OnLoadRegistration | undefined;
|
|
18
|
+
const builder = {
|
|
19
|
+
onLoad(
|
|
20
|
+
constraints: { filter: RegExp },
|
|
21
|
+
callback: OnLoadRegistration["callback"],
|
|
22
|
+
): void {
|
|
23
|
+
captured = { filter: constraints.filter, callback };
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
createStoryAssetsPlugin().setup(
|
|
28
|
+
builder as unknown as Parameters<BunPlugin["setup"]>[0],
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
if (!captured) throw new Error("plugin did not register an onLoad handler");
|
|
32
|
+
return captured;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("STORY_ASSET_PATH_PATTERN", () => {
|
|
36
|
+
it("matches JSON directly inside a stories directory", () => {
|
|
37
|
+
expect(
|
|
38
|
+
STORY_ASSET_PATH_PATTERN.test(
|
|
39
|
+
"/repo/node_modules/@canonical/design-system/stories/color.json",
|
|
40
|
+
),
|
|
41
|
+
).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("rejects manifests, nested files, near-miss directories, and non-JSON", () => {
|
|
45
|
+
const nonStories = [
|
|
46
|
+
"/repo/node_modules/@canonical/design-system/package.json",
|
|
47
|
+
"/repo/node_modules/@canonical/design-system/stories/nested/color.json",
|
|
48
|
+
"/repo/node_modules/@canonical/design-system/histories/color.json",
|
|
49
|
+
"/repo/node_modules/@canonical/design-system/stories/color.jsonc",
|
|
50
|
+
"/repo/node_modules/@canonical/design-system/stories/color.ttl",
|
|
51
|
+
];
|
|
52
|
+
for (const path of nonStories) {
|
|
53
|
+
expect(STORY_ASSET_PATH_PATTERN.test(path), path).toBe(false);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("createStoryAssetsPlugin", () => {
|
|
59
|
+
let tmpDir: string | undefined;
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
|
63
|
+
tmpDir = undefined;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("registers its load hook under the story path filter", () => {
|
|
67
|
+
const { filter } = captureOnLoadRegistration();
|
|
68
|
+
expect(filter).toBe(STORY_ASSET_PATH_PATTERN);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("loads story JSON contents under the file loader", () => {
|
|
72
|
+
tmpDir = mkdtempSync(join(tmpdir(), "pragma-story-assets-"));
|
|
73
|
+
const storyPath = join(tmpDir, "color.json");
|
|
74
|
+
const content = JSON.stringify({ noun: "color" });
|
|
75
|
+
writeFileSync(storyPath, content);
|
|
76
|
+
|
|
77
|
+
const { callback } = captureOnLoadRegistration();
|
|
78
|
+
|
|
79
|
+
expect(callback({ path: storyPath })).toEqual({
|
|
80
|
+
contents: content,
|
|
81
|
+
loader: "file",
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time bundler plugin: embed story-pack JSON as file assets.
|
|
3
|
+
*
|
|
4
|
+
* Bun compiles `.json` build inputs into JSON modules, which never
|
|
5
|
+
* surface in `Bun.embeddedFiles`. Story-pack files must instead be
|
|
6
|
+
* embedded as file assets — like TTL graphs — so the bundled loader can
|
|
7
|
+
* serve them inside the compiled binary. This plugin scopes the `file`
|
|
8
|
+
* loader to story paths only, leaving every other JSON input (e.g.
|
|
9
|
+
* `package.json` imported for the CLI version) compiled as a module.
|
|
10
|
+
*
|
|
11
|
+
* Contract with the bundled loader: story files are the only `.json`
|
|
12
|
+
* content embedded as file assets, so every `.json` blob in
|
|
13
|
+
* `Bun.embeddedFiles` that is not named like a package manifest is a
|
|
14
|
+
* story definition. Consequence: no module in the bundle may import a
|
|
15
|
+
* `stories/*.json` file as a JSON module — the plugin would rewrite
|
|
16
|
+
* such an import into a file-path string.
|
|
17
|
+
*
|
|
18
|
+
* Consumed by `scripts/build.ts` only; never bundled into the binary.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { readFileSync } from "node:fs";
|
|
22
|
+
import type { BunPlugin } from "bun";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Matches story-pack files: a `.json` directly inside a `stories/`
|
|
26
|
+
* directory — the same non-recursive scope the local loader scans.
|
|
27
|
+
*/
|
|
28
|
+
export const STORY_ASSET_PATH_PATTERN = /[\\/]stories[\\/][^\\/]+\.json$/;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create the bundler plugin that embeds `stories/*.json` as file assets.
|
|
32
|
+
*
|
|
33
|
+
* @returns A `Bun.build` plugin for the compiled-binary build.
|
|
34
|
+
* @note Impure — the registered load callback reads the filesystem.
|
|
35
|
+
*/
|
|
36
|
+
export default function createStoryAssetsPlugin(): BunPlugin {
|
|
37
|
+
return {
|
|
38
|
+
name: "story-assets",
|
|
39
|
+
setup(build) {
|
|
40
|
+
build.onLoad({ filter: STORY_ASSET_PATH_PATTERN }, (args) => ({
|
|
41
|
+
contents: readFileSync(args.path, "utf-8"),
|
|
42
|
+
loader: "file",
|
|
43
|
+
}));
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|