@canonical/pragma-cli 0.33.0 → 0.34.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 +7 -7
- package/dist/pragma +0 -0
- package/package.json +18 -15
- package/pragma.conf.ts +664 -0
- package/src/bin.ts +13 -4
- package/src/capabilities/block.parity.test.ts +250 -0
- package/src/capabilities/capabilities/capabilities.render.ts +3 -2
- package/src/capabilities/capabilities/capabilities.verb.ts +31 -13
- package/src/capabilities/capabilities/catalog.ts +18 -10
- package/src/capabilities/capabilities/hints.ts +13 -19
- package/src/capabilities/colophon/collectColophon.ts +34 -19
- package/src/capabilities/colophon/colophon.verb.ts +8 -4
- package/src/capabilities/colophon/types.ts +16 -7
- package/src/capabilities/config/collectConfigShow.test.ts +57 -0
- package/src/capabilities/config/collectConfigShow.ts +41 -4
- package/src/capabilities/config/fields.ts +0 -27
- package/src/capabilities/config/set.verb.ts +7 -3
- package/src/capabilities/config/show.render.test.ts +77 -5
- package/src/capabilities/config/show.render.ts +7 -12
- package/src/capabilities/config/show.verb.ts +4 -3
- package/src/capabilities/config/types.ts +8 -0
- package/src/capabilities/create/byteEquality.test.ts +78 -100
- package/src/capabilities/create/compiledCreate.subprocess.test.ts +52 -15
- package/src/capabilities/create/constants.ts +75 -0
- package/src/capabilities/create/create.mcp.test.ts +2 -2
- package/src/capabilities/create/create.test.ts +162 -15
- package/src/capabilities/create/create.verb.ts +140 -69
- package/src/capabilities/create/index.ts +2 -10
- package/src/capabilities/create/pickGenerator.ts +34 -28
- package/src/capabilities/create/templates.embedded.generated.ts +4 -29
- package/src/capabilities/create/types.ts +10 -6
- package/src/capabilities/{standard/parity.test.ts → distribution.parity.test.ts} +24 -14
- package/src/capabilities/distribution.test.ts +120 -0
- package/src/capabilities/distribution.ts +59 -0
- package/src/capabilities/doctor/checks/checkConfigFile.ts +6 -5
- package/src/capabilities/doctor/checks/checkKeStore.ts +2 -2
- package/src/capabilities/doctor/checks/checkMcpConfigured.ts +7 -6
- package/src/capabilities/doctor/checks/checkPackageRefs.ts +79 -45
- package/src/capabilities/doctor/checks/checkPragmaVersion.ts +3 -2
- package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +61 -11
- package/src/capabilities/doctor/checks/checkShellCompletions.ts +63 -23
- package/src/capabilities/doctor/checks/checkSkillsSymlinked.ts +3 -2
- package/src/capabilities/doctor/doctor.render.test.ts +6 -6
- package/src/capabilities/doctor/doctor.render.ts +2 -1
- package/src/capabilities/doctor/doctor.test.ts +37 -8
- package/src/capabilities/doctor/doctor.verb.ts +6 -2
- package/src/capabilities/doctor/runChecks.ts +5 -5
- package/src/capabilities/doctor/types.ts +1 -1
- package/src/capabilities/emptyRecovery.test.ts +32 -15
- package/src/capabilities/graph/inspect.verb.ts +3 -2
- package/src/capabilities/graph/query.verb.ts +3 -2
- package/src/capabilities/graph/runQuery.ts +8 -5
- package/src/capabilities/index.ts +22 -18
- package/src/capabilities/info/collectInfo.ts +10 -6
- package/src/capabilities/info/info.render.ts +3 -2
- package/src/capabilities/info/info.verb.ts +3 -2
- package/src/capabilities/lazy.test.ts +93 -2
- package/src/capabilities/ontology/verbs.ts +21 -21
- package/src/capabilities/prompt/prompt.test.ts +12 -6
- package/src/capabilities/prompt/verbs.ts +24 -11
- package/src/capabilities/resources/provider.ts +30 -11
- package/src/capabilities/resources/resources.test.ts +85 -29
- package/src/capabilities/setup/operations/setupGenerator.ts +16 -9
- package/src/capabilities/setup/operations/setupMcp.ts +2 -2
- package/src/capabilities/setup/operations/setupSkills.ts +3 -3
- package/src/capabilities/setup/setup.verb.ts +12 -5
- package/src/capabilities/setup/shell.ts +29 -3
- package/src/capabilities/skill/discover.ts +11 -5
- package/src/capabilities/skill/render.ts +4 -5
- package/src/capabilities/skill/verbs.ts +7 -7
- package/src/capabilities/sources/collectStatus.ts +55 -50
- package/src/capabilities/sources/index.ts +1 -1
- package/src/capabilities/sources/installSkills.ts +2 -2
- package/src/capabilities/sources/runUpdate.ts +113 -122
- package/src/capabilities/sources/sources.test.ts +111 -117
- package/src/capabilities/sources/status.render.test.ts +53 -24
- package/src/capabilities/sources/status.render.ts +24 -29
- package/src/capabilities/sources/status.verb.ts +15 -8
- package/src/capabilities/sources/types.ts +18 -25
- package/src/capabilities/sources/update.render.ts +9 -2
- package/src/capabilities/sources/update.verb.ts +12 -23
- package/src/capabilities/surface.test.ts +33 -10
- package/src/capabilities/tier.parity.test.ts +144 -0
- package/src/capabilities/upgrade/upgrade.verb.ts +7 -3
- package/src/constants.test.ts +8 -6
- package/src/constants.ts +47 -21
- package/src/identity.test.ts +333 -0
- package/src/index.ts +2 -2
- package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +5 -2
- package/src/kernel/completion/emitScripts.test.ts +1 -1
- package/src/kernel/completion/emitScripts.ts +4 -2
- package/src/kernel/completion/entitySource.test.ts +185 -41
- package/src/kernel/completion/entitySource.ts +117 -107
- package/src/kernel/completion/model.test.ts +1 -1
- package/src/kernel/completion/model.ts +11 -0
- package/src/kernel/completion/safety.test.ts +82 -36
- package/src/kernel/completion/shellDrive.test.ts +592 -0
- package/src/kernel/completion/templates/fish.ts +28 -6
- package/src/kernel/completion/types.ts +3 -3
- package/src/kernel/config/__snapshots__/firstRun.test.ts.snap +1 -1
- package/src/kernel/config/defaults.test.ts +85 -0
- package/src/kernel/config/defaults.ts +12 -23
- package/src/kernel/config/evaluateProjectConfig.ts +2 -3
- package/src/kernel/config/findProjectConfig.ts +4 -3
- package/src/kernel/config/firstRun.ts +12 -5
- package/src/kernel/config/globalConfig.ts +2 -2
- package/src/kernel/config/index.ts +4 -5
- package/src/kernel/config/paths.ts +17 -17
- package/src/kernel/config/readConfig.test.ts +242 -6
- package/src/kernel/config/readConfig.ts +8 -8
- package/src/kernel/config/schema.test.ts +50 -0
- package/src/kernel/config/schema.ts +97 -10
- package/src/kernel/config/types.ts +73 -19
- package/src/kernel/copy.test.ts +453 -0
- package/src/kernel/error/PragmaError.test.ts +1 -1
- package/src/kernel/error/constants.ts +3 -2
- package/src/kernel/error/index.ts +1 -1
- package/src/kernel/error/recovery.test.ts +22 -45
- package/src/kernel/error/recovery.ts +22 -27
- package/src/kernel/error/types.ts +5 -4
- package/src/kernel/packs/collect.test.ts +195 -11
- package/src/kernel/packs/collect.ts +213 -58
- package/src/kernel/packs/compile.test.ts +163 -7
- package/src/kernel/packs/compile.ts +15 -12
- package/src/kernel/packs/disclosure.test.ts +5 -2
- package/src/kernel/packs/graphql/fetchGraphqlLookup.ts +11 -10
- package/src/kernel/packs/graphqlSource.test.ts +6 -3
- package/src/kernel/packs/iri.ts +5 -7
- package/src/kernel/packs/renderPack.ts +5 -3
- package/src/kernel/packs/resolveEntity.ts +21 -17
- package/src/kernel/packs/runBodies.ts +15 -14
- package/src/kernel/packs/schema.ts +96 -10
- package/src/kernel/packs/sparql/runSelect.test.ts +104 -10
- package/src/kernel/packs/sparql/runSelect.ts +73 -34
- package/src/kernel/packs/toolDescription.test.ts +10 -2
- package/src/kernel/packs/types.ts +39 -4
- package/src/kernel/packs/uniqueness.ts +4 -4
- package/src/kernel/project/cli/buildProgram.test.ts +9 -4
- package/src/kernel/project/cli/dispatch.test.ts +67 -0
- package/src/kernel/project/cli/dispatch.ts +34 -12
- package/src/kernel/project/cli/helpFormat.ts +1 -1
- package/src/kernel/project/cli/rootHelp.ts +105 -105
- package/src/kernel/project/mcp/buildServer.ts +11 -6
- package/src/kernel/project/mcp/instructions.ts +11 -6
- package/src/kernel/project/mcp/parity.test.ts +39 -1
- package/src/kernel/project/mcp/prompts/prompts.test.ts +120 -5
- package/src/kernel/project/mcp/prompts/provider.ts +22 -8
- package/src/kernel/project/mcp/prompts/source.ts +68 -67
- package/src/kernel/project/mcp/registerVerb.ts +25 -6
- package/src/kernel/project/mcp/serve.ts +11 -3
- package/src/kernel/project/mcp/toolDescriptions.test.ts +6 -2
- package/src/kernel/render/disclosure.ts +4 -2
- package/src/kernel/render/prefixes.test.ts +36 -0
- package/src/kernel/render/prefixes.ts +40 -13
- package/src/kernel/render/renderers.test.ts +1 -1
- package/src/kernel/runtime/graphpack/build.ts +29 -3
- package/src/kernel/runtime/graphpack/buildIndex.test.ts +46 -0
- package/src/kernel/runtime/graphpack/buildIndex.ts +52 -17
- package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +5 -5
- package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +3 -3
- package/src/kernel/runtime/graphpack/embedded/pack.stories.generated.ts +3 -0
- package/src/kernel/runtime/graphpack/embedded.ts +37 -11
- package/src/kernel/runtime/graphpack/graphpack.test.ts +205 -5
- package/src/kernel/runtime/graphpack/hash.ts +3 -2
- package/src/kernel/runtime/graphpack/index.ts +1 -1
- package/src/kernel/runtime/graphpack/manifest.ts +114 -12
- package/src/kernel/runtime/graphpack/read.ts +4 -9
- package/src/kernel/runtime/graphpack/schemas.ts +74 -0
- package/src/kernel/runtime/graphpack/stories.ts +84 -0
- package/src/kernel/runtime/graphpack/types.ts +61 -48
- package/src/kernel/runtime/paths.ts +64 -15
- package/src/kernel/runtime/probe.test.ts +8 -4
- package/src/kernel/runtime/readEntity.ts +6 -5
- package/src/kernel/runtime/refs/gitOps.ts +1 -19
- package/src/kernel/runtime/refs/parseRef.ts +7 -7
- package/src/kernel/runtime/refs/resolve.ts +78 -62
- package/src/kernel/runtime/resolveSources.test.ts +63 -24
- package/src/kernel/runtime/resolveSources.ts +36 -25
- package/src/kernel/runtime/storeReadiness.ts +9 -8
- package/src/kernel/runtime/storeless.test.ts +1 -1
- package/src/kernel/runtime/wasmEmbed.test.ts +22 -19
- package/src/kernel/spec/emitReference.test.ts +3 -3
- package/src/kernel/spec/emitReference.ts +144 -11
- package/src/kernel/spec/emitSurface.ts +9 -5
- package/src/kernel/spec/surfaceConformance.test.ts +2 -2
- package/src/kernel/spec/surfaceConformance.ts +8 -0
- package/src/kernel/spec/types.ts +29 -9
- package/src/kernel/spec/validate.ts +2 -1
- package/src/kernel/vocabulary.test.ts +164 -0
- package/src/kernel/vocabulary.ts +146 -0
- package/src/testing/PARITY_GAPS.ts +8 -2
- package/src/testing/behavioral/README.md +3 -3
- package/src/testing/behavioral/completion.test.ts +19 -6
- package/src/testing/behavioral/errorMatrix.mcp.test.ts +4 -3
- package/src/testing/behavioral/errorMatrix.storeless.test.ts +1 -1
- package/src/testing/behavioral/exitCodes.e2e.test.ts +6 -5
- package/src/testing/behavioral/harness.foundation.test.ts +14 -5
- package/src/testing/behavioral/journeys.cli.test.ts +116 -75
- package/src/testing/behavioral/journeys.defaultPack.test.ts +82 -176
- package/src/testing/behavioral/journeys.firstInstall.test.ts +167 -0
- package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +9 -6
- package/src/testing/behavioral/journeys.packageStories.test.ts +188 -0
- package/src/testing/behavioral/liveReadSurface.ts +4 -3
- package/src/testing/behavioral/mcpSurface.test.ts +9 -4
- package/src/testing/behavioral/staleCommands.test.ts +10 -0
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +7 -7
- package/src/testing/eval/cases/assembly.ts +13 -6
- package/src/testing/eval/cases/effectful.ts +25 -13
- package/src/testing/eval/cases/readNouns.ts +22 -20
- package/src/testing/eval/cases/stable.ts +5 -1
- package/src/testing/eval/eval.test.ts +7 -1
- package/src/testing/fixtures/fixtureCapability.ts +45 -1
- package/src/testing/fixtures/graph/canonical.ts +6 -3
- package/src/testing/fixtures/graph/defaultPack.ts +6 -5
- package/src/testing/fixtures/graph/owlExport.ts +7 -5
- package/src/testing/helpers/fixtureGraph.ts +33 -16
- package/src/testing/helpers/packRuntime.ts +2 -3
- package/src/testing/helpers/projectCli.ts +2 -1
- package/src/testing/helpers/projectMcp.ts +6 -0
- package/src/testing/perf/budgets.test.ts +4 -0
- package/src/testing/perf/budgets.ts +22 -7
- package/src/testing/perf/globalSetup.ts +121 -6
- package/src/types.ts +2 -2
- package/src/capabilities/block/blockList.test.ts +0 -79
- package/src/capabilities/block/blockList.verb.ts +0 -163
- package/src/capabilities/block/index.ts +0 -25
- package/src/capabilities/block/pack.ts +0 -166
- package/src/capabilities/block/parity.test.ts +0 -187
- package/src/capabilities/block/tierChain.ts +0 -78
- package/src/capabilities/colophon/pragmaColophon.ts +0 -66
- package/src/capabilities/modifier/index.ts +0 -15
- package/src/capabilities/modifier/pack.ts +0 -64
- package/src/capabilities/standard/index.ts +0 -18
- package/src/capabilities/standard/pack.ts +0 -128
- package/src/capabilities/tier/index.ts +0 -19
- package/src/capabilities/tier/lookup.render.test.ts +0 -72
- package/src/capabilities/tier/lookup.render.ts +0 -59
- package/src/capabilities/tier/lookup.test.ts +0 -83
- package/src/capabilities/tier/lookup.verb.ts +0 -44
- package/src/capabilities/tier/pack.ts +0 -28
- package/src/capabilities/tier/runLookup.ts +0 -87
- package/src/capabilities/token/addConfig.render.ts +0 -27
- package/src/capabilities/token/addConfig.test.ts +0 -62
- package/src/capabilities/token/addConfig.verb.ts +0 -40
- package/src/capabilities/token/index.ts +0 -19
- package/src/capabilities/token/pack.ts +0 -58
- package/src/capabilities/token/runAddConfig.ts +0 -95
- package/src/kernel/completion/bashDrive.test.ts +0 -73
- package/src/kernel/runtime/graphpack/embedded/sample.ttl +0 -39
- package/src/kernel/runtime/lock.ts +0 -90
|
@@ -1,38 +1,46 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Select the summon generator for a `create` invocation.
|
|
3
|
-
*
|
|
4
|
-
* This module statically imports the summon generator packages' `generators`
|
|
5
|
-
* maps — and importing them runs a top-level `await loadTemplate` that reads
|
|
6
|
-
* every template file from disk. So this module MUST stay behind `create`'s
|
|
7
|
-
* lazy `import()` (R9): the fast paths (`buildProgram` / `__complete` / `--help`
|
|
8
|
-
* / reads) never load it, and `create --yes` never loads React either (the
|
|
9
|
-
* generators pull summon-core, whose Ink UI is dynamic-only).
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
1
|
import { generators as applicationGenerators } from "@canonical/summon-application";
|
|
13
2
|
import { generators as componentGenerators } from "@canonical/summon-component";
|
|
14
3
|
import type { GeneratorDefinition } from "@canonical/summon-core";
|
|
15
4
|
import { generators as packageGenerators } from "@canonical/summon-package";
|
|
16
5
|
import { PragmaError } from "../../kernel/error/PragmaError.js";
|
|
6
|
+
import { CREATE_GENERATORS } from "./constants.js";
|
|
17
7
|
import type { CreateKind } from "./types.js";
|
|
18
8
|
|
|
19
|
-
|
|
20
|
-
export const FRAMEWORKS = ["react", "svelte", "lit"] as const;
|
|
9
|
+
type GeneratorMap = Record<string, GeneratorDefinition | undefined>;
|
|
21
10
|
|
|
22
11
|
/**
|
|
23
|
-
* The
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
12
|
+
* The declared packages' generator maps, bound by noun. The STATIC imports above
|
|
13
|
+
* are load-bearing: `bun build --compile` bundles only statically analysable
|
|
14
|
+
* specifiers, so `import(CREATE_GENERATORS[kind].name)` would leave the
|
|
15
|
+
* generators out of the binary (measured: `Cannot find module
|
|
16
|
+
* '@canonical/summon-component' from '/$bunfs/root/…'`). The declaration decides
|
|
17
|
+
* WHICH generator runs; it cannot decide which module is linked.
|
|
18
|
+
*
|
|
19
|
+
* The maps' `generate` is invariant in its (specific) answer type, so they are
|
|
20
|
+
* erased to the base definition via `unknown` — runtime-safe because `execute`
|
|
21
|
+
* calls `generate(answers)` with the resolved answers, which carry the fields
|
|
22
|
+
* the specific generator reads.
|
|
27
23
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
const GENERATOR_MAPS: Record<CreateKind, GeneratorMap> = {
|
|
25
|
+
component: componentGenerators as unknown as GeneratorMap,
|
|
26
|
+
package: packageGenerators as unknown as GeneratorMap,
|
|
27
|
+
application: applicationGenerators as unknown as GeneratorMap,
|
|
28
|
+
};
|
|
32
29
|
|
|
33
30
|
/**
|
|
34
31
|
* Pick the generator for a `create <kind>` run.
|
|
35
32
|
*
|
|
33
|
+
* The module this lives in statically imports the summon generator packages'
|
|
34
|
+
* `generators` maps — and importing them pulls summon-core. So it MUST stay
|
|
35
|
+
* behind `create`'s lazy `import()` (R9): the fast paths (`buildProgram` /
|
|
36
|
+
* `__complete` / `--help` / reads) never load it, and `create --yes` never loads
|
|
37
|
+
* React either (summon-core's Ink UI is dynamic-only).
|
|
38
|
+
*
|
|
39
|
+
* `component` is branched on literally: it is the only noun with a framework
|
|
40
|
+
* axis (which is why the verb synthesises a `--framework` enum), and narrowing
|
|
41
|
+
* on `kind` here lets `CREATE_GENERATORS[kind].key` type-check over the
|
|
42
|
+
* remaining nouns without `in`-narrowing gymnastics.
|
|
43
|
+
*
|
|
36
44
|
* @param kind - The create noun.
|
|
37
45
|
* @param params - The coerced params (used for `--framework` on `component`).
|
|
38
46
|
* @returns The selected generator definition.
|
|
@@ -43,19 +51,17 @@ export function pickGenerator(
|
|
|
43
51
|
params: Readonly<Record<string, unknown>>,
|
|
44
52
|
): GeneratorDefinition {
|
|
45
53
|
if (kind === "component") {
|
|
46
|
-
const
|
|
47
|
-
const
|
|
54
|
+
const { frameworks } = CREATE_GENERATORS.component;
|
|
55
|
+
const framework = String(params.framework ?? frameworks[0]);
|
|
56
|
+
const generator = GENERATOR_MAPS.component[`component/${framework}`];
|
|
48
57
|
if (!generator) {
|
|
49
58
|
throw PragmaError.invalidInput("framework", framework, {
|
|
50
|
-
validOptions: [...
|
|
59
|
+
validOptions: [...frameworks],
|
|
51
60
|
});
|
|
52
61
|
}
|
|
53
62
|
return generator;
|
|
54
63
|
}
|
|
55
|
-
const generator =
|
|
56
|
-
kind === "package"
|
|
57
|
-
? packageMap.package
|
|
58
|
-
: applicationMap["application/react"];
|
|
64
|
+
const generator = GENERATOR_MAPS[kind][CREATE_GENERATORS[kind].key];
|
|
59
65
|
if (!generator) {
|
|
60
66
|
throw PragmaError.internalError(`missing ${kind} generator`);
|
|
61
67
|
}
|
|
@@ -1,24 +1,10 @@
|
|
|
1
1
|
// AUTO-GENERATED by scripts/build.ts — do not edit by hand.
|
|
2
|
-
// Regenerate: `bun run scripts/build.ts`. Inlines the
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// Regenerate: `bun run scripts/build.ts`. Inlines the .ejs templates of the
|
|
3
|
+
// generators that read through this manifest (`create component`) as strings, so
|
|
4
|
+
// `pragma create component` works from the standalone --compile binary (the .ejs
|
|
5
|
+
// files are absent from the binary's virtual filesystem).
|
|
5
6
|
/** Directory-qualified template path → file contents. */
|
|
6
7
|
export const TEMPLATES: Record<string, string> = {
|
|
7
|
-
"application/.storybook/main.ts.ejs": "import { createConfig } from \"@canonical/storybook-config\";\n\nexport default createConfig(\"react\", {\n staticDirs: [\"../src/assets\", \"../public\"],\n<% if (relay) { -%>\n // Wraps stories that declare `parameters.relay` in a relay-test-utils mock\n // environment, so components using Relay hooks render without a server.\n extraAddons: [\"@canonical/storybook-addon-relay\"],\n<% } -%>\n});\n",
|
|
8
|
-
"application/README.md.ejs": "# <%= name %>\n\nA React application with server-side rendering and routing, generated by\n`@canonical/summon`. It is built on Canonical's shared design system, router, and\nSSR packages, and ships SSR on two runtimes (Bun and Node), file-based routing, a\ncomponent library wired to design tokens, and Storybook.\n\n## Quick start\n\n```bash\nbun install\nbun run dev:bun # SSR dev server with HMR, on http://localhost:5174\n```\n\nEdit anything under `src/` and the page hot-reloads. To check the production\nbuild before shipping, run `bun run preview:bun` instead.\n\n## Running the app\n\nThe server scripts form a **2×3 matrix** — two modes (development and preview)\nacross three targets (a client-only SPA, SSR on Bun, SSR on Node/Express):\n\n| | `dev` — transform, HMR | `preview` — compiled, production-faithful |\n| ---------------- | ---------------------- | ----------------------------------------- |\n| **SPA** (no SSR) | `bun run dev` | `bun run preview` |\n| **SSR · Bun** | `bun run dev:bun` | `bun run preview:bun` |\n| **SSR · Node** | `bun run dev:express` | `bun run preview:express` |\n\nThe naming is systematic: the bare name is the **SPA**, the `:bun` / `:express`\nsuffix selects the **SSR runtime**, and the `dev` / `preview` prefix selects the\n**mode**. Reach for `dev:*` while building — it is the fast inner loop, with HMR\nand no build step — and for `preview:*` when verifying that the production bundle\nbehaves before a deploy: `preview:*` builds the client and runs a compiled\nrenderer over the built assets, the same artifact that deploys.\n\n```bash\nbun run storybook # component workshop\n```\n<% if (relay) { -%>\n\nRelay's compiler artifacts are committed, not rebuilt on the fly:\n\n```bash\nbun run relay # regenerate src/relay/__generated__ after schema/query edits\nbun run relay:watch # the same, re-running on change — pair with dev:*\n```\n<% } -%>\n\n## Adding routes\n\n`summon` scaffolds the building blocks of the app so new features follow the\nestablished structure rather than diverging from it.\n\n```bash\nsummon domain billing # a new feature domain (routes + pages)\nsummon route billing/invoices # a route within a domain\nsummon wrapper sidebar # a layout wrapper\n```\n\n## Testing\n\n```bash\nbun run test # unit + component tests (Vitest, jsdom)\nbun run test:coverage # the same, with a coverage report\nbun run test:e2e # boots all six matrix servers and asserts each serves\n```\n\n`test:e2e` is an end-to-end test *of the build*: it runs each of the six server\nscripts — including the production builds that `preview:*` performs — and asserts\nthat every server returns a rendered document and serves its client assets with\nthe correct content type.\n\n## Project structure\n\n```\nsrc/\n├── client/ Client entry (hydration)\n├── server/ Server entry, compiled renderer, dev servers, sitemap\n├── domains/ Feature domains (each owns its routes and pages)\n├── lib/ Shared components\n<% if (relay) { -%>\n├── relay/ Relay environment factory, executable mock schema,\n│ and generated artifacts (__generated__/)\n<% } -%>\n├── styles/ Application CSS\n├── assets/ Assets imported in code (bundled and hashed by Vite)\n└── routes.tsx Root route map\npublic/ Files served verbatim at a fixed URL (favicon, robots.txt)\ntest/e2e/ The 2×3 server-matrix end-to-end suite\n```\n\nDomains are the unit of feature organisation: each owns its routes and pages, so\nadding a feature is adding a domain rather than threading changes through shared\nfiles. This app ships both `src/assets/` (assets imported in code) and `public/`\n(files served by fixed URL); either may be removed if the app uses only one.\n<% if (relay) { -%>\n\n## Data layer (Relay)\n\nThe `catalog` domain demonstrates the app's [Relay](https://relay.dev) data\nlayer: `ProductList` fetches a page of products with `useLazyLoadQuery`, and\neach `ProductCard` reads its own colocated `useFragment` — the component owns\nits field selection, the parent query just spreads it.\n\n**By default the app needs no backend.** `createEnvironment`\n(`src/relay/environment.ts`) resolves every GraphQL operation in-process\nagainst an executable mock schema (`src/relay/schema.ts`, built from the SDL\nin `src/relay/schema.graphql`) backed by a small deterministic catalog. Set\n`VITE_GRAPHQL_URL` (or pass `graphqlUrl` to `createEnvironment`) to switch the\nenvironment to posting operations to a real GraphQL endpoint instead — no code\nchanges needed.\n\n**Generated artifacts are committed.** The Vite plugin runs with\n`codegen: false`, so `src/relay/__generated__/` is not rebuilt on the fly:\nafter editing a `graphql` tag or the schema, run `bun run relay` (or keep\n`bun run relay:watch` running) to regenerate the artifacts, and commit them.\n\n**SSR ships no data yet.** `useLazyLoadQuery` fetches (and suspends) while\nrendering, and the server cannot serialize the fetched store for the client\nyet, so the catalog page wraps its query subtree in `ClientOnly`\n(`src/lib/ClientOnly/`): the server streams the fallback and the browser\nfetches after hydration.\n\n<% if (standalone) { -%>\n**Patched dependencies.** `patches/` carries three package patches applied via\n`patchedDependencies` in `package.json` (a bun feature — with another package\nmanager, apply the equivalent mechanism): `react-relay@21.0.1` needs a\ncjs-module-lexer export hint so named imports survive Node SSR,\n`relay-runtime@21.0.1` needs real runtime bindings for type-only names its\ncompiler artifacts import as values (`ConcreteRequest`, `ReaderFragment`,\n`FragmentRefs`), and `relay-runtime-network@0.1.0` ships a broken `imports`\nmap. The last patch is temporary until the fixed upstream release lands\n([advl/lit-relay#32](https://github.com/advl/lit-relay/pull/32)); drop it and\nits `patchedDependencies` entry once a fixed version is published.\n<% } else { -%>\n**Patched dependencies.** This app was scaffolded inside a bun workspace, so\ndependency patching is owned by the workspace root — bun resolves\n`patchedDependencies` paths from the root, which is why the scaffold emitted\nno app-local `patches/` directory or `patchedDependencies` block. The root's\n`patches/` must carry the `react-relay@21.0.1`, `relay-runtime@21.0.1` and\n`relay-runtime-network@0.1.0` patches (named imports under Node SSR, runtime\nbindings for type-only artifact imports, and a broken `imports` map,\nrespectively).\n<% } -%>\n\n**Not yet on npm.** `@canonical/storybook-addon-relay` and the fixed\n`relay-runtime-network` release have not been published yet, so a fresh\ninstall of this app will not fully resolve until they land on npm.\n<% } -%>\n",
|
|
9
|
-
"application/biome.json.ejs": "{\n \"extends\": [\"@canonical/biome-config\"],\n \"files\": {\n \"includes\": [\"src\", \"*.json\", \"vite.config.ts\"<% if (relay) { %>, \"!src/relay/__generated__\"<% } %>]\n }\n}\n",
|
|
10
|
-
"application/index.html.ejs": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <meta name=\"description\" content=\"<%= name %> — a React application built with the Canonical design system, router, and SSR packages.\" />\n <link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"https://assets.ubuntu.com/v1/be7e4cc6-COF-favicon-32x32.png\">\n <title><%= name %></title>\n </head>\n <body>\n <div id=\"root\"></div>\n <script type=\"module\" src=\"/src/client/entry.tsx\"></script>\n </body>\n</html>\n",
|
|
11
|
-
"application/package.json.ejs": "{\n \"name\": \"<%= name %>\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"license\": \"GPL-3.0-only\",\n \"imports\": {\n \"#lib/*\": \"./src/lib/*\",\n \"#domains/*\": \"./src/domains/*\",\n<% if (relay) { -%>\n \"#relay/*\": \"./src/relay/*\",\n<% } -%>\n \"#styles/*\": \"./src/styles/*\"\n },\n \"scripts\": {\n \"dev\": \"vite\",\n \"dev:express\": \"node --import tsx src/server/server.express.ts\",\n \"dev:bun\": \"bun src/server/server.bun.ts\",\n \"preview\": \"bun run build:client && vite preview --outDir dist/client\",\n \"preview:bun\": \"bun run build:client && bun run build:server && bun src/server/preview.bun.ts\",\n \"preview:express\": \"bun run build:client && bun run build:server && node --import tsx src/server/preview.express.ts\",\n \"build\": \"bun run build:client\",\n \"build:all\": \"bun run build:client && bun run build:storybook\",\n \"build:client\": \"vite build --ssrManifest --outDir dist/client\",\n \"build:server\": \"vite build --mode server\",\n \"build:storybook\": \"storybook build\",\n \"check\": \"bun run check:biome && bun run check:ts\",\n \"check:fix\": \"bun run check:biome:fix && bun run check:ts\",\n \"check:biome\": \"biome check\",\n \"check:biome:fix\": \"biome check --write\",\n \"check:ts\": \"tsc --noEmit\",\n<% if (relay) { -%>\n \"relay\": \"relay-compiler\",\n \"relay:watch\": \"relay-compiler --watch\",\n<% } -%>\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"test:coverage\": \"vitest run --coverage\",\n \"test:e2e\": \"vitest run --config vitest.e2e.config.ts\",\n \"storybook\": \"storybook dev -p 6010 --no-open --host 0.0.0.0\"\n },\n \"dependencies\": {\n \"@canonical/react-ds-global\": \"<%= pragmaVersion %>\",\n<% if (forms) { -%>\n \"@canonical/react-ds-global-form\": \"<%= pragmaVersion %>\",\n<% } -%>\n \"@canonical/react-head\": \"<%= pragmaVersion %>\",\n \"@canonical/react-hooks\": \"<%= pragmaVersion %>\",\n \"@canonical/react-ssr\": \"<%= pragmaVersion %>\",\n \"@canonical/router-core\": \"<%= pragmaVersion %>\",\n \"@canonical/router-react\": \"<%= pragmaVersion %>\",\n<% if (relay) { -%>\n \"@canonical/storybook-addon-relay\": \"<%= pragmaVersion %>\",\n<% } -%>\n \"@canonical/storybook-config\": \"<%= pragmaVersion %>\",\n \"@canonical/styles\": \"<%= pragmaVersion %>\",\n \"express\": \"^5.2.1\",\n<% if (relay) { -%>\n \"graphql\": \"^16.11.0\",\n<% } -%>\n \"react\": \"^19.2.4\",\n \"react-dom\": \"^19.2.4\"<% if (relay) { %>,\n \"react-relay\": \"^21.0.1\",\n \"relay-runtime\": \"^21.0.1\",\n \"relay-runtime-network\": \"^0.1.0\"<% } %>\n },\n \"devDependencies\": {\n \"@biomejs/biome\": \"2.4.9\",\n \"@canonical/biome-config\": \"<%= pragmaVersion %>\",\n \"@canonical/typescript-config-react\": \"<%= pragmaVersion %>\",\n \"@storybook/react-vite\": \"^10.3.1\",\n \"@testing-library/jest-dom\": \"^6.9.1\",\n \"@testing-library/react\": \"^16.3.2\",\n \"@types/express\": \"^5.0.6\",\n \"@types/node\": \"^24.12.0\",\n \"@types/react\": \"^19.2.14\",\n \"@types/react-dom\": \"^19.2.3\",\n<% if (relay) { -%>\n \"@types/react-relay\": \"^18.2.1\",\n \"@types/relay-runtime\": \"^20.1.1\",\n \"@types/relay-test-utils\": \"^19.0.0\",\n<% } -%>\n \"@vitejs/plugin-react\": \"^6.0.0\",\n \"@vitest/coverage-v8\": \"^4.0.18\",\n \"browserslist\": \"^4.28.1\",\n \"bun-types\": \"^1.3.9\",\n \"jsdom\": \"^28.1.0\",\n \"lightningcss\": \"^1.32.0\",\n<% if (relay) { -%>\n \"relay-compiler\": \"^21.0.1\",\n \"relay-test-utils\": \"^21.0.1\",\n<% } -%>\n \"storybook\": \"^10.3.1\",\n \"tsx\": \"^4.19.0\",\n \"typescript\": \"^5.9.3\",\n \"vite\": \"^8.0.1\",\n<% if (relay) { -%>\n \"vite-plugin-relay-lite\": \"^0.12.0\",\n<% } -%>\n \"vitest\": \"^4.0.18\"\n }<% if (relay && standalone) { %>,\n \"patchedDependencies\": {\n \"react-relay@21.0.1\": \"patches/react-relay@21.0.1.patch\",\n \"relay-runtime@21.0.1\": \"patches/relay-runtime@21.0.1.patch\",\n \"relay-runtime-network@0.1.0\": \"patches/relay-runtime-network@0.1.0.patch\"\n }<% } %>\n}\n",
|
|
12
|
-
"application/src/client/entry.tsx.ejs": "import { HeadProvider } from \"@canonical/react-head\";\nimport { createBrowserRouter } from \"@canonical/router-core\";\nimport { Outlet, RouterProvider } from \"@canonical/router-react\";\nimport { hydrateRoot } from \"react-dom/client\";\n<% if (relay) { -%>\nimport { RelayEnvironmentProvider } from \"react-relay\";\nimport { createEnvironment } from \"#relay/environment.js\";\n<% } -%>\nimport { appRoutes, middleware, notFoundRoute } from \"../routes.js\";\nimport \"#styles/index.css\";\n\nconst router = createBrowserRouter(appRoutes, {\n middleware: [...middleware],\n notFound: notFoundRoute,\n});\n<% if (relay) { -%>\n\n// One Relay environment (network + normalized store) for the whole browser\n// session — module scope, so client-side navigations share the cache.\nconst relayEnvironment = createEnvironment();\n<% } -%>\n\nconst root = document.getElementById(\"root\");\nif (!root) {\n throw new Error('Root element \"#root\" not found');\n}\n\n<% if (relay) { -%>\nhydrateRoot(\n root,\n <HeadProvider>\n <RelayEnvironmentProvider environment={relayEnvironment}>\n <RouterProvider router={router}>\n <Outlet fallback={<p>Loading…</p>} />\n </RouterProvider>\n </RelayEnvironmentProvider>\n </HeadProvider>,\n);\n<% } else { -%>\nhydrateRoot(\n root,\n <HeadProvider>\n <RouterProvider router={router}>\n <Outlet fallback={<p>Loading…</p>} />\n </RouterProvider>\n </HeadProvider>,\n);\n<% } -%>\n",
|
|
13
|
-
"application/src/lib/Navigation/Navigation.tsx.ejs": "import { Link } from \"@canonical/router-react\";\nimport type { ReactElement } from \"react\";\nimport ThemeSelector from \"../ThemeSelector/index.js\";\n\nexport default function Navigation(): ReactElement {\n return (\n <nav aria-label=\"Main\">\n <Link to=\"home\">Home</Link>\n <Link params={{ slug: \"router-core\" }} to=\"guide\">\n Guide\n </Link>\n<% if (relay) { -%>\n <Link to=\"catalog\">Catalog</Link>\n<% } -%>\n<% if (forms) { -%>\n <Link to=\"contact\">Contact</Link>\n<% } -%>\n <Link search={{ auth: \"1\" }} to=\"account\">\n Demo sign-in\n </Link>\n <ThemeSelector />\n </nav>\n );\n}\n",
|
|
14
|
-
"application/src/lib/index.ts.ejs": "<% if (relay) { -%>\nexport { default as ClientOnly } from \"./ClientOnly/index.js\";\n<% } -%>\nexport { ExampleComponent } from \"./ExampleComponent/index.js\";\nexport { default as LazyComponent } from \"./LazyComponent/index.js\";\nexport { default as Navigation } from \"./Navigation/index.js\";\nexport { default as ThemeSelector } from \"./ThemeSelector/index.js\";\n",
|
|
15
|
-
"application/src/routes.tsx.ejs": "import {\n type AnyRoute,\n group,\n type NavigationContext,\n type RouteMiddleware,\n type RouteParamValues,\n redirect,\n route,\n wrapper,\n} from \"@canonical/router-core\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport accountRoutes from \"#domains/account/routes.js\";\n<% if (relay) { -%>\nimport catalogRoutes from \"#domains/catalog/routes.js\";\n<% } -%>\n<% if (forms) { -%>\nimport contactRoutes from \"#domains/contact/routes.js\";\n<% } -%>\nimport marketingRoutes from \"#domains/marketing/routes.js\";\nimport Navigation from \"#lib/Navigation/index.js\";\n\nconst protectedPaths = new Set([\"/account\"]);\n\nfunction hasDemoAuth(search: unknown): boolean {\n const authValue = (search as Record<string, unknown>)?.auth;\n\n return authValue === \"1\";\n}\n\nexport function getAuthRedirectHref(input: string | URL): string | null {\n // Absolute URLs are used as-is; relative paths resolve against a dummy base.\n const url =\n input instanceof URL ? input : new URL(input, \"https://router.local\");\n\n if (\n !protectedPaths.has(url.pathname) ||\n hasDemoAuth({ auth: url.searchParams.get(\"auth\") })\n ) {\n return null;\n }\n\n return `/login?from=${encodeURIComponent(url.pathname)}`;\n}\n\nexport function withAuth(loginPath: string): RouteMiddleware {\n return ((currentRoute: AnyRoute) => {\n if (!protectedPaths.has(currentRoute.url)) {\n return currentRoute;\n }\n\n const currentPrefetch = currentRoute.prefetch;\n\n return {\n ...currentRoute,\n prefetch: (\n params: unknown,\n search: unknown,\n context: NavigationContext,\n ) => {\n if (!hasDemoAuth(search)) {\n const from = currentRoute.render(\n (params ?? {}) as RouteParamValues | Record<string, never>,\n );\n\n redirect(`${loginPath}?from=${encodeURIComponent(from)}`, 302);\n }\n\n if (currentPrefetch) {\n return currentPrefetch(params, search, context);\n }\n },\n };\n }) as RouteMiddleware;\n}\n\nconst publicLayout = wrapper<ReactElement>({\n id: \"public-layout\",\n component: ({ children }: { children: ReactNode }) => (\n <div className=\"subgrid app-shell\">\n <header className=\"subgrid shell-header\">\n <Navigation />\n </header>\n <main className=\"subgrid\">{children}</main>\n </div>\n ),\n});\n\nconst notFoundRoute = route({\n url: \"/not-found\",\n content: () => (\n <section>\n <h1>Page not found</h1>\n <p>The page you are looking for does not exist.</p>\n </section>\n ),\n});\n\nconst [guide, home] = group(publicLayout, [\n marketingRoutes.guide,\n marketingRoutes.home,\n] as const);\n\nconst [account, login] = group(publicLayout, [\n accountRoutes.account,\n accountRoutes.login,\n] as const);\n\n<% if (forms) { -%>\nconst [contact] = group(publicLayout, [contactRoutes.contact] as const);\n\n<% } -%>\n<% if (relay) { -%>\nconst [catalog] = group(publicLayout, [catalogRoutes.catalog] as const);\n\n<% } -%>\nconst appRoutes = {\n guide,\n home,\n account,\n login,\n<% if (forms) { -%>\n contact,\n<% } -%>\n<% if (relay) { -%>\n catalog,\n<% } -%>\n} as const;\n\nexport type AppRoutes = typeof appRoutes;\n\ndeclare module \"@canonical/router-react\" {\n interface RouterRegister {\n routes: AppRoutes;\n }\n}\n\nexport const middleware = [withAuth(\"/login\")] as const;\n\nexport { appRoutes, notFoundRoute };\n",
|
|
16
|
-
"application/src/server/entry.tsx.ejs": "import { HeadProvider } from \"@canonical/react-head\";\nimport type { ServerEntrypointProps } from \"@canonical/react-ssr/renderer\";\nimport { createStaticRouter } from \"@canonical/router-core\";\nimport { Outlet, RouterProvider } from \"@canonical/router-react\";\n<% if (relay) { -%>\nimport { RelayEnvironmentProvider } from \"react-relay\";\nimport { createEnvironment } from \"#relay/environment.js\";\n<% } -%>\nimport { appRoutes, middleware, notFoundRoute } from \"../routes.js\";\nimport \"#styles/app.css\";\n\ninterface InitialData extends Record<string, unknown> {\n readonly url?: string;\n /** Colour-scheme preference resolved from the request cookie, if any. */\n readonly theme?: \"light\" | \"dark\";\n}\n\nexport default function EntryServer(props: ServerEntrypointProps<InitialData>) {\n const initialData = props.initialData ?? {};\n const url = initialData.url ?? \"/\";\n const router = createStaticRouter(appRoutes, url, {\n middleware: [...middleware],\n notFound: notFoundRoute,\n });\n<% if (relay) { -%>\n\n // A fresh Relay environment per server render, so no store state leaks\n // between requests. Nothing fetches through it yet: components that issue\n // queries are wrapped in `ClientOnly` (see CatalogPage) until data\n // serialization/hydration is supported; the provider is here so any\n // component touching Relay context renders without branching on runtime.\n const relayEnvironment = createEnvironment();\n<% } -%>\n\n // Paint the cookie-resolved theme on <html> for a flash-free first render —\n // the same element `usePreferredTheme` toggles on the client, and one React\n // does not hydrate (only `#root` is), so there is no mismatch to reconcile.\n<% if (relay) { -%>\n return (\n <html lang={props.lang} className={initialData.theme}>\n <head>\n {props.otherHeadElements}\n {props.scriptElements}\n {props.linkElements}\n </head>\n <body>\n <div id=\"root\">\n <HeadProvider>\n <RelayEnvironmentProvider environment={relayEnvironment}>\n <RouterProvider router={router}>\n <Outlet fallback={<p>Loading…</p>} />\n </RouterProvider>\n </RelayEnvironmentProvider>\n </HeadProvider>\n </div>\n </body>\n </html>\n );\n<% } else { -%>\n return (\n <html lang={props.lang} className={initialData.theme}>\n <head>\n {props.otherHeadElements}\n {props.scriptElements}\n {props.linkElements}\n </head>\n <body>\n <div id=\"root\">\n <HeadProvider>\n <RouterProvider router={router}>\n <Outlet fallback={<p>Loading…</p>} />\n </RouterProvider>\n </HeadProvider>\n </div>\n </body>\n </html>\n );\n<% } -%>\n}\n\nexport type { InitialData };\n",
|
|
17
|
-
"application/src/sitemap/getSitemapItems.ts.ejs": "import type { SitemapItem } from \"@canonical/react-ssr/renderer\";\n\n/**\n * Returns sitemap entries for all known routes.\n *\n * Update this list as you add domains and routes. For dynamic content\n * (blog posts, product pages), fetch entries from your data source.\n *\n * @example\n * const posts = await fetchPosts();\n * return [\n * ...staticEntries,\n * ...posts.map((post) => ({\n * loc: `/blog/${post.slug}`,\n * lastmod: post.updatedAt,\n * changefreq: \"weekly\" as const,\n * priority: 0.7,\n * })),\n * ];\n */\nexport default async function getSitemapItems(): Promise<SitemapItem[]> {\n return [\n { loc: \"/\", changefreq: \"weekly\", priority: 1.0 },\n { loc: \"/guides/router-core\", changefreq: \"monthly\", priority: 0.8 },\n<% if (relay) { -%>\n { loc: \"/catalog\", changefreq: \"weekly\", priority: 0.7 },\n<% } -%>\n<% if (forms) { -%>\n { loc: \"/contact\", changefreq: \"monthly\", priority: 0.7 },\n<% } -%>\n { loc: \"/account\", changefreq: \"monthly\", priority: 0.5 },\n { loc: \"/login\", changefreq: \"yearly\", priority: 0.3 },\n ];\n}\n",
|
|
18
|
-
"application/src/styles/index.css.ejs": "@import url(\"@canonical/styles\");\n<% if (forms) { -%>\n@import url(\"@canonical/react-ds-global-form/dist/esm/index.css\");\n<% } -%>\n@import url(\"./app.css\");\n",
|
|
19
|
-
"application/tsconfig.json.ejs": "{\n \"extends\": \"@canonical/typescript-config-react\",\n \"compilerOptions\": {\n \"baseUrl\": \"src\",\n \"skipLibCheck\": true,\n \"types\": [\n \"bun-types\",\n \"node\",\n \"react\",\n \"react-dom\",\n \"vitest/globals\",\n \"@testing-library/jest-dom\"\n ],\n \"noEmit\": true,\n \"paths\": {\n \"#lib/*\": [\"./lib/*\"],\n \"#domains/*\": [\"./domains/*\"],\n<% if (relay) { -%>\n \"#relay/*\": [\"./relay/*\"],\n<% } -%>\n \"#styles/*\": [\"./styles/*\"]\n }\n },\n \"include\": [\n \"src/**/*.ts\",\n \"src/**/*.tsx\",\n \".storybook/*.ts\",\n \".storybook/*.tsx\",\n \"vite.config.ts\",\n \"vitest.setup.ts\",\n \"vitest.config.ts\",\n \"vitest.e2e.config.ts\",\n \"test/**/*.ts\"\n ]\n}\n",
|
|
20
|
-
"application/vite.config.ts.ejs": "import react from \"@vitejs/plugin-react\";\nimport browserslist from \"browserslist\";\nimport { browserslistToTargets } from \"lightningcss\";\nimport { defineConfig } from \"vite\";\n<% if (relay) { -%>\nimport relay from \"vite-plugin-relay-lite\";\n<% } -%>\n\n// Path aliases (#lib, #domains, #styles) are declared as Node subpath imports\n// in package.json \"imports\" and resolved natively by Vite — no resolver plugin.\nconst PORT = Number(process.env.PORT) || undefined;\n\n// `build:server` runs `vite build --mode server` to compile the two server\n// renderers in a single pass. Each is a separate, individually-instantiated\n// Lego brick, so they are named Rollup inputs — the build emits\n// `dist/server/renderer.js` (the JSX app) + `dist/server/sitemap.js` (the XML\n// sitemap), which the preview servers import directly. Every other command\n// (`dev`, `build:client`, `preview`) uses the default mode and the SPA client\n// build (`--ssrManifest --outDir dist/client`).\nexport default defineConfig(({ mode }) => ({\n<% if (relay) { -%>\n // Relay's `graphql\\`...\\`` tags must be rewritten into imports of the\n // compiler-generated artifacts. `@vitejs/plugin-react` v6 is OXC-based (no\n // Babel), so the classic `babel-plugin-relay` route would need the\n // `@rolldown/plugin-babel` escape hatch; `vite-plugin-relay-lite` does the\n // same rewrite with its own parser and no Babel, and works on Vite 8 /\n // rolldown despite its peer range stopping at Vite 7 (bun installs it with\n // only a peer-version warning). Footgun: its tag scanner matches the word\n // \"graphql\" followed by a backtick even inside comments, so never\n // backtick-quote a graphql file name in a comment. `codegen: false` because\n // artifacts are committed and regenerated explicitly via the `relay` /\n // `relay:watch` scripts.\n plugins: [relay({ codegen: false }), react()],\n<% } else { -%>\n plugins: [react()],\n<% } -%>\n // Honour the PORT env var for `dev` (SPA) and `preview` so all server scripts\n // — including the SSR ones, which already read PORT — respond to it uniformly.\n server: { port: PORT },\n preview: { port: PORT },\n // CSS delivery is the app's job, not the libraries': @canonical/* packages\n // deliberately ship modern, uncompiled per-component CSS and take no stance\n // on browser support. Lightning CSS (already bundled with Vite 8) compiles\n // that CSS down to this app's declared browser floor — the browserslist\n // query in .browserslistrc, which is the one knob to turn when support\n // requirements change. Not everything can be downleveled: math functions\n // like `mod()` (used by the typography baseline grid) have no polyfillable\n // form, so that feature's effective Chromium ≥ 125 floor is unchanged.\n css: {\n transformer: \"lightningcss\",\n lightningcss: { targets: browserslistToTargets(browserslist()) },\n },\n build: {\n // Minify CSS with Lightning CSS too (instead of the esbuild default), so\n // a single tool owns both downleveling and minification — in the client\n // build and the server build alike.\n cssMinify: \"lightningcss\",\n ...(mode === \"server\"\n ? {\n ssr: true,\n outDir: \"dist/server\",\n rollupOptions: {\n input: {\n renderer: \"src/server/renderer.tsx\",\n sitemap: \"src/sitemap/renderer.ts\",\n },\n },\n }\n : undefined),\n },\n ssr: {\n // Bundle @canonical/* for SSR rather than externalising them, for two\n // reasons: (1) some packages declare only a \"module\" entry (no\n // \"main\"/\"exports\"), which Vite's SSR (Node-style) resolver ignores —\n // externalising them fails with ERR_RESOLVE_PACKAGE_ENTRY_FAIL; (2) their\n // built output imports CSS as a side effect (e.g. `import \"./x.css\"`),\n // which Node cannot load (ERR_UNKNOWN_FILE_EXTENSION) but Vite's SSR\n // transform no-ops. The regex covers the whole scope so any current or\n // future @canonical dependency is handled.\n<% if (relay) { -%>\n //\n // The Relay packages (react-relay, relay-runtime, relay-runtime-network,\n // graphql) stay externalised like react itself. Three packaging defects\n // made that possible and are fixed via `patchedDependencies` (app-local\n // patches/ for a standalone app; the workspace root's patches/ when the\n // app lives inside a bun workspace): react-relay's CJS exports are member\n // expressions cjs-module-lexer cannot detect (breaking named imports\n // under Node SSR and Vite's dev module runner), relay-runtime lacks\n // runtime bindings for type-only names its compiler artifacts import as\n // values (ConcreteRequest, ReaderFragment, FragmentRefs), and\n // relay-runtime-network@0.1.0 ships an `imports` map pointing at its\n // unpublished src/ directory.\n<% } -%>\n noExternal: [/^@canonical\\//],\n },\n}));\n",
|
|
21
|
-
"application/vitest.setup.ts.ejs": "import * as matchers from \"@testing-library/jest-dom/matchers\";\nimport { cleanup } from \"@testing-library/react\";\n<% if (relay) { -%>\nimport { afterEach, expect, vi } from \"vitest\";\n<% } else { -%>\nimport { afterEach, expect } from \"vitest\";\n<% } -%>\n\nexpect.extend(matchers as unknown as Parameters<typeof expect.extend>[0]);\n<% if (relay) { -%>\n\n// relay-test-utils' `createMockEnvironment` wraps environment methods in\n// `jest.fn` mocks whenever NODE_ENV === \"test\", reaching for a global `jest`\n// object. Vitest sets NODE_ENV=test but exposes `vi` instead, so alias it —\n// `vi.fn` is API-compatible with the `jest.fn` surface relay-test-utils uses\n// (`.mock`, `.mockClear`).\n(globalThis as { jest?: typeof vi }).jest = vi;\n<% } -%>\n\nafterEach(() => {\n cleanup();\n});\n",
|
|
22
8
|
"component/lit/component.ts.ejs": "import { html, LitElement } from \"lit\";\nimport { customElement } from \"lit/decorators.js\";\n<% if (withStyles) { -%>\nimport styles from \"./styles.css\";\n<% } -%>\nimport type { <%= name %>Props } from \"./types.js\";\n\nconst componentCssClassName = \"ds <%= kebabName %>\";\n\n/**\n * <%= name %> web component.\n *\n * @implements ds:global.component.<%= kebabName %>\n */\n@customElement(\"ds-<%= kebabName %>\")\nexport default class <%= name %> extends LitElement implements <%= name %>Props {\n<% if (withStyles) { -%>\n static styles = styles;\n\n<% } -%>\n render() {\n return html`\n <div class=\"${componentCssClassName}\">\n <slot></slot>\n </div>\n `;\n }\n}\n",
|
|
23
9
|
"component/lit/index.ts.ejs": "export { default as <%= name %> } from \"./<%= name %>.js\";\nexport type { <%= name %>Props } from \"./types.js\";\n",
|
|
24
10
|
"component/lit/stories.ts.ejs": "import type { Meta, StoryObj } from \"@storybook/web-components-vite\";\nimport { html } from \"lit\";\n\nimport \"./<%= name %>.js\";\nimport type <%= name %> from \"./<%= name %>.js\";\n\nconst meta = {\n title: \"Web Components/<%= name %>\",\n tags: [\"autodocs\"],\n} satisfies Meta<<%= name %>>;\n\nexport default meta;\ntype Story = StoryObj<<%= name %>>;\n\nexport const Default: Story = {\n render: () => html`<ds-<%= kebabName %>>Hello, <%= name %>!</ds-<%= kebabName %>>`,\n};\n",
|
|
@@ -39,15 +25,4 @@ export const TEMPLATES: Record<string, string> = {
|
|
|
39
25
|
"component/svelte/stories.ts.ejs": "import type { Meta, StoryObj } from \"@storybook/svelte\";\nimport <%= name %> from \"./<%= name %>.svelte\";\n\nconst meta: Meta<typeof <%= name %>> = {\n\ttitle: \"Components/<%= name %>\",\n\tcomponent: <%= name %>,\n\ttags: [\"autodocs\"],\n};\n\nexport default meta;\ntype Story = StoryObj<typeof <%= name %>>;\n\nexport const Default: Story = {};\n\nexport const WithCustomClass: Story = {\n\targs: {\n\t\tclass: \"custom-class\",\n\t},\n};\n",
|
|
40
26
|
"component/svelte/test.ts.ejs": "import type { Locator } from \"@vitest/browser/context\";\nimport { createRawSnippet } from \"svelte\";\nimport type { ComponentProps } from \"svelte\";\nimport { describe, expect, it } from \"vitest\";\nimport { render } from \"vitest-browser-svelte\";\nimport type { RenderResult } from \"vitest-browser-svelte\";\nimport Component from \"./<%= name %>.svelte\";\n\ndescribe(\"<%= name %> component\", () => {\n\tconst baseProps = {\n\t\tchildren: createRawSnippet(() => ({\n\t\t\trender: () => `<span><%= name %></span>`,\n\t\t})),\n\t} satisfies ComponentProps<typeof Component>;\n\n\tit(\"renders\", async () => {\n\t\tconst page = render(Component, { ...baseProps });\n\t\tawait expect.element(componentLocator(page)).toBeInTheDocument();\n\t\tawait expect.element(page.getByText(\"<%= name %>\")).toBeVisible();\n\t});\n\n\tdescribe(\"attributes\", () => {\n\t\tit.each([\n\t\t\t[\"id\", \"test-id\"],\n\t\t\t[\"aria-label\", \"test-aria-label\"],\n\t\t])(\"applies %s\", async (attribute, expected) => {\n\t\t\tconst page = render(Component, { ...baseProps, [attribute]: expected });\n\t\t\tawait expect\n\t\t\t\t.element(componentLocator(page))\n\t\t\t\t.toHaveAttribute(attribute, expected);\n\t\t});\n\n\t\tit(\"applies classes\", async () => {\n\t\t\tconst page = render(Component, { ...baseProps, class: \"test-class\" });\n\t\t\tawait expect.element(componentLocator(page)).toHaveClass(\"test-class\");\n<% if (withStyles) { _%>\n\t\t\tawait expect.element(componentLocator(page)).toHaveClass(\"ds\");\n\t\t\tawait expect\n\t\t\t\t.element(componentLocator(page))\n\t\t\t\t.toHaveClass(\"<%= kebabName %>\");<% } %>\n\t\t});\n\n\t\tit(\"applies style\", async () => {\n\t\t\tconst page = render(Component, {\n\t\t\t\t...baseProps,\n\t\t\t\tstyle: \"color: orange;\",\n\t\t\t});\n\t\t\tawait expect\n\t\t\t\t.element(componentLocator(page))\n\t\t\t\t.toHaveStyle({ color: \"orange\" });\n\t\t});\n\t});\n});\n\n// Note: Prefer role/semantics-oriented ways of selecting elements (e.g., by role, label, etc.) not only for component roots but for all elements to enhance accessibility and maintainability.\n// To select the component's root element, use one of the available [Locators](https://vitest.dev/guide/browser/locators.html).\nfunction componentLocator(page: RenderResult<typeof Component>): Locator {\n\t// 1. SEMANTIC ROLES (PREFERRED) - These the most closely resemble how assistive technologies interact with your component\n\t// return page.getByRole(\"link\", { name: \"Learn more\" });\n\n\t// 2. LABELS - To test the association between labels and form controls\n\t// return page.getByLabel(\"Email address\");\n\n\t// 3. PLACEHOLDER TEXT - Useful for input fields with placeholders\n\t// return page.getByPlaceholder(\"Enter your email\");\n\n\t// 4. TEXT CONTENT - Good for testing the presence of unique texts and selecting non-interactive elements\n\t// return page.getByText(\"<%= name %>\");\n\n\t// 5. ALT TEXT - For images and visual elements\n\t// return page.getByAltText(\"Company logo\");\n\n\t// 6. TITLE ATTRIBUTE - For elements with title tooltips\n\t// return page.getByTitle(\"Close dialog\");\n\n\t// 7. TEST IDS (LAST RESORT) - When semantic options aren't available\n\t// return page.getByTestId(\"<%= kebabName %>\");\n}\n",
|
|
41
27
|
"component/svelte/types.ts.ejs": "import type { Snippet } from \"svelte\";\nimport type { SvelteHTMLElements } from \"svelte/elements\";\n\n/*\n\tFor components that render specific HTML elements with extended prop sets,\n\tmake sure to change `\"div\"` to the appropriate HTML element type.\n*/\ntype BaseProps = SvelteHTMLElements[\"div\"];\n\nexport interface <%= name %>Props extends BaseProps {\n\t/** Content to render inside the component */\n\tchildren?: Snippet;\n\t// Define additional props specific to <%= name %> here\n}\n\n/*\n\t...or consider using appropriate interfaces directly exported from `svelte/elements`.\n\n\tFor example:\n\t- `HTMLButtonAttributes` (provides `disabled`, `type`, etc.)\n\t- `HTMLAnchorAttributes` (provides `href`, `target`, etc.)\n\t- `HTMLInputAttributes` (provides `type`, `checked`, `value`, etc.)\n\t- `HTMLFormAttributes` (provides `action`, `method`, etc.)\n\n\t```typescript\n\timport type { HTMLButtonAttributes } from \"svelte/elements\";\n\n\texport interface MyButtonProps extends HTMLButtonAttributes {\n\t\t// Your custom props here\n\t}\n\t```\n\n\tSee Svelte documentation on typing wrapper components:\n\thttps://svelte.dev/docs/svelte/typescript#Typing-wrapper-components\n*/\n",
|
|
42
|
-
"package/PULL_REQUEST_TEMPLATE.md.ejs": "## Summary\n\n[What this PR does and why]\n\n## QA\n\n- [Add QA steps]\n\n### Checklist\n\n- [ ] PR title follows [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) format\n- [ ] Code follows the [code standards](https://github.com/canonical/web-code-standards)\n- [ ] `check`, `test` pass locally\n",
|
|
43
|
-
"package/README.md.ejs": "# <%= name %>\n\n<%= description || 'TODO: Add description' %>\n\n## Installation\n\n```bash\nbun add <%= name %>\n```\n\n## Usage\n\n```typescript\nimport { } from \"<%= name %>\";\n```\n\n## Development\n\n```bash\n# Run checks\nbun run check\n\n# Run tests\nbun run test\n```\n\n## License\n\n<%= license %>\n",
|
|
44
|
-
"package/biome.json.ejs": "{\n \"extends\": [\"@canonical/biome-config\"],\n \"files\": {\n \"includes\": [\"src\", \"*.json\"]\n }\n}\n",
|
|
45
|
-
"package/cli.ts.ejs": "#!/usr/bin/env bun\n/**\n * <%= name %> CLI\n *\n * <%= description || 'TODO: Add description' %>\n */\n\nconsole.log(\"Hello from <%= shortName %>!\");\n",
|
|
46
|
-
"package/index.css.ejs": "/**\n * <%= name %>\n *\n * <%= description || 'TODO: Add description' %>\n */\n",
|
|
47
|
-
"package/index.ts.ejs": "/**\n * <%= name %>\n *\n * <%= description || 'TODO: Add description' %>\n */\n\nexport {};\n",
|
|
48
|
-
"package/package.json.ejs": "{\n \"name\": \"<%= name %>\",\n \"description\": \"<%= description %>\",\n \"version\": \"<%= version %>\",\n \"type\": \"module\",\n<% if (type === 'tool-ts') { -%>\n \"module\": \"src/index.ts\",\n \"types\": \"src/index.ts\",\n<% } else if (type === 'css') { -%>\n \"module\": \"src/index.css\",\n<% } else { -%>\n \"module\": \"dist/esm/index.js\",\n \"types\": \"dist/types/index.d.ts\",\n<% } -%>\n<% if (withCli && type !== 'css') { -%>\n \"bin\": {\n \"<%= shortName %>\": \"src/cli.ts\"\n },\n<% } -%>\n \"files\": [\n<% if (type === 'tool-ts' || type === 'css') { -%>\n \"src\"\n<% } else { -%>\n \"dist\"\n<% } -%>\n ],\n \"author\": {\n \"email\": \"webteam@canonical.com\",\n \"name\": \"Canonical Webteam\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/canonical/pragma\"\n },\n \"license\": \"<%= license %>\",\n \"bugs\": {\n \"url\": \"https://github.com/canonical/pragma/issues\"\n },\n \"homepage\": \"https://github.com/canonical/pragma#readme\",\n \"scripts\": {\n<% if (type === 'tool-ts' || type === 'css') { -%>\n \"build\": \"echo 'No build needed'\",\n<% } else { -%>\n \"build\": \"tsc -p tsconfig.build.json\",\n<% } -%>\n<% if (withStorybook) { -%>\n \"build:all\": \"bun run build && bun run build:storybook\",\n \"build:storybook\": \"storybook build\",\n<% } else { -%>\n \"build:all\": \"bun run build\",\n<% } -%>\n<% if (type === 'css') { -%>\n \"check\": \"bun run check:biome && bun run check:webarchitect\",\n \"check:fix\": \"bun run check:biome:fix\",\n<% } else { -%>\n \"check\": \"bun run check:biome && bun run check:ts && bun run check:webarchitect\",\n \"check:fix\": \"bun run check:biome:fix && bun run check:ts\",\n<% } -%>\n \"check:biome\": \"biome check\",\n \"check:biome:fix\": \"biome check --write\",\n<% if (type !== 'css') { -%>\n \"check:ts\": \"tsc --noEmit\",\n<% } -%>\n \"check:webarchitect\": \"webarchitect <%= ruleset %>\",\n<% if (withStorybook) { -%>\n \"storybook\": \"storybook dev -p 6006 --no-open --host 0.0.0.0\",\n<% } -%>\n<% if (type === 'css') { -%>\n \"test\": \"echo 'No tests for CSS package'\"\n<% } else { -%>\n \"test\": \"vitest run\"\n<% } -%>\n },\n<% if (type !== 'css') { -%>\n \"devDependencies\": {\n \"@biomejs/biome\": \"2.4.6\",\n \"@canonical/biome-config\": \"^<%= monorepoVersion || '0.1.0' %>\",\n<% if (withReact) { -%>\n \"@canonical/typescript-config-react\": \"^<%= monorepoVersion || '0.1.0' %>\",\n<% } else { -%>\n \"@canonical/typescript-config\": \"^<%= monorepoVersion || '0.1.0' %>\",\n<% } -%>\n<% if (withStorybook) { -%>\n \"@chromatic-com/storybook\": \"^5.0.0\",\n<% } -%>\n<% if (withReact) { -%>\n \"@types/react\": \"^19.0.0\",\n \"@types/react-dom\": \"^19.0.0\",\n<% } -%>\n \"bun-types\": \"^1.0.0\",\n<% if (withStorybook) { -%>\n \"storybook\": \"^10.1.11\",\n<% } -%>\n \"typescript\": \"^5.9.3\",\n \"vitest\": \"^3.2.4\"\n },\n \"dependencies\": {\n<% if (withStorybook) { -%>\n \"@canonical/storybook-config\": \"^<%= monorepoVersion || '0.1.0' %>\",\n<% } -%>\n<% if (withReact) { -%>\n \"react\": \"^19.0.0\",\n \"react-dom\": \"^19.0.0\"\n<% } -%>\n }\n<% } else { -%>\n \"devDependencies\": {\n \"@biomejs/biome\": \"2.4.6\",\n \"@canonical/biome-config\": \"^<%= monorepoVersion || '0.1.0' %>\"\n }\n<% } -%>\n}\n",
|
|
49
|
-
"package/storybook-main.ts.ejs": "import { createConfig } from \"@canonical/storybook-config\";\n\nexport default createConfig(\"react\", {\n staticDirs: [\"../src/assets\", \"../public\"],\n});\n",
|
|
50
|
-
"package/storybook-preview.ts.ejs": "import { withThemeByClassName } from \"@storybook/addon-themes\";\nimport type { Preview, ReactRenderer } from \"@storybook/react-vite\";\n\nimport \"index.css\";\nimport \"@canonical/styles-debug/baseline-grid\";\n\nconst preview: Preview = {\n tags: [\"autodocs\"],\n decorators: [\n withThemeByClassName<ReactRenderer>({\n themes: {\n light: \"is-light\",\n dark: \"dark\",\n paper: \"is-paper\",\n },\n defaultTheme: \"light\",\n }),\n ],\n parameters: {\n docs: {\n codePanel: true,\n },\n },\n};\n\nexport default preview;\n",
|
|
51
|
-
"package/tsconfig-react.json.ejs": "{\n \"extends\": \"@canonical/typescript-config-react\",\n \"compilerOptions\": {\n \"baseUrl\": \"src\",\n \"types\": [\"bun-types\"]\n },\n \"include\": [\"src/**/*.ts\", \"src/**/*.tsx\"]\n}\n",
|
|
52
|
-
"package/tsconfig.json.ejs": "{\n \"extends\": \"@canonical/typescript-config\",\n \"compilerOptions\": {\n \"baseUrl\": \"src\",\n \"types\": [\"bun-types\"]\n },\n \"include\": [\"src/**/*.ts\"]\n}\n",
|
|
53
28
|
};
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import type { CREATE_GENERATORS } from "./constants.js";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
4
|
+
* The `create` nouns — one per declared generator binding (path capped at two
|
|
5
|
+
* segments). Derived from {@link CREATE_GENERATORS} so the union cannot drift
|
|
6
|
+
* from the bindings.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately free of any summon-core / generator import, so a module that only
|
|
9
|
+
* needs this type (e.g. `create.verb`) does not drag `pickGenerator` — and its
|
|
10
|
+
* heavy generator imports — into the static graph.
|
|
5
11
|
*/
|
|
6
|
-
|
|
7
|
-
/** The three `create` nouns (path capped at two segments). */
|
|
8
|
-
export type CreateKind = "component" | "package" | "application";
|
|
12
|
+
export type CreateKind = keyof typeof CREATE_GENERATORS;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Standard-noun semantic parity (PROTECTED) — the
|
|
3
|
-
* code-standards fixture graph.
|
|
2
|
+
* Standard-noun semantic parity (PROTECTED) — the `standard` story
|
|
3
|
+
* `pragma.conf.ts` declares, over a code-standards fixture graph.
|
|
4
4
|
*
|
|
5
5
|
* The hand-written standard domain is gone, so parity is asserted directly
|
|
6
6
|
* against the graph: every standard is reachable through list/lookup/categories/
|
|
@@ -10,16 +10,22 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
13
|
-
import { compilePack } from "
|
|
14
|
-
import type { LookupOutput } from "
|
|
15
|
-
import { parsePackDefinition } from "
|
|
16
|
-
import type { PackRow } from "
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import
|
|
20
|
-
import type {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
13
|
+
import { compilePack } from "../kernel/packs/compile.js";
|
|
14
|
+
import type { LookupOutput } from "../kernel/packs/resolveEntity.js";
|
|
15
|
+
import { parsePackDefinition } from "../kernel/packs/schema.js";
|
|
16
|
+
import type { PackRow } from "../kernel/packs/types.js";
|
|
17
|
+
import { distributionSource } from "../kernel/packs/types.js";
|
|
18
|
+
import { verbKey } from "../kernel/packs/uniqueness.js";
|
|
19
|
+
import { DEFAULT_PREFIX_MAP } from "../kernel/render/prefixes.js";
|
|
20
|
+
import type { PragmaRuntime } from "../kernel/runtime/types.js";
|
|
21
|
+
import type { VerbSpec } from "../kernel/spec/types.js";
|
|
22
|
+
import { buildFixtureRuntime } from "../testing/helpers/packRuntime.js";
|
|
23
|
+
import { declaredStories } from "./distribution.js";
|
|
24
|
+
|
|
25
|
+
const standardPack = declaredStories.get("standard");
|
|
26
|
+
if (!standardPack) {
|
|
27
|
+
throw new Error('pragma.conf.ts declares no story for "standard"');
|
|
28
|
+
}
|
|
23
29
|
|
|
24
30
|
const CS = "http://pragma.canonical.com/codestandards#";
|
|
25
31
|
const PREFIXES = {
|
|
@@ -72,7 +78,11 @@ cs:code.function.purity a cs:CodeStandard ;
|
|
|
72
78
|
`;
|
|
73
79
|
|
|
74
80
|
const verbs = () =>
|
|
75
|
-
compilePack(
|
|
81
|
+
compilePack(
|
|
82
|
+
standardPack,
|
|
83
|
+
distributionSource("pragma.conf.ts"),
|
|
84
|
+
DEFAULT_PREFIX_MAP,
|
|
85
|
+
);
|
|
76
86
|
const verb = (label: string) =>
|
|
77
87
|
verbs().find((v) => verbKey(v.path) === `standard ${label}`) as VerbSpec;
|
|
78
88
|
|
|
@@ -98,7 +108,7 @@ async function lookupAt(
|
|
|
98
108
|
return out.results.at(0) as Record<string, unknown>;
|
|
99
109
|
}
|
|
100
110
|
|
|
101
|
-
describe("standard
|
|
111
|
+
describe("standard story definition (PROTECTED)", () => {
|
|
102
112
|
it("round-trips as declarative JSON and validates", () => {
|
|
103
113
|
expect(
|
|
104
114
|
parsePackDefinition(JSON.parse(JSON.stringify(standardPack)), "t"),
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The distribution's own declared read stories (PROTECTED).
|
|
3
|
+
*
|
|
4
|
+
* `pragma.conf.ts` is hand-authored data that the TypeScript compiler checks
|
|
5
|
+
* against `PackDefinition`; `kernel/packs/schema.ts` is an independently written
|
|
6
|
+
* zod grammar that every config- and package-declared story must pass. Nothing
|
|
7
|
+
* forced the two to agree until the distribution started coming through the same
|
|
8
|
+
* door — and the first thing that comparison found was `sample.fixedCount`,
|
|
9
|
+
* documented and used by three of these five stories yet missing from the
|
|
10
|
+
* `.strict()` sample schema, which made any third-party story declaring it die
|
|
11
|
+
* with a fatal CONFIG_ERROR. Round-tripping ALL five is what keeps that shut.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, expect, it } from "vitest";
|
|
15
|
+
import { compilePack } from "../kernel/packs/compile.js";
|
|
16
|
+
import { parsePackDefinition } from "../kernel/packs/schema.js";
|
|
17
|
+
import { DEFAULT_PREFIX_MAP } from "../kernel/render/prefixes.js";
|
|
18
|
+
import type { VerbSpec } from "../kernel/spec/types.js";
|
|
19
|
+
import { VOCABULARY } from "../kernel/vocabulary.js";
|
|
20
|
+
import { declaredStories, storyModules } from "./distribution.js";
|
|
21
|
+
import { capabilities } from "./index.js";
|
|
22
|
+
|
|
23
|
+
/** A verb's DECLARED shape — everything but the closures a compile mints fresh. */
|
|
24
|
+
function declaredShape(verb: VerbSpec): Record<string, unknown> {
|
|
25
|
+
return {
|
|
26
|
+
path: verb.path.join(" "),
|
|
27
|
+
summary: verb.summary,
|
|
28
|
+
doc: verb.doc,
|
|
29
|
+
params: verb.params,
|
|
30
|
+
capability: verb.capability,
|
|
31
|
+
examples: verb.examples,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("the distribution's declared stories (PROTECTED)", () => {
|
|
36
|
+
it("declares exactly the five domain nouns", () => {
|
|
37
|
+
// Every domain noun is now nothing BUT its story (L-OPEN-9), so a renamed
|
|
38
|
+
// or dropped noun deletes a whole command rather than shrinking one — it
|
|
39
|
+
// must fail HERE.
|
|
40
|
+
expect([...declaredStories.keys()].sort()).toEqual([
|
|
41
|
+
"block",
|
|
42
|
+
"modifier",
|
|
43
|
+
"standard",
|
|
44
|
+
"tier",
|
|
45
|
+
"token",
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it.each([
|
|
50
|
+
...declaredStories.keys(),
|
|
51
|
+
])("the %s story validates against the pack grammar, unchanged", (noun) => {
|
|
52
|
+
const story = declaredStories.get(noun);
|
|
53
|
+
expect(
|
|
54
|
+
parsePackDefinition(JSON.parse(JSON.stringify(story)), "pragma.conf.ts"),
|
|
55
|
+
).toEqual(story);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("every declared noun reaches the registry as an overridable module", () => {
|
|
59
|
+
for (const noun of declaredStories.keys()) {
|
|
60
|
+
const module = capabilities.find((entry) => entry.name === noun);
|
|
61
|
+
expect(module, `no capability module for "${noun}"`).toBeDefined();
|
|
62
|
+
// `story: true` is what lets a project's own story REPLACE it at
|
|
63
|
+
// dispatch; without it the noun would be treated as authored CLI.
|
|
64
|
+
expect(module?.story).toBe(true);
|
|
65
|
+
expect(module?.verbs.length).toBeGreaterThan(0);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("ZERO hand-written data commands: a registered noun IS its compiled story", () => {
|
|
70
|
+
// The machine form of L-OPEN-9's end state — "a fork defines its entire
|
|
71
|
+
// read surface in pragma.conf.ts". Prose can go stale; this cannot. Every
|
|
72
|
+
// declared noun's REGISTERED module (what the CLI, the MCP server and the
|
|
73
|
+
// generated reference all project) must be verb-for-verb what recompiling
|
|
74
|
+
// its story alone produces. Re-introducing a composite — an authored module
|
|
75
|
+
// that prepends or appends a hand-written verb beside the story, which is
|
|
76
|
+
// exactly how `block`, `token` and `tier` used to be built — fails here,
|
|
77
|
+
// whatever the docblocks say.
|
|
78
|
+
for (const [noun, story] of declaredStories) {
|
|
79
|
+
const registered = capabilities.find((entry) => entry.name === noun);
|
|
80
|
+
const compiled = compilePack(story, "pragma.conf.ts", DEFAULT_PREFIX_MAP);
|
|
81
|
+
// Two halves, because a spec carries closures a fresh compile cannot
|
|
82
|
+
// reproduce by identity. (1) SHAPE: recompiling the story alone must
|
|
83
|
+
// yield the same verbs, declared the same way — an extra verb or an extra
|
|
84
|
+
// flag beside the story fails here.
|
|
85
|
+
expect(
|
|
86
|
+
registered?.verbs.map(declaredShape),
|
|
87
|
+
`"${noun}" registers verbs its story does not declare`,
|
|
88
|
+
).toEqual(compiled.map(declaredShape));
|
|
89
|
+
// (2) IDENTITY: each registered verb must BE the compiled story's verb
|
|
90
|
+
// object, not a look-alike. A composite module — the `[...story.verbs,
|
|
91
|
+
// handWrittenVerb]` shape `block`, `token` and `tier` used to be built
|
|
92
|
+
// with — constructs new objects, so it cannot pass this even if its
|
|
93
|
+
// shapes happen to match.
|
|
94
|
+
const fromStory = storyModules.get(noun)?.verbs ?? [];
|
|
95
|
+
expect(registered?.verbs.length).toBe(fromStory.length);
|
|
96
|
+
registered?.verbs.forEach((verb, index) => {
|
|
97
|
+
expect(verb, `"${noun}" verb ${index} is not the story's`).toBe(
|
|
98
|
+
fromStory[index],
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("the tier story is internally coherent, and coherent with the vocabulary", () => {
|
|
105
|
+
// Both tier verbs are declared data now (L-OPEN-9), so the noun can no
|
|
106
|
+
// longer disagree with ITSELF in code — but the list query spells its
|
|
107
|
+
// class and property literally (`pragma.conf.ts` is inert data and imports
|
|
108
|
+
// nothing) while the lookup declares them as fields, and the vocabulary
|
|
109
|
+
// declares the addressing property once more for the index builder. This
|
|
110
|
+
// holds all three to one class and one property, so a candidate that
|
|
111
|
+
// completes still resolves and the index projects the names the lookup
|
|
112
|
+
// matches.
|
|
113
|
+
const story = declaredStories.get("tier");
|
|
114
|
+
const query = story?.list?.query ?? "";
|
|
115
|
+
expect(story?.lookup?.type).toBe("ds:Tier");
|
|
116
|
+
expect(query).toContain(`a ${story?.lookup?.type}`);
|
|
117
|
+
expect(story?.lookup?.by).toBe(VOCABULARY.altName);
|
|
118
|
+
expect(query).toContain(`${VOCABULARY.altName} ?name`);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The distribution's own read stories, compiled at module load.
|
|
3
|
+
*
|
|
4
|
+
* `pragma.conf.ts` declares them on the packs that supply them; this is the ONE
|
|
5
|
+
* place those declarations become capabilities. They come through the SAME door
|
|
6
|
+
* a third-party story does — {@link compilePack} — so the mechanism the CLI
|
|
7
|
+
* offers forks is the mechanism the CLI itself runs on.
|
|
8
|
+
*
|
|
9
|
+
* They are compiled STATICALLY rather than merged at dispatch on purpose: the
|
|
10
|
+
* `--help` / `__complete` fast paths read the static capability set, and
|
|
11
|
+
* `surface/surface.v2.json` freezes the emitted surface, so a noun that only
|
|
12
|
+
* arrived at dispatch would vanish from help and completion on a fresh install.
|
|
13
|
+
* That is affordable because this module is zod-free and pure — `pragma.conf.ts`
|
|
14
|
+
* is inert data and `compilePack` builds specs without touching the store.
|
|
15
|
+
*
|
|
16
|
+
* Config- and package-declared stories still OVERRIDE these at dispatch; the
|
|
17
|
+
* `story` marker on each module is what tells `assembleEffectiveModules` which
|
|
18
|
+
* modules may be replaced.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import conf from "../../pragma.conf.js";
|
|
22
|
+
import { compilePack } from "../kernel/packs/compile.js";
|
|
23
|
+
import {
|
|
24
|
+
distributionSource,
|
|
25
|
+
type PackDefinition,
|
|
26
|
+
} from "../kernel/packs/types.js";
|
|
27
|
+
import { DEFAULT_PREFIX_MAP } from "../kernel/render/prefixes.js";
|
|
28
|
+
import type { CapabilityModule } from "../kernel/spec/types.js";
|
|
29
|
+
|
|
30
|
+
/** The read stories `pragma.conf.ts` declares on its packs, by noun. */
|
|
31
|
+
export const declaredStories: ReadonlyMap<string, PackDefinition> = new Map(
|
|
32
|
+
conf.packs
|
|
33
|
+
.flatMap((pack) => pack.stories ?? [])
|
|
34
|
+
.map((story): readonly [string, PackDefinition] => [story.noun, story]),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Those stories compiled to capability modules, by noun.
|
|
39
|
+
*
|
|
40
|
+
* `capabilities/index.ts` appends every module no authored one claims — and
|
|
41
|
+
* since L-OPEN-9 that is every one of them: no data noun is authored, so a
|
|
42
|
+
* declared story IS the whole noun. `distribution.test.ts` holds that shut by
|
|
43
|
+
* asserting each registered module's verbs deep-equal its compiled story's.
|
|
44
|
+
*/
|
|
45
|
+
export const storyModules: ReadonlyMap<string, CapabilityModule> = new Map(
|
|
46
|
+
[...declaredStories].map(([noun, story]) => [
|
|
47
|
+
noun,
|
|
48
|
+
{
|
|
49
|
+
name: noun,
|
|
50
|
+
story: true,
|
|
51
|
+
verbs: compilePack(
|
|
52
|
+
story,
|
|
53
|
+
distributionSource("pragma.conf.ts"),
|
|
54
|
+
DEFAULT_PREFIX_MAP,
|
|
55
|
+
),
|
|
56
|
+
colophon: story.colophon,
|
|
57
|
+
},
|
|
58
|
+
]),
|
|
59
|
+
);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BIN_NAME } from "../../../constants.js";
|
|
1
2
|
import { readConfig } from "../../../kernel/config/readConfig.js";
|
|
2
3
|
import type { PragmaRuntime } from "../../../kernel/runtime/types.js";
|
|
3
4
|
import type { CheckResult } from "../types.js";
|
|
@@ -5,7 +6,7 @@ import type { CheckResult } from "../types.js";
|
|
|
5
6
|
/**
|
|
6
7
|
* Check which config layer is active for the working directory.
|
|
7
8
|
*
|
|
8
|
-
* Passes when a project
|
|
9
|
+
* Passes when a project config is found up the tree, or — the CLI is
|
|
9
10
|
* global-first — when only the global XDG config exists. Fails only when
|
|
10
11
|
* neither layer is configured.
|
|
11
12
|
*
|
|
@@ -18,7 +19,7 @@ export async function checkConfigFile(rt: PragmaRuntime): Promise<CheckResult> {
|
|
|
18
19
|
|
|
19
20
|
if (layers.project.exists) {
|
|
20
21
|
return {
|
|
21
|
-
name:
|
|
22
|
+
name: `${BIN_NAME} config`,
|
|
22
23
|
status: "pass",
|
|
23
24
|
detail: `project config active (${layers.project.path})`,
|
|
24
25
|
};
|
|
@@ -26,16 +27,16 @@ export async function checkConfigFile(rt: PragmaRuntime): Promise<CheckResult> {
|
|
|
26
27
|
|
|
27
28
|
if (layers.global.exists) {
|
|
28
29
|
return {
|
|
29
|
-
name:
|
|
30
|
+
name: `${BIN_NAME} config`,
|
|
30
31
|
status: "pass",
|
|
31
32
|
detail: `no project config — global config active (${layers.global.path})`,
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
return {
|
|
36
|
-
name:
|
|
37
|
+
name: `${BIN_NAME} config`,
|
|
37
38
|
status: "fail",
|
|
38
39
|
detail: "not found",
|
|
39
|
-
remedy:
|
|
40
|
+
remedy: `${BIN_NAME} config set tier <path>`,
|
|
40
41
|
};
|
|
41
42
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BIN_NAME } from "../../../constants.js";
|
|
1
2
|
import { entityTotal } from "../../../kernel/completion/entitySource.js";
|
|
2
3
|
import type { PragmaRuntime } from "../../../kernel/runtime/types.js";
|
|
3
4
|
import type { CheckResult } from "../types.js";
|
|
@@ -32,8 +33,7 @@ export async function checkKeStore(rt: PragmaRuntime): Promise<CheckResult> {
|
|
|
32
33
|
name: "ke store",
|
|
33
34
|
status: "fail",
|
|
34
35
|
detail: "failed to boot",
|
|
35
|
-
remedy:
|
|
36
|
-
"Ensure design-system packages are installed and run `pragma sources update`.",
|
|
36
|
+
remedy: `Ensure design-system packs are configured and run \`${BIN_NAME} sources update\`.`,
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { DetectedHarness } from "@canonical/harnesses";
|
|
2
2
|
import { detectHarnesses, readMcpConfig } from "@canonical/harnesses";
|
|
3
3
|
import { runTask } from "@canonical/task/node";
|
|
4
|
+
import { BIN_NAME, MCP_SERVER_NAME } from "../../../constants.js";
|
|
4
5
|
import type { CheckResult } from "../types.js";
|
|
5
6
|
import { deriveBand } from "./deriveBand.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
* Check that at least one AI harness has
|
|
9
|
+
* Check that at least one AI harness has this CLI configured as an MCP server. The
|
|
9
10
|
* result's band is derived from the harnesses actually found (Windsurf ⇒ global,
|
|
10
11
|
* Cursor ⇒ project), not the check name — so a global-scope harness is not
|
|
11
12
|
* mislabeled PROJECT.
|
|
@@ -23,7 +24,7 @@ export async function checkMcpConfigured(cwd: string): Promise<CheckResult> {
|
|
|
23
24
|
name: "MCP configured",
|
|
24
25
|
status: "fail",
|
|
25
26
|
detail: "harness detection failed",
|
|
26
|
-
remedy:
|
|
27
|
+
remedy: `${BIN_NAME} setup mcp`,
|
|
27
28
|
};
|
|
28
29
|
}
|
|
29
30
|
|
|
@@ -32,7 +33,7 @@ export async function checkMcpConfigured(cwd: string): Promise<CheckResult> {
|
|
|
32
33
|
name: "MCP configured",
|
|
33
34
|
status: "fail",
|
|
34
35
|
detail: "no AI harnesses detected",
|
|
35
|
-
remedy:
|
|
36
|
+
remedy: `${BIN_NAME} setup mcp`,
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -41,7 +42,7 @@ export async function checkMcpConfigured(cwd: string): Promise<CheckResult> {
|
|
|
41
42
|
if (!d.configExists) continue;
|
|
42
43
|
try {
|
|
43
44
|
const servers = await runTask(readMcpConfig(d.harness, cwd));
|
|
44
|
-
if (
|
|
45
|
+
if (MCP_SERVER_NAME in servers) configured.push(d);
|
|
45
46
|
} catch {
|
|
46
47
|
// Config unreadable — skip.
|
|
47
48
|
}
|
|
@@ -60,8 +61,8 @@ export async function checkMcpConfigured(cwd: string): Promise<CheckResult> {
|
|
|
60
61
|
return {
|
|
61
62
|
name: "MCP configured",
|
|
62
63
|
status: "fail",
|
|
63
|
-
detail: `detected ${names} but
|
|
64
|
-
remedy:
|
|
64
|
+
detail: `detected ${names} but ${MCP_SERVER_NAME} not configured`,
|
|
65
|
+
remedy: `${BIN_NAME} setup mcp`,
|
|
65
66
|
band: deriveBand(detected),
|
|
66
67
|
};
|
|
67
68
|
}
|