@usefragments/core 1.0.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/LICENSE +21 -0
- package/dist/chunk-243QYRUF.js +2145 -0
- package/dist/chunk-243QYRUF.js.map +1 -0
- package/dist/chunk-3LLRNCPX.js +111 -0
- package/dist/chunk-3LLRNCPX.js.map +1 -0
- package/dist/chunk-EIYNNS77.js +48 -0
- package/dist/chunk-EIYNNS77.js.map +1 -0
- package/dist/chunk-JNBFJ34I.js +326 -0
- package/dist/chunk-JNBFJ34I.js.map +1 -0
- package/dist/chunk-R3X7UBHU.js +228 -0
- package/dist/chunk-R3X7UBHU.js.map +1 -0
- package/dist/chunk-SH4KPIYH.js +393 -0
- package/dist/chunk-SH4KPIYH.js.map +1 -0
- package/dist/chunk-V4VQB57N.js +56 -0
- package/dist/chunk-V4VQB57N.js.map +1 -0
- package/dist/chunk-X34IA4LR.js +319 -0
- package/dist/chunk-X34IA4LR.js.map +1 -0
- package/dist/chunk-YF65VYRY.js +493 -0
- package/dist/chunk-YF65VYRY.js.map +1 -0
- package/dist/codes/index.d.ts +40 -0
- package/dist/codes/index.js +23 -0
- package/dist/codes/index.js.map +1 -0
- package/dist/compiled-types/index.d.ts +5 -0
- package/dist/compiled-types/index.js +1 -0
- package/dist/compiled-types/index.js.map +1 -0
- package/dist/generate/index.d.ts +47 -0
- package/dist/generate/index.js +11 -0
- package/dist/generate/index.js.map +1 -0
- package/dist/governance-BOa3KyiJ.d.ts +3279 -0
- package/dist/graph/index.d.ts +66 -0
- package/dist/graph/index.js +17 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/index.d.ts +11388 -0
- package/dist/index.js +8711 -0
- package/dist/index.js.map +1 -0
- package/dist/package-identity.d.ts +42 -0
- package/dist/package-identity.js +23 -0
- package/dist/package-identity.js.map +1 -0
- package/dist/preview/index.d.ts +484 -0
- package/dist/preview/index.js +1129 -0
- package/dist/preview/index.js.map +1 -0
- package/dist/react-types.d.ts +22 -0
- package/dist/react-types.js +1 -0
- package/dist/react-types.js.map +1 -0
- package/dist/registry.d.ts +2367 -0
- package/dist/registry.js +62 -0
- package/dist/registry.js.map +1 -0
- package/dist/schemas/index.d.ts +2584 -0
- package/dist/schemas/index.js +55 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/severity.d.ts +18 -0
- package/dist/severity.js +27 -0
- package/dist/severity.js.map +1 -0
- package/dist/test-utils.d.ts +14 -0
- package/dist/test-utils.js +63 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/topology/index.d.ts +48 -0
- package/dist/topology/index.js +7 -0
- package/dist/topology/index.js.map +1 -0
- package/dist/types-xJ2xyp_G.d.ts +119 -0
- package/package.json +120 -0
- package/src/__tests__/contract-parser.test.ts +614 -0
- package/src/__tests__/dtcg-generators.test.ts +439 -0
- package/src/__tests__/dtcg-parser.test.ts +884 -0
- package/src/__tests__/preview-runtime.test.tsx +111 -0
- package/src/__tests__/schema.test.ts +31 -0
- package/src/__tests__/severity.test.ts +34 -0
- package/src/__tests__/style-comparison.test.ts +662 -0
- package/src/__tests__/tokens-compat.test.ts +51 -0
- package/src/agent-format.test.ts +105 -0
- package/src/agent-format.ts +141 -0
- package/src/bundle.ts +149 -0
- package/src/codes/__tests__/codes.test.ts +73 -0
- package/src/codes/codes.ts +463 -0
- package/src/codes/index.ts +7 -0
- package/src/codes/source-finding-bridge.ts +113 -0
- package/src/compiled-types/index.ts +293 -0
- package/src/component-contract.ts +231 -0
- package/src/component-discovery.ts +80 -0
- package/src/composition.test.ts +262 -0
- package/src/composition.ts +318 -0
- package/src/config-paths.test.ts +78 -0
- package/src/config-paths.ts +47 -0
- package/src/config.ts +18 -0
- package/src/conform-prove.test.ts +274 -0
- package/src/conform-prove.ts +326 -0
- package/src/conform.ts +160 -0
- package/src/constants.ts +114 -0
- package/src/context.ts +2 -0
- package/src/contract/fixture.ts +110 -0
- package/src/contract/hash.test.ts +86 -0
- package/src/contract/hash.ts +195 -0
- package/src/contract/index.ts +43 -0
- package/src/contract/preimage.test.ts +254 -0
- package/src/contract/preimage.ts +365 -0
- package/src/contract/stamp.test.ts +100 -0
- package/src/contract/stamp.ts +105 -0
- package/src/contract-parser.ts +141 -0
- package/src/defineFragment.test.ts +257 -0
- package/src/defineFragment.ts +293 -0
- package/src/dtcg-generators.ts +324 -0
- package/src/dtcg-parser.ts +1 -0
- package/src/dtcg.ts +237 -0
- package/src/facts/__snapshots__/facts.test.ts.snap +124 -0
- package/src/facts/builders.ts +835 -0
- package/src/facts/compile.ts +422 -0
- package/src/facts/fact-index.ts +394 -0
- package/src/facts/facts.test.ts +494 -0
- package/src/facts/ids.ts +125 -0
- package/src/facts/index.ts +119 -0
- package/src/facts/types.ts +614 -0
- package/src/figma.ts +263 -0
- package/src/fragment-types.ts +214 -0
- package/src/generate/generate.test.ts +594 -0
- package/src/generate/index.ts +438 -0
- package/src/governance-integrity.test.ts +186 -0
- package/src/governance-integrity.ts +282 -0
- package/src/governance.test.ts +300 -0
- package/src/governance.ts +605 -0
- package/src/governance.type-test.ts +75 -0
- package/src/graph/engine.ts +446 -0
- package/src/graph/graph.test.ts +746 -0
- package/src/graph/index.ts +32 -0
- package/src/graph/serialization.ts +55 -0
- package/src/graph/types.ts +178 -0
- package/src/index.ts +891 -0
- package/src/local-canonical.ts +102 -0
- package/src/mcp-snapshot.ts +222 -0
- package/src/package-identity-match.ts +33 -0
- package/src/package-identity.generated.ts +233 -0
- package/src/package-identity.test.ts +99 -0
- package/src/package-identity.ts +157 -0
- package/src/performance-presets.ts +142 -0
- package/src/preview/comparison-artifact.test.ts +228 -0
- package/src/preview/comparison-artifact.ts +181 -0
- package/src/preview/comparison-context.ts +93 -0
- package/src/preview/comparison-evidence.ts +112 -0
- package/src/preview/comparison-format.ts +21 -0
- package/src/preview/comparison-presentation.ts +187 -0
- package/src/preview/comparison-references.test.ts +430 -0
- package/src/preview/comparison-references.ts +72 -0
- package/src/preview/comparison-score.ts +59 -0
- package/src/preview/comparison-styles.ts +197 -0
- package/src/preview/comparison.test.ts +559 -0
- package/src/preview/comparison.ts +488 -0
- package/src/preview/index.ts +10 -0
- package/src/preview/types.ts +309 -0
- package/src/preview/validation.test.ts +290 -0
- package/src/preview/validation.ts +356 -0
- package/src/preview-runtime.tsx +144 -0
- package/src/raw-html-canonical.test.ts +41 -0
- package/src/raw-html-canonical.ts +179 -0
- package/src/react-types.ts +21 -0
- package/src/registry-install-plan.ts +344 -0
- package/src/registry.test.ts +245 -0
- package/src/registry.ts +369 -0
- package/src/rules/a11y-required-accessible-name.ts +93 -0
- package/src/rules/color-math.test.ts +72 -0
- package/src/rules/color-math.ts +192 -0
- package/src/rules/components-forbidden-prop-value.ts +108 -0
- package/src/rules/components-prefer-library.ts +943 -0
- package/src/rules/components-unknown-prop.ts +94 -0
- package/src/rules/composition-pattern-schema.ts +125 -0
- package/src/rules/composition-pattern.test.ts +320 -0
- package/src/rules/composition-pattern.ts +271 -0
- package/src/rules/emit-gate.test.ts +42 -0
- package/src/rules/emit-gate.ts +60 -0
- package/src/rules/finding.ts +54 -0
- package/src/rules/index.ts +235 -0
- package/src/rules/jsx-preferred-component.ts +68 -0
- package/src/rules/jsx-preferred-import-path.ts +67 -0
- package/src/rules/presets.ts +57 -0
- package/src/rules/props-invalid-value.ts +115 -0
- package/src/rules/rules.test.ts +3636 -0
- package/src/rules/spacing-resolution.ts +184 -0
- package/src/rules/styles-no-raw-color.ts +329 -0
- package/src/rules/styles-no-raw-dimensions.ts +357 -0
- package/src/rules/styles-no-raw-spacing.ts +236 -0
- package/src/rules/styles-no-raw-typography.ts +237 -0
- package/src/rules/tailwind-arbitrary-color.test.ts +128 -0
- package/src/rules/tailwind-arbitrary-color.ts +93 -0
- package/src/rules/tailwind-arbitrary-spacing.test.ts +157 -0
- package/src/rules/tailwind-arbitrary-spacing.ts +120 -0
- package/src/rules/tailwind-forbidden-palette.test.ts +116 -0
- package/src/rules/tailwind-forbidden-palette.ts +107 -0
- package/src/rules/tailwind-off-scale-spacing-token.test.ts +100 -0
- package/src/rules/tailwind-off-scale-spacing-token.ts +108 -0
- package/src/rules/tailwind-raw-color-via-token.test.ts +108 -0
- package/src/rules/tailwind-raw-color-via-token.ts +82 -0
- package/src/rules/tailwind-rule-utils.ts +106 -0
- package/src/rules/tailwind-unknown-class.test.ts +157 -0
- package/src/rules/tailwind-unknown-class.ts +121 -0
- package/src/rules/taxonomy.test.ts +238 -0
- package/src/rules/theme-no-theme-coupled-literal.ts +132 -0
- package/src/rules/tiers.ts +111 -0
- package/src/rules/tokens-css-vars-must-be-defined.test.ts +244 -0
- package/src/rules/tokens-css-vars-must-be-defined.ts +156 -0
- package/src/rules/tokens-require-dual-fallback.ts +78 -0
- package/src/rules/types.ts +27 -0
- package/src/rules/utils.test.ts +110 -0
- package/src/rules/utils.ts +599 -0
- package/src/schema.ts +427 -0
- package/src/schemas/index.ts +275 -0
- package/src/schemas/schemas.test.ts +124 -0
- package/src/severity.ts +54 -0
- package/src/storyAdapter.test.ts +571 -0
- package/src/storyAdapter.ts +761 -0
- package/src/storyFilters.test.ts +350 -0
- package/src/storyFilters.ts +253 -0
- package/src/storybook-csf.ts +11 -0
- package/src/style-comparison.ts +423 -0
- package/src/test-utils.ts +68 -0
- package/src/token-parser.ts +2 -0
- package/src/token-types.ts +287 -0
- package/src/tokens/categories.ts +131 -0
- package/src/tokens/color.ts +193 -0
- package/src/tokens/design-token-parser.test.ts +159 -0
- package/src/tokens/design-token-parser.ts +402 -0
- package/src/tokens/dtcg.ts +428 -0
- package/src/tokens/index.ts +130 -0
- package/src/tokens/lookup.ts +19 -0
- package/src/tokens/scss.ts +171 -0
- package/src/tokens/tailwind-v4.ts +187 -0
- package/src/tokens/types.ts +43 -0
- package/src/topology/index.ts +9 -0
- package/src/topology/resolve-area.test.ts +174 -0
- package/src/topology/resolve-area.ts +215 -0
- package/src/types.ts +1057 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
export type RawHtmlPrecisionTier =
|
|
2
|
+
| "exact-html"
|
|
3
|
+
| "html-advisory"
|
|
4
|
+
| "input-type"
|
|
5
|
+
| "role-reimpl"
|
|
6
|
+
| "interactive-nonsemantic"
|
|
7
|
+
| "icon-only";
|
|
8
|
+
|
|
9
|
+
export interface RawHtmlCanonicalMatch {
|
|
10
|
+
canonical: string;
|
|
11
|
+
tier: RawHtmlPrecisionTier;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const EXACT_HTML_CANONICALS = new Map<string, string>([["button", "Button"]]);
|
|
15
|
+
|
|
16
|
+
const ADVISORY_HTML_CANONICALS = new Map<string, string>([
|
|
17
|
+
["textarea", "Textarea"],
|
|
18
|
+
["select", "Select"],
|
|
19
|
+
["dialog", "Dialog"],
|
|
20
|
+
["progress", "Progress"],
|
|
21
|
+
["details", "Collapsible"],
|
|
22
|
+
["table", "Table"],
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const INPUT_TYPE_CANONICALS = new Map<string, string>([
|
|
26
|
+
["checkbox", "Checkbox"],
|
|
27
|
+
["radio", "Radio"],
|
|
28
|
+
["number", "NumberInput"],
|
|
29
|
+
["password", "PasswordInput"],
|
|
30
|
+
["range", "Slider"],
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const ROLE_CANONICALS = new Map<string, string>([
|
|
34
|
+
["button", "Button"],
|
|
35
|
+
["checkbox", "Checkbox"],
|
|
36
|
+
["radio", "Radio"],
|
|
37
|
+
["switch", "Switch"],
|
|
38
|
+
["tab", "Tabs"],
|
|
39
|
+
["tablist", "Tabs"],
|
|
40
|
+
["dialog", "Dialog"],
|
|
41
|
+
["menu", "Menu"],
|
|
42
|
+
["status", "Alert"],
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
export const RAW_HTML_CANONICAL_TAGS = Object.freeze(
|
|
46
|
+
Array.from(EXACT_HTML_CANONICALS, ([tagName, canonical]) => ({
|
|
47
|
+
tagName,
|
|
48
|
+
canonical,
|
|
49
|
+
tier: "exact-html" as const,
|
|
50
|
+
}))
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
export const RAW_HTML_ADVISORY_TAGS = Object.freeze(
|
|
54
|
+
Array.from(ADVISORY_HTML_CANONICALS, ([tagName, canonical]) => ({
|
|
55
|
+
tagName,
|
|
56
|
+
canonical,
|
|
57
|
+
tier: "html-advisory" as const,
|
|
58
|
+
}))
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
export const RAW_HTML_INPUT_TYPE_CANONICALS = Object.freeze(
|
|
62
|
+
Array.from(INPUT_TYPE_CANONICALS, ([type, canonical]) => ({
|
|
63
|
+
type,
|
|
64
|
+
canonical,
|
|
65
|
+
tier: "input-type" as const,
|
|
66
|
+
}))
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
export const RAW_HTML_ROLE_CANONICALS = Object.freeze(
|
|
70
|
+
Array.from(ROLE_CANONICALS, ([role, canonical]) => ({
|
|
71
|
+
role,
|
|
72
|
+
canonical,
|
|
73
|
+
tier: "role-reimpl" as const,
|
|
74
|
+
}))
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
export function resolveCanonicalForRawHtml(
|
|
78
|
+
tagName: string,
|
|
79
|
+
inputType?: string
|
|
80
|
+
): RawHtmlCanonicalMatch | null {
|
|
81
|
+
const tag = tagName.toLowerCase();
|
|
82
|
+
if (tag === "input") {
|
|
83
|
+
const normalizedType = inputType?.trim().toLowerCase();
|
|
84
|
+
if (!normalizedType) return null;
|
|
85
|
+
const canonical = INPUT_TYPE_CANONICALS.get(normalizedType);
|
|
86
|
+
return canonical ? { canonical, tier: "input-type" } : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const exactCanonical = EXACT_HTML_CANONICALS.get(tag);
|
|
90
|
+
if (exactCanonical) return { canonical: exactCanonical, tier: "exact-html" };
|
|
91
|
+
const advisoryCanonical = ADVISORY_HTML_CANONICALS.get(tag);
|
|
92
|
+
return advisoryCanonical ? { canonical: advisoryCanonical, tier: "html-advisory" } : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function resolveCanonicalForAriaRole(role: string): RawHtmlCanonicalMatch | null {
|
|
96
|
+
const canonical = ROLE_CANONICALS.get(role.trim().toLowerCase());
|
|
97
|
+
return canonical ? { canonical, tier: "role-reimpl" } : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function isRawHtmlAdvisoryTier(tier: RawHtmlPrecisionTier): boolean {
|
|
101
|
+
return (
|
|
102
|
+
tier === "html-advisory" ||
|
|
103
|
+
tier === "role-reimpl" ||
|
|
104
|
+
tier === "interactive-nonsemantic" ||
|
|
105
|
+
tier === "icon-only"
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Low-frequency, typically-1:1 semantic tags where a per-component inferred
|
|
111
|
+
* `htmlEquivalent` mapping is safe even with a custom-named component
|
|
112
|
+
* (`<dialog>` → Modal). Mirrors `ADVISORY_HTML_CANONICALS` + the `input` family.
|
|
113
|
+
*
|
|
114
|
+
* Deliberately EXCLUDES generic structural containers (`div`, `span`) and the
|
|
115
|
+
* ambiguous common tags (`button`, `a`): those are handled by the stricter
|
|
116
|
+
* agreement check in `isEnforceableHtmlEquivalent`.
|
|
117
|
+
*/
|
|
118
|
+
const SEMANTIC_HTML_EQUIVALENT_TAGS = new Set<string>([
|
|
119
|
+
"input",
|
|
120
|
+
"textarea",
|
|
121
|
+
"select",
|
|
122
|
+
"dialog",
|
|
123
|
+
"progress",
|
|
124
|
+
"details",
|
|
125
|
+
"table",
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Specific interactive tags that an EXPLICITLY-DECLARED project mapping may
|
|
130
|
+
* enforce against ANY canonical component NAME — making the rule component-
|
|
131
|
+
* library agnostic (so `@acme/ui`'s `AcmeButton` fires on a raw `<button>`, not
|
|
132
|
+
* only the curated built-in "Button").
|
|
133
|
+
*
|
|
134
|
+
* Deliberately EXCLUDES generic structural containers (`div`, `span`): even an
|
|
135
|
+
* explicit declaration must never turn every `<div>` into a bespoke
|
|
136
|
+
* re-implementation. The `explicit` gate only applies to user-declared tag
|
|
137
|
+
* mappings (provenance: `localCanonical.resolves`), never to inferred
|
|
138
|
+
* `htmlEquivalent`s — so the `<button>` → Chip flood class stays name-gated.
|
|
139
|
+
*/
|
|
140
|
+
const EXPLICITLY_ENFORCEABLE_AMBIGUOUS_TAGS = new Set<string>(["button", "a"]);
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Decide whether a per-component `htmlEquivalent` mapping (raw `tag` → a
|
|
144
|
+
* `canonical` component) may drive a bare-tag canonical-usage finding.
|
|
145
|
+
*
|
|
146
|
+
* The contract:
|
|
147
|
+
* - Low-frequency SEMANTIC tags (dialog, select, textarea, input, …) are always
|
|
148
|
+
* enforceable, so a custom-named component (`<dialog>` → Modal) still maps.
|
|
149
|
+
* - Common/ambiguous tags (button, a, …) enforce ONLY when the mapping AGREES
|
|
150
|
+
* with the framework's curated canonical for that tag — so `<button>` → Button
|
|
151
|
+
* fires, but `<button>` → Chip does not.
|
|
152
|
+
* - Generic structural containers (div, span) have no curated answer and are not
|
|
153
|
+
* semantic → never enforce.
|
|
154
|
+
*
|
|
155
|
+
* This is the guard that stops the canonical-usage flood: a library component
|
|
156
|
+
* being built on a `<div>` must not make every `<div>` a bespoke re-implementation
|
|
157
|
+
* of it (every `<div>` → Accordion, every `<span>` → Badge, every `<button>` → Chip).
|
|
158
|
+
*/
|
|
159
|
+
export function isEnforceableHtmlEquivalent(args: {
|
|
160
|
+
tag: string;
|
|
161
|
+
canonical: string;
|
|
162
|
+
inputType?: string;
|
|
163
|
+
/**
|
|
164
|
+
* The mapping is an EXPLICIT project declaration of this tag → component
|
|
165
|
+
* (provenance: `localCanonical.resolves`), not a low-confidence inferred
|
|
166
|
+
* `htmlEquivalent`. Explicit declarations enforce a specific interactive tag
|
|
167
|
+
* (`button`/`a`) against any canonical NAME — the agnosticism path — while
|
|
168
|
+
* inferred mappings stay name-gated to the curated canonical (the flood
|
|
169
|
+
* guard). Generic containers (`div`/`span`) are never enforceable even when
|
|
170
|
+
* explicit.
|
|
171
|
+
*/
|
|
172
|
+
explicit?: boolean;
|
|
173
|
+
}): boolean {
|
|
174
|
+
const tag = args.tag.toLowerCase();
|
|
175
|
+
if (SEMANTIC_HTML_EQUIVALENT_TAGS.has(tag)) return true;
|
|
176
|
+
if (args.explicit && EXPLICITLY_ENFORCEABLE_AMBIGUOUS_TAGS.has(tag)) return true;
|
|
177
|
+
const builtIn = resolveCanonicalForRawHtml(tag, args.inputType);
|
|
178
|
+
return builtIn !== null && builtIn.canonical === args.canonical;
|
|
179
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React-specific types for Fragments.
|
|
3
|
+
*
|
|
4
|
+
* Import from '@usefragments/core/react' when you need React-typed
|
|
5
|
+
* component definitions. The main '@usefragments/core' entrypoint
|
|
6
|
+
* works without React installed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ComponentType, ReactNode, JSX } from 'react';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A React component that can be used in a fragment definition.
|
|
13
|
+
* This type is intentionally broad to support various React component patterns
|
|
14
|
+
* including FC, forwardRef, memo, and class components across different React versions.
|
|
15
|
+
*/
|
|
16
|
+
export type FragmentComponent<TProps = any> =
|
|
17
|
+
| ComponentType<TProps>
|
|
18
|
+
| ((props: TProps) => ReactNode | JSX.Element | null);
|
|
19
|
+
|
|
20
|
+
// Re-export definition types for convenience
|
|
21
|
+
export type { FragmentDefinition, FragmentDefinitionV2 } from './types.js';
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { portableRepoPathError } from "./config-paths.js";
|
|
2
|
+
import {
|
|
3
|
+
REGISTRY_INSTALL_RECEIPT_SCHEMA_VERSION,
|
|
4
|
+
hashRegistryFileContent,
|
|
5
|
+
registryInstallReceiptSchema,
|
|
6
|
+
type RegistryArtifact,
|
|
7
|
+
type RegistryDependency,
|
|
8
|
+
type RegistryFile,
|
|
9
|
+
type RegistryInstallReceipt,
|
|
10
|
+
} from "./registry.js";
|
|
11
|
+
|
|
12
|
+
export interface RegistryWritePlan {
|
|
13
|
+
path: string;
|
|
14
|
+
sourcePath: string;
|
|
15
|
+
content: string;
|
|
16
|
+
componentId?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface BuildRegistryInstallPlanInput {
|
|
20
|
+
artifact: RegistryArtifact;
|
|
21
|
+
requestedComponents: readonly string[];
|
|
22
|
+
targetPath: string;
|
|
23
|
+
all?: boolean;
|
|
24
|
+
includeTests?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface RegistryInstallPlan {
|
|
28
|
+
componentNames: string[];
|
|
29
|
+
plans: RegistryWritePlan[];
|
|
30
|
+
dependencies: RegistryDependency[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function normalizeRegistryRepoPath(path: string): string {
|
|
34
|
+
return path
|
|
35
|
+
.split("\\")
|
|
36
|
+
.join("/")
|
|
37
|
+
.replace(/^\.\/+/, "");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function assertPortableRegistryPath(path: string, label: string): string {
|
|
41
|
+
const normalized = normalizeRegistryRepoPath(path);
|
|
42
|
+
const error = portableRepoPathError(normalized);
|
|
43
|
+
if (error) throw new Error(`${label}: ${error}`);
|
|
44
|
+
return normalized;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function joinRegistryPath(...parts: string[]): string {
|
|
48
|
+
return normalizeRegistryRepoPath(parts.filter(Boolean).join("/"));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function sourceToRegistryTargetPath(targetRoot: string, sourcePath: string): string {
|
|
52
|
+
const source = normalizeRegistryRepoPath(sourcePath);
|
|
53
|
+
const withoutSrc = source.startsWith("src/") ? source.slice("src/".length) : source;
|
|
54
|
+
return assertPortableRegistryPath(
|
|
55
|
+
joinRegistryPath(targetRoot, withoutSrc),
|
|
56
|
+
"Registry install target"
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function selectedRegistryComponentNames(
|
|
61
|
+
artifact: RegistryArtifact,
|
|
62
|
+
requested: readonly string[],
|
|
63
|
+
all: boolean | undefined
|
|
64
|
+
): string[] {
|
|
65
|
+
if (all || requested.includes("*")) {
|
|
66
|
+
return Object.keys(artifact.manifest.components).sort((left, right) =>
|
|
67
|
+
left.localeCompare(right)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (requested.length === 0) {
|
|
71
|
+
throw new Error("Choose at least one registry component to install.");
|
|
72
|
+
}
|
|
73
|
+
for (const name of requested) {
|
|
74
|
+
if (!artifact.manifest.components[name]) {
|
|
75
|
+
throw new Error(`Unknown registry component ${name}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return [...new Set(requested)].sort();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function installsCompleteRegistry(
|
|
82
|
+
artifact: RegistryArtifact,
|
|
83
|
+
componentNames: readonly string[]
|
|
84
|
+
): boolean {
|
|
85
|
+
const allComponents = Object.keys(artifact.manifest.components);
|
|
86
|
+
return (
|
|
87
|
+
componentNames.length === allComponents.length &&
|
|
88
|
+
allComponents.every((name) => componentNames.includes(name))
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function shouldInstallFile(file: RegistryFile, includeTests: boolean | undefined): boolean {
|
|
93
|
+
if (includeTests) return true;
|
|
94
|
+
return file.role !== "test" && file.role !== "story";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function dependencyKey(dependency: RegistryDependency): string {
|
|
98
|
+
return `${dependency.type}:${dependency.name}:${dependency.versionRange}:${dependency.optional ? 1 : 0}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function collectRegistryInstallDependencies(
|
|
102
|
+
artifact: RegistryArtifact,
|
|
103
|
+
componentNames: readonly string[]
|
|
104
|
+
): RegistryDependency[] {
|
|
105
|
+
const dependencies = new Map<string, RegistryDependency>();
|
|
106
|
+
for (const dependency of artifact.manifest.dependencies) {
|
|
107
|
+
dependencies.set(dependencyKey(dependency), dependency);
|
|
108
|
+
}
|
|
109
|
+
for (const name of componentNames) {
|
|
110
|
+
const component = artifact.manifest.components[name];
|
|
111
|
+
for (const dependency of [...component.runtimeDependencies, ...component.peerDependencies]) {
|
|
112
|
+
dependencies.set(dependencyKey(dependency), dependency);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return [...dependencies.values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function collectComponentWritePlan(args: {
|
|
119
|
+
artifact: RegistryArtifact;
|
|
120
|
+
componentNames: readonly string[];
|
|
121
|
+
targetPath: string;
|
|
122
|
+
includeTests?: boolean;
|
|
123
|
+
}): RegistryWritePlan[] {
|
|
124
|
+
const writes = new Map<string, RegistryWritePlan>();
|
|
125
|
+
for (const componentName of args.componentNames) {
|
|
126
|
+
const component = args.artifact.manifest.components[componentName];
|
|
127
|
+
for (const file of component.files) {
|
|
128
|
+
if (!shouldInstallFile(file, args.includeTests)) continue;
|
|
129
|
+
const content = args.artifact.files[file.path];
|
|
130
|
+
if (!content) throw new Error(`Artifact is missing file content for ${file.path}`);
|
|
131
|
+
const target = sourceToRegistryTargetPath(args.targetPath, file.path);
|
|
132
|
+
writes.set(target, {
|
|
133
|
+
path: target,
|
|
134
|
+
sourcePath: file.path,
|
|
135
|
+
content: content.content,
|
|
136
|
+
componentId: component.componentId,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return [...writes.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function rewriteInstalledPackagePath(value: string): string {
|
|
144
|
+
if (value.startsWith("./src/")) return `./${value.slice("./src/".length)}`;
|
|
145
|
+
if (value.startsWith("src/")) return value.slice("src/".length);
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function rewriteInstalledPackageExport(value: unknown): unknown {
|
|
150
|
+
if (typeof value === "string") return rewriteInstalledPackagePath(value);
|
|
151
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
152
|
+
return Object.fromEntries(
|
|
153
|
+
Object.entries(value).map(([key, nested]) => [key, rewriteInstalledPackageExport(nested)])
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function rewriteInstalledPackageJson(content: string): string {
|
|
158
|
+
const parsed = JSON.parse(content) as {
|
|
159
|
+
name?: string;
|
|
160
|
+
version?: string;
|
|
161
|
+
type?: string;
|
|
162
|
+
sideEffects?: unknown;
|
|
163
|
+
main?: string;
|
|
164
|
+
types?: string;
|
|
165
|
+
fragments?: string;
|
|
166
|
+
exports?: unknown;
|
|
167
|
+
dependencies?: unknown;
|
|
168
|
+
peerDependencies?: unknown;
|
|
169
|
+
peerDependenciesMeta?: unknown;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const installed = {
|
|
173
|
+
...(parsed.name && { name: parsed.name }),
|
|
174
|
+
...(parsed.version && { version: parsed.version }),
|
|
175
|
+
private: true,
|
|
176
|
+
...(parsed.type && { type: parsed.type }),
|
|
177
|
+
...(parsed.sideEffects !== undefined && { sideEffects: parsed.sideEffects }),
|
|
178
|
+
...(parsed.main && { main: rewriteInstalledPackagePath(parsed.main) }),
|
|
179
|
+
...(parsed.types && { types: rewriteInstalledPackagePath(parsed.types) }),
|
|
180
|
+
fragments: rewriteInstalledPackagePath(parsed.fragments ?? "fragments.json"),
|
|
181
|
+
...(parsed.exports !== undefined && {
|
|
182
|
+
exports: rewriteInstalledPackageExport(parsed.exports),
|
|
183
|
+
}),
|
|
184
|
+
...(parsed.dependencies !== undefined && { dependencies: parsed.dependencies }),
|
|
185
|
+
...(parsed.peerDependencies !== undefined && { peerDependencies: parsed.peerDependencies }),
|
|
186
|
+
...(parsed.peerDependenciesMeta !== undefined && {
|
|
187
|
+
peerDependenciesMeta: parsed.peerDependenciesMeta,
|
|
188
|
+
}),
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
return `${JSON.stringify(installed, null, 2)}\n`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function collectPackageWritePlan(args: {
|
|
195
|
+
artifact: RegistryArtifact;
|
|
196
|
+
targetPath: string;
|
|
197
|
+
includeTests?: boolean;
|
|
198
|
+
}): RegistryWritePlan[] {
|
|
199
|
+
const writes = new Map<string, RegistryWritePlan>();
|
|
200
|
+
const packageFiles = new Map(
|
|
201
|
+
args.artifact.manifest.packageFiles.map((file) => [file.path, file])
|
|
202
|
+
);
|
|
203
|
+
for (const entrypoint of args.artifact.manifest.entrypoints) {
|
|
204
|
+
const file = packageFiles.get(entrypoint.sourcePath);
|
|
205
|
+
if (file) continue;
|
|
206
|
+
const content = args.artifact.files[entrypoint.sourcePath];
|
|
207
|
+
if (!content) throw new Error(`Artifact is missing file content for ${entrypoint.sourcePath}`);
|
|
208
|
+
packageFiles.set(entrypoint.sourcePath, {
|
|
209
|
+
path: entrypoint.sourcePath,
|
|
210
|
+
role: "barrel",
|
|
211
|
+
sha256: content.sha256,
|
|
212
|
+
size: content.size,
|
|
213
|
+
contentType: content.contentType,
|
|
214
|
+
contentRef: `sha256:${content.sha256}`,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
for (const file of packageFiles.values()) {
|
|
219
|
+
if (!shouldInstallFile(file, args.includeTests)) continue;
|
|
220
|
+
const content = args.artifact.files[file.path];
|
|
221
|
+
if (!content) throw new Error(`Artifact is missing file content for ${file.path}`);
|
|
222
|
+
const target = sourceToRegistryTargetPath(args.targetPath, file.path);
|
|
223
|
+
writes.set(target, {
|
|
224
|
+
path: target,
|
|
225
|
+
sourcePath: file.path,
|
|
226
|
+
content:
|
|
227
|
+
file.path === "package.json"
|
|
228
|
+
? rewriteInstalledPackageJson(content.content)
|
|
229
|
+
: content.content,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return [...writes.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function generatedIndexPlan(args: {
|
|
237
|
+
componentNames: readonly string[];
|
|
238
|
+
targetPath: string;
|
|
239
|
+
artifact: RegistryArtifact;
|
|
240
|
+
}): RegistryWritePlan {
|
|
241
|
+
const lines = args.componentNames.map((name) => {
|
|
242
|
+
const component = args.artifact.manifest.components[name];
|
|
243
|
+
const exportName = component.exports[0]?.name ?? component.name;
|
|
244
|
+
return `export { ${exportName} } from "./components/${component.componentId}";`;
|
|
245
|
+
});
|
|
246
|
+
return {
|
|
247
|
+
path: assertPortableRegistryPath(
|
|
248
|
+
joinRegistryPath(args.targetPath, "index.ts"),
|
|
249
|
+
"Registry index target"
|
|
250
|
+
),
|
|
251
|
+
sourcePath: "registry/generated-index.ts",
|
|
252
|
+
content: `${lines.join("\n")}\n`,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function dedupePlans(plans: readonly RegistryWritePlan[]): RegistryWritePlan[] {
|
|
257
|
+
const byPath = new Map<string, RegistryWritePlan>();
|
|
258
|
+
for (const plan of plans) {
|
|
259
|
+
const existing = byPath.get(plan.path);
|
|
260
|
+
if (existing && existing.content !== plan.content) {
|
|
261
|
+
throw new Error(`Conflicting registry write plans for ${plan.path}`);
|
|
262
|
+
}
|
|
263
|
+
byPath.set(plan.path, existing ?? plan);
|
|
264
|
+
}
|
|
265
|
+
return [...byPath.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function buildRegistryInstallPlan(
|
|
269
|
+
input: BuildRegistryInstallPlanInput
|
|
270
|
+
): RegistryInstallPlan {
|
|
271
|
+
const targetPath = assertPortableRegistryPath(input.targetPath, "Registry target");
|
|
272
|
+
const componentNames = selectedRegistryComponentNames(
|
|
273
|
+
input.artifact,
|
|
274
|
+
input.requestedComponents,
|
|
275
|
+
input.all
|
|
276
|
+
);
|
|
277
|
+
const dependencies = collectRegistryInstallDependencies(input.artifact, componentNames);
|
|
278
|
+
const isCompleteInstall = installsCompleteRegistry(input.artifact, componentNames);
|
|
279
|
+
const hasPackageRoot = input.artifact.manifest.entrypoints.some(
|
|
280
|
+
(entrypoint) => entrypoint.specifier === "."
|
|
281
|
+
);
|
|
282
|
+
const plans = dedupePlans([
|
|
283
|
+
...(isCompleteInstall
|
|
284
|
+
? collectPackageWritePlan({
|
|
285
|
+
artifact: input.artifact,
|
|
286
|
+
targetPath,
|
|
287
|
+
includeTests: input.includeTests,
|
|
288
|
+
})
|
|
289
|
+
: []),
|
|
290
|
+
...collectComponentWritePlan({
|
|
291
|
+
artifact: input.artifact,
|
|
292
|
+
componentNames,
|
|
293
|
+
targetPath,
|
|
294
|
+
includeTests: input.includeTests,
|
|
295
|
+
}),
|
|
296
|
+
...(isCompleteInstall && hasPackageRoot
|
|
297
|
+
? []
|
|
298
|
+
: [generatedIndexPlan({ artifact: input.artifact, componentNames, targetPath })]),
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
return { componentNames, plans, dependencies };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function buildRegistryInstallReceipt(args: {
|
|
305
|
+
artifact: RegistryArtifact;
|
|
306
|
+
targetPath: string;
|
|
307
|
+
importPath: string;
|
|
308
|
+
componentNames: readonly string[];
|
|
309
|
+
plans: readonly RegistryWritePlan[];
|
|
310
|
+
dependencies: readonly RegistryDependency[];
|
|
311
|
+
installedAt?: string;
|
|
312
|
+
}): RegistryInstallReceipt {
|
|
313
|
+
const installedComponents = args.componentNames.map((name) => {
|
|
314
|
+
const component = args.artifact.manifest.components[name];
|
|
315
|
+
return {
|
|
316
|
+
componentId: component.componentId,
|
|
317
|
+
publicRef: component.publicRef,
|
|
318
|
+
name: component.name,
|
|
319
|
+
files: args.plans
|
|
320
|
+
.filter((plan) => plan.componentId === component.componentId)
|
|
321
|
+
.map((plan) => plan.path),
|
|
322
|
+
};
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
return registryInstallReceiptSchema.parse({
|
|
326
|
+
schemaVersion: REGISTRY_INSTALL_RECEIPT_SCHEMA_VERSION,
|
|
327
|
+
registryId: args.artifact.manifest.registryId,
|
|
328
|
+
registryVersion: args.artifact.manifest.version,
|
|
329
|
+
registryHash: args.artifact.manifest.registryHash,
|
|
330
|
+
fcid: args.artifact.manifest.fcid,
|
|
331
|
+
channel: args.artifact.manifest.channel,
|
|
332
|
+
targetPath: assertPortableRegistryPath(args.targetPath, "Registry target"),
|
|
333
|
+
importPath: args.importPath,
|
|
334
|
+
installedComponents,
|
|
335
|
+
files: args.plans.map((plan) => ({
|
|
336
|
+
path: plan.path,
|
|
337
|
+
sourcePath: plan.sourcePath,
|
|
338
|
+
sha256: hashRegistryFileContent(plan.content),
|
|
339
|
+
componentId: plan.componentId,
|
|
340
|
+
})),
|
|
341
|
+
dependencies: args.dependencies,
|
|
342
|
+
installedAt: args.installedAt ?? new Date().toISOString(),
|
|
343
|
+
});
|
|
344
|
+
}
|