@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.
- package/README.md +95 -2
- package/cem-plugins/angular.mjs +551 -0
- package/cem-plugins/deps.mjs +154 -0
- package/cem-plugins/form-control.mjs +94 -0
- package/cem-plugins/index.mjs +12 -0
- package/cem-plugins/lib/core.d.mts +79 -0
- package/cem-plugins/lib/core.mjs +288 -0
- package/cem-plugins/react.mjs +143 -0
- package/cem-plugins/vue.mjs +228 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/manifest/components.d.ts +6 -0
- package/dist/manifest/components.d.ts.map +1 -0
- package/dist/manifest/components.js +5 -0
- package/dist/manifest/filters.d.ts +6 -0
- package/dist/manifest/filters.d.ts.map +1 -0
- package/dist/manifest/filters.js +5 -0
- package/dist/manifest/form-control.d.ts +6 -0
- package/dist/manifest/form-control.d.ts.map +1 -0
- package/dist/manifest/form-control.js +5 -0
- package/dist/manifest/load.d.ts +11 -0
- package/dist/manifest/load.d.ts.map +1 -0
- package/dist/manifest/load.js +64 -0
- package/dist/manifest/naming.d.ts +7 -0
- package/dist/manifest/naming.d.ts.map +1 -0
- package/dist/manifest/naming.js +6 -0
- package/dist/manifest/packages.d.ts +6 -0
- package/dist/manifest/packages.d.ts.map +1 -0
- package/dist/manifest/packages.js +5 -0
- package/dist/manifest/types.d.ts +7 -0
- package/dist/manifest/types.d.ts.map +1 -0
- package/dist/manifest/types.js +1 -0
- package/dist/transform/index.d.ts +20 -0
- package/dist/transform/index.d.ts.map +1 -0
- package/dist/transform/index.js +11 -0
- package/dist/transform/parse.d.ts +19 -0
- package/dist/transform/parse.d.ts.map +1 -0
- package/dist/transform/parse.js +33 -0
- package/dist/transform/shared.d.ts +62 -0
- package/dist/transform/shared.d.ts.map +1 -0
- package/dist/transform/shared.js +96 -0
- package/dist/transform/to-angular.d.ts +7 -0
- package/dist/transform/to-angular.d.ts.map +1 -0
- package/dist/transform/to-angular.js +153 -0
- package/dist/transform/to-jsx.d.ts +7 -0
- package/dist/transform/to-jsx.d.ts.map +1 -0
- package/dist/transform/to-jsx.js +172 -0
- package/package.json +46 -4
package/README.md
CHANGED
|
@@ -1,3 +1,96 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @nysds/codegen
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
}
|