@stencil/angular-output-target 1.4.1 → 1.5.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 +89 -0
- package/angular-component-lib/boolean-attribute.ts +43 -0
- package/dist/generate-angular-component.d.ts +7 -1
- package/dist/generate-angular-component.js +19 -5
- package/dist/generate-transformtag-script.d.ts +3 -1
- package/dist/generate-transformtag-script.js +6 -2
- package/dist/index.cjs +129 -58
- package/dist/index.js +129 -58
- package/dist/output-angular.d.ts +6 -0
- package/dist/output-angular.js +102 -51
- package/dist/plugin.d.ts +7 -2
- package/dist/types.d.ts +41 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +3 -1
- package/dist/wizard.js +14 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -57,3 +57,92 @@ export const config: Config = {
|
|
|
57
57
|
| `outputType` | Specifies the type of output to be generated. It can take one of the following values: <br />1. `component`: Generates all the component wrappers to be declared on an Angular module. This option is required for Stencil projects using the `dist` hydrated output.<br /> 2. `scam`: Generates a separate Angular module for each component.<br /> 3. `standalone`: Generates standalone component wrappers.<br /> Both `scam` and `standalone` options are compatible with the `dist-custom-elements` output. <br />Note: Please choose the appropriate `outputType` based on your project's requirements and the desired output structure. Defaults to `component`. |
|
|
58
58
|
| `customElementsDir` | This is the directory where the custom elements are imported from when using the [Custom Elements Bundle](https://stenciljs.com/docs/custom-elements). Defaults to the `components` directory. Only applies for `outputType: "scam"` or `outputType: "standalone"`. |
|
|
59
59
|
| `inlineProperties` | Experimental. When true, tries to inline the properties of components. This is required to enable Angular Language Service to type-check and show jsdocs when using the components in html-templates. |
|
|
60
|
+
| `booleanAttributes` | When `true`, boolean properties are declared with an Angular input transform so they can be set by attribute presence, e.g. `<my-component disabled>` instead of `<my-component [disabled]="true">`. Type-checking the attribute form also requires `inlineProperties`. Defaults to `false`. Refer to [Boolean attributes](#boolean-attributes). |
|
|
61
|
+
|
|
62
|
+
## Boolean attributes
|
|
63
|
+
|
|
64
|
+
Angular resolves a bare attribute to the empty string, so a boolean property has to be bound
|
|
65
|
+
explicitly by default:
|
|
66
|
+
|
|
67
|
+
```html
|
|
68
|
+
<!-- Type 'string' is not assignable to type 'boolean' under strictTemplates -->
|
|
69
|
+
<my-component disabled></my-component>
|
|
70
|
+
|
|
71
|
+
<my-component [disabled]="true"></my-component>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Setting `booleanAttributes: true` declares boolean properties with an Angular input transform,
|
|
75
|
+
which accepts the attribute form and coerces it:
|
|
76
|
+
|
|
77
|
+
```html
|
|
78
|
+
<my-component disabled></my-component>
|
|
79
|
+
<my-component disabled="false"></my-component>
|
|
80
|
+
<my-component [disabled]="isDisabled"></my-component>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The transform used is `nullableBooleanAttribute` rather than Angular's `booleanAttribute`. It
|
|
84
|
+
coerces strings identically, but passes `null` and `undefined` through instead of coercing them to
|
|
85
|
+
`false`. Components frequently treat those as a state distinct from `false`:
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
// `undefined` means "decide based on the mode", which is not the same as `false`
|
|
89
|
+
const showDetail = detail !== undefined ? detail : mode === 'ios';
|
|
90
|
+
|
|
91
|
+
// a strict comparison also behaves differently for `null` than it does for `false`
|
|
92
|
+
const showHandle = handle !== false;
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Both values reach inputs routinely, from the `async` pipe before its first emission and from form
|
|
96
|
+
control values, so coercing them would change the behavior of bindings that work today.
|
|
97
|
+
|
|
98
|
+
### Type-checking requires `inlineProperties`
|
|
99
|
+
|
|
100
|
+
This option changes what the Angular compiler accepts, so it only has a visible effect where the
|
|
101
|
+
compiler type-checks the wrapper's inputs in the first place. That needs `inlineProperties`
|
|
102
|
+
enabled as well:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
angularOutputTarget({
|
|
106
|
+
// ...
|
|
107
|
+
booleanAttributes: true,
|
|
108
|
+
inlineProperties: true,
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Without `inlineProperties` the generated wrappers declare no typed members for their inputs, so
|
|
113
|
+
Angular does not check these bindings at all and there is no `TS2322` for the transform to
|
|
114
|
+
resolve.
|
|
115
|
+
|
|
116
|
+
The transform itself still runs either way. Angular applies it from the component definition's
|
|
117
|
+
input map, which does not depend on a declared class member, so enabling `booleanAttributes` on
|
|
118
|
+
its own moves boolean coercion from Stencil to the transform without the compiler checking what
|
|
119
|
+
you pass. The two agree on strings, booleans, `null` and `undefined`. If you bind a value outside
|
|
120
|
+
those, prefer enabling `inlineProperties` too so that `ngAcceptInputType_*` rejects it at compile
|
|
121
|
+
time rather than silently coercing it.
|
|
122
|
+
|
|
123
|
+
Note that `inlineProperties` is itself experimental, so enabling both is opting into that. With
|
|
124
|
+
`booleanAttributes` on its own you still get the attribute syntax at runtime, just without the
|
|
125
|
+
compiler checking it.
|
|
126
|
+
|
|
127
|
+
### Limitations
|
|
128
|
+
|
|
129
|
+
A property is only transformed when Stencil reports its type as exactly `boolean`. Three kinds of
|
|
130
|
+
property fall outside that, and they behave differently from each other. In all three the property
|
|
131
|
+
receives the attribute's empty string rather than `true`, so bind them explicitly when you need a
|
|
132
|
+
boolean.
|
|
133
|
+
|
|
134
|
+
**Virtual properties** are not transformed and get no inlined class member either, so Angular has
|
|
135
|
+
nothing to check the binding against and skips it. A bare attribute compiles, and the property
|
|
136
|
+
receives `''`. Nothing warns you.
|
|
137
|
+
|
|
138
|
+
**Properties declared as `any`** are not transformed, but they do get an inlined member typed
|
|
139
|
+
`any`, so `''` is assignable. A bare attribute compiles, and the property receives `''`. Nothing
|
|
140
|
+
warns you here either.
|
|
141
|
+
|
|
142
|
+
**Properties whose type unions `boolean` with something else**, such as `boolean | 'auto'`, are
|
|
143
|
+
reported to the output target as `any`, so they get no transform, but their inlined member keeps
|
|
144
|
+
the real union type. This is the only case the compiler catches:
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
TS2322: Type '""' is not assignable to type 'boolean | "auto"'
|
|
148
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
/* tslint:disable */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Transforms a value to a boolean so that boolean properties can be set by attribute presence,
|
|
6
|
+
* e.g. `<my-component disabled>` instead of `<my-component [disabled]="true">`.
|
|
7
|
+
*
|
|
8
|
+
* Strings are coerced the same way Angular's `booleanAttribute` coerces them, so `''` (a bare
|
|
9
|
+
* attribute) becomes `true` and `'false'` becomes `false`.
|
|
10
|
+
*
|
|
11
|
+
* Unlike Angular's `booleanAttribute`, `null` and `undefined` are passed through rather than
|
|
12
|
+
* coerced to `false`. Components frequently treat them as a state distinct from `false`:
|
|
13
|
+
*
|
|
14
|
+
* ```tsx
|
|
15
|
+
* // `undefined` means "decide based on the mode", which is not the same as `false`
|
|
16
|
+
* const showDetail = detail !== undefined ? detail : mode === 'ios';
|
|
17
|
+
*
|
|
18
|
+
* // a strict comparison also behaves differently for `null` than it does for `false`
|
|
19
|
+
* const showHandle = handle !== false;
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Both values reach inputs routinely in Angular templates, from the `async` pipe before its
|
|
23
|
+
* first emission and from form control values, so coercing them would change the behavior of
|
|
24
|
+
* bindings that work today.
|
|
25
|
+
*
|
|
26
|
+
* This is implemented here rather than imported from `@angular/core` so that consumers on
|
|
27
|
+
* Angular versions without `booleanAttribute` are unaffected by this file being generated.
|
|
28
|
+
*
|
|
29
|
+
* The parameter type is what Angular derives `ngAcceptInputType_*` from, so it decides which
|
|
30
|
+
* template bindings compile. Widening it to `unknown` would let any expression through.
|
|
31
|
+
*
|
|
32
|
+
* Declared as a function rather than an arrow constant because Angular has to resolve input
|
|
33
|
+
* transforms statically when compiling a library in partial compilation mode.
|
|
34
|
+
*
|
|
35
|
+
* This lives in its own file, separate from `utils.ts`, so that it carries no runtime imports.
|
|
36
|
+
* That keeps it independently type-checkable without pulling `rxjs` in for `proxyOutputs`.
|
|
37
|
+
*/
|
|
38
|
+
export function nullableBooleanAttribute(value: boolean | string | null | undefined): boolean | null | undefined {
|
|
39
|
+
if (value === null || value === undefined) {
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
return typeof value === 'boolean' ? value : value !== 'false';
|
|
43
|
+
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { ComponentCompilerEvent, ComponentCompilerProperty } from '@stencil/core/internal';
|
|
2
2
|
import type { ComponentInputProperty, OutputType } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* The transform function to reference in generated code, exported from
|
|
5
|
+
* `angular-component-lib/boolean-attribute`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const INPUT_TRANSFORM_FUNCTION = "nullableBooleanAttribute";
|
|
3
8
|
/**
|
|
4
9
|
* Creates an Angular component declaration from formatted Stencil compiler metadata.
|
|
5
10
|
*
|
|
@@ -20,6 +25,7 @@ export declare const createAngularComponentDefinition: (tagName: string, inputs:
|
|
|
20
25
|
* @param events The events to generate the interface properties for.
|
|
21
26
|
* @param componentCorePackage The component core package.
|
|
22
27
|
* @param customElementsDir The custom elements directory.
|
|
28
|
+
* @param exportMaps Import through the package's `exports` map instead of a deep path.
|
|
23
29
|
* @returns The component interface type definition as a string.
|
|
24
30
|
*/
|
|
25
|
-
export declare const createComponentTypeDefinition: (outputType: OutputType, tagNameAsPascal: string, events: readonly ComponentCompilerEvent[], componentCorePackage: string, customElementsDir?: string) => string;
|
|
31
|
+
export declare const createComponentTypeDefinition: (outputType: OutputType, tagNameAsPascal: string, events: readonly ComponentCompilerEvent[], componentCorePackage: string, customElementsDir?: string, exportMaps?: boolean) => string;
|
|
@@ -25,21 +25,33 @@ function createPropertyDeclaration(prop, type, inlinePropertyAsSetter = false) {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
28
|
+
* The transform function to reference in generated code, exported from
|
|
29
|
+
* `angular-component-lib/boolean-attribute`.
|
|
30
|
+
*/
|
|
31
|
+
export const INPUT_TRANSFORM_FUNCTION = 'nullableBooleanAttribute';
|
|
32
|
+
/**
|
|
33
|
+
* Creates a formatted inputs text with required and transform declarations.
|
|
34
|
+
*
|
|
35
|
+
* Inputs are emitted as bare strings unless they carry options, in which case Angular's object
|
|
36
|
+
* form is used (e.g. `{ name: 'myInput', required: true }`).
|
|
29
37
|
*
|
|
30
|
-
* @param prop A ComponentCompilerEvent or ComponentCompilerProperty to turn into a property declaration.
|
|
31
38
|
* @param inputs The inputs of the Stencil component (e.g. [{name: 'myInput', required: true]).
|
|
32
39
|
* @returns The inputs list declaration as a string.
|
|
33
40
|
*/
|
|
34
41
|
function formatInputs(inputs) {
|
|
35
42
|
return inputs
|
|
36
43
|
.map((item) => {
|
|
44
|
+
const options = [];
|
|
37
45
|
if (item.required) {
|
|
38
|
-
|
|
46
|
+
options.push('required: true');
|
|
47
|
+
}
|
|
48
|
+
if (item.transform) {
|
|
49
|
+
options.push(`transform: ${INPUT_TRANSFORM_FUNCTION}`);
|
|
39
50
|
}
|
|
40
|
-
|
|
51
|
+
if (options.length === 0) {
|
|
41
52
|
return `'${item.name}'`;
|
|
42
53
|
}
|
|
54
|
+
return `{ name: '${item.name}', ${options.join(', ')} }`;
|
|
43
55
|
})
|
|
44
56
|
.join(', ');
|
|
45
57
|
}
|
|
@@ -200,14 +212,16 @@ const createDocComment = (doc) => {
|
|
|
200
212
|
* @param events The events to generate the interface properties for.
|
|
201
213
|
* @param componentCorePackage The component core package.
|
|
202
214
|
* @param customElementsDir The custom elements directory.
|
|
215
|
+
* @param exportMaps Import through the package's `exports` map instead of a deep path.
|
|
203
216
|
* @returns The component interface type definition as a string.
|
|
204
217
|
*/
|
|
205
|
-
export const createComponentTypeDefinition = (outputType, tagNameAsPascal, events, componentCorePackage, customElementsDir) => {
|
|
218
|
+
export const createComponentTypeDefinition = (outputType, tagNameAsPascal, events, componentCorePackage, customElementsDir, exportMaps = false) => {
|
|
206
219
|
const publicEvents = events.filter((ev) => !ev.internal);
|
|
207
220
|
const eventTypeImports = createComponentEventTypeImports(tagNameAsPascal, publicEvents, {
|
|
208
221
|
componentCorePackage,
|
|
209
222
|
customElementsDir,
|
|
210
223
|
outputType,
|
|
224
|
+
exportMaps,
|
|
211
225
|
});
|
|
212
226
|
const eventTypes = publicEvents.map((event) => createPropertyDeclaration(event, `EventEmitter<${tagNameAsPascal}CustomEvent<${formatOutputType(tagNameAsPascal, event)}>>`));
|
|
213
227
|
const interfaceDeclaration = `export declare interface ${tagNameAsPascal} extends Components.${tagNameAsPascal} {`;
|
|
@@ -5,4 +5,6 @@ import type { OutputTargetAngular } from './types';
|
|
|
5
5
|
* This script patches component selectors in the built Angular library to use the
|
|
6
6
|
* transformed tag names (e.g., 'my-component' -> 'v1-my-component').
|
|
7
7
|
*/
|
|
8
|
-
export declare function generateTransformTagScript(compilerCtx: CompilerCtx, components: ComponentCompilerMeta[], outputTarget: OutputTargetAngular,
|
|
8
|
+
export declare function generateTransformTagScript(compilerCtx: CompilerCtx, components: ComponentCompilerMeta[], outputTarget: OutputTargetAngular,
|
|
9
|
+
/** Import through the package's `exports` map (`<pkg>/standalone`) instead of a deep path. */
|
|
10
|
+
exportMaps?: boolean): Promise<void>;
|
|
@@ -5,10 +5,14 @@ import { dashToPascalCase } from './utils';
|
|
|
5
5
|
* This script patches component selectors in the built Angular library to use the
|
|
6
6
|
* transformed tag names (e.g., 'my-component' -> 'v1-my-component').
|
|
7
7
|
*/
|
|
8
|
-
export async function generateTransformTagScript(compilerCtx, components, outputTarget,
|
|
8
|
+
export async function generateTransformTagScript(compilerCtx, components, outputTarget,
|
|
9
|
+
/** Import through the package's `exports` map (`<pkg>/standalone`) instead of a deep path. */
|
|
10
|
+
exportMaps = false) {
|
|
9
11
|
const scriptsDirectory = path.join(path.dirname(outputTarget.directivesProxyFile), '../../scripts');
|
|
10
12
|
const customElementsDir = outputTarget.customElementsDir || 'dist/components';
|
|
11
|
-
const stencilImportPath =
|
|
13
|
+
const stencilImportPath = exportMaps
|
|
14
|
+
? `${outputTarget.componentCorePackage}/standalone`
|
|
15
|
+
: `${outputTarget.componentCorePackage}/${customElementsDir}/index.js`;
|
|
12
16
|
// Generate the mappings object
|
|
13
17
|
const mappings = components
|
|
14
18
|
.map((component) => {
|
package/dist/index.cjs
CHANGED
|
@@ -126,7 +126,9 @@ const createComponentEventTypeImports = (componentTagName, events, options) => {
|
|
|
126
126
|
const imports = [];
|
|
127
127
|
const namedImports = new Set();
|
|
128
128
|
const isCustomElementsBuild = isOutputTypeCustomElementsBuild(options.outputType);
|
|
129
|
-
const
|
|
129
|
+
const exportMapEntry = isCustomElementsBuild ? '/standalone' : '/loader';
|
|
130
|
+
const importPathName = normalizePath(componentCorePackage) +
|
|
131
|
+
(options.exportMaps ? exportMapEntry : isCustomElementsBuild ? `/${customElementsDir}` : '');
|
|
130
132
|
/**
|
|
131
133
|
* Each component's events are typed with the per-component CustomEvent type
|
|
132
134
|
* generated by Stencil (e.g. `MyComponentCustomEvent<Detail>`), which carries a
|
|
@@ -179,21 +181,33 @@ function createPropertyDeclaration(prop, type, inlinePropertyAsSetter = false) {
|
|
|
179
181
|
}
|
|
180
182
|
}
|
|
181
183
|
/**
|
|
182
|
-
*
|
|
184
|
+
* The transform function to reference in generated code, exported from
|
|
185
|
+
* `angular-component-lib/boolean-attribute`.
|
|
186
|
+
*/
|
|
187
|
+
const INPUT_TRANSFORM_FUNCTION = 'nullableBooleanAttribute';
|
|
188
|
+
/**
|
|
189
|
+
* Creates a formatted inputs text with required and transform declarations.
|
|
190
|
+
*
|
|
191
|
+
* Inputs are emitted as bare strings unless they carry options, in which case Angular's object
|
|
192
|
+
* form is used (e.g. `{ name: 'myInput', required: true }`).
|
|
183
193
|
*
|
|
184
|
-
* @param prop A ComponentCompilerEvent or ComponentCompilerProperty to turn into a property declaration.
|
|
185
194
|
* @param inputs The inputs of the Stencil component (e.g. [{name: 'myInput', required: true]).
|
|
186
195
|
* @returns The inputs list declaration as a string.
|
|
187
196
|
*/
|
|
188
197
|
function formatInputs(inputs) {
|
|
189
198
|
return inputs
|
|
190
199
|
.map((item) => {
|
|
200
|
+
const options = [];
|
|
191
201
|
if (item.required) {
|
|
192
|
-
|
|
202
|
+
options.push('required: true');
|
|
203
|
+
}
|
|
204
|
+
if (item.transform) {
|
|
205
|
+
options.push(`transform: ${INPUT_TRANSFORM_FUNCTION}`);
|
|
193
206
|
}
|
|
194
|
-
|
|
207
|
+
if (options.length === 0) {
|
|
195
208
|
return `'${item.name}'`;
|
|
196
209
|
}
|
|
210
|
+
return `{ name: '${item.name}', ${options.join(', ')} }`;
|
|
197
211
|
})
|
|
198
212
|
.join(', ');
|
|
199
213
|
}
|
|
@@ -354,14 +368,16 @@ const createDocComment = (doc) => {
|
|
|
354
368
|
* @param events The events to generate the interface properties for.
|
|
355
369
|
* @param componentCorePackage The component core package.
|
|
356
370
|
* @param customElementsDir The custom elements directory.
|
|
371
|
+
* @param exportMaps Import through the package's `exports` map instead of a deep path.
|
|
357
372
|
* @returns The component interface type definition as a string.
|
|
358
373
|
*/
|
|
359
|
-
const createComponentTypeDefinition = (outputType, tagNameAsPascal, events, componentCorePackage, customElementsDir) => {
|
|
374
|
+
const createComponentTypeDefinition = (outputType, tagNameAsPascal, events, componentCorePackage, customElementsDir, exportMaps = false) => {
|
|
360
375
|
const publicEvents = events.filter((ev) => !ev.internal);
|
|
361
376
|
const eventTypeImports = createComponentEventTypeImports(tagNameAsPascal, publicEvents, {
|
|
362
377
|
componentCorePackage,
|
|
363
378
|
customElementsDir,
|
|
364
379
|
outputType,
|
|
380
|
+
exportMaps,
|
|
365
381
|
});
|
|
366
382
|
const eventTypes = publicEvents.map((event) => createPropertyDeclaration(event, `EventEmitter<${tagNameAsPascal}CustomEvent<${formatOutputType(tagNameAsPascal, event)}>>`));
|
|
367
383
|
const interfaceDeclaration = `export declare interface ${tagNameAsPascal} extends Components.${tagNameAsPascal} {`;
|
|
@@ -479,10 +495,14 @@ export class ${moduleClassName} { }`;
|
|
|
479
495
|
* This script patches component selectors in the built Angular library to use the
|
|
480
496
|
* transformed tag names (e.g., 'my-component' -> 'v1-my-component').
|
|
481
497
|
*/
|
|
482
|
-
async function generateTransformTagScript(compilerCtx, components, outputTarget,
|
|
498
|
+
async function generateTransformTagScript(compilerCtx, components, outputTarget,
|
|
499
|
+
/** Import through the package's `exports` map (`<pkg>/standalone`) instead of a deep path. */
|
|
500
|
+
exportMaps = false) {
|
|
483
501
|
const scriptsDirectory = path__default["default"].join(path__default["default"].dirname(outputTarget.directivesProxyFile), '../../scripts');
|
|
484
502
|
const customElementsDir = outputTarget.customElementsDir || 'dist/components';
|
|
485
|
-
const stencilImportPath =
|
|
503
|
+
const stencilImportPath = exportMaps
|
|
504
|
+
? `${outputTarget.componentCorePackage}/standalone`
|
|
505
|
+
: `${outputTarget.componentCorePackage}/${customElementsDir}/index.js`;
|
|
486
506
|
// Generate the mappings object
|
|
487
507
|
const mappings = components
|
|
488
508
|
.map((component) => {
|
|
@@ -782,6 +802,73 @@ stencilSetTagTransformer(\${transformerArg});
|
|
|
782
802
|
|
|
783
803
|
const __filename$1 = url.fileURLToPath((typeof document === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __filename).href : (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT' && document.currentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
784
804
|
const __dirname$1 = path__default["default"].dirname(__filename$1);
|
|
805
|
+
const filterInternalProps = (prop) => !prop.internal;
|
|
806
|
+
/**
|
|
807
|
+
* Whether a property declares an input transform.
|
|
808
|
+
*
|
|
809
|
+
* This is the single definition of the rule. Both the generated declaration and the import that
|
|
810
|
+
* satisfies it are derived from it, so widening the rule can't leave a generated file referencing
|
|
811
|
+
* a transform it never imports.
|
|
812
|
+
*
|
|
813
|
+
* @param prop The property compiler metadata.
|
|
814
|
+
* @param booleanAttributes Whether boolean properties should declare an input transform.
|
|
815
|
+
* @returns `true` when the property should be transformed.
|
|
816
|
+
*/
|
|
817
|
+
const isTransformedProp = (prop, booleanAttributes) => booleanAttributes && prop.type === 'boolean';
|
|
818
|
+
/**
|
|
819
|
+
* Maps a Stencil property to an Angular input declaration.
|
|
820
|
+
*
|
|
821
|
+
* Virtual properties are mapped with `booleanAttributes` left at its default of `false`. They
|
|
822
|
+
* carry a free-form type string and no `required` flag, so they are never transformed and always
|
|
823
|
+
* come out as optional.
|
|
824
|
+
*
|
|
825
|
+
* @param prop The property compiler metadata.
|
|
826
|
+
* @param booleanAttributes Whether boolean properties should declare an input transform.
|
|
827
|
+
* @returns The Angular input declaration.
|
|
828
|
+
*/
|
|
829
|
+
const mapInputProp = (prop, booleanAttributes = false) => ({
|
|
830
|
+
name: prop.name,
|
|
831
|
+
required: prop.required ?? false,
|
|
832
|
+
transform: isTransformedProp(prop, booleanAttributes) ? true : undefined,
|
|
833
|
+
});
|
|
834
|
+
/**
|
|
835
|
+
* Whether any of a component's properties will declare an input transform, which determines
|
|
836
|
+
* whether the transform needs to be imported.
|
|
837
|
+
*
|
|
838
|
+
* @param components The components in the generated file.
|
|
839
|
+
* @param booleanAttributes Whether boolean properties should declare an input transform.
|
|
840
|
+
* @returns `true` when at least one property is transformed.
|
|
841
|
+
*/
|
|
842
|
+
const usesInputTransform = (components, booleanAttributes) => components.some((cmpMeta) => (cmpMeta.properties ?? []).filter(filterInternalProps).some((prop) => isTransformedProp(prop, booleanAttributes)));
|
|
843
|
+
/**
|
|
844
|
+
* Whether the Stencil project generates a package.json `exports` map. Deep `dist/` imports are
|
|
845
|
+
* blocked for consumers then, so the proxies import through the map's entries instead
|
|
846
|
+
* (`<pkg>/<tag>`, `<pkg>/loader`, `<pkg>/standalone`).
|
|
847
|
+
*/
|
|
848
|
+
function usesExportMaps(config) {
|
|
849
|
+
// Stencil v5 only: v4's map has no `./standalone` entry, and its root types aren't
|
|
850
|
+
// guaranteed to export the component types.
|
|
851
|
+
const isV5 = (config.outputTargets || []).some((o) => ['loader-bundle', 'standalone', 'ssr', 'types'].includes(o.type));
|
|
852
|
+
return isV5 && config.generateExportMaps === true;
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* The module specifier a proxy imports a component's `defineCustomElement` from.
|
|
856
|
+
*/
|
|
857
|
+
function getComponentModule(config, outputTarget, tagName) {
|
|
858
|
+
const basePkg = normalizePath(outputTarget.componentCorePackage);
|
|
859
|
+
if (usesExportMaps(config)) {
|
|
860
|
+
return `${basePkg}/${tagName}`;
|
|
861
|
+
}
|
|
862
|
+
return `${basePkg}/${outputTarget.customElementsDir}/${tagName}.js`;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* The relative import of the project's own `components.d.ts`, for when there's no
|
|
866
|
+
* `componentCorePackage`. Only resolved when needed: package.json may have no `types` field.
|
|
867
|
+
*/
|
|
868
|
+
function getComponentsTypeFile(pkgData, outputTarget, rootDir) {
|
|
869
|
+
const dtsFilePath = path__default["default"].join(rootDir, path__default["default"].dirname(pkgData.types), GENERATED_DTS);
|
|
870
|
+
return relativeImport(outputTarget.directivesProxyFile, dtsFilePath, '.d.ts');
|
|
871
|
+
}
|
|
785
872
|
async function angularDirectiveProxyOutput(compilerCtx, outputTarget, components, config) {
|
|
786
873
|
const filteredComponents = getFilteredComponents(outputTarget.excludeComponents, components);
|
|
787
874
|
const rootDir = config.rootDir;
|
|
@@ -815,27 +902,7 @@ async function angularDirectiveProxyOutput(compilerCtx, outputTarget, components
|
|
|
815
902
|
}
|
|
816
903
|
// Generate transformer script if transformTag is enabled
|
|
817
904
|
if (outputTarget.transformTag) {
|
|
818
|
-
|
|
819
|
-
// directivesProxyFile is like: projects/library/src/directives/proxies.ts
|
|
820
|
-
// We need to go up to: projects/library/package.json
|
|
821
|
-
const angularLibraryDir = path__default["default"].dirname(path__default["default"].dirname(path__default["default"].dirname(outputTarget.directivesProxyFile)));
|
|
822
|
-
const angularPkgJsonPath = path__default["default"].join(angularLibraryDir, 'package.json');
|
|
823
|
-
let angularPackageName = '';
|
|
824
|
-
try {
|
|
825
|
-
const angularPkgJson = JSON.parse(await compilerCtx.fs.readFile(angularPkgJsonPath));
|
|
826
|
-
if (angularPkgJson.name) {
|
|
827
|
-
angularPackageName = angularPkgJson.name;
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
catch (e) {
|
|
831
|
-
throw new Error(`Could not read Angular library package.json at ${angularPkgJsonPath}. ` +
|
|
832
|
-
`The package name is required to generate the transformTag patch script.`);
|
|
833
|
-
}
|
|
834
|
-
if (!angularPackageName) {
|
|
835
|
-
throw new Error(`Angular library package.json at ${angularPkgJsonPath} does not have a "name" field. ` +
|
|
836
|
-
`The package name is required to generate the transformTag patch script.`);
|
|
837
|
-
}
|
|
838
|
-
tasks.push(generateTransformTagScript(compilerCtx, filteredComponents, outputTarget));
|
|
905
|
+
tasks.push(generateTransformTagScript(compilerCtx, filteredComponents, outputTarget, usesExportMaps(config)));
|
|
839
906
|
}
|
|
840
907
|
await Promise.all(tasks);
|
|
841
908
|
}
|
|
@@ -859,10 +926,7 @@ async function copyResources(config, outputTarget) {
|
|
|
859
926
|
], srcDirectory);
|
|
860
927
|
}
|
|
861
928
|
function generateProxies(components, pkgData, outputTarget, rootDir, config) {
|
|
862
|
-
const distTypesDir = path__default["default"].dirname(pkgData.types);
|
|
863
|
-
const dtsFilePath = path__default["default"].join(rootDir, distTypesDir, GENERATED_DTS);
|
|
864
929
|
const { outputType } = outputTarget;
|
|
865
|
-
const componentsTypeFile = relativeImport(outputTarget.directivesProxyFile, dtsFilePath, '.d.ts');
|
|
866
930
|
const includeSingleComponentAngularModules = outputType === OutputTypes.Scam;
|
|
867
931
|
const isCustomElementsBuild = isOutputTypeCustomElementsBuild(outputType);
|
|
868
932
|
const isStandaloneBuild = outputType === OutputTypes.Standalone;
|
|
@@ -879,6 +943,14 @@ function generateProxies(components, pkgData, outputTarget, rootDir, config) {
|
|
|
879
943
|
* The collection of named imports from the angular-component-lib/utils.
|
|
880
944
|
*/
|
|
881
945
|
const componentLibImports = ['ProxyCmp'];
|
|
946
|
+
/**
|
|
947
|
+
* The input transform lives in its own module so that it carries no runtime imports.
|
|
948
|
+
*/
|
|
949
|
+
let transformImport = '';
|
|
950
|
+
const booleanAttributes = outputTarget.booleanAttributes === true;
|
|
951
|
+
if (usesInputTransform(components, booleanAttributes)) {
|
|
952
|
+
transformImport = `\n${createImportStatement([INPUT_TRANSFORM_FUNCTION], './angular-component-lib/boolean-attribute')}`;
|
|
953
|
+
}
|
|
882
954
|
if (includeSingleComponentAngularModules) {
|
|
883
955
|
angularCoreImports.push('NgModule');
|
|
884
956
|
}
|
|
@@ -886,7 +958,7 @@ function generateProxies(components, pkgData, outputTarget, rootDir, config) {
|
|
|
886
958
|
/* auto-generated angular directive proxies */
|
|
887
959
|
${createImportStatement(angularCoreImports, '@angular/core')}
|
|
888
960
|
|
|
889
|
-
${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n`;
|
|
961
|
+
${createImportStatement(componentLibImports, './angular-component-lib/utils')}${transformImport}\n`;
|
|
890
962
|
/**
|
|
891
963
|
* Generate JSX import type from correct location.
|
|
892
964
|
* When using custom elements build, we need to import from
|
|
@@ -896,7 +968,7 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n
|
|
|
896
968
|
const generateTypeImports = () => {
|
|
897
969
|
const importLocation = outputTarget.componentCorePackage
|
|
898
970
|
? getPathToComponentTypes(config, outputTarget)
|
|
899
|
-
: normalizePath(
|
|
971
|
+
: normalizePath(getComponentsTypeFile(pkgData, outputTarget, rootDir));
|
|
900
972
|
return `import ${isCustomElementsBuild ? 'type ' : ''}{ ${IMPORT_TYPES} } from '${importLocation}';\n`;
|
|
901
973
|
};
|
|
902
974
|
const typeImports = generateTypeImports();
|
|
@@ -910,17 +982,11 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n
|
|
|
910
982
|
if (isCustomElementsBuild && outputTarget.componentCorePackage !== undefined) {
|
|
911
983
|
const cmpImports = components.map((component) => {
|
|
912
984
|
const pascalImport = dashToPascalCase(component.tagName);
|
|
913
|
-
return `import { defineCustomElement as define${pascalImport} } from '${
|
|
985
|
+
return `import { defineCustomElement as define${pascalImport} } from '${getComponentModule(config, outputTarget, component.tagName)}';`;
|
|
914
986
|
});
|
|
915
987
|
sourceImports = cmpImports.join('\n');
|
|
916
988
|
}
|
|
917
989
|
const proxyFileOutput = [];
|
|
918
|
-
const filterInternalProps = (prop) => !prop.internal;
|
|
919
|
-
// Ensure that virtual properties has required as false.
|
|
920
|
-
const mapInputProp = (prop) => ({
|
|
921
|
-
name: prop.name,
|
|
922
|
-
required: prop.required ?? false,
|
|
923
|
-
});
|
|
924
990
|
const { componentCorePackage, customElementsDir } = outputTarget;
|
|
925
991
|
for (let cmpMeta of components) {
|
|
926
992
|
const tagNameAsPascal = dashToPascalCase(cmpMeta.tagName);
|
|
@@ -928,9 +994,9 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n
|
|
|
928
994
|
if (cmpMeta.properties) {
|
|
929
995
|
internalProps.push(...cmpMeta.properties.filter(filterInternalProps));
|
|
930
996
|
}
|
|
931
|
-
const inputs = internalProps.map(mapInputProp);
|
|
997
|
+
const inputs = internalProps.map((prop) => mapInputProp(prop, booleanAttributes));
|
|
932
998
|
if (cmpMeta.virtualProperties) {
|
|
933
|
-
inputs.push(...cmpMeta.virtualProperties.map(mapInputProp));
|
|
999
|
+
inputs.push(...cmpMeta.virtualProperties.map((prop) => mapInputProp(prop)));
|
|
934
1000
|
}
|
|
935
1001
|
const orderedInputs = sortBy(inputs, (cip) => cip.name);
|
|
936
1002
|
const methods = [];
|
|
@@ -946,7 +1012,7 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n
|
|
|
946
1012
|
*/
|
|
947
1013
|
const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, orderedInputs, methods, isCustomElementsBuild, isStandaloneBuild, inlineComponentProps, cmpMeta.events || []);
|
|
948
1014
|
const moduleDefinition = generateAngularModuleForComponent(cmpMeta.tagName);
|
|
949
|
-
const componentTypeDefinition = createComponentTypeDefinition(outputType, tagNameAsPascal, cmpMeta.events, componentCorePackage, customElementsDir);
|
|
1015
|
+
const componentTypeDefinition = createComponentTypeDefinition(outputType, tagNameAsPascal, cmpMeta.events, componentCorePackage, customElementsDir, usesExportMaps(config));
|
|
950
1016
|
proxyFileOutput.push(componentDefinition, '\n');
|
|
951
1017
|
if (includeSingleComponentAngularModules) {
|
|
952
1018
|
proxyFileOutput.push(moduleDefinition, '\n');
|
|
@@ -961,9 +1027,6 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n
|
|
|
961
1027
|
*/
|
|
962
1028
|
function generateComponentProxy(cmpMeta, pkgData, outputTarget, rootDir, config) {
|
|
963
1029
|
const { outputType, componentCorePackage, customElementsDir } = outputTarget;
|
|
964
|
-
const distTypesDir = path__default["default"].dirname(pkgData.types);
|
|
965
|
-
const dtsFilePath = path__default["default"].join(rootDir, distTypesDir, GENERATED_DTS);
|
|
966
|
-
const componentsTypeFile = relativeImport(outputTarget.directivesProxyFile, dtsFilePath, '.d.ts');
|
|
967
1030
|
const includeSingleComponentAngularModules = outputType === OutputTypes.Scam;
|
|
968
1031
|
const isCustomElementsBuild = isOutputTypeCustomElementsBuild(outputType);
|
|
969
1032
|
const isStandaloneBuild = outputType === OutputTypes.Standalone;
|
|
@@ -977,34 +1040,36 @@ function generateComponentProxy(cmpMeta, pkgData, outputTarget, rootDir, config)
|
|
|
977
1040
|
if (includeSingleComponentAngularModules) {
|
|
978
1041
|
angularCoreImports.push('NgModule');
|
|
979
1042
|
}
|
|
1043
|
+
const booleanAttributes = outputTarget.booleanAttributes === true;
|
|
1044
|
+
const componentLibImports = ['ProxyCmp'];
|
|
1045
|
+
// The input transform lives in its own module so that it carries no runtime imports.
|
|
1046
|
+
let transformImport = '';
|
|
1047
|
+
if (usesInputTransform([cmpMeta], booleanAttributes)) {
|
|
1048
|
+
transformImport = `\n${createImportStatement([INPUT_TRANSFORM_FUNCTION], './angular-component-lib/boolean-attribute')}`;
|
|
1049
|
+
}
|
|
980
1050
|
const imports = `/* tslint:disable */
|
|
981
1051
|
/* auto-generated angular directive proxies */
|
|
982
1052
|
${createImportStatement(angularCoreImports, '@angular/core')}
|
|
983
1053
|
|
|
984
|
-
${createImportStatement(
|
|
1054
|
+
${createImportStatement(componentLibImports, './angular-component-lib/utils')}${transformImport}\n`;
|
|
985
1055
|
// Type imports
|
|
986
1056
|
const importLocation = componentCorePackage
|
|
987
1057
|
? getPathToComponentTypes(config, outputTarget)
|
|
988
|
-
: normalizePath(
|
|
1058
|
+
: normalizePath(getComponentsTypeFile(pkgData, outputTarget, rootDir));
|
|
989
1059
|
const typeImports = `import ${isCustomElementsBuild ? 'type ' : ''}{ ${IMPORT_TYPES} } from '${importLocation}';\n`;
|
|
990
1060
|
// defineCustomElement import
|
|
991
1061
|
let sourceImport = '';
|
|
992
1062
|
if (isCustomElementsBuild && componentCorePackage !== undefined) {
|
|
993
|
-
sourceImport = `import { defineCustomElement as define${tagNameAsPascal} } from '${
|
|
1063
|
+
sourceImport = `import { defineCustomElement as define${tagNameAsPascal} } from '${getComponentModule(config, outputTarget, cmpMeta.tagName)}';\n`;
|
|
994
1064
|
}
|
|
995
1065
|
// Generate component definition
|
|
996
|
-
const filterInternalProps = (prop) => !prop.internal;
|
|
997
|
-
const mapInputProp = (prop) => ({
|
|
998
|
-
name: prop.name,
|
|
999
|
-
required: prop.required ?? false,
|
|
1000
|
-
});
|
|
1001
1066
|
const internalProps = [];
|
|
1002
1067
|
if (cmpMeta.properties) {
|
|
1003
1068
|
internalProps.push(...cmpMeta.properties.filter(filterInternalProps));
|
|
1004
1069
|
}
|
|
1005
|
-
const inputs = internalProps.map(mapInputProp);
|
|
1070
|
+
const inputs = internalProps.map((prop) => mapInputProp(prop, booleanAttributes));
|
|
1006
1071
|
if (cmpMeta.virtualProperties) {
|
|
1007
|
-
inputs.push(...cmpMeta.virtualProperties.map(mapInputProp));
|
|
1072
|
+
inputs.push(...cmpMeta.virtualProperties.map((prop) => mapInputProp(prop)));
|
|
1008
1073
|
}
|
|
1009
1074
|
const orderedInputs = sortBy(inputs, (cip) => cip.name);
|
|
1010
1075
|
const methods = [];
|
|
@@ -1014,7 +1079,7 @@ ${createImportStatement(['ProxyCmp'], './angular-component-lib/utils')}\n`;
|
|
|
1014
1079
|
const inlineComponentProps = outputTarget.inlineProperties ? internalProps : [];
|
|
1015
1080
|
const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, orderedInputs, methods, isCustomElementsBuild, isStandaloneBuild, inlineComponentProps, cmpMeta.events || []);
|
|
1016
1081
|
const moduleDefinition = generateAngularModuleForComponent(cmpMeta.tagName);
|
|
1017
|
-
const componentTypeDefinition = createComponentTypeDefinition(outputType, tagNameAsPascal, cmpMeta.events, componentCorePackage, customElementsDir);
|
|
1082
|
+
const componentTypeDefinition = createComponentTypeDefinition(outputType, tagNameAsPascal, cmpMeta.events, componentCorePackage, customElementsDir, usesExportMaps(config));
|
|
1018
1083
|
const proxyFileOutput = [componentDefinition, '\n'];
|
|
1019
1084
|
if (includeSingleComponentAngularModules) {
|
|
1020
1085
|
proxyFileOutput.push(moduleDefinition, '\n');
|
|
@@ -1045,6 +1110,12 @@ function generateBarrelFile(components, outputTarget) {
|
|
|
1045
1110
|
}
|
|
1046
1111
|
function getPathToComponentTypes(config, outputTarget) {
|
|
1047
1112
|
const basePkg = outputTarget.componentCorePackage || '';
|
|
1113
|
+
// The entry of the output the proxies are built on always exports the component types - the
|
|
1114
|
+
// package root only does when the project has no `src/index.ts`, or that file re-exports them
|
|
1115
|
+
if (usesExportMaps(config)) {
|
|
1116
|
+
const entry = isOutputTypeCustomElementsBuild(outputTarget.outputType) ? 'standalone' : 'loader';
|
|
1117
|
+
return `${normalizePath(basePkg)}/${entry}`;
|
|
1118
|
+
}
|
|
1048
1119
|
// in v5, all types (including components.d.ts) are generated in the dist/types directory
|
|
1049
1120
|
const typesTarget = config.outputTargets?.find((o) => o.type === 'types');
|
|
1050
1121
|
if (typesTarget) {
|