@genesislcap/ts-builder 15.3.5 → 15.4.1-GENC-1475.5

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.
@@ -1 +1 @@
1
- {"version":3,"file":"react-wrapper-generator.d.ts","sourceRoot":"","sources":["../src/react-wrapper-generator.ts"],"names":[],"mappings":"AAyCA,KAAK,cAAc,GAAG;IAAE,SAAS,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,SAAS,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AA0jC/F,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAgDhF"}
1
+ {"version":3,"file":"react-wrapper-generator.d.ts","sourceRoot":"","sources":["../src/react-wrapper-generator.ts"],"names":[],"mappings":"AA0IA,KAAK,cAAc,GAAG;IAAE,SAAS,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,SAAS,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AA8lC/F,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAoDhF"}
@@ -5,6 +5,63 @@ const tslib_1 = require("tslib");
5
5
  const node_fs_1 = require("node:fs");
6
6
  const promises_1 = require("node:fs/promises");
7
7
  const node_path_1 = require("node:path");
8
+ const consola_1 = tslib_1.__importDefault(require("consola"));
9
+ /** A bare TS identifier — `type` is emitted into a `.d.ts` verbatim. */
10
+ const OVERRIDE_TYPE_RE = /^[A-Za-z_$][\w$]*$/;
11
+ /** A safe module specifier — `from` is emitted inside single quotes (`from '<spec>';`), so it must
12
+ * not be able to close the quote and inject further declarations. Covers relative paths and
13
+ * scoped package names; rejects quotes, semicolons and whitespace. */
14
+ const OVERRIDE_FROM_RE = /^[.\w@/-]+$/;
15
+ /** A safe prop name. Also emitted into the `.d.ts` — both quoted in the `Omit` key list and as the
16
+ * re-declared key — so it carries the same injection risk as `from`. Hyphens are allowed because
17
+ * React props legitimately include `aria-*` and `data-*`; quotes, pipes and whitespace are not.
18
+ * Without this, a name like `appearance' | 'disabled` widened the `Omit` to two props AND emitted
19
+ * a union as a key, which is not valid in a type literal. */
20
+ const OVERRIDE_PROP_RE = /^[A-Za-z_$][\w$-]*$/;
21
+ /**
22
+ * Read + validate the `reactWrapperPropTypes` map.
23
+ *
24
+ * Malformed entries are skipped rather than thrown: bad config must never abort a build — the
25
+ * wrapper still generates, just without that override. But skipping SILENTLY makes a typo
26
+ * undiagnosable, so every skip is reported with the component, prop and reason.
27
+ */
28
+ function readPropTypeOverrides(packageJson) {
29
+ const raw = packageJson.reactWrapperPropTypes;
30
+ if (raw === undefined)
31
+ return {};
32
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
33
+ consola_1.default.warn('[ts-builder] Ignoring "reactWrapperPropTypes": expected an object.');
34
+ return {};
35
+ }
36
+ const out = {};
37
+ for (const [component, props] of Object.entries(raw)) {
38
+ if (!props || typeof props !== 'object' || Array.isArray(props)) {
39
+ consola_1.default.warn(`[ts-builder] Ignoring reactWrapperPropTypes.${component}: expected an object of prop → { type, from }.`);
40
+ continue;
41
+ }
42
+ const byProp = {};
43
+ for (const [prop, spec] of Object.entries(props)) {
44
+ const s = spec;
45
+ const where = `reactWrapperPropTypes.${component}.${prop}`;
46
+ if (!OVERRIDE_PROP_RE.test(prop)) {
47
+ consola_1.default.warn(`[ts-builder] Skipping ${where}: prop name must match ${OVERRIDE_PROP_RE} (letters, digits, _, $, -).`);
48
+ continue;
49
+ }
50
+ if (!s || typeof s.type !== 'string' || !OVERRIDE_TYPE_RE.test(s.type)) {
51
+ consola_1.default.warn(`[ts-builder] Skipping ${where}: "type" must be a bare TypeScript identifier (got ${JSON.stringify(s === null || s === void 0 ? void 0 : s.type)}).`);
52
+ continue;
53
+ }
54
+ if (typeof s.from !== 'string' || !OVERRIDE_FROM_RE.test(s.from.trim())) {
55
+ consola_1.default.warn(`[ts-builder] Skipping ${where}: "from" must be a module specifier matching ${OVERRIDE_FROM_RE} (got ${JSON.stringify(s.from)}).`);
56
+ continue;
57
+ }
58
+ byProp[prop] = { type: s.type, from: s.from.trim() };
59
+ }
60
+ if (Object.keys(byProp).length > 0)
61
+ out[component] = byProp;
62
+ }
63
+ return out;
64
+ }
8
65
  // ── Constants ────────────────────────────────────────────────────────────────
9
66
  const PRIMITIVE_UNION_REGEX = /^(?:\s*(?:string|number|boolean|bigint|null|undefined|unknown|any|void|'[^']*'|"[^"]*"|`[^`]*`|(?:\d+(?:\.\d+)?))\s*)(?:\|\s*(?:string|number|boolean|bigint|null|undefined|unknown|any|void|'[^']*'|"[^"]*"|`[^`]*`|(?:\d+(?:\.\d+)?))\s*)*$/;
10
67
  const IDENTIFIER_TOKEN_REGEX = /[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*/g;
@@ -661,8 +718,8 @@ function generateReactWrapperJs(entries, format) {
661
718
  }
662
719
  return lines.join('\n');
663
720
  }
664
- function generateReactWrapperDts(entries, typeImportState) {
665
- var _a;
721
+ function generateReactWrapperDts(entries, typeImportState, propTypeOverrides = {}) {
722
+ var _a, _b;
666
723
  const valid = entries.filter((e) => e.declaration.name && e.declaration.tagName && e.modulePath);
667
724
  if (!valid.length)
668
725
  return '';
@@ -686,7 +743,38 @@ function generateReactWrapperDts(entries, typeImportState) {
686
743
  const handlerType = toEventHandlerType((_a = event === null || event === void 0 ? void 0 : event.type) === null || _a === void 0 ? void 0 : _a.text, wrapperTypeState, event === null || event === void 0 ? void 0 : event.description);
687
744
  return ` ${handlerName}?: ${handlerType};`;
688
745
  });
689
- declarationLines.push(`export declare const ${name}: React.ForwardRefExoticComponent<`, ` React.PropsWithChildren<`, ` Omit<PublicOf<${name}WC>, 'children' | 'style'> &`, ` HTMLWCProps & {`, ...eventLines, ' }', ` > & React.RefAttributes<${name}WC>`, '>;', `export type ${name}Ref = ${name}WC;`, '');
746
+ // Prop-type overrides (see PropTypeOverrides): drop the configured props out of the
747
+ // `PublicOf` mapping and re-declare them with the named type, so a design system can correct
748
+ // a prop whose inherited type it has outgrown. No config ⇒ identical output to before.
749
+ // Route each override's type through the SAME import machinery every other type uses
750
+ // (registerTypeImport + trackImportedIdentifierUsage) rather than writing `usedImports`
751
+ // directly. That is what detects an identifier already imported from a DIFFERENT module: two
752
+ // `import type { X }` lines from different specifiers would be a duplicate-identifier error in
753
+ // the generated .d.ts. On a collision, drop the override and warn — emitting the widened prop
754
+ // without its import would leave the declaration referencing an unresolvable type, which is
755
+ // worse than the original narrow type.
756
+ const overrides = (_b = propTypeOverrides[name]) !== null && _b !== void 0 ? _b : {};
757
+ const applied = [];
758
+ for (const prop of Object.keys(overrides).sort()) {
759
+ const { type, from } = overrides[prop];
760
+ registerTypeImport(wrapperTypeState, type, from);
761
+ if (wrapperTypeState.importsByIdentifier.get(type) !== from) {
762
+ consola_1.default.warn(`[ts-builder] Skipping the ${name}.${prop} prop-type override: "${type}" is already imported from a different module, so importing it from "${from}" would clash.`);
763
+ continue;
764
+ }
765
+ trackImportedIdentifierUsage(wrapperTypeState, type, from);
766
+ applied.push(prop);
767
+ }
768
+ const omitKeys = ["'children'", "'style'", ...applied.map((p) => `'${p}'`)].join(' | ');
769
+ const overrideLines = applied.map((prop) => {
770
+ // React props are legitimately allowed to be hyphenated (`aria-*`, `data-*`), which is not a
771
+ // valid bare key in a type literal — quote those. Identifier-safe names stay unquoted so the
772
+ // common output reads naturally. (The Omit key list above is always quoted, so it is safe
773
+ // for either form.)
774
+ const key = OVERRIDE_TYPE_RE.test(prop) ? prop : `'${prop}'`;
775
+ return ` ${key}?: ${overrides[prop].type};`;
776
+ });
777
+ declarationLines.push(`export declare const ${name}: React.ForwardRefExoticComponent<`, ` React.PropsWithChildren<`, ` Omit<PublicOf<${name}WC>, ${omitKeys}> &`, ` HTMLWCProps & {`, ...overrideLines, ...eventLines, ' }', ` > & React.RefAttributes<${name}WC>`, '>;', `export type ${name}Ref = ${name}WC;`, '');
690
778
  }
691
779
  return [
692
780
  '/**',
@@ -970,7 +1058,7 @@ function generateReactWrappers(cwd) {
970
1058
  yield Promise.all([
971
1059
  (0, promises_1.writeFile)((0, node_path_1.resolve)(cwd, 'dist/react.mjs'), generateReactWrapperJs(entries, 'esm'), 'utf8'),
972
1060
  (0, promises_1.writeFile)((0, node_path_1.resolve)(cwd, 'dist/react.cjs'), generateReactWrapperJs(entries, 'cjs'), 'utf8'),
973
- (0, promises_1.writeFile)(reactDtsPath, generateReactWrapperDts(entries, typeImportState), 'utf8'),
1061
+ (0, promises_1.writeFile)(reactDtsPath, generateReactWrapperDts(entries, typeImportState, readPropTypeOverrides(packageJson)), 'utf8'),
974
1062
  ]);
975
1063
  return { generated: true, path: reactDtsPath };
976
1064
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@genesislcap/ts-builder",
3
3
  "description": "Typescript builder",
4
- "version": "15.3.5",
4
+ "version": "15.4.1-GENC-1475.5",
5
5
  "license": "SEE LICENSE IN license.txt",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@custom-elements-manifest/analyzer": "^0.8.2",
20
- "@genesislcap/build-kit": "15.3.5",
20
+ "@genesislcap/build-kit": "15.4.1-GENC-1475.5",
21
21
  "consola": "^3.0.2",
22
22
  "copyfiles": "^2.4.1",
23
23
  "pkg-types": "^1.0.2"
@@ -30,5 +30,5 @@
30
30
  "publishConfig": {
31
31
  "access": "public"
32
32
  },
33
- "gitHead": "dc772a3a6f1b3d1c249f1ffae27a13d29878609c"
33
+ "gitHead": "62912fb73a7769428e0f42955fd4bf26ac1d8050"
34
34
  }
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
3
3
  import { dirname, resolve } from 'node:path';
4
+ import consola from 'consola';
4
5
 
5
6
  // ── CEM type shapes ──────────────────────────────────────────────────────────
6
7
 
@@ -37,6 +38,102 @@ type TypeImportState = {
37
38
  wildcardExportModules: Set<string>;
38
39
  };
39
40
 
41
+ // ── Prop type overrides ──────────────────────────────────────────────────────
42
+ //
43
+ // WHY THIS EXISTS. Wrapper props are derived from the element CLASS via
44
+ // `Omit<PublicOf<XWC>, …>`, so a prop's type is whatever TypeScript resolves up the inheritance
45
+ // chain. That is normally exactly right — but it breaks when a design system RESTYLES a prop whose
46
+ // type is a closed union owned by a base package. The reported case: rapid-design-system's
47
+ // `button.styles.ts` styles 12 `appearance` values (primary, secondary, danger, …) while the
48
+ // inherited FAST type is `ButtonAppearance = 'accent' | 'lightweight' | 'neutral' | 'outline' |
49
+ // 'stealth'`. Consumers writing `appearance="danger"` get TS2322 for a value that renders
50
+ // correctly, and the three FAST-only values type-check while rendering unstyled.
51
+ //
52
+ // It CANNOT be fixed on the component. TypeScript requires a derived property to be assignable to
53
+ // the base's, so widening an inherited property is rejected — verified for all three candidate
54
+ // forms: `declare appearance: RapidButtonAppearance` and `declare appearance: string` both give
55
+ // TS2416, and class/interface declaration merging gives TS2415 + TS2430. (`foundation-zero` exports
56
+ // a `ZeroButtonAppearance` union but never wires it to the class, which is why that precedent does
57
+ // not actually work; `SearchBar` gets away with `appearance: string` only because its own base has
58
+ // no `appearance`.) The wrapper boundary is therefore the only place the public prop type can be
59
+ // corrected.
60
+ //
61
+ // Opt-in per package via package.json — same convention as the existing `customElements` field:
62
+ //
63
+ // "reactWrapperPropTypes": {
64
+ // "Button": { "appearance": { "type": "RapidButtonAppearance", "from": "./button/button.styles" } }
65
+ // }
66
+ //
67
+ // A configured prop is removed from the `PublicOf` mapping and re-declared as optional with the
68
+ // named type, which is imported into react.d.ts. Packages that configure nothing are byte-for-byte
69
+ // unchanged, so this is inert for every wrapper that does not opt in.
70
+ type PropTypeOverride = { type: string; from: string };
71
+ type PropTypeOverrides = Record<string, Record<string, PropTypeOverride>>;
72
+
73
+ /** A bare TS identifier — `type` is emitted into a `.d.ts` verbatim. */
74
+ const OVERRIDE_TYPE_RE = /^[A-Za-z_$][\w$]*$/;
75
+ /** A safe module specifier — `from` is emitted inside single quotes (`from '<spec>';`), so it must
76
+ * not be able to close the quote and inject further declarations. Covers relative paths and
77
+ * scoped package names; rejects quotes, semicolons and whitespace. */
78
+ const OVERRIDE_FROM_RE = /^[.\w@/-]+$/;
79
+ /** A safe prop name. Also emitted into the `.d.ts` — both quoted in the `Omit` key list and as the
80
+ * re-declared key — so it carries the same injection risk as `from`. Hyphens are allowed because
81
+ * React props legitimately include `aria-*` and `data-*`; quotes, pipes and whitespace are not.
82
+ * Without this, a name like `appearance' | 'disabled` widened the `Omit` to two props AND emitted
83
+ * a union as a key, which is not valid in a type literal. */
84
+ const OVERRIDE_PROP_RE = /^[A-Za-z_$][\w$-]*$/;
85
+
86
+ /**
87
+ * Read + validate the `reactWrapperPropTypes` map.
88
+ *
89
+ * Malformed entries are skipped rather than thrown: bad config must never abort a build — the
90
+ * wrapper still generates, just without that override. But skipping SILENTLY makes a typo
91
+ * undiagnosable, so every skip is reported with the component, prop and reason.
92
+ */
93
+ function readPropTypeOverrides(packageJson: Record<string, unknown>): PropTypeOverrides {
94
+ const raw = packageJson.reactWrapperPropTypes;
95
+ if (raw === undefined) return {};
96
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
97
+ consola.warn('[ts-builder] Ignoring "reactWrapperPropTypes": expected an object.');
98
+ return {};
99
+ }
100
+ const out: PropTypeOverrides = {};
101
+ for (const [component, props] of Object.entries(raw as Record<string, unknown>)) {
102
+ if (!props || typeof props !== 'object' || Array.isArray(props)) {
103
+ consola.warn(
104
+ `[ts-builder] Ignoring reactWrapperPropTypes.${component}: expected an object of prop → { type, from }.`,
105
+ );
106
+ continue;
107
+ }
108
+ const byProp: Record<string, PropTypeOverride> = {};
109
+ for (const [prop, spec] of Object.entries(props as Record<string, unknown>)) {
110
+ const s = spec as Partial<PropTypeOverride> | undefined;
111
+ const where = `reactWrapperPropTypes.${component}.${prop}`;
112
+ if (!OVERRIDE_PROP_RE.test(prop)) {
113
+ consola.warn(
114
+ `[ts-builder] Skipping ${where}: prop name must match ${OVERRIDE_PROP_RE} (letters, digits, _, $, -).`,
115
+ );
116
+ continue;
117
+ }
118
+ if (!s || typeof s.type !== 'string' || !OVERRIDE_TYPE_RE.test(s.type)) {
119
+ consola.warn(
120
+ `[ts-builder] Skipping ${where}: "type" must be a bare TypeScript identifier (got ${JSON.stringify(s?.type)}).`,
121
+ );
122
+ continue;
123
+ }
124
+ if (typeof s.from !== 'string' || !OVERRIDE_FROM_RE.test(s.from.trim())) {
125
+ consola.warn(
126
+ `[ts-builder] Skipping ${where}: "from" must be a module specifier matching ${OVERRIDE_FROM_RE} (got ${JSON.stringify(s.from)}).`,
127
+ );
128
+ continue;
129
+ }
130
+ byProp[prop] = { type: s.type, from: s.from.trim() };
131
+ }
132
+ if (Object.keys(byProp).length > 0) out[component] = byProp;
133
+ }
134
+ return out;
135
+ }
136
+
40
137
  // ── Public API return type ───────────────────────────────────────────────────
41
138
 
42
139
  type GenerateResult = { generated: true; path: string } | { generated: false; reason: string };
@@ -810,6 +907,7 @@ function generateReactWrapperJs(entries: CEMElementEntry[], format: 'esm' | 'cjs
810
907
  function generateReactWrapperDts(
811
908
  entries: CEMElementEntry[],
812
909
  typeImportState: TypeImportState,
910
+ propTypeOverrides: PropTypeOverrides = {},
813
911
  ): string {
814
912
  const valid = entries.filter((e) => e.declaration.name && e.declaration.tagName && e.modulePath);
815
913
  if (!valid.length) return '';
@@ -844,11 +942,46 @@ function generateReactWrapperDts(
844
942
  return ` ${handlerName}?: ${handlerType};`;
845
943
  });
846
944
 
945
+ // Prop-type overrides (see PropTypeOverrides): drop the configured props out of the
946
+ // `PublicOf` mapping and re-declare them with the named type, so a design system can correct
947
+ // a prop whose inherited type it has outgrown. No config ⇒ identical output to before.
948
+ // Route each override's type through the SAME import machinery every other type uses
949
+ // (registerTypeImport + trackImportedIdentifierUsage) rather than writing `usedImports`
950
+ // directly. That is what detects an identifier already imported from a DIFFERENT module: two
951
+ // `import type { X }` lines from different specifiers would be a duplicate-identifier error in
952
+ // the generated .d.ts. On a collision, drop the override and warn — emitting the widened prop
953
+ // without its import would leave the declaration referencing an unresolvable type, which is
954
+ // worse than the original narrow type.
955
+ const overrides = propTypeOverrides[name] ?? {};
956
+ const applied: string[] = [];
957
+ for (const prop of Object.keys(overrides).sort()) {
958
+ const { type, from } = overrides[prop];
959
+ registerTypeImport(wrapperTypeState, type, from);
960
+ if (wrapperTypeState.importsByIdentifier.get(type) !== from) {
961
+ consola.warn(
962
+ `[ts-builder] Skipping the ${name}.${prop} prop-type override: "${type}" is already imported from a different module, so importing it from "${from}" would clash.`,
963
+ );
964
+ continue;
965
+ }
966
+ trackImportedIdentifierUsage(wrapperTypeState, type, from);
967
+ applied.push(prop);
968
+ }
969
+ const omitKeys = ["'children'", "'style'", ...applied.map((p) => `'${p}'`)].join(' | ');
970
+ const overrideLines = applied.map((prop) => {
971
+ // React props are legitimately allowed to be hyphenated (`aria-*`, `data-*`), which is not a
972
+ // valid bare key in a type literal — quote those. Identifier-safe names stay unquoted so the
973
+ // common output reads naturally. (The Omit key list above is always quoted, so it is safe
974
+ // for either form.)
975
+ const key = OVERRIDE_TYPE_RE.test(prop) ? prop : `'${prop}'`;
976
+ return ` ${key}?: ${overrides[prop].type};`;
977
+ });
978
+
847
979
  declarationLines.push(
848
980
  `export declare const ${name}: React.ForwardRefExoticComponent<`,
849
981
  ` React.PropsWithChildren<`,
850
- ` Omit<PublicOf<${name}WC>, 'children' | 'style'> &`,
982
+ ` Omit<PublicOf<${name}WC>, ${omitKeys}> &`,
851
983
  ` HTMLWCProps & {`,
984
+ ...overrideLines,
852
985
  ...eventLines,
853
986
  ' }',
854
987
  ` > & React.RefAttributes<${name}WC>`,
@@ -1165,7 +1298,11 @@ export async function generateReactWrappers(cwd: string): Promise<GenerateResult
1165
1298
  await Promise.all([
1166
1299
  writeFile(resolve(cwd, 'dist/react.mjs'), generateReactWrapperJs(entries, 'esm'), 'utf8'),
1167
1300
  writeFile(resolve(cwd, 'dist/react.cjs'), generateReactWrapperJs(entries, 'cjs'), 'utf8'),
1168
- writeFile(reactDtsPath, generateReactWrapperDts(entries, typeImportState), 'utf8'),
1301
+ writeFile(
1302
+ reactDtsPath,
1303
+ generateReactWrapperDts(entries, typeImportState, readPropTypeOverrides(packageJson)),
1304
+ 'utf8',
1305
+ ),
1169
1306
  ]);
1170
1307
 
1171
1308
  return { generated: true, path: reactDtsPath };