create-kerf-component 5.0.0-beta.51 → 5.0.0-beta.53

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 CHANGED
@@ -54,6 +54,11 @@ 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.
57
62
 
58
63
  ## Layout produced
59
64
 
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`,
@@ -621,6 +634,9 @@ function toEntry(packageName, component) {
621
634
  publicExports: component.publicExports,
622
635
  sourceLinks: component.sourceLinks,
623
636
  source: component.source,
637
+ ...(component.composition.rendersAs
638
+ ? { rendersAs: component.composition.rendersAs }
639
+ : {}),
624
640
  parents: component.composition.parents,
625
641
  contexts: component.composition.contexts,
626
642
  zones: component.composition.zones,
@@ -636,6 +652,75 @@ function toEntry(packageName, component) {
636
652
  };
637
653
  }
638
654
 
655
+ // The catalog keys a package's `rendersAs` may name: entries generated in this
656
+ // run, or those of an installed dependency's shipped catalog (its
657
+ // package.json#kerfComponentCatalog.output, or @kerfjs/ui's ai/ catalog).
658
+ function dependencyCatalogKeys(packageRoot, packageName, cache) {
659
+ const cacheKey = `${packageRoot}\0${packageName}`;
660
+ if (cache.has(cacheKey)) return cache.get(cacheKey);
661
+ let keys;
662
+ for (let dir = packageRoot; ; dir = dirname(dir)) {
663
+ const manifestPath = join(dir, 'node_modules', packageName, 'package.json');
664
+ if (existsSync(manifestPath)) {
665
+ const dependencyRoot = dirname(manifestPath);
666
+ let catalogPath;
667
+ try {
668
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
669
+ if (manifest[CONFIG_KEY]?.output)
670
+ catalogPath = resolve(dependencyRoot, manifest[CONFIG_KEY].output);
671
+ } catch {
672
+ // An unreadable manifest leaves the package unresolved below.
673
+ }
674
+ catalogPath ??= join(dependencyRoot, 'ai', 'component-catalog-v2.json');
675
+ try {
676
+ const catalog = JSON.parse(readFileSync(catalogPath, 'utf8'));
677
+ keys = new Set(
678
+ (catalog.entries ?? []).map(
679
+ (entry) => entry.key ?? `${catalog.package}:${entry.id}`,
680
+ ),
681
+ );
682
+ } catch {
683
+ // No readable catalog: the package stays unresolved.
684
+ }
685
+ break;
686
+ }
687
+ if (dirname(dir) === dir) break;
688
+ }
689
+ cache.set(cacheKey, keys);
690
+ return keys;
691
+ }
692
+
693
+ function validateRendersAs(results, diagnostics) {
694
+ const generated = new Map(
695
+ results.map(({ catalog }) => [
696
+ catalog.package,
697
+ new Set(catalog.entries.map((entry) => entry.key)),
698
+ ]),
699
+ );
700
+ const cache = new Map();
701
+ for (const { packageRoot, metadataPath, catalog } of results)
702
+ for (const entry of catalog.entries)
703
+ for (const root of entry.rendersAs ?? []) {
704
+ const at = `${metadataPath}: components.${entry.id}.composition.rendersAs`;
705
+ const rootPackage = root.slice(0, root.lastIndexOf(':'));
706
+ if (root === entry.key) {
707
+ diagnostics.push(`${at}: ${root} names the component itself`);
708
+ continue;
709
+ }
710
+ const keys =
711
+ generated.get(rootPackage) ??
712
+ dependencyCatalogKeys(packageRoot, rootPackage, cache);
713
+ if (!keys)
714
+ diagnostics.push(
715
+ `${at}: cannot resolve a component catalog for package ${rootPackage}; install it or generate its catalog`,
716
+ );
717
+ else if (!keys.has(root))
718
+ diagnostics.push(
719
+ `${at}: ${root} is not an entry in ${rootPackage}'s catalog`,
720
+ );
721
+ }
722
+ }
723
+
639
724
  export function generateCatalogs(root = process.cwd()) {
640
725
  const diagnostics = [];
641
726
  const packages = configuredPackages(resolve(root), diagnostics);
@@ -707,10 +792,12 @@ export function generateCatalogs(root = process.cwd()) {
707
792
  );
708
793
  results.push({
709
794
  packageRoot,
795
+ metadataPath,
710
796
  outputPath: resolve(packageRoot, config.output),
711
797
  catalog,
712
798
  });
713
799
  }
800
+ validateRendersAs(results, diagnostics);
714
801
  if (diagnostics.length) throw new CatalogError(diagnostics.sort());
715
802
  return results;
716
803
  }
@@ -151,6 +151,13 @@
151
151
  },
152
152
  "sourceLinks": { "$ref": "#/$defs/stringList" },
153
153
  "source": { "type": "string", "minLength": 1 },
154
+ "rendersAs": {
155
+ "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.",
156
+ "type": "array",
157
+ "minItems": 1,
158
+ "items": { "type": "string", "pattern": "^[^:]+:[^:]+$" },
159
+ "uniqueItems": true
160
+ },
154
161
  "parents": {
155
162
  "type": "object",
156
163
  "required": ["mode", "entries"],
@@ -159,6 +159,13 @@
159
159
  "layout"
160
160
  ],
161
161
  "properties": {
162
+ "rendersAs": {
163
+ "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.",
164
+ "type": "array",
165
+ "minItems": 1,
166
+ "items": { "type": "string", "pattern": "^[^:]+:[^:]+$" },
167
+ "uniqueItems": true
168
+ },
162
169
  "parents": {
163
170
  "type": "object",
164
171
  "required": ["mode", "entries"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kerf-component",
3
- "version": "5.0.0-beta.51",
3
+ "version": "5.0.0-beta.53",
4
4
  "description": "Scaffold a publishable kerf component package that already follows kerf's hard packaging rules.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,7 +36,7 @@
36
36
  "kerfjs": "^5.0.0-0"
37
37
  },
38
38
  "devDependencies": {
39
- "kerfjs": "^5.0.0-beta.51",
39
+ "kerfjs": "^5.0.0-beta.53",
40
40
  "tsup": "^8",
41
41
  "typescript": "^5 || ^6"
42
42
  }