@nysds/codegen 0.0.0-stage → 1.20.1

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.
Files changed (49) hide show
  1. package/README.md +95 -2
  2. package/cem-plugins/angular.mjs +551 -0
  3. package/cem-plugins/deps.mjs +154 -0
  4. package/cem-plugins/form-control.mjs +94 -0
  5. package/cem-plugins/index.mjs +12 -0
  6. package/cem-plugins/lib/core.d.mts +79 -0
  7. package/cem-plugins/lib/core.mjs +288 -0
  8. package/cem-plugins/react.mjs +143 -0
  9. package/cem-plugins/vue.mjs +228 -0
  10. package/dist/index.d.ts +17 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +14 -0
  13. package/dist/manifest/components.d.ts +6 -0
  14. package/dist/manifest/components.d.ts.map +1 -0
  15. package/dist/manifest/components.js +5 -0
  16. package/dist/manifest/filters.d.ts +6 -0
  17. package/dist/manifest/filters.d.ts.map +1 -0
  18. package/dist/manifest/filters.js +5 -0
  19. package/dist/manifest/form-control.d.ts +6 -0
  20. package/dist/manifest/form-control.d.ts.map +1 -0
  21. package/dist/manifest/form-control.js +5 -0
  22. package/dist/manifest/load.d.ts +11 -0
  23. package/dist/manifest/load.d.ts.map +1 -0
  24. package/dist/manifest/load.js +64 -0
  25. package/dist/manifest/naming.d.ts +7 -0
  26. package/dist/manifest/naming.d.ts.map +1 -0
  27. package/dist/manifest/naming.js +6 -0
  28. package/dist/manifest/packages.d.ts +6 -0
  29. package/dist/manifest/packages.d.ts.map +1 -0
  30. package/dist/manifest/packages.js +5 -0
  31. package/dist/manifest/types.d.ts +7 -0
  32. package/dist/manifest/types.d.ts.map +1 -0
  33. package/dist/manifest/types.js +1 -0
  34. package/dist/transform/index.d.ts +20 -0
  35. package/dist/transform/index.d.ts.map +1 -0
  36. package/dist/transform/index.js +11 -0
  37. package/dist/transform/parse.d.ts +19 -0
  38. package/dist/transform/parse.d.ts.map +1 -0
  39. package/dist/transform/parse.js +33 -0
  40. package/dist/transform/shared.d.ts +62 -0
  41. package/dist/transform/shared.d.ts.map +1 -0
  42. package/dist/transform/shared.js +96 -0
  43. package/dist/transform/to-angular.d.ts +7 -0
  44. package/dist/transform/to-angular.d.ts.map +1 -0
  45. package/dist/transform/to-angular.js +153 -0
  46. package/dist/transform/to-jsx.d.ts +7 -0
  47. package/dist/transform/to-jsx.d.ts.map +1 -0
  48. package/dist/transform/to-jsx.js +172 -0
  49. package/package.json +46 -4
package/README.md CHANGED
@@ -1,3 +1,96 @@
1
- # Temporary Holding Version
1
+ # @nysds/codegen
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Reads the NYSDS custom elements manifest and derives every framework
4
+ artifact from it: component metadata, naming rules, the CEM plugins that
5
+ generate the React and Angular wrappers, and the transformer that turns an
6
+ HTML example into a JSX or Angular snippet.
7
+
8
+ The manifest is the single source of truth. This package contains no
9
+ per-component tables; if a generator needs a fact, that fact belongs in the
10
+ component's JSDoc and flows through the manifest.
11
+
12
+ ## Layout
13
+
14
+ - `cem-plugins/` — plain JavaScript plugins for
15
+ `@custom-elements-manifest/analyzer`. No build step, so the analyzer can
16
+ run them before this package is built.
17
+ - `cem-plugins/lib/core.mjs` — the shared rules: naming, filters,
18
+ `@formControl` parsing, package derivation, and component metadata.
19
+ `core.d.mts` carries the types.
20
+ - `src/` — the TypeScript API. The `src/manifest/` modules re-export
21
+ `core.mjs`; the import direction is always TypeScript to `.mjs`, never
22
+ the other way, so no rule exists twice.
23
+ - `src/transform/` — the parse5-based snippet transformer.
24
+
25
+ ## Public API
26
+
27
+ ```ts
28
+ import {
29
+ loadManifest,
30
+ listComponents,
31
+ transformExample,
32
+ } from "@nysds/codegen";
33
+
34
+ const manifest = loadManifest();
35
+ const components = listComponents(manifest);
36
+ const { code, imports, warnings } = transformExample({
37
+ html: example,
38
+ framework: "react",
39
+ manifest,
40
+ });
41
+ ```
42
+
43
+ `loadManifest()` without a path resolves the manifest that ships with
44
+ `@nysds/components`. `listComponents()` returns one `ComponentMeta` per
45
+ component: tag, class name, package name, subpath, public props, exposed
46
+ events with React and Angular names, slots, and the `formControl` block
47
+ when the manifest carries one.
48
+
49
+ ## CEM plugins
50
+
51
+ ```js
52
+ import {
53
+ formControlPlugin,
54
+ reactPlugin,
55
+ angularPlugin,
56
+ depsPlugin,
57
+ } from "@nysds/codegen/cem-plugins";
58
+ ```
59
+
60
+ Register `formControlPlugin()` before the wrapper plugins. Paths are
61
+ resolved from the process working directory; run the analyzer from the
62
+ repository root.
63
+
64
+ | Plugin | What it does |
65
+ |---|---|
66
+ | `formControlPlugin` | Attaches `declaration.formControl` from the `@formControl` JSDoc tag. Fails the analyze run on a malformed tag. A form component without the tag throws with `strict: true` and logs one warning with `strict: false`. |
67
+ | `reactPlugin` | Writes the `@lit/react` wrapper per component to `packages/react/src/generated/`. |
68
+ | `angularPlugin` | Writes the typed Angular proxy component per component, the barrel, and `NysAngularModule` to `packages/angular/src/generated/`. |
69
+ | `depsPlugin` | Rewrites the generated `dependencies`, `peerDependencies["@nysds/components"]`, and `exports` blocks in both framework `package.json` files. Skips a target that doesn't exist yet. |
70
+
71
+ Both wrapper plugins clear their output directory first and skip any
72
+ component that has a hand-written file in the package's `src/overrides/`
73
+ directory. Both handle a manifest without typed event aliases by falling
74
+ back to `CustomEvent`.
75
+
76
+ ## Transformer
77
+
78
+ `transformExample({ html, framework, manifest, formsMode })` turns the
79
+ canonical HTML example into a snippet. Only design-system elements are
80
+ transformed; native elements pass through. Unknown attributes pass through
81
+ with a warning. `<script>` and `<style>` blocks are dropped; a script that
82
+ references a design-system element marks the result `unsupported`, as does
83
+ an inline `onclick`-style handler. `formsMode` controls form bindings:
84
+ `"none"` (default) keeps snippets static, `"template"` emits `[(ngModel)]`
85
+ or the React controlled pattern, `"reactive"` emits `formControlName`.
86
+
87
+ ## Test
88
+
89
+ ```sh
90
+ npm test -w @nysds/codegen
91
+ ```
92
+
93
+ Golden files under `test/fixtures/examples/` pin every transformer rule.
94
+ `test/fixtures/site-corpus/` holds preview blocks copied from the docs
95
+ site. Set `NYSDS_SITE_DIR` to a checkout of the docs site to also sweep
96
+ every live example.
@@ -0,0 +1,551 @@
1
+ /**
2
+ * CEM plugin: generates a typed Angular proxy `@Component` for every NYSDS
3
+ * component into `packages/angular/src/generated/` (decision L7), plus the
4
+ * barrel and `NysAngularModule`.
5
+ *
6
+ * The output directory is cleared and recreated on every run. A hand-written
7
+ * file at `src/overrides/<tag>.component.ts` replaces the generated one.
8
+ * Form components (manifest `formControl` block) extend the accessor base
9
+ * class for their kind and provide `NG_VALUE_ACCESSOR`.
10
+ */
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+ import { listComponents, pascalize, classToAngularClass, NYS_BLUR_EVENT } from "./lib/core.mjs";
14
+
15
+ const GLOBAL_EVENT_TYPES = new Set([
16
+ "Event",
17
+ "CustomEvent",
18
+ "UIEvent",
19
+ "FocusEvent",
20
+ "InputEvent",
21
+ "KeyboardEvent",
22
+ "MouseEvent",
23
+ "PointerEvent",
24
+ "TouchEvent",
25
+ "WheelEvent",
26
+ "AnimationEvent",
27
+ "TransitionEvent",
28
+ "ClipboardEvent",
29
+ "DragEvent",
30
+ "SubmitEvent",
31
+ ]);
32
+
33
+ const IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
34
+
35
+ /** npm name of the Angular wrapper package; also its ng-packagr module id. */
36
+ const ANGULAR_PACKAGE = "@nysds/angular";
37
+
38
+ /**
39
+ * Properties Angular or the platform already manage on any host element,
40
+ * or read-only properties that cannot be set via setters.
41
+ * `id` is reflected by the component and set by Angular as an attribute,
42
+ * so the wrapper must not shadow it.
43
+ */
44
+ const HOST_MANAGED_PROPS = new Set([
45
+ "id",
46
+ "title",
47
+ "hidden",
48
+ "tabIndex",
49
+ "className",
50
+ "style",
51
+ "validity",
52
+ "validationMessage",
53
+ "ariaAttributes",
54
+ ]);
55
+
56
+ /**
57
+ * Platform and common library types that never need imports.
58
+ */
59
+ const PLATFORM_TYPES = new Set([
60
+ "string",
61
+ "number",
62
+ "boolean",
63
+ "null",
64
+ "undefined",
65
+ "any",
66
+ "never",
67
+ "void",
68
+ "Event",
69
+ "CustomEvent",
70
+ "UIEvent",
71
+ "FocusEvent",
72
+ "InputEvent",
73
+ "KeyboardEvent",
74
+ "MouseEvent",
75
+ "PointerEvent",
76
+ "TouchEvent",
77
+ "WheelEvent",
78
+ "AnimationEvent",
79
+ "TransitionEvent",
80
+ "ClipboardEvent",
81
+ "DragEvent",
82
+ "SubmitEvent",
83
+ "Array",
84
+ "Object",
85
+ "Date",
86
+ "RegExp",
87
+ "Map",
88
+ "Set",
89
+ "WeakMap",
90
+ "WeakSet",
91
+ "Promise",
92
+ "FileList",
93
+ "ValidityState",
94
+ ]);
95
+
96
+ /**
97
+ * Check if a type looks like it can be safely imported from the component
98
+ * package. Very conservative: only import if it's a known event type
99
+ * or explicitly exported from the component package (which we don't have
100
+ * a list for, so default to false for unknown types).
101
+ */
102
+ function canImportType(typeText) {
103
+ if (!typeText) return false;
104
+ if (PLATFORM_TYPES.has(typeText)) return false;
105
+ if (/^(HTML|SVG|XMLHttp|Blob|FormData|Worker|IDB|File)/.test(typeText)) {
106
+ return false;
107
+ }
108
+ // For unknown types, default to false (use element property type instead)
109
+ // Only import event types and types we know are exported from component packages
110
+ return /Event$/.test(typeText);
111
+ }
112
+
113
+ /**
114
+ * Check if a type is primitive or simple enough to use inline.
115
+ * Handles unions of primitives (e.g., "number | null", "string | undefined").
116
+ */
117
+ function isPrimitiveType(typeText) {
118
+ if (!typeText) return true;
119
+ // Remove whitespace and split by |
120
+ const parts = typeText
121
+ .split("|")
122
+ .map((part) => part.trim())
123
+ .filter(Boolean);
124
+ // If any part is not a primitive keyword, it's not primitive
125
+ return parts.every((part) =>
126
+ /^(string|number|boolean|null|undefined|any|never|void|unknown)$/.test(part)
127
+ );
128
+ }
129
+
130
+ /**
131
+ * Extract custom type names from a type string that might need importing.
132
+ */
133
+ function extractCustomTypes(typeText) {
134
+ if (!typeText) return [];
135
+ const customTypes = new Set();
136
+ // Match bare identifiers
137
+ const matches = typeText.match(/\b[A-Z][A-Za-z0-9]*\b/g) || [];
138
+ for (const match of matches) {
139
+ if (canImportType(match)) {
140
+ customTypes.add(match);
141
+ }
142
+ }
143
+ return [...customTypes];
144
+ }
145
+
146
+ /**
147
+ * Shared accessor bases live in their own ng-packagr entry point. Wrappers
148
+ * must reach them by bare specifier, never relatively: a TypeScript file
149
+ * pulled into two entry points is compiled into both bundles, which would
150
+ * give every form control its own copy of the accessor class.
151
+ */
152
+ const FORMS_ENTRY_POINT = `${ANGULAR_PACKAGE}/forms`;
153
+
154
+ const ACCESSOR_BY_KIND = {
155
+ value: { className: "NysValueAccessor", file: FORMS_ENTRY_POINT },
156
+ checked: { className: "NysCheckedAccessor", file: FORMS_ENTRY_POINT },
157
+ files: { className: "NysFilesAccessor", file: FORMS_ENTRY_POINT },
158
+ };
159
+
160
+ function resolveEventType(typeText) {
161
+ if (IDENTIFIER_RE.test(typeText) && !GLOBAL_EVENT_TYPES.has(typeText)) {
162
+ return { text: typeText, importName: typeText };
163
+ }
164
+ return { text: typeText, importName: undefined };
165
+ }
166
+
167
+ function docComment(description, indent) {
168
+ if (!description) return [];
169
+ const oneLine = description.replace(/\s+/g, " ").trim();
170
+ return [`${indent}/** ${oneLine} */`];
171
+ }
172
+
173
+ function renderAngularWrapper(component, warnings) {
174
+ const { tag, className, packageName, props, events, formControl } = component;
175
+ const angularClass = classToAngularClass(className);
176
+ const accessor = formControl ? ACCESSOR_BY_KIND[formControl.kind] : undefined;
177
+
178
+ const usableProps = props.filter((prop) => !HOST_MANAGED_PROPS.has(prop.name));
179
+
180
+ const coreImports = new Set(["ChangeDetectionStrategy", "Component", "ElementRef", "inject"]);
181
+ if (usableProps.length > 0) coreImports.add("Input");
182
+ if (events.length > 0) {
183
+ coreImports.add("HostListener");
184
+ coreImports.add("output");
185
+ }
186
+ if (accessor) coreImports.add("forwardRef");
187
+
188
+ const typeImports = new Set();
189
+ const bodyLines = [];
190
+
191
+ const overrideModifier = accessor ? "override " : "";
192
+ bodyLines.push(
193
+ ` protected ${overrideModifier}readonly el: ElementRef<${className}> = inject(ElementRef);`
194
+ );
195
+ if (accessor) {
196
+ bodyLines.push(
197
+ ` protected override readonly changeEvent = "${formControl.changeEvent}";`
198
+ );
199
+ if (formControl.inputEvent) {
200
+ bodyLines.push(
201
+ ` protected override readonly inputEvent = "${formControl.inputEvent}";`
202
+ );
203
+ }
204
+ }
205
+
206
+ for (const prop of usableProps) {
207
+ let type = prop.type;
208
+ const hasComplexType = type && !isPrimitiveType(type);
209
+ if (type === "unknown" || hasComplexType) {
210
+ // Fall back to the element's own property type so the wrapper still
211
+ // compiles and never widens to `any`. Complex types (like enums or
212
+ // interfaces not in the manifest) fallback to the element's property type.
213
+ const fallbackType = `${className}["${prop.name}"]`;
214
+ if (type === "unknown" || !canImportType(type)) {
215
+ type = fallbackType;
216
+ warnings.push(
217
+ `${tag}: property "${prop.name}" type cannot be imported; using ${type}`
218
+ );
219
+ } else {
220
+ // Extract and import custom type names
221
+ const customTypes = extractCustomTypes(type);
222
+ for (const customType of customTypes) {
223
+ if (canImportType(customType)) {
224
+ typeImports.add(customType);
225
+ }
226
+ }
227
+ }
228
+ }
229
+ let decorator = "@Input()";
230
+ if (prop.isBoolean) {
231
+ coreImports.add("booleanAttribute");
232
+ decorator = "@Input({ transform: booleanAttribute })";
233
+ } else if (prop.type === "number") {
234
+ coreImports.add("numberAttribute");
235
+ decorator = "@Input({ transform: numberAttribute })";
236
+ }
237
+ bodyLines.push(``);
238
+ bodyLines.push(...docComment(prop.description, " "));
239
+
240
+ // Generate defensive setter logic for Signal Forms compatibility.
241
+ // pattern: ignore empty arrays (formField metadata), null, undefined.
242
+ // min/max: normalize undefined to null for (number | undefined) values.
243
+ let setterBody = `this.el.nativeElement.${prop.name} = v;`;
244
+ if (prop.name === "pattern") {
245
+ setterBody = `if (v && v !== "" && (!Array.isArray(v) || v.length > 0)) { this.el.nativeElement.${prop.name} = v; }`;
246
+ } else if ((prop.name === "min" || prop.name === "max" || prop.name === "maxlength" || prop.name === "minlength") && type.includes("null")) {
247
+ // Normalize undefined to null for nullable numeric props
248
+ setterBody = `this.el.nativeElement.${prop.name} = v ?? null;`;
249
+ }
250
+
251
+ bodyLines.push(
252
+ ` ${decorator} set ${prop.name}(v: ${type}) { ${setterBody} }`
253
+ );
254
+ bodyLines.push(
255
+ ` get ${prop.name}(): ${type} { return this.el.nativeElement.${prop.name}; }`
256
+ );
257
+ }
258
+
259
+ if (events.length > 0) bodyLines.push(``);
260
+ const listenerLines = [];
261
+ const seenOutputs = new Set();
262
+ for (const event of events) {
263
+ // Two event names can map to one Angular output (a deprecated camelCase
264
+ // alias like `nys-fileRemove` next to `nys-file-remove`). Keep the
265
+ // first, which is the canonical name in manifest order.
266
+ if (seenOutputs.has(event.angularOutput)) continue;
267
+ seenOutputs.add(event.angularOutput);
268
+
269
+ const { text, importName } = resolveEventType(event.typeText);
270
+ if (importName) typeImports.add(importName);
271
+ bodyLines.push(...docComment(event.description, " "));
272
+ bodyLines.push(` readonly ${event.angularOutput} = output<${text}>();`);
273
+
274
+ // Cast to typed event for internal use, but type parameter as Event for
275
+ // typeCheckHostBindings compatibility (Angular 21+ strict mode).
276
+ const cast = text !== "Event" ? `(e as ${text})` : "e";
277
+ const extras = [`this.${event.angularOutput}.emit(${cast});`];
278
+ if (accessor) {
279
+ if (event.name === formControl.changeEvent) {
280
+ // Files accessor reads from element, doesn't use event detail
281
+ const handleArgs = formControl.kind === "files" ? "" : cast;
282
+ extras.push(`this.handleChange(${handleArgs});`);
283
+ } else if (event.name === formControl.inputEvent) {
284
+ // Files accessor reads from element, doesn't use event detail
285
+ const handleArgs = formControl.kind === "files" ? "" : cast;
286
+ extras.push(`this.handleInput(${handleArgs});`);
287
+ } else if (event.name === NYS_BLUR_EVENT) {
288
+ extras.push(`this.handleBlur();`);
289
+ }
290
+ }
291
+ listenerLines.push(
292
+ ` @HostListener("${event.name}", ["$event"]) protected _on${pascalize(event.name)}(e: Event) { ${extras.join(" ")} }`
293
+ );
294
+ }
295
+ if (listenerLines.length > 0) {
296
+ bodyLines.push(``);
297
+ bodyLines.push(...listenerLines);
298
+ }
299
+
300
+ const lines = [];
301
+ lines.push(
302
+ `// GENERATED by @nysds/codegen — do not edit. Override: src/overrides/${tag}.component.ts`
303
+ );
304
+ lines.push(
305
+ `import { ${[...coreImports].sort().join(", ")} } from "@angular/core";`
306
+ );
307
+ if (accessor) {
308
+ lines.push(`import { NG_VALUE_ACCESSOR } from "@angular/forms";`);
309
+ }
310
+ lines.push(`import "${packageName}";`);
311
+ const allTypeImports = [className, ...[...typeImports].sort()];
312
+ lines.push(`import type { ${allTypeImports.join(", ")} } from "${packageName}";`);
313
+ if (accessor) {
314
+ lines.push(`import { ${accessor.className} } from "${accessor.file}";`);
315
+ }
316
+ lines.push(``);
317
+ lines.push(`@Component({`);
318
+ lines.push(` selector: "${tag}",`);
319
+ lines.push(` standalone: true,`);
320
+ lines.push(` template: "<ng-content></ng-content>",`);
321
+ lines.push(` changeDetection: ChangeDetectionStrategy.OnPush,`);
322
+ if (accessor) {
323
+ lines.push(` providers: [`);
324
+ lines.push(
325
+ ` { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ${angularClass}), multi: true },`
326
+ );
327
+ lines.push(` ],`);
328
+ }
329
+ lines.push(`})`);
330
+
331
+ let extendsClause = "";
332
+ if (accessor) {
333
+ const generic =
334
+ formControl.kind === "value"
335
+ ? `<${props.find((prop) => prop.name === "value")?.type ?? "string"}>`
336
+ : "";
337
+ extendsClause = ` extends ${accessor.className}${generic}`;
338
+ }
339
+ lines.push(`export class ${angularClass}${extendsClause} {`);
340
+ lines.push(...bodyLines);
341
+ lines.push(`}`);
342
+ lines.push(``);
343
+ return lines.join("\n");
344
+ }
345
+
346
+ /** The NgModule that re-exports every wrapper, in either import mode. */
347
+ function renderNgModule(moduleImports, moduleClasses) {
348
+ return [
349
+ `// GENERATED by @nysds/codegen — do not edit.`,
350
+ `import { NgModule } from "@angular/core";`,
351
+ ...moduleImports,
352
+ ``,
353
+ `const NYS_COMPONENTS = [`,
354
+ ...moduleClasses.map((name) => ` ${name},`),
355
+ `];`,
356
+ ``,
357
+ `@NgModule({`,
358
+ ` imports: NYS_COMPONENTS,`,
359
+ ` exports: NYS_COMPONENTS,`,
360
+ `})`,
361
+ `export class NysAngularModule {}`,
362
+ ``,
363
+ ].join("\n");
364
+ }
365
+
366
+ function writeJson(filePath, value) {
367
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
368
+ fs.writeFileSync(filePath, JSON.stringify(value, null, 2) + "\n");
369
+ }
370
+
371
+ /** Path from `fromDir` to `to`, always written with forward slashes. */
372
+ function relativeSpecifier(fromDir, to) {
373
+ return path.relative(fromDir, to).split(path.sep).join("/");
374
+ }
375
+
376
+ /**
377
+ * Writes the throwaway ng-packagr project that turns each wrapper into a real
378
+ * secondary entry point.
379
+ *
380
+ * ng-packagr finds secondary entry points by globbing for nested `ng-package.json`
381
+ * files and derives both the import subpath and the output directory from the folder's
382
+ * path relative to the project root. Putting that project in a gitignored
383
+ * staging directory keeps one config folder per component out of `src/` and out
384
+ * of the package root while still producing `dist/<subpath>/` and
385
+ * `dist/fesm2022/nysds-angular-<subpath>.mjs`.
386
+ *
387
+ * Each `entryFile` points straight at the real source rather than at a re-export
388
+ * shim: ng-packagr sets `rootDir` to the directory of the entry file, so a shim
389
+ * would leave the actual component outside `rootDir` and push its emit out of
390
+ * the entry point's own output folder.
391
+ */
392
+ function writeNgBuildTree(components, context) {
393
+ const { ngbuildDir, outDir, overridesDir, packageJsonPath } = context;
394
+ const packageDir = path.dirname(packageJsonPath);
395
+ const srcDir = path.dirname(outDir);
396
+
397
+ fs.rmSync(ngbuildDir, { recursive: true, force: true });
398
+ fs.mkdirSync(ngbuildDir, { recursive: true });
399
+
400
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
401
+
402
+ // Deliberately minimal. ng-packagr seeds the `exports` map it writes into
403
+ // dist/package.json from this file, so carrying over `main`, `module`,
404
+ // `types` or `exports` from the real manifest is what produced the doubled
405
+ // `./dist/` prefixes and the "conflicting export condition" warning. With no
406
+ // `dependencies` here, `allowedNonPeerDependencies` has nothing to check and
407
+ // no longer has to mirror the component list by hand.
408
+ writeJson(path.join(ngbuildDir, "package.json"), {
409
+ name: pkg.name,
410
+ version: pkg.version,
411
+ peerDependencies: pkg.peerDependencies,
412
+ });
413
+
414
+ writeJson(path.join(ngbuildDir, "ng-package.json"), {
415
+ $schema: relativeSpecifier(
416
+ ngbuildDir,
417
+ "node_modules/ng-packagr/ng-package.schema.json"
418
+ ),
419
+ lib: { entryFile: "public-api.ts" },
420
+ dest: relativeSpecifier(ngbuildDir, path.join(packageDir, "dist")),
421
+ allowedNonPeerDependencies: [],
422
+ });
423
+
424
+ const publicApi = [
425
+ `// GENERATED by @nysds/codegen — do not edit.`,
426
+ `export * from "${FORMS_ENTRY_POINT}";`,
427
+ ];
428
+ const moduleImports = [];
429
+ const moduleClasses = [];
430
+
431
+ const entryPoints = [
432
+ { dir: "forms", source: path.join(srcDir, "lib/forms/index.ts") },
433
+ ];
434
+
435
+ for (const component of components) {
436
+ const fileBase = `${component.tag}.component`;
437
+ const overridePath = path.join(overridesDir, `${fileBase}.ts`);
438
+ const sourceDir = fs.existsSync(overridePath) ? overridesDir : outDir;
439
+ entryPoints.push({
440
+ dir: component.subpath,
441
+ source: path.join(sourceDir, `${fileBase}.ts`),
442
+ });
443
+
444
+ const angularClass = classToAngularClass(component.className);
445
+ moduleClasses.push(angularClass);
446
+ moduleImports.push(
447
+ `import { ${angularClass} } from "${ANGULAR_PACKAGE}/${component.subpath}";`
448
+ );
449
+ publicApi.push(`export * from "${ANGULAR_PACKAGE}/${component.subpath}";`);
450
+ }
451
+
452
+ for (const entryPoint of entryPoints) {
453
+ const entryPointDir = path.join(ngbuildDir, entryPoint.dir);
454
+ writeJson(path.join(entryPointDir, "ng-package.json"), {
455
+ lib: { entryFile: relativeSpecifier(entryPointDir, entryPoint.source) },
456
+ });
457
+ }
458
+
459
+ publicApi.push(`export { NysAngularModule } from "./nys-angular.module";`);
460
+ fs.writeFileSync(
461
+ path.join(ngbuildDir, "public-api.ts"),
462
+ publicApi.join("\n") + "\n"
463
+ );
464
+ fs.writeFileSync(
465
+ path.join(ngbuildDir, "nys-angular.module.ts"),
466
+ renderNgModule(moduleImports, moduleClasses)
467
+ );
468
+
469
+ // ng-packagr copies README.md out of the project root, and refuses assets
470
+ // from outside it. The published tarball carries the real README from the
471
+ // package root, so this only keeps dist/ self-describing.
472
+ fs.writeFileSync(
473
+ path.join(ngbuildDir, "README.md"),
474
+ fs.readFileSync(path.join(packageDir, "README.md"))
475
+ );
476
+ }
477
+
478
+ export function angularPlugin(options = {}) {
479
+ const {
480
+ outDir = "packages/angular/src/generated",
481
+ overridesDir = "packages/angular/src/overrides",
482
+ ngbuildDir = "packages/angular/.ngbuild",
483
+ packageJsonPath = "packages/angular/package.json",
484
+ } = options;
485
+
486
+ return {
487
+ name: "nysds-angular-wrappers",
488
+
489
+ packageLinkPhase({ customElementsManifest }) {
490
+ const components = listComponents(customElementsManifest);
491
+ if (components.length === 0) return;
492
+
493
+ const reserved = components.find(
494
+ (component) => component.subpath === "forms"
495
+ );
496
+ if (reserved) {
497
+ throw new Error(
498
+ `[nysds-angular-wrappers] <${reserved.tag}> would claim the "forms" subpath, which is reserved for the shared form-control accessors.`
499
+ );
500
+ }
501
+
502
+ fs.rmSync(outDir, { recursive: true, force: true });
503
+ fs.mkdirSync(outDir, { recursive: true });
504
+
505
+ const warnings = [];
506
+ const barrel = [`// GENERATED by @nysds/codegen — do not edit.`];
507
+ const moduleImports = [];
508
+ const moduleClasses = [];
509
+
510
+ for (const component of components) {
511
+ const fileBase = `${component.tag}.component`;
512
+ const angularClass = classToAngularClass(component.className);
513
+ moduleClasses.push(angularClass);
514
+
515
+ const overridePath = path.join(overridesDir, `${fileBase}.ts`);
516
+ if (fs.existsSync(overridePath)) {
517
+ barrel.push(`export * from "../overrides/${fileBase}";`);
518
+ moduleImports.push(
519
+ `import { ${angularClass} } from "../overrides/${fileBase}";`
520
+ );
521
+ continue;
522
+ }
523
+ fs.writeFileSync(
524
+ path.join(outDir, `${fileBase}.ts`),
525
+ renderAngularWrapper(component, warnings)
526
+ );
527
+ barrel.push(`export * from "./${fileBase}";`);
528
+ moduleImports.push(`import { ${angularClass} } from "./${fileBase}";`);
529
+ }
530
+
531
+ fs.writeFileSync(
532
+ path.join(outDir, "nys-angular.module.ts"),
533
+ renderNgModule(moduleImports, moduleClasses)
534
+ );
535
+
536
+ barrel.push(`export { NysAngularModule } from "./nys-angular.module";`);
537
+ fs.writeFileSync(path.join(outDir, "index.ts"), barrel.join("\n") + "\n");
538
+
539
+ writeNgBuildTree(components, {
540
+ ngbuildDir,
541
+ outDir,
542
+ overridesDir,
543
+ packageJsonPath,
544
+ });
545
+
546
+ for (const warning of warnings) {
547
+ console.warn(`[nysds-angular-wrappers] ${warning}`);
548
+ }
549
+ },
550
+ };
551
+ }