bref-ui 0.3.0 → 2.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/cli/commands/add/approve-overwrites.ts +17 -0
- package/cli/commands/add/display-changes.ts +11 -0
- package/cli/commands/add/index.ts +30 -0
- package/cli/commands/add/review-changes.ts +18 -0
- package/cli/commands/add/utils.ts +11 -0
- package/cli/commands/execute.ts +39 -0
- package/cli/commands/index.ts +19 -0
- package/cli/commands/init.ts +17 -0
- package/cli/commands/parse.ts +24 -0
- package/cli/commands/types.ts +2 -0
- package/cli/config/read/index.ts +15 -0
- package/cli/config/read/normalize-config.ts +21 -0
- package/cli/config/read/read-config-file.ts +13 -0
- package/cli/config/resolve/index.ts +20 -0
- package/cli/config/resolve/resolve-alias-target.ts +17 -0
- package/cli/config/resolve/resolve-alias.ts +17 -0
- package/cli/config/types.ts +5 -0
- package/cli/index.ts +4 -0
- package/cli/installation/copy/index.ts +25 -0
- package/cli/installation/copy/validate-overwrite-approvals.ts +18 -0
- package/cli/installation/copy/write-prepared-files.ts +26 -0
- package/cli/installation/exports/collect-exports.ts +19 -0
- package/cli/installation/exports/create-additions.ts +26 -0
- package/cli/installation/exports/index.ts +27 -0
- package/cli/installation/exports/merge-content.ts +12 -0
- package/cli/installation/exports/read-declared-export-names.ts +24 -0
- package/cli/installation/exports/read-exported-names.ts +21 -0
- package/cli/installation/exports/read-index.ts +14 -0
- package/cli/installation/exports/read-reexported-names.ts +24 -0
- package/cli/installation/exports/types.ts +10 -0
- package/cli/installation/exports/utils.ts +54 -0
- package/cli/installation/index.ts +35 -0
- package/cli/installation/parser/index.ts +29 -0
- package/cli/installation/parser/types.ts +12 -0
- package/cli/installation/parser/utils.ts +45 -0
- package/cli/installation/prepare/collect-source-files.ts +15 -0
- package/cli/installation/prepare/index.ts +15 -0
- package/cli/installation/prepare/read-source-files.ts +21 -0
- package/cli/installation/prepare/rewrite-imports.ts +50 -0
- package/cli/installation/prepare/rewrite-source.ts +36 -0
- package/cli/installation/prepare/types.ts +7 -0
- package/cli/installation/preview/compare-prepared-files.ts +18 -0
- package/cli/installation/preview/index.ts +18 -0
- package/cli/installation/preview/read-destination-files.ts +25 -0
- package/cli/installation/preview/types.ts +9 -0
- package/cli/installation/types.ts +15 -0
- package/cli/pack.ts +6 -0
- package/cli/registry/load/create-registry.ts +25 -0
- package/cli/registry/load/discover-manifests.ts +9 -0
- package/cli/registry/load/index.ts +21 -0
- package/cli/registry/load/load-entry.ts +22 -0
- package/cli/registry/load/utils.ts +70 -0
- package/cli/registry/load/validate-dependencies.ts +33 -0
- package/cli/registry/plan/collect-entries.ts +28 -0
- package/cli/registry/plan/index.ts +14 -0
- package/cli/registry/types.ts +17 -0
- package/dist/button/button.svelte +126 -0
- package/dist/button/button.svelte.d.ts +4 -0
- package/dist/button/registry.json +6 -0
- package/dist/button/types.d.ts +32 -0
- package/dist/checkbox/checkbox.svelte +143 -0
- package/dist/checkbox/checkbox.svelte.d.ts +4 -0
- package/dist/checkbox/registry.json +6 -0
- package/dist/checkbox/types.d.ts +6 -0
- package/dist/combobox/combobox.svelte +156 -0
- package/dist/combobox/combobox.svelte.d.ts +4 -0
- package/dist/combobox/form.svelte +61 -0
- package/dist/combobox/form.svelte.d.ts +4 -0
- package/dist/combobox/input.svelte +132 -0
- package/dist/combobox/input.svelte.d.ts +4 -0
- package/dist/combobox/list.svelte +78 -0
- package/dist/combobox/list.svelte.d.ts +4 -0
- package/dist/combobox/option.svelte +77 -0
- package/dist/combobox/option.svelte.d.ts +4 -0
- package/dist/combobox/registry.json +6 -0
- package/dist/combobox/types.d.ts +61 -0
- package/dist/dialog/dialog.svelte +182 -0
- package/dist/dialog/dialog.svelte.d.ts +4 -0
- package/dist/dialog/footer.svelte +26 -0
- package/dist/dialog/footer.svelte.d.ts +7 -0
- package/dist/dialog/header.svelte +56 -0
- package/dist/dialog/header.svelte.d.ts +4 -0
- package/dist/dialog/registry.json +6 -0
- package/dist/dialog/types.d.ts +31 -0
- package/dist/icon/icon.svelte +108 -0
- package/dist/icon/icon.svelte.d.ts +4 -0
- package/dist/icon/names.d.ts +7 -0
- package/dist/icon/registry.json +6 -0
- package/dist/icon/types.d.ts +16 -0
- package/dist/index.d.ts +20 -2
- package/dist/index.js +20 -2
- package/dist/pill/content.svelte +23 -0
- package/dist/pill/content.svelte.d.ts +4 -0
- package/dist/pill/on-delete.svelte +26 -0
- package/dist/pill/on-delete.svelte.d.ts +4 -0
- package/dist/pill/pill.svelte +200 -0
- package/dist/{base/pill → pill}/pill.svelte.d.ts +1 -1
- package/dist/pill/registry.json +6 -0
- package/dist/pill/types.d.ts +19 -0
- package/dist/pill-choice-group/pill-choice-group.svelte +131 -0
- package/dist/pill-choice-group/pill-choice-group.svelte.d.ts +4 -0
- package/dist/pill-choice-group/registry.json +6 -0
- package/dist/pill-choice-group/types.d.ts +13 -0
- package/dist/pill-group/container.svelte +40 -0
- package/dist/pill-group/container.svelte.d.ts +4 -0
- package/dist/pill-group/pill-group.svelte +27 -0
- package/dist/pill-group/pill-group.svelte.d.ts +4 -0
- package/dist/pill-group/registry.json +6 -0
- package/dist/pill-group/types.d.ts +13 -0
- package/dist/popover/popover.svelte +86 -0
- package/dist/popover/popover.svelte.d.ts +4 -0
- package/dist/popover/registry.json +6 -0
- package/dist/popover/types.d.ts +8 -0
- package/dist/progress/progress.svelte +256 -0
- package/dist/progress/progress.svelte.d.ts +4 -0
- package/dist/progress/registry.json +6 -0
- package/dist/progress/types.d.ts +11 -0
- package/dist/select/empty.svelte +25 -0
- package/dist/select/empty.svelte.d.ts +4 -0
- package/dist/select/list.svelte +27 -0
- package/dist/select/list.svelte.d.ts +4 -0
- package/dist/select/option.svelte +64 -0
- package/dist/select/option.svelte.d.ts +4 -0
- package/dist/select/registry.json +6 -0
- package/dist/select/select.svelte +70 -0
- package/dist/{base/select → select}/select.svelte.d.ts +2 -2
- package/dist/select/trigger.svelte +86 -0
- package/dist/select/trigger.svelte.d.ts +4 -0
- package/dist/select/types.d.ts +31 -0
- package/dist/select/types.js +1 -0
- package/dist/spinner/registry.json +6 -0
- package/dist/spinner/spinner.svelte +114 -0
- package/dist/spinner/spinner.svelte.d.ts +4 -0
- package/dist/spinner/types.d.ts +6 -0
- package/dist/spinner/types.js +1 -0
- package/dist/surface/registry.json +6 -0
- package/dist/surface/surface.svelte +96 -0
- package/dist/surface/surface.svelte.d.ts +4 -0
- package/dist/surface/types.d.ts +25 -0
- package/dist/surface/types.js +1 -0
- package/dist/switch/registry.json +6 -0
- package/dist/switch/switch.svelte +162 -0
- package/dist/switch/switch.svelte.d.ts +4 -0
- package/dist/switch/types.d.ts +8 -0
- package/dist/switch/types.js +1 -0
- package/dist/text-area/registry.json +8 -0
- package/dist/text-area/text-area.svelte +186 -0
- package/dist/text-area/text-area.svelte.d.ts +4 -0
- package/dist/text-area/types.d.ts +12 -0
- package/dist/text-area/types.js +1 -0
- package/dist/text-input/registry.json +8 -0
- package/dist/text-input/text-input.svelte +189 -0
- package/dist/text-input/text-input.svelte.d.ts +4 -0
- package/dist/text-input/types.d.ts +11 -0
- package/dist/text-input/types.js +1 -0
- package/dist/theme/cli-theme.svelte +16 -0
- package/dist/theme/cli-theme.svelte.d.ts +7 -0
- package/dist/theme/colors.svelte +102 -0
- package/dist/theme/colors.svelte.d.ts +4 -0
- package/dist/theme/fonts.svelte +10 -0
- package/dist/theme/fonts.svelte.d.ts +26 -0
- package/dist/theme/registry.json +13 -0
- package/dist/theme/styles.svelte +50 -0
- package/dist/theme/styles.svelte.d.ts +26 -0
- package/dist/theme/surface.d.ts +3 -0
- package/dist/theme/surface.js +10 -0
- package/dist/theme/surfaces.svelte +208 -0
- package/dist/theme/surfaces.svelte.d.ts +18 -0
- package/dist/theme/theme-mode-toggle.svelte +9 -0
- package/dist/theme/theme-mode-toggle.svelte.d.ts +4 -0
- package/dist/theme/theme.svelte +16 -0
- package/dist/theme/theme.svelte.d.ts +4 -0
- package/dist/theme/types.d.ts +17 -0
- package/dist/theme/types.js +1 -0
- package/dist/tree-view/registry.json +15 -0
- package/dist/tree-view/tree-node/chevron-button.svelte +56 -0
- package/dist/tree-view/tree-node/chevron-button.svelte.d.ts +4 -0
- package/dist/tree-view/tree-node/content.svelte +62 -0
- package/dist/tree-view/tree-node/content.svelte.d.ts +4 -0
- package/dist/tree-view/tree-node/delete-button.svelte +52 -0
- package/dist/tree-view/tree-node/delete-button.svelte.d.ts +4 -0
- package/dist/tree-view/tree-node/tree-node.svelte +111 -0
- package/dist/{base/tree-view → tree-view/tree-node}/tree-node.svelte.d.ts +2 -3
- package/dist/tree-view/tree-view-section.svelte +42 -0
- package/dist/tree-view/tree-view-section.svelte.d.ts +4 -0
- package/dist/tree-view/tree-view.svelte +65 -0
- package/dist/{base/tree-view → tree-view}/tree-view.svelte.d.ts +2 -2
- package/dist/tree-view/types.d.ts +31 -0
- package/dist/tree-view/types.js +1 -0
- package/dist/types.d.ts +25 -0
- package/dist/types.js +1 -0
- package/package.json +39 -45
- package/readme.md +157 -0
- package/src/lib/button/button.svelte +126 -0
- package/src/lib/button/registry.json +6 -0
- package/src/lib/button/types.ts +42 -0
- package/src/lib/checkbox/checkbox.svelte +143 -0
- package/src/lib/checkbox/registry.json +6 -0
- package/src/lib/checkbox/types.ts +7 -0
- package/src/lib/combobox/combobox.svelte +156 -0
- package/src/lib/combobox/form.svelte +61 -0
- package/src/lib/combobox/input.svelte +132 -0
- package/src/lib/combobox/list.svelte +78 -0
- package/src/lib/combobox/option.svelte +77 -0
- package/src/lib/combobox/registry.json +6 -0
- package/src/lib/combobox/types.ts +89 -0
- package/src/lib/dialog/dialog.svelte +182 -0
- package/src/lib/dialog/footer.svelte +26 -0
- package/src/lib/dialog/header.svelte +56 -0
- package/src/lib/dialog/registry.json +6 -0
- package/src/lib/dialog/types.ts +35 -0
- package/src/lib/icon/icon.svelte +108 -0
- package/src/lib/icon/names.ts +3794 -0
- package/src/lib/icon/registry.json +6 -0
- package/src/lib/icon/types.ts +18 -0
- package/src/lib/index.ts +20 -0
- package/src/lib/pill/content.svelte +23 -0
- package/src/lib/pill/on-delete.svelte +26 -0
- package/src/lib/pill/pill.svelte +200 -0
- package/src/lib/pill/registry.json +6 -0
- package/src/lib/pill/types.ts +22 -0
- package/src/lib/pill-choice-group/pill-choice-group.svelte +131 -0
- package/src/lib/pill-choice-group/registry.json +6 -0
- package/src/lib/pill-choice-group/types.ts +14 -0
- package/src/lib/pill-group/container.svelte +40 -0
- package/src/lib/pill-group/pill-group.svelte +27 -0
- package/src/lib/pill-group/registry.json +6 -0
- package/src/lib/pill-group/types.ts +15 -0
- package/src/lib/popover/popover.svelte +86 -0
- package/src/lib/popover/registry.json +6 -0
- package/src/lib/popover/types.ts +9 -0
- package/src/lib/progress/progress.svelte +256 -0
- package/src/lib/progress/registry.json +6 -0
- package/src/lib/progress/types.ts +15 -0
- package/src/lib/select/empty.svelte +25 -0
- package/src/lib/select/list.svelte +27 -0
- package/src/lib/select/option.svelte +64 -0
- package/src/lib/select/registry.json +6 -0
- package/src/lib/select/select.svelte +70 -0
- package/src/lib/select/trigger.svelte +86 -0
- package/src/lib/select/types.ts +41 -0
- package/src/lib/spinner/registry.json +6 -0
- package/src/lib/spinner/spinner.svelte +114 -0
- package/src/lib/spinner/types.ts +7 -0
- package/src/lib/surface/registry.json +6 -0
- package/src/lib/surface/surface.svelte +96 -0
- package/src/lib/surface/types.ts +28 -0
- package/src/lib/switch/registry.json +6 -0
- package/src/lib/switch/switch.svelte +162 -0
- package/src/lib/switch/types.ts +12 -0
- package/src/lib/text-area/registry.json +8 -0
- package/src/lib/text-area/text-area.svelte +186 -0
- package/src/lib/text-area/types.ts +13 -0
- package/src/lib/text-input/registry.json +8 -0
- package/src/lib/text-input/text-input.svelte +189 -0
- package/src/lib/text-input/types.ts +12 -0
- package/src/lib/theme/cli-theme.svelte +16 -0
- package/src/lib/theme/colors.svelte +102 -0
- package/src/lib/theme/fonts.svelte +10 -0
- package/src/lib/theme/registry.json +13 -0
- package/src/lib/theme/styles.svelte +50 -0
- package/src/lib/theme/surface.ts +18 -0
- package/src/lib/theme/surfaces.svelte +208 -0
- package/src/lib/theme/theme-mode-toggle.svelte +9 -0
- package/src/lib/theme/theme.svelte +16 -0
- package/src/lib/theme/types.ts +18 -0
- package/src/lib/tree-view/registry.json +15 -0
- package/src/lib/tree-view/tree-node/chevron-button.svelte +56 -0
- package/src/lib/tree-view/tree-node/content.svelte +62 -0
- package/src/lib/tree-view/tree-node/delete-button.svelte +52 -0
- package/src/lib/tree-view/tree-node/tree-node.svelte +111 -0
- package/src/lib/tree-view/tree-view-section.svelte +42 -0
- package/src/lib/tree-view/tree-view.svelte +65 -0
- package/src/lib/tree-view/types.ts +39 -0
- package/src/lib/types.ts +45 -0
- package/README.md +0 -110
- package/dist/base/button/button.svelte +0 -260
- package/dist/base/button/button.svelte.d.ts +0 -6
- package/dist/base/button/icon-button.svelte +0 -226
- package/dist/base/button/icon-button.svelte.d.ts +0 -6
- package/dist/base/button/index.d.ts +0 -2
- package/dist/base/button/index.js +0 -2
- package/dist/base/button/types.d.ts +0 -17
- package/dist/base/icon/icon.svelte +0 -118
- package/dist/base/icon/icon.svelte.d.ts +0 -7
- package/dist/base/icon/index.d.ts +0 -2
- package/dist/base/icon/index.js +0 -2
- package/dist/base/icon/types.d.ts +0 -10
- package/dist/base/index.d.ts +0 -12
- package/dist/base/index.js +0 -12
- package/dist/base/loading/circular-loading.svelte +0 -95
- package/dist/base/loading/circular-loading.svelte.d.ts +0 -4
- package/dist/base/loading/index.d.ts +0 -4
- package/dist/base/loading/index.js +0 -4
- package/dist/base/loading/morphing-shapes-loading.svelte +0 -241
- package/dist/base/loading/morphing-shapes-loading.svelte.d.ts +0 -4
- package/dist/base/loading/pulsing-dots-loading.svelte +0 -115
- package/dist/base/loading/pulsing-dots-loading.svelte.d.ts +0 -4
- package/dist/base/loading/textual-loading.svelte +0 -179
- package/dist/base/loading/textual-loading.svelte.d.ts +0 -4
- package/dist/base/loading/types.d.ts +0 -16
- package/dist/base/pill/index.d.ts +0 -1
- package/dist/base/pill/index.js +0 -1
- package/dist/base/pill/pill.svelte +0 -146
- package/dist/base/pill/types.d.ts +0 -9
- package/dist/base/progress-bar/index.d.ts +0 -1
- package/dist/base/progress-bar/index.js +0 -1
- package/dist/base/progress-bar/progress-bar.svelte +0 -174
- package/dist/base/progress-bar/progress-bar.svelte.d.ts +0 -4
- package/dist/base/progress-bar/types.d.ts +0 -9
- package/dist/base/select/index.d.ts +0 -2
- package/dist/base/select/index.js +0 -2
- package/dist/base/select/select.svelte +0 -145
- package/dist/base/select/types.d.ts +0 -16
- package/dist/base/slider/index.d.ts +0 -1
- package/dist/base/slider/index.js +0 -1
- package/dist/base/slider/slider.svelte +0 -192
- package/dist/base/slider/slider.svelte.d.ts +0 -6
- package/dist/base/slider/types.d.ts +0 -12
- package/dist/base/text-input/area-text-input.svelte +0 -105
- package/dist/base/text-input/area-text-input.svelte.d.ts +0 -6
- package/dist/base/text-input/index.d.ts +0 -2
- package/dist/base/text-input/index.js +0 -2
- package/dist/base/text-input/text-input.svelte +0 -213
- package/dist/base/text-input/text-input.svelte.d.ts +0 -6
- package/dist/base/text-input/types.d.ts +0 -27
- package/dist/base/theme/color-tokens.d.ts +0 -36
- package/dist/base/theme/color-tokens.js +0 -105
- package/dist/base/theme/index.d.ts +0 -3
- package/dist/base/theme/index.js +0 -3
- package/dist/base/theme/mode.d.ts +0 -3
- package/dist/base/theme/mode.js +0 -14
- package/dist/base/theme/theme.svelte +0 -87
- package/dist/base/theme/theme.svelte.d.ts +0 -3
- package/dist/base/theme/types.d.ts +0 -27
- package/dist/base/theme/types.js +0 -20
- package/dist/base/toggle/index.d.ts +0 -1
- package/dist/base/toggle/index.js +0 -1
- package/dist/base/toggle/toggle.svelte +0 -109
- package/dist/base/toggle/toggle.svelte.d.ts +0 -4
- package/dist/base/toggle/types.d.ts +0 -7
- package/dist/base/tree-view/index.d.ts +0 -3
- package/dist/base/tree-view/index.js +0 -3
- package/dist/base/tree-view/tree-node.svelte +0 -132
- package/dist/base/tree-view/tree-view.svelte +0 -56
- package/dist/base/tree-view/types.d.ts +0 -23
- package/dist/base/types.d.ts +0 -4
- /package/dist/{base/button → button}/types.js +0 -0
- /package/dist/{base/icon → checkbox}/types.js +0 -0
- /package/dist/{base/loading → combobox}/types.js +0 -0
- /package/dist/{base/pill → dialog}/types.js +0 -0
- /package/dist/{base/progress-bar/types.js → icon/names.js} +0 -0
- /package/dist/{base/select → icon}/types.js +0 -0
- /package/dist/{base/toggle → pill}/types.js +0 -0
- /package/dist/{base/slider → pill-choice-group}/types.js +0 -0
- /package/dist/{base/text-input → pill-group}/types.js +0 -0
- /package/dist/{base/tree-view → popover}/types.js +0 -0
- /package/dist/{base → progress}/types.js +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ExportNamedDeclaration, ExportSpecifier, Pattern } from 'estree';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Read an AST identifier or string-literal name, including TypeScript declaration IDs.
|
|
5
|
+
* @param node Parser node with a name or string value, or an absent declaration name.
|
|
6
|
+
* @returns The name, or null when the node does not identify a public name.
|
|
7
|
+
*/
|
|
8
|
+
export const readName = (node: unknown): string | null => {
|
|
9
|
+
if (typeof node !== 'object' || node === null) return null;
|
|
10
|
+
if ('name' in node && typeof node.name === 'string') return node.name;
|
|
11
|
+
return 'value' in node && typeof node.value === 'string' ? node.value : null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Find a module path only for a direct runtime default re-export.
|
|
16
|
+
* @param declaration Export list containing the specifier and optional module path.
|
|
17
|
+
* @param specifier Named export such as default as Button.
|
|
18
|
+
* @returns Module path, or null for type-only, named or local exports.
|
|
19
|
+
*/
|
|
20
|
+
export const readDefaultReExportPath = (
|
|
21
|
+
declaration: ExportNamedDeclaration,
|
|
22
|
+
specifier: ExportSpecifier
|
|
23
|
+
): string | null => {
|
|
24
|
+
if (
|
|
25
|
+
('exportKind' in declaration && declaration.exportKind === 'type') ||
|
|
26
|
+
('exportKind' in specifier && specifier.exportKind === 'type') ||
|
|
27
|
+
readName(specifier.local) !== 'default'
|
|
28
|
+
)
|
|
29
|
+
return null;
|
|
30
|
+
const module = declaration.source?.value;
|
|
31
|
+
return typeof module === 'string' ? module : null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Read variable names from identifiers, defaults, rest and nested destructuring patterns.
|
|
36
|
+
* @param binding Variable binding pattern from an exported declaration.
|
|
37
|
+
* @returns Declared names in source order, skipping omitted array elements.
|
|
38
|
+
*/
|
|
39
|
+
export const readBindingNames = (binding: Pattern): string[] => {
|
|
40
|
+
if (binding.type === 'Identifier') return [binding.name];
|
|
41
|
+
if (binding.type === 'AssignmentPattern') return readBindingNames(binding.left);
|
|
42
|
+
if (binding.type === 'RestElement') return readBindingNames(binding.argument);
|
|
43
|
+
if (binding.type === 'ObjectPattern')
|
|
44
|
+
return binding.properties.flatMap((property) =>
|
|
45
|
+
readBindingNames(
|
|
46
|
+
property.type === 'RestElement' ? property.argument : (property.value as Pattern)
|
|
47
|
+
)
|
|
48
|
+
);
|
|
49
|
+
if (binding.type === 'ArrayPattern')
|
|
50
|
+
return binding.elements.flatMap((element) =>
|
|
51
|
+
element === null ? [] : readBindingNames(element)
|
|
52
|
+
);
|
|
53
|
+
return [];
|
|
54
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { RegistryEntry } from '../registry/types.js';
|
|
2
|
+
import { copyFiles } from './copy/index.js';
|
|
3
|
+
import { prepareExports } from './exports/index.js';
|
|
4
|
+
import { prepareSource } from './prepare/index.js';
|
|
5
|
+
import { previewChanges } from './preview/index.js';
|
|
6
|
+
import type { PreviewChange } from './types.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Prepare component sources and index.ts, review their changes, then copy approved files.
|
|
10
|
+
* No destination files are written until review accepts the complete preview.
|
|
11
|
+
* @param entries Validated dependency-first entries returned by planDependencies.
|
|
12
|
+
* @param destination Absolute consumer UI directory.
|
|
13
|
+
* @param review Receive the preview and return approved relative overwrite paths,
|
|
14
|
+
* including index.ts when it conflicts. Return an empty list to accept without
|
|
15
|
+
* overwrites, or null to cancel the installation without writing files.
|
|
16
|
+
* @returns installed after copying completes, or cancelled when review returns null.
|
|
17
|
+
* Identical files are skipped; accepting an unchanged installation returns installed.
|
|
18
|
+
* @throws When preparation, preview or review fails, or a conflict lacks approval.
|
|
19
|
+
* Copy failures propagate and may leave some files copied; writes are not atomic.
|
|
20
|
+
*/
|
|
21
|
+
export const installComponent = async (
|
|
22
|
+
entries: RegistryEntry[],
|
|
23
|
+
destination: string,
|
|
24
|
+
review: (changes: PreviewChange[]) => Promise<string[] | null>
|
|
25
|
+
): Promise<'installed' | 'cancelled'> => {
|
|
26
|
+
const sources = await prepareSource(entries);
|
|
27
|
+
const exports = await prepareExports(entries, destination);
|
|
28
|
+
const files = [...sources, exports];
|
|
29
|
+
const changes = await previewChanges(files, destination);
|
|
30
|
+
const overwrites = await review(changes);
|
|
31
|
+
if (overwrites === null) return 'cancelled';
|
|
32
|
+
|
|
33
|
+
await copyFiles(files, destination, overwrites);
|
|
34
|
+
return 'installed';
|
|
35
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { parse } from 'svelte/compiler';
|
|
2
|
+
import type { ParsedScript } from './types.js';
|
|
3
|
+
import { escapeClosingTags, restoreModulePositions } from './utils.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse standalone script text with Svelte's existing TypeScript-aware parser.
|
|
7
|
+
* A synthetic module script exposes imports and exports without compiling or changing text.
|
|
8
|
+
* @param content Original JavaScript or TypeScript text.
|
|
9
|
+
* @param filename Source path used in syntax errors.
|
|
10
|
+
* @returns Top-level statements and the wrapper offset for corrected module positions.
|
|
11
|
+
* @throws When the script contains invalid syntax or syntax unsupported by Svelte's parser.
|
|
12
|
+
*/
|
|
13
|
+
export const parseScript = (content: string, filename: string): ParsedScript => {
|
|
14
|
+
const prefix = '<script module lang="ts">';
|
|
15
|
+
const escaped = escapeClosingTags(content);
|
|
16
|
+
try {
|
|
17
|
+
const component = parse(`${prefix}${escaped.content}\n</script>`, { filename, modern: true });
|
|
18
|
+
const statements = restoreModulePositions(
|
|
19
|
+
component.module!.content.body,
|
|
20
|
+
escaped.positions,
|
|
21
|
+
prefix.length
|
|
22
|
+
);
|
|
23
|
+
return { statements, offset: prefix.length };
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw new Error(`${filename}: ${error instanceof Error ? error.message : String(error)}`, {
|
|
26
|
+
cause: error
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AST } from 'svelte/compiler';
|
|
2
|
+
|
|
3
|
+
/** Top-level ESTree statement returned by Svelte's TypeScript-aware parser. */
|
|
4
|
+
export type ScriptStatement = AST.Script['content']['body'][number];
|
|
5
|
+
|
|
6
|
+
/** Parsed statements and the position adjustment for the synthetic script wrapper. */
|
|
7
|
+
export interface ParsedScript {
|
|
8
|
+
/** Statements from the original script, including TypeScript declarations. */
|
|
9
|
+
statements: ScriptStatement[];
|
|
10
|
+
/** Prefix length to subtract from corrected module positions to address original text. */
|
|
11
|
+
offset: number;
|
|
12
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ScriptStatement } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Escape literal closing script tags so the synthetic HTML wrapper cannot truncate JavaScript.
|
|
5
|
+
* @param content Original script; Unicode escapes keep string-literal values unchanged.
|
|
6
|
+
* @returns Parser text and original positions of the expanded less-than characters.
|
|
7
|
+
*/
|
|
8
|
+
export const escapeClosingTags = (content: string): { content: string; positions: number[] } => ({
|
|
9
|
+
content: content.replace(/<(?=\/script\s*>)/g, '\\u003c'),
|
|
10
|
+
positions: [...content.matchAll(/<(?=\/script\s*>)/g)].map((match) => match.index)
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Undo escape expansion for import/export module positions used by source rewriting.
|
|
15
|
+
* @param statements Locally parsed statements; only their module literals need original positions.
|
|
16
|
+
* @param positions Original escaped character positions, before the wrapper prefix.
|
|
17
|
+
* @param prefix Length of the synthetic script prefix.
|
|
18
|
+
* @returns Statements with module start/end positions corrected; literal values stay unchanged.
|
|
19
|
+
*/
|
|
20
|
+
export const restoreModulePositions = (
|
|
21
|
+
statements: ScriptStatement[],
|
|
22
|
+
positions: number[],
|
|
23
|
+
prefix: number
|
|
24
|
+
): ScriptStatement[] =>
|
|
25
|
+
statements.map((statement) => {
|
|
26
|
+
if (!('source' in statement) || !statement.source) return statement;
|
|
27
|
+
const module = statement.source as typeof statement.source & { start: number; end: number };
|
|
28
|
+
module.start = restorePosition(module.start, positions, prefix);
|
|
29
|
+
module.end = restorePosition(module.end, positions, prefix);
|
|
30
|
+
return statement;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Remove the five extra characters inserted by each earlier Unicode escape.
|
|
35
|
+
* @param position Position reported in the wrapped and escaped parser input.
|
|
36
|
+
* @param positions Original escaped character positions.
|
|
37
|
+
* @param prefix Wrapper prefix length, retained for the caller's final adjustment.
|
|
38
|
+
* @returns Position before escape expansion, still including the wrapper prefix.
|
|
39
|
+
*/
|
|
40
|
+
const restorePosition = (position: number, positions: number[], prefix: number): number =>
|
|
41
|
+
position -
|
|
42
|
+
positions.reduce(
|
|
43
|
+
(count, start, index) => count + (start + prefix + index * 5 < position ? 5 : 0),
|
|
44
|
+
0
|
|
45
|
+
);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RegistryEntry } from '../../registry/types.js';
|
|
2
|
+
import type { SourceFile } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build source and destination-relative paths in dependency and manifest order.
|
|
6
|
+
* @param entries Dependency-first entries returned by planDependencies.
|
|
7
|
+
* @returns Each source path paired with its destination-relative path.
|
|
8
|
+
*/
|
|
9
|
+
export const collectSourceFiles = (entries: RegistryEntry[]): SourceFile[] =>
|
|
10
|
+
entries.flatMap((entry) =>
|
|
11
|
+
entry.files.map((file) => ({
|
|
12
|
+
source: `${entry.directory}/${file}`,
|
|
13
|
+
path: `${entry.directory.split('/').filter(Boolean).at(-1)}/${file}`
|
|
14
|
+
}))
|
|
15
|
+
);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RegistryEntry } from '../../registry/types.js';
|
|
2
|
+
import type { PreparedFile } from '../types.js';
|
|
3
|
+
import { collectSourceFiles } from './collect-source-files.js';
|
|
4
|
+
import { readSourceFiles } from './read-source-files.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Read planned sources and redirect uncopied local type imports to bref-ui/types.
|
|
8
|
+
* @param entries Dependency-first entries returned by planDependencies.
|
|
9
|
+
* @returns Prepared files in entry and manifest order, retaining source folder names.
|
|
10
|
+
* @throws When a file cannot be read or parsed, or a local import mixes types and values.
|
|
11
|
+
*/
|
|
12
|
+
export const prepareSource = async (entries: RegistryEntry[]): Promise<PreparedFile[]> => {
|
|
13
|
+
const sources = collectSourceFiles(entries);
|
|
14
|
+
return readSourceFiles(sources);
|
|
15
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PreparedFile } from '../types.js';
|
|
2
|
+
import type { SourceFile } from './types.js';
|
|
3
|
+
import { rewriteSource } from './rewrite-source.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Read and prepare source files concurrently while retaining input order.
|
|
7
|
+
* @param files Source paths and their destination-relative paths.
|
|
8
|
+
* @returns Prepared files in the supplied order.
|
|
9
|
+
* @throws When a source cannot be read or its contents cannot be prepared.
|
|
10
|
+
*/
|
|
11
|
+
export const readSourceFiles = async (files: SourceFile[]): Promise<PreparedFile[]> =>
|
|
12
|
+
Promise.all(
|
|
13
|
+
files.map(async ({ source, path }) => ({
|
|
14
|
+
path,
|
|
15
|
+
content: rewriteSource(
|
|
16
|
+
await Bun.file(source).text(),
|
|
17
|
+
source,
|
|
18
|
+
files.map((file) => file.source)
|
|
19
|
+
)
|
|
20
|
+
}))
|
|
21
|
+
);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { dirname, resolve } from 'node:path';
|
|
2
|
+
import { parseScript } from '../parser/index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Replace only the module text of relative, type-only imports.
|
|
6
|
+
* @param content Script content to parse as TypeScript.
|
|
7
|
+
* @param filename Source path included in preparation errors.
|
|
8
|
+
* @param copiedFiles Source files copied alongside this file, whose relative imports stay local.
|
|
9
|
+
* @returns Script text preserving import syntax, quotes, formatting and runtime imports.
|
|
10
|
+
* @throws When syntax is invalid or a relative non-component import mixes type and value specifiers.
|
|
11
|
+
*/
|
|
12
|
+
export const rewriteImports = (
|
|
13
|
+
content: string,
|
|
14
|
+
filename: string,
|
|
15
|
+
copiedFiles: readonly string[] = []
|
|
16
|
+
): string => {
|
|
17
|
+
const { statements, offset } = parseScript(content, filename);
|
|
18
|
+
return statements.reduceRight((source, statement) => {
|
|
19
|
+
if (statement.type !== 'ImportDeclaration' || typeof statement.source.value !== 'string')
|
|
20
|
+
return source;
|
|
21
|
+
|
|
22
|
+
const module = statement.source;
|
|
23
|
+
const path = module.value as string;
|
|
24
|
+
if (!/^\.{1,2}\//.test(path) || path.endsWith('.svelte')) return source;
|
|
25
|
+
const importedFile = resolve(dirname(filename), path);
|
|
26
|
+
if (
|
|
27
|
+
copiedFiles.some((file) => {
|
|
28
|
+
const copiedFile = resolve(file);
|
|
29
|
+
return copiedFile === importedFile || copiedFile === importedFile.replace(/\.js$/, '.ts');
|
|
30
|
+
})
|
|
31
|
+
)
|
|
32
|
+
return source;
|
|
33
|
+
const specifiers = statement.specifiers;
|
|
34
|
+
if (specifiers.length === 0) return source;
|
|
35
|
+
|
|
36
|
+
const namedTypes = specifiers.filter((item) => item.type === 'ImportSpecifier');
|
|
37
|
+
const onlyTypes =
|
|
38
|
+
('importKind' in statement && statement.importKind === 'type') ||
|
|
39
|
+
(namedTypes.length === specifiers.length &&
|
|
40
|
+
namedTypes.every((item) => 'importKind' in item && item.importKind === 'type'));
|
|
41
|
+
if (!onlyTypes) {
|
|
42
|
+
if (namedTypes.some((item) => 'importKind' in item && item.importKind === 'type'))
|
|
43
|
+
throw new Error(`${filename}: separate type and value imports from "${path}".`);
|
|
44
|
+
return source;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { start, end } = module as typeof module & { start: number; end: number };
|
|
48
|
+
return source.slice(0, start - offset + 1) + 'bref-ui/types' + source.slice(end - offset - 1);
|
|
49
|
+
}, content);
|
|
50
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { parse } from 'svelte/compiler';
|
|
2
|
+
import { rewriteImports } from './rewrite-imports.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Rewrite imports only within scripts, preserving markup, CSS and all other text.
|
|
6
|
+
* @param content Original file content.
|
|
7
|
+
* @param filename Source path used to select the parser and report errors.
|
|
8
|
+
* @param copiedFiles Source files copied alongside this file, whose relative imports stay local.
|
|
9
|
+
* @returns Content with local type module specifiers replaced.
|
|
10
|
+
* @throws When Svelte parsing or import preparation fails.
|
|
11
|
+
*/
|
|
12
|
+
export const rewriteSource = (
|
|
13
|
+
content: string,
|
|
14
|
+
filename: string,
|
|
15
|
+
copiedFiles: readonly string[] = []
|
|
16
|
+
): string => {
|
|
17
|
+
if (!filename.endsWith('.svelte'))
|
|
18
|
+
return /\.[cm]?[jt]s$/.test(filename)
|
|
19
|
+
? rewriteImports(content, filename, copiedFiles)
|
|
20
|
+
: content;
|
|
21
|
+
|
|
22
|
+
const component = parse(content, { filename, modern: true });
|
|
23
|
+
const scripts = [component.module, component.instance]
|
|
24
|
+
.filter((script) => script !== null && script !== undefined)
|
|
25
|
+
.map((script) => script.content as typeof script.content & { start: number; end: number })
|
|
26
|
+
.sort((left, right) => right.start - left.start);
|
|
27
|
+
|
|
28
|
+
return scripts.reduce((source, script) => {
|
|
29
|
+
const { start, end } = script;
|
|
30
|
+
return (
|
|
31
|
+
source.slice(0, start) +
|
|
32
|
+
rewriteImports(source.slice(start, end), filename, copiedFiles) +
|
|
33
|
+
source.slice(end)
|
|
34
|
+
);
|
|
35
|
+
}, content);
|
|
36
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { PreparedFile, PreviewChange } from '../types.js';
|
|
2
|
+
import type { DestinationFile } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Compare each prepared file with the destination result at the same position.
|
|
6
|
+
* @param files Prepared sources in caller order.
|
|
7
|
+
* @param existingFiles Destination results in the same order as files.
|
|
8
|
+
* @returns Addition, unchanged or conflict statuses in prepared-file order.
|
|
9
|
+
*/
|
|
10
|
+
export const comparePreparedFiles = (
|
|
11
|
+
files: PreparedFile[],
|
|
12
|
+
existingFiles: DestinationFile[]
|
|
13
|
+
): PreviewChange[] =>
|
|
14
|
+
files.map(({ path, content }, index) => {
|
|
15
|
+
const existing = existingFiles[index];
|
|
16
|
+
if (!existing.exists) return { path, status: 'add' };
|
|
17
|
+
return { path, status: existing.content === content ? 'unchanged' : 'conflict' };
|
|
18
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { PreparedFile, PreviewChange } from '../types.js';
|
|
2
|
+
import { comparePreparedFiles } from './compare-prepared-files.js';
|
|
3
|
+
import { readDestinationFiles } from './read-destination-files.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Compare prepared sources with destination files without writing changes.
|
|
7
|
+
* @param files Prepared files with paths relative to the destination directory.
|
|
8
|
+
* @param destination Consumer project's configured UI directory.
|
|
9
|
+
* @returns Addition, unchanged or conflict statuses in prepared-file order.
|
|
10
|
+
* @throws When an existing destination file cannot be read.
|
|
11
|
+
*/
|
|
12
|
+
export const previewChanges = async (
|
|
13
|
+
files: PreparedFile[],
|
|
14
|
+
destination: string
|
|
15
|
+
): Promise<PreviewChange[]> => {
|
|
16
|
+
const existingFiles = await readDestinationFiles(files, destination);
|
|
17
|
+
return comparePreparedFiles(files, existingFiles);
|
|
18
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { PreparedFile } from '../types.js';
|
|
2
|
+
import type { DestinationFile } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Read destination files concurrently while preserving prepared-file order.
|
|
6
|
+
* @param files Prepared files whose destination paths should be inspected.
|
|
7
|
+
* @param destination Consumer project's configured UI directory.
|
|
8
|
+
* @returns Existing contents, or a missing marker, for each prepared file.
|
|
9
|
+
* @throws When a destination file cannot be read for a reason other than ENOENT.
|
|
10
|
+
*/
|
|
11
|
+
export const readDestinationFiles = async (
|
|
12
|
+
files: PreparedFile[],
|
|
13
|
+
destination: string
|
|
14
|
+
): Promise<DestinationFile[]> =>
|
|
15
|
+
Promise.all(
|
|
16
|
+
files.map(async ({ path }): Promise<DestinationFile> => {
|
|
17
|
+
try {
|
|
18
|
+
return { path, exists: true, content: await Bun.file(`${destination}/${path}`).text() };
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
|
|
21
|
+
return { path, exists: false };
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Read result for one destination path. */
|
|
2
|
+
export interface DestinationFile {
|
|
3
|
+
/** Destination path relative to the configured UI directory. */
|
|
4
|
+
path: string;
|
|
5
|
+
/** Whether the destination file exists. */
|
|
6
|
+
exists: boolean;
|
|
7
|
+
/** Existing file content, present when the file exists. */
|
|
8
|
+
content?: string;
|
|
9
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Component source or index.ts ready for preview and copying into the consumer project. */
|
|
2
|
+
export interface PreparedFile {
|
|
3
|
+
/** Destination path relative to the configured UI directory. */
|
|
4
|
+
path: string;
|
|
5
|
+
/** Prepared content, including rewritten source imports or merged component exports. */
|
|
6
|
+
content: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Result of comparing one prepared source against its destination file. */
|
|
10
|
+
export interface PreviewChange {
|
|
11
|
+
/** Destination path relative to the configured UI directory. */
|
|
12
|
+
path: string;
|
|
13
|
+
/** Whether the file is absent, identical or contains different content. */
|
|
14
|
+
status: 'add' | 'unchanged' | 'conflict';
|
|
15
|
+
}
|
package/cli/pack.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { loadRegistry } from './registry/load/index.js';
|
|
2
|
+
|
|
3
|
+
/** Validate the canonical registry before assembling the local package. */
|
|
4
|
+
const sourceRoot = Bun.fileURLToPath(new URL('../src/lib/', import.meta.url));
|
|
5
|
+
const registry = await loadRegistry(sourceRoot);
|
|
6
|
+
console.log(`Validated ${registry.size} registry entries for packaging.`);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { RegistryEntry } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Index entries while enforcing globally unique component IDs and export names.
|
|
5
|
+
* @param entries Loaded entries in discovery order.
|
|
6
|
+
* @returns A catalogue keyed by component ID, preserving entry order.
|
|
7
|
+
* @throws When a component ID or export name appears more than once.
|
|
8
|
+
*/
|
|
9
|
+
export const createRegistry = (entries: RegistryEntry[]): Map<string, RegistryEntry> => {
|
|
10
|
+
const exports = new Set<string>();
|
|
11
|
+
|
|
12
|
+
return entries.reduce((registry, entry) => {
|
|
13
|
+
const manifest = `${entry.directory}/registry.json`;
|
|
14
|
+
if (registry.has(entry.id))
|
|
15
|
+
throw new Error(`${manifest}: duplicate component ID "${entry.id}".`);
|
|
16
|
+
|
|
17
|
+
Object.keys(entry.exports).forEach((name) => {
|
|
18
|
+
if (exports.has(name)) throw new Error(`${manifest}: duplicate component export "${name}".`);
|
|
19
|
+
exports.add(name);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
registry.set(entry.id, entry);
|
|
23
|
+
return registry;
|
|
24
|
+
}, new Map<string, RegistryEntry>());
|
|
25
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find registry manifests one folder below the source root.
|
|
3
|
+
* @param sourceRoot Directory to scan.
|
|
4
|
+
* @returns Sorted manifest paths prefixed with the source root.
|
|
5
|
+
*/
|
|
6
|
+
export const discoverManifests = (sourceRoot: string): string[] =>
|
|
7
|
+
Array.from(new Bun.Glob('*/registry.json').scanSync({ cwd: sourceRoot }))
|
|
8
|
+
.sort()
|
|
9
|
+
.map((path) => `${sourceRoot}/${path}`);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { RegistryEntry } from '../types.js';
|
|
2
|
+
import { createRegistry } from './create-registry.js';
|
|
3
|
+
import { discoverManifests } from './discover-manifests.js';
|
|
4
|
+
import { loadEntry } from './load-entry.js';
|
|
5
|
+
import { validateDependencies } from './validate-dependencies.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Load and validate the component catalogue without writing files.
|
|
9
|
+
* @param sourceRoot Directory containing immediate component folders.
|
|
10
|
+
* @returns Entries keyed by component ID, in sorted manifest-path order.
|
|
11
|
+
* @throws When manifests cannot be read, metadata or files are invalid,
|
|
12
|
+
* IDs or exports collide, or dependencies are missing or circular.
|
|
13
|
+
*/
|
|
14
|
+
export const loadRegistry = async (sourceRoot: string): Promise<Map<string, RegistryEntry>> => {
|
|
15
|
+
const manifests = discoverManifests(sourceRoot);
|
|
16
|
+
const entries = await Promise.all(manifests.map(loadEntry));
|
|
17
|
+
const registry = createRegistry(entries);
|
|
18
|
+
validateDependencies(registry);
|
|
19
|
+
|
|
20
|
+
return registry;
|
|
21
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RegistryEntry } from '../types.js';
|
|
2
|
+
import { parseItem } from './utils.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Read a manifest, validate its metadata and check that listed files exist.
|
|
6
|
+
* @param manifest Path to a component's registry.json.
|
|
7
|
+
* @returns Validated metadata with its containing directory.
|
|
8
|
+
* @throws When the manifest cannot be read, metadata is invalid or a file is missing.
|
|
9
|
+
*/
|
|
10
|
+
export const loadEntry = async (manifest: string): Promise<RegistryEntry> => {
|
|
11
|
+
const item = parseItem(await Bun.file(manifest).json(), manifest);
|
|
12
|
+
const directory = manifest.slice(0, -'/registry.json'.length);
|
|
13
|
+
|
|
14
|
+
await Promise.all(
|
|
15
|
+
item.files.map(async (file) => {
|
|
16
|
+
if (!(await Bun.file(`${directory}/${file}`).exists()))
|
|
17
|
+
throw new Error(`${manifest}: missing file "${file}".`);
|
|
18
|
+
})
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
return { ...item, directory };
|
|
22
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { RegistryItem } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Check for a lowercase kebab-case identifier beginning with a letter.
|
|
5
|
+
* @param value Unvalidated manifest value.
|
|
6
|
+
* @returns Whether the value is a valid component ID.
|
|
7
|
+
*/
|
|
8
|
+
const isId = (value: unknown): value is string =>
|
|
9
|
+
typeof value === 'string' && /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(value);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Check for a relative, slash-separated path without empty, dot or parent segments.
|
|
13
|
+
* Backslashes and colons are rejected; filesystem symlinks are not resolved.
|
|
14
|
+
* @param value Unvalidated manifest value.
|
|
15
|
+
* @returns Whether the path satisfies the registry's lexical path rules.
|
|
16
|
+
*/
|
|
17
|
+
const isRelativeFile = (value: unknown): value is string =>
|
|
18
|
+
typeof value === 'string' &&
|
|
19
|
+
!value.includes('\\') &&
|
|
20
|
+
!value.includes(':') &&
|
|
21
|
+
value.split('/').every((part) => part !== '' && part !== '.' && part !== '..');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Validate manifest fields, including unique files and dependencies and exports
|
|
25
|
+
* referencing listed files. File existence and dependency graphs are checked later.
|
|
26
|
+
* @param value Decoded JSON to validate.
|
|
27
|
+
* @param manifest Manifest path used in error messages.
|
|
28
|
+
* @returns The validated manifest object.
|
|
29
|
+
* @throws When any required field has an invalid shape or value.
|
|
30
|
+
*/
|
|
31
|
+
export const parseItem = (value: unknown, manifest: string): RegistryItem => {
|
|
32
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
33
|
+
throw new Error(`${manifest}: expected a registry item object.`);
|
|
34
|
+
|
|
35
|
+
if (!('id' in value) || !isId(value.id))
|
|
36
|
+
throw new Error(`${manifest}: "id" must be a kebab-case identifier.`);
|
|
37
|
+
|
|
38
|
+
if (
|
|
39
|
+
!('files' in value) ||
|
|
40
|
+
!Array.isArray(value.files) ||
|
|
41
|
+
value.files.length === 0 ||
|
|
42
|
+
!value.files.every(isRelativeFile) ||
|
|
43
|
+
new Set(value.files).size !== value.files.length
|
|
44
|
+
)
|
|
45
|
+
throw new Error(`${manifest}: "files" must contain unique relative file paths.`);
|
|
46
|
+
const files = value.files;
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
!('exports' in value) ||
|
|
50
|
+
typeof value.exports !== 'object' ||
|
|
51
|
+
value.exports === null ||
|
|
52
|
+
Array.isArray(value.exports) ||
|
|
53
|
+
Object.keys(value.exports).length === 0 ||
|
|
54
|
+
!Object.entries(value.exports).every(
|
|
55
|
+
([name, file]) =>
|
|
56
|
+
/^[A-Z][a-zA-Z0-9]*$/.test(name) && typeof file === 'string' && files.includes(file)
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
throw new Error(`${manifest}: "exports" must map component names to listed files.`);
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
!('dependencies' in value) ||
|
|
63
|
+
!Array.isArray(value.dependencies) ||
|
|
64
|
+
!value.dependencies.every(isId) ||
|
|
65
|
+
new Set(value.dependencies).size !== value.dependencies.length
|
|
66
|
+
)
|
|
67
|
+
throw new Error(`${manifest}: "dependencies" must contain unique component IDs.`);
|
|
68
|
+
|
|
69
|
+
return value as RegistryItem;
|
|
70
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { RegistryEntry } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Traverse dependency chains to reject missing components and cycles.
|
|
5
|
+
* @param registry Catalogue to validate; its entries and order remain unchanged.
|
|
6
|
+
* @throws When a dependency ID is unknown or a dependency chain is circular.
|
|
7
|
+
*/
|
|
8
|
+
export const validateDependencies = (registry: Map<string, RegistryEntry>): void => {
|
|
9
|
+
/** Component IDs in the current recursive dependency chain. */
|
|
10
|
+
const visiting = new Set<string>();
|
|
11
|
+
/** Component IDs whose dependency chains have already passed validation. */
|
|
12
|
+
const visited = new Set<string>();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validate one component and its dependencies, skipping completed checks.
|
|
16
|
+
* @param id Component ID to visit.
|
|
17
|
+
* @throws When the ID is missing or already belongs to the active chain.
|
|
18
|
+
*/
|
|
19
|
+
const visit = (id: string): void => {
|
|
20
|
+
if (visiting.has(id)) throw new Error(`Circular registry dependency at "${id}".`);
|
|
21
|
+
if (visited.has(id)) return;
|
|
22
|
+
|
|
23
|
+
const entry = registry.get(id);
|
|
24
|
+
if (!entry) throw new Error(`Unknown registry dependency "${id}".`);
|
|
25
|
+
|
|
26
|
+
visiting.add(id);
|
|
27
|
+
entry.dependencies.forEach(visit);
|
|
28
|
+
visiting.delete(id);
|
|
29
|
+
visited.add(id);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
registry.forEach((entry) => visit(entry.id));
|
|
33
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { RegistryEntry } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Collect dependency entries before their dependent component.
|
|
5
|
+
* @param id Component ID to collect.
|
|
6
|
+
* @param registry Catalogue to resolve IDs against.
|
|
7
|
+
* @param planned Component IDs already included in this plan's traversal.
|
|
8
|
+
* @returns Ordered entries, or an empty list when the component was already included.
|
|
9
|
+
* @throws When the component ID is absent from the catalogue.
|
|
10
|
+
*/
|
|
11
|
+
export const collectEntries = (
|
|
12
|
+
id: string,
|
|
13
|
+
registry: Map<string, RegistryEntry>,
|
|
14
|
+
planned: Set<string>
|
|
15
|
+
): RegistryEntry[] => {
|
|
16
|
+
if (planned.has(id)) return [];
|
|
17
|
+
|
|
18
|
+
const entry = registry.get(id);
|
|
19
|
+
if (!entry) throw new Error(`Unknown registry component "${id}".`);
|
|
20
|
+
|
|
21
|
+
planned.add(id);
|
|
22
|
+
return [
|
|
23
|
+
...entry.dependencies.flatMap((dependencyId) =>
|
|
24
|
+
collectEntries(dependencyId, registry, planned)
|
|
25
|
+
),
|
|
26
|
+
entry
|
|
27
|
+
];
|
|
28
|
+
};
|