create-kerf-component 5.0.0-beta.52 → 5.0.0-beta.54
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 +16 -0
- package/catalog.js +117 -4
- package/component-catalog-v2.schema.json +9 -1
- package/component-metadata.schema.json +9 -1
- package/package.json +1 -1
- package/template/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,6 +54,22 @@ A ready-to-publish component package that encodes the rules from the kerf docs
|
|
|
54
54
|
not render, remove, or treat those attributes as their own state. The field
|
|
55
55
|
is optional; names must be unique per component, and the checker rejects a
|
|
56
56
|
helper that is not listed in `wiring.helpers`.
|
|
57
|
+
A wrapper that renders a cataloged component declares it under
|
|
58
|
+
`composition.rendersAs` (for example
|
|
59
|
+
`["@kerfjs/ui:toolbar-control-group"]`), so the `ui-composition` lint treats
|
|
60
|
+
it as that root. Every key must resolve to an entry generated in the same run
|
|
61
|
+
or to an installed package's catalog.
|
|
62
|
+
Each `publicExports` item names a `package.json#exports` subpath that maps to
|
|
63
|
+
the `src/` file exporting it. A private, bundled application
|
|
64
|
+
(`package.json#private: true`) has no per-file `dist/` entries and is never
|
|
65
|
+
imported by package name, so its items may omit `subpath`
|
|
66
|
+
(`{ "name": "DemandSegmentsControl" }`); the checker then verifies the name
|
|
67
|
+
against the component's `source` file, which is how the UI lint resolves the
|
|
68
|
+
wrapper. A subpath it does declare is still checked against `exports`.
|
|
69
|
+
`private: true` is the only opt-in: a `.kerf-ui-profile.json` `scope` does
|
|
70
|
+
not change this. An application that is published to npm (an app shell), or
|
|
71
|
+
whose manifest omits `private`, either declares a real `subpath` for each
|
|
72
|
+
item or sets `"private": true`.
|
|
57
73
|
|
|
58
74
|
## Layout produced
|
|
59
75
|
|
package/catalog.js
CHANGED
|
@@ -186,8 +186,21 @@ function valueAt(value, path) {
|
|
|
186
186
|
return path.split('.').reduce((current, part) => current?.[part], value);
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
// Paths where an explicit `null` is itself the author's decision: a component
|
|
190
|
+
// whose root element belongs to another catalog (a `rendersAs` wrapper) or that
|
|
191
|
+
// exposes no public root class.
|
|
192
|
+
const NULLABLE_DECISIONS = new Set(['boundaries.rootClass']);
|
|
193
|
+
|
|
189
194
|
function requireDecision(component, path, at, diagnostics) {
|
|
190
195
|
const value = valueAt(component, path);
|
|
196
|
+
const explicitNull =
|
|
197
|
+
value === null &&
|
|
198
|
+
NULLABLE_DECISIONS.has(path) &&
|
|
199
|
+
Object.hasOwn(
|
|
200
|
+
valueAt(component, path.slice(0, path.lastIndexOf('.'))) ?? {},
|
|
201
|
+
path.slice(path.lastIndexOf('.') + 1),
|
|
202
|
+
);
|
|
203
|
+
if (explicitNull) return;
|
|
191
204
|
if (value === undefined || value === null || value === '') {
|
|
192
205
|
diagnostics.push(
|
|
193
206
|
`${at}.${path}: author decision required; the generator does not infer semantics or geometry from rendered appearance`,
|
|
@@ -519,6 +532,11 @@ function validateComponent(
|
|
|
519
532
|
diagnostics.push(`${at}.source: file does not exist: ${component.source}`);
|
|
520
533
|
|
|
521
534
|
const exports = packageJson.exports ?? {};
|
|
535
|
+
// A private application (package.json#private) is bundled, never imported
|
|
536
|
+
// by package name, and has no per-file dist entries. Its exports may omit
|
|
537
|
+
// `subpath`; each name is then verified against the component's own
|
|
538
|
+
// `source` file, which is how the Kerf UI rules resolve its wrappers.
|
|
539
|
+
const privateApplication = packageJson.private === true;
|
|
522
540
|
if (
|
|
523
541
|
!Array.isArray(component.publicExports) ||
|
|
524
542
|
!component.publicExports.length
|
|
@@ -528,13 +546,34 @@ function validateComponent(
|
|
|
528
546
|
const seen = new Set();
|
|
529
547
|
for (const item of component.publicExports) {
|
|
530
548
|
const exportAt = `${at}.publicExports`;
|
|
531
|
-
|
|
532
|
-
|
|
549
|
+
const sourceOnly =
|
|
550
|
+
privateApplication &&
|
|
551
|
+
Boolean(item?.name) &&
|
|
552
|
+
!Object.hasOwn(item, 'subpath');
|
|
553
|
+
if (!item?.name || (!sourceOnly && typeof item?.subpath !== 'string')) {
|
|
554
|
+
diagnostics.push(
|
|
555
|
+
`${exportAt}: every export requires name and subpath (only a private application, package.json#private, may omit subpath)`,
|
|
556
|
+
);
|
|
533
557
|
continue;
|
|
534
558
|
}
|
|
535
|
-
const key =
|
|
536
|
-
|
|
559
|
+
const key = sourceOnly
|
|
560
|
+
? `source:${item.name}`
|
|
561
|
+
: `${item.subpath}:${item.name}`;
|
|
562
|
+
if (seen.has(key))
|
|
563
|
+
diagnostics.push(
|
|
564
|
+
`${exportAt}: duplicate ${sourceOnly ? item.name : key}`,
|
|
565
|
+
);
|
|
537
566
|
seen.add(key);
|
|
567
|
+
if (sourceOnly) {
|
|
568
|
+
if (
|
|
569
|
+
existsSync(sourcePath) &&
|
|
570
|
+
!exportedNamesFromFile(sourcePath).has(item.name)
|
|
571
|
+
)
|
|
572
|
+
diagnostics.push(
|
|
573
|
+
`${exportAt}: ${item.name} is not exported by ${component.source}`,
|
|
574
|
+
);
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
538
577
|
if (!(item.subpath in exports))
|
|
539
578
|
diagnostics.push(
|
|
540
579
|
`${exportAt}: package.json does not export subpath ${item.subpath}`,
|
|
@@ -621,6 +660,9 @@ function toEntry(packageName, component) {
|
|
|
621
660
|
publicExports: component.publicExports,
|
|
622
661
|
sourceLinks: component.sourceLinks,
|
|
623
662
|
source: component.source,
|
|
663
|
+
...(component.composition.rendersAs
|
|
664
|
+
? { rendersAs: component.composition.rendersAs }
|
|
665
|
+
: {}),
|
|
624
666
|
parents: component.composition.parents,
|
|
625
667
|
contexts: component.composition.contexts,
|
|
626
668
|
zones: component.composition.zones,
|
|
@@ -636,6 +678,75 @@ function toEntry(packageName, component) {
|
|
|
636
678
|
};
|
|
637
679
|
}
|
|
638
680
|
|
|
681
|
+
// The catalog keys a package's `rendersAs` may name: entries generated in this
|
|
682
|
+
// run, or those of an installed dependency's shipped catalog (its
|
|
683
|
+
// package.json#kerfComponentCatalog.output, or @kerfjs/ui's ai/ catalog).
|
|
684
|
+
function dependencyCatalogKeys(packageRoot, packageName, cache) {
|
|
685
|
+
const cacheKey = `${packageRoot}\0${packageName}`;
|
|
686
|
+
if (cache.has(cacheKey)) return cache.get(cacheKey);
|
|
687
|
+
let keys;
|
|
688
|
+
for (let dir = packageRoot; ; dir = dirname(dir)) {
|
|
689
|
+
const manifestPath = join(dir, 'node_modules', packageName, 'package.json');
|
|
690
|
+
if (existsSync(manifestPath)) {
|
|
691
|
+
const dependencyRoot = dirname(manifestPath);
|
|
692
|
+
let catalogPath;
|
|
693
|
+
try {
|
|
694
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
695
|
+
if (manifest[CONFIG_KEY]?.output)
|
|
696
|
+
catalogPath = resolve(dependencyRoot, manifest[CONFIG_KEY].output);
|
|
697
|
+
} catch {
|
|
698
|
+
// An unreadable manifest leaves the package unresolved below.
|
|
699
|
+
}
|
|
700
|
+
catalogPath ??= join(dependencyRoot, 'ai', 'component-catalog-v2.json');
|
|
701
|
+
try {
|
|
702
|
+
const catalog = JSON.parse(readFileSync(catalogPath, 'utf8'));
|
|
703
|
+
keys = new Set(
|
|
704
|
+
(catalog.entries ?? []).map(
|
|
705
|
+
(entry) => entry.key ?? `${catalog.package}:${entry.id}`,
|
|
706
|
+
),
|
|
707
|
+
);
|
|
708
|
+
} catch {
|
|
709
|
+
// No readable catalog: the package stays unresolved.
|
|
710
|
+
}
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
if (dirname(dir) === dir) break;
|
|
714
|
+
}
|
|
715
|
+
cache.set(cacheKey, keys);
|
|
716
|
+
return keys;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function validateRendersAs(results, diagnostics) {
|
|
720
|
+
const generated = new Map(
|
|
721
|
+
results.map(({ catalog }) => [
|
|
722
|
+
catalog.package,
|
|
723
|
+
new Set(catalog.entries.map((entry) => entry.key)),
|
|
724
|
+
]),
|
|
725
|
+
);
|
|
726
|
+
const cache = new Map();
|
|
727
|
+
for (const { packageRoot, metadataPath, catalog } of results)
|
|
728
|
+
for (const entry of catalog.entries)
|
|
729
|
+
for (const root of entry.rendersAs ?? []) {
|
|
730
|
+
const at = `${metadataPath}: components.${entry.id}.composition.rendersAs`;
|
|
731
|
+
const rootPackage = root.slice(0, root.lastIndexOf(':'));
|
|
732
|
+
if (root === entry.key) {
|
|
733
|
+
diagnostics.push(`${at}: ${root} names the component itself`);
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
const keys =
|
|
737
|
+
generated.get(rootPackage) ??
|
|
738
|
+
dependencyCatalogKeys(packageRoot, rootPackage, cache);
|
|
739
|
+
if (!keys)
|
|
740
|
+
diagnostics.push(
|
|
741
|
+
`${at}: cannot resolve a component catalog for package ${rootPackage}; install it or generate its catalog`,
|
|
742
|
+
);
|
|
743
|
+
else if (!keys.has(root))
|
|
744
|
+
diagnostics.push(
|
|
745
|
+
`${at}: ${root} is not an entry in ${rootPackage}'s catalog`,
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
639
750
|
export function generateCatalogs(root = process.cwd()) {
|
|
640
751
|
const diagnostics = [];
|
|
641
752
|
const packages = configuredPackages(resolve(root), diagnostics);
|
|
@@ -707,10 +818,12 @@ export function generateCatalogs(root = process.cwd()) {
|
|
|
707
818
|
);
|
|
708
819
|
results.push({
|
|
709
820
|
packageRoot,
|
|
821
|
+
metadataPath,
|
|
710
822
|
outputPath: resolve(packageRoot, config.output),
|
|
711
823
|
catalog,
|
|
712
824
|
});
|
|
713
825
|
}
|
|
826
|
+
validateRendersAs(results, diagnostics);
|
|
714
827
|
if (diagnostics.length) throw new CatalogError(diagnostics.sort());
|
|
715
828
|
return results;
|
|
716
829
|
}
|
|
@@ -137,10 +137,11 @@
|
|
|
137
137
|
"kind": { "enum": ["component", "composition", "recipe"] },
|
|
138
138
|
"purpose": { "type": "string", "minLength": 1 },
|
|
139
139
|
"publicExports": {
|
|
140
|
+
"description": "Each export's name and the package.json#exports subpath that serves it. An entry from a private application omits subpath: the application is bundled, never imported by package name, so the name resolves through the entry's source file.",
|
|
140
141
|
"type": "array",
|
|
141
142
|
"items": {
|
|
142
143
|
"type": "object",
|
|
143
|
-
"required": ["name"
|
|
144
|
+
"required": ["name"],
|
|
144
145
|
"properties": {
|
|
145
146
|
"name": { "type": "string", "minLength": 1 },
|
|
146
147
|
"subpath": { "type": "string", "pattern": "^\\." }
|
|
@@ -151,6 +152,13 @@
|
|
|
151
152
|
},
|
|
152
153
|
"sourceLinks": { "$ref": "#/$defs/stringList" },
|
|
153
154
|
"source": { "type": "string", "minLength": 1 },
|
|
155
|
+
"rendersAs": {
|
|
156
|
+
"description": "For a wrapper component: the cataloged roots it renders (package-qualified keys), any one of them or nothing. Composition checks treat an element of this entry as each declared root: a zone accepts it only if it accepts every root, and each root's parent contract applies to where it is placed.",
|
|
157
|
+
"type": "array",
|
|
158
|
+
"minItems": 1,
|
|
159
|
+
"items": { "type": "string", "pattern": "^[^:]+:[^:]+$" },
|
|
160
|
+
"uniqueItems": true
|
|
161
|
+
},
|
|
154
162
|
"parents": {
|
|
155
163
|
"type": "object",
|
|
156
164
|
"required": ["mode", "entries"],
|
|
@@ -97,9 +97,10 @@
|
|
|
97
97
|
"publicExports": {
|
|
98
98
|
"type": "array",
|
|
99
99
|
"minItems": 1,
|
|
100
|
+
"description": "Each export's name and package.json#exports subpath. A private application (package.json#private) may omit subpath; the name is then verified against the component's source file.",
|
|
100
101
|
"items": {
|
|
101
102
|
"type": "object",
|
|
102
|
-
"required": ["name"
|
|
103
|
+
"required": ["name"],
|
|
103
104
|
"properties": {
|
|
104
105
|
"name": { "type": "string", "minLength": 1 },
|
|
105
106
|
"subpath": { "type": "string", "pattern": "^\\." }
|
|
@@ -159,6 +160,13 @@
|
|
|
159
160
|
"layout"
|
|
160
161
|
],
|
|
161
162
|
"properties": {
|
|
163
|
+
"rendersAs": {
|
|
164
|
+
"description": "For a wrapper component: the cataloged roots it renders (package-qualified keys such as @kerfjs/ui:toolbar-control-group), any one of them or nothing. UI composition checks treat the wrapper as each declared root.",
|
|
165
|
+
"type": "array",
|
|
166
|
+
"minItems": 1,
|
|
167
|
+
"items": { "type": "string", "pattern": "^[^:]+:[^:]+$" },
|
|
168
|
+
"uniqueItems": true
|
|
169
|
+
},
|
|
162
170
|
"parents": {
|
|
163
171
|
"type": "object",
|
|
164
172
|
"required": ["mode", "entries"],
|
package/package.json
CHANGED