@stencil/angular-output-target 1.4.0 → 1.5.0
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 +5 -0
- package/dist/generate-angular-component.js +16 -4
- package/dist/index.cjs +75 -21
- package/dist/index.js +75 -21
- package/dist/output-angular.js +60 -18
- package/dist/types.d.ts +41 -0
- package/dist/wizard.d.ts +1 -1
- package/dist/wizard.js +32 -67
- package/package.json +10 -12
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
|
*
|
|
@@ -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
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -179,21 +179,33 @@ function createPropertyDeclaration(prop, type, inlinePropertyAsSetter = false) {
|
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
/**
|
|
182
|
-
*
|
|
182
|
+
* The transform function to reference in generated code, exported from
|
|
183
|
+
* `angular-component-lib/boolean-attribute`.
|
|
184
|
+
*/
|
|
185
|
+
const INPUT_TRANSFORM_FUNCTION = 'nullableBooleanAttribute';
|
|
186
|
+
/**
|
|
187
|
+
* Creates a formatted inputs text with required and transform declarations.
|
|
188
|
+
*
|
|
189
|
+
* Inputs are emitted as bare strings unless they carry options, in which case Angular's object
|
|
190
|
+
* form is used (e.g. `{ name: 'myInput', required: true }`).
|
|
183
191
|
*
|
|
184
|
-
* @param prop A ComponentCompilerEvent or ComponentCompilerProperty to turn into a property declaration.
|
|
185
192
|
* @param inputs The inputs of the Stencil component (e.g. [{name: 'myInput', required: true]).
|
|
186
193
|
* @returns The inputs list declaration as a string.
|
|
187
194
|
*/
|
|
188
195
|
function formatInputs(inputs) {
|
|
189
196
|
return inputs
|
|
190
197
|
.map((item) => {
|
|
198
|
+
const options = [];
|
|
191
199
|
if (item.required) {
|
|
192
|
-
|
|
200
|
+
options.push('required: true');
|
|
201
|
+
}
|
|
202
|
+
if (item.transform) {
|
|
203
|
+
options.push(`transform: ${INPUT_TRANSFORM_FUNCTION}`);
|
|
193
204
|
}
|
|
194
|
-
|
|
205
|
+
if (options.length === 0) {
|
|
195
206
|
return `'${item.name}'`;
|
|
196
207
|
}
|
|
208
|
+
return `{ name: '${item.name}', ${options.join(', ')} }`;
|
|
197
209
|
})
|
|
198
210
|
.join(', ');
|
|
199
211
|
}
|
|
@@ -782,6 +794,44 @@ stencilSetTagTransformer(\${transformerArg});
|
|
|
782
794
|
|
|
783
795
|
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
796
|
const __dirname$1 = path__default["default"].dirname(__filename$1);
|
|
797
|
+
const filterInternalProps = (prop) => !prop.internal;
|
|
798
|
+
/**
|
|
799
|
+
* Whether a property declares an input transform.
|
|
800
|
+
*
|
|
801
|
+
* This is the single definition of the rule. Both the generated declaration and the import that
|
|
802
|
+
* satisfies it are derived from it, so widening the rule can't leave a generated file referencing
|
|
803
|
+
* a transform it never imports.
|
|
804
|
+
*
|
|
805
|
+
* @param prop The property compiler metadata.
|
|
806
|
+
* @param booleanAttributes Whether boolean properties should declare an input transform.
|
|
807
|
+
* @returns `true` when the property should be transformed.
|
|
808
|
+
*/
|
|
809
|
+
const isTransformedProp = (prop, booleanAttributes) => booleanAttributes && prop.type === 'boolean';
|
|
810
|
+
/**
|
|
811
|
+
* Maps a Stencil property to an Angular input declaration.
|
|
812
|
+
*
|
|
813
|
+
* Virtual properties are mapped with `booleanAttributes` left at its default of `false`. They
|
|
814
|
+
* carry a free-form type string and no `required` flag, so they are never transformed and always
|
|
815
|
+
* come out as optional.
|
|
816
|
+
*
|
|
817
|
+
* @param prop The property compiler metadata.
|
|
818
|
+
* @param booleanAttributes Whether boolean properties should declare an input transform.
|
|
819
|
+
* @returns The Angular input declaration.
|
|
820
|
+
*/
|
|
821
|
+
const mapInputProp = (prop, booleanAttributes = false) => ({
|
|
822
|
+
name: prop.name,
|
|
823
|
+
required: prop.required ?? false,
|
|
824
|
+
transform: isTransformedProp(prop, booleanAttributes) ? true : undefined,
|
|
825
|
+
});
|
|
826
|
+
/**
|
|
827
|
+
* Whether any of a component's properties will declare an input transform, which determines
|
|
828
|
+
* whether the transform needs to be imported.
|
|
829
|
+
*
|
|
830
|
+
* @param components The components in the generated file.
|
|
831
|
+
* @param booleanAttributes Whether boolean properties should declare an input transform.
|
|
832
|
+
* @returns `true` when at least one property is transformed.
|
|
833
|
+
*/
|
|
834
|
+
const usesInputTransform = (components, booleanAttributes) => components.some((cmpMeta) => (cmpMeta.properties ?? []).filter(filterInternalProps).some((prop) => isTransformedProp(prop, booleanAttributes)));
|
|
785
835
|
async function angularDirectiveProxyOutput(compilerCtx, outputTarget, components, config) {
|
|
786
836
|
const filteredComponents = getFilteredComponents(outputTarget.excludeComponents, components);
|
|
787
837
|
const rootDir = config.rootDir;
|
|
@@ -879,6 +929,14 @@ function generateProxies(components, pkgData, outputTarget, rootDir, config) {
|
|
|
879
929
|
* The collection of named imports from the angular-component-lib/utils.
|
|
880
930
|
*/
|
|
881
931
|
const componentLibImports = ['ProxyCmp'];
|
|
932
|
+
/**
|
|
933
|
+
* The input transform lives in its own module so that it carries no runtime imports.
|
|
934
|
+
*/
|
|
935
|
+
let transformImport = '';
|
|
936
|
+
const booleanAttributes = outputTarget.booleanAttributes === true;
|
|
937
|
+
if (usesInputTransform(components, booleanAttributes)) {
|
|
938
|
+
transformImport = `\n${createImportStatement([INPUT_TRANSFORM_FUNCTION], './angular-component-lib/boolean-attribute')}`;
|
|
939
|
+
}
|
|
882
940
|
if (includeSingleComponentAngularModules) {
|
|
883
941
|
angularCoreImports.push('NgModule');
|
|
884
942
|
}
|
|
@@ -886,7 +944,7 @@ function generateProxies(components, pkgData, outputTarget, rootDir, config) {
|
|
|
886
944
|
/* auto-generated angular directive proxies */
|
|
887
945
|
${createImportStatement(angularCoreImports, '@angular/core')}
|
|
888
946
|
|
|
889
|
-
${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n`;
|
|
947
|
+
${createImportStatement(componentLibImports, './angular-component-lib/utils')}${transformImport}\n`;
|
|
890
948
|
/**
|
|
891
949
|
* Generate JSX import type from correct location.
|
|
892
950
|
* When using custom elements build, we need to import from
|
|
@@ -915,12 +973,6 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n
|
|
|
915
973
|
sourceImports = cmpImports.join('\n');
|
|
916
974
|
}
|
|
917
975
|
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
976
|
const { componentCorePackage, customElementsDir } = outputTarget;
|
|
925
977
|
for (let cmpMeta of components) {
|
|
926
978
|
const tagNameAsPascal = dashToPascalCase(cmpMeta.tagName);
|
|
@@ -928,9 +980,9 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n
|
|
|
928
980
|
if (cmpMeta.properties) {
|
|
929
981
|
internalProps.push(...cmpMeta.properties.filter(filterInternalProps));
|
|
930
982
|
}
|
|
931
|
-
const inputs = internalProps.map(mapInputProp);
|
|
983
|
+
const inputs = internalProps.map((prop) => mapInputProp(prop, booleanAttributes));
|
|
932
984
|
if (cmpMeta.virtualProperties) {
|
|
933
|
-
inputs.push(...cmpMeta.virtualProperties.map(mapInputProp));
|
|
985
|
+
inputs.push(...cmpMeta.virtualProperties.map((prop) => mapInputProp(prop)));
|
|
934
986
|
}
|
|
935
987
|
const orderedInputs = sortBy(inputs, (cip) => cip.name);
|
|
936
988
|
const methods = [];
|
|
@@ -977,11 +1029,18 @@ function generateComponentProxy(cmpMeta, pkgData, outputTarget, rootDir, config)
|
|
|
977
1029
|
if (includeSingleComponentAngularModules) {
|
|
978
1030
|
angularCoreImports.push('NgModule');
|
|
979
1031
|
}
|
|
1032
|
+
const booleanAttributes = outputTarget.booleanAttributes === true;
|
|
1033
|
+
const componentLibImports = ['ProxyCmp'];
|
|
1034
|
+
// The input transform lives in its own module so that it carries no runtime imports.
|
|
1035
|
+
let transformImport = '';
|
|
1036
|
+
if (usesInputTransform([cmpMeta], booleanAttributes)) {
|
|
1037
|
+
transformImport = `\n${createImportStatement([INPUT_TRANSFORM_FUNCTION], './angular-component-lib/boolean-attribute')}`;
|
|
1038
|
+
}
|
|
980
1039
|
const imports = `/* tslint:disable */
|
|
981
1040
|
/* auto-generated angular directive proxies */
|
|
982
1041
|
${createImportStatement(angularCoreImports, '@angular/core')}
|
|
983
1042
|
|
|
984
|
-
${createImportStatement(
|
|
1043
|
+
${createImportStatement(componentLibImports, './angular-component-lib/utils')}${transformImport}\n`;
|
|
985
1044
|
// Type imports
|
|
986
1045
|
const importLocation = componentCorePackage
|
|
987
1046
|
? getPathToComponentTypes(config, outputTarget)
|
|
@@ -993,18 +1052,13 @@ ${createImportStatement(['ProxyCmp'], './angular-component-lib/utils')}\n`;
|
|
|
993
1052
|
sourceImport = `import { defineCustomElement as define${tagNameAsPascal} } from '${normalizePath(componentCorePackage)}/${customElementsDir}/${cmpMeta.tagName}.js';\n`;
|
|
994
1053
|
}
|
|
995
1054
|
// Generate component definition
|
|
996
|
-
const filterInternalProps = (prop) => !prop.internal;
|
|
997
|
-
const mapInputProp = (prop) => ({
|
|
998
|
-
name: prop.name,
|
|
999
|
-
required: prop.required ?? false,
|
|
1000
|
-
});
|
|
1001
1055
|
const internalProps = [];
|
|
1002
1056
|
if (cmpMeta.properties) {
|
|
1003
1057
|
internalProps.push(...cmpMeta.properties.filter(filterInternalProps));
|
|
1004
1058
|
}
|
|
1005
|
-
const inputs = internalProps.map(mapInputProp);
|
|
1059
|
+
const inputs = internalProps.map((prop) => mapInputProp(prop, booleanAttributes));
|
|
1006
1060
|
if (cmpMeta.virtualProperties) {
|
|
1007
|
-
inputs.push(...cmpMeta.virtualProperties.map(mapInputProp));
|
|
1061
|
+
inputs.push(...cmpMeta.virtualProperties.map((prop) => mapInputProp(prop)));
|
|
1008
1062
|
}
|
|
1009
1063
|
const orderedInputs = sortBy(inputs, (cip) => cip.name);
|
|
1010
1064
|
const methods = [];
|
package/dist/index.js
CHANGED
|
@@ -171,21 +171,33 @@ function createPropertyDeclaration(prop, type, inlinePropertyAsSetter = false) {
|
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
/**
|
|
174
|
-
*
|
|
174
|
+
* The transform function to reference in generated code, exported from
|
|
175
|
+
* `angular-component-lib/boolean-attribute`.
|
|
176
|
+
*/
|
|
177
|
+
const INPUT_TRANSFORM_FUNCTION = 'nullableBooleanAttribute';
|
|
178
|
+
/**
|
|
179
|
+
* Creates a formatted inputs text with required and transform declarations.
|
|
180
|
+
*
|
|
181
|
+
* Inputs are emitted as bare strings unless they carry options, in which case Angular's object
|
|
182
|
+
* form is used (e.g. `{ name: 'myInput', required: true }`).
|
|
175
183
|
*
|
|
176
|
-
* @param prop A ComponentCompilerEvent or ComponentCompilerProperty to turn into a property declaration.
|
|
177
184
|
* @param inputs The inputs of the Stencil component (e.g. [{name: 'myInput', required: true]).
|
|
178
185
|
* @returns The inputs list declaration as a string.
|
|
179
186
|
*/
|
|
180
187
|
function formatInputs(inputs) {
|
|
181
188
|
return inputs
|
|
182
189
|
.map((item) => {
|
|
190
|
+
const options = [];
|
|
183
191
|
if (item.required) {
|
|
184
|
-
|
|
192
|
+
options.push('required: true');
|
|
193
|
+
}
|
|
194
|
+
if (item.transform) {
|
|
195
|
+
options.push(`transform: ${INPUT_TRANSFORM_FUNCTION}`);
|
|
185
196
|
}
|
|
186
|
-
|
|
197
|
+
if (options.length === 0) {
|
|
187
198
|
return `'${item.name}'`;
|
|
188
199
|
}
|
|
200
|
+
return `{ name: '${item.name}', ${options.join(', ')} }`;
|
|
189
201
|
})
|
|
190
202
|
.join(', ');
|
|
191
203
|
}
|
|
@@ -774,6 +786,44 @@ stencilSetTagTransformer(\${transformerArg});
|
|
|
774
786
|
|
|
775
787
|
const __filename = fileURLToPath(import.meta.url);
|
|
776
788
|
const __dirname = path.dirname(__filename);
|
|
789
|
+
const filterInternalProps = (prop) => !prop.internal;
|
|
790
|
+
/**
|
|
791
|
+
* Whether a property declares an input transform.
|
|
792
|
+
*
|
|
793
|
+
* This is the single definition of the rule. Both the generated declaration and the import that
|
|
794
|
+
* satisfies it are derived from it, so widening the rule can't leave a generated file referencing
|
|
795
|
+
* a transform it never imports.
|
|
796
|
+
*
|
|
797
|
+
* @param prop The property compiler metadata.
|
|
798
|
+
* @param booleanAttributes Whether boolean properties should declare an input transform.
|
|
799
|
+
* @returns `true` when the property should be transformed.
|
|
800
|
+
*/
|
|
801
|
+
const isTransformedProp = (prop, booleanAttributes) => booleanAttributes && prop.type === 'boolean';
|
|
802
|
+
/**
|
|
803
|
+
* Maps a Stencil property to an Angular input declaration.
|
|
804
|
+
*
|
|
805
|
+
* Virtual properties are mapped with `booleanAttributes` left at its default of `false`. They
|
|
806
|
+
* carry a free-form type string and no `required` flag, so they are never transformed and always
|
|
807
|
+
* come out as optional.
|
|
808
|
+
*
|
|
809
|
+
* @param prop The property compiler metadata.
|
|
810
|
+
* @param booleanAttributes Whether boolean properties should declare an input transform.
|
|
811
|
+
* @returns The Angular input declaration.
|
|
812
|
+
*/
|
|
813
|
+
const mapInputProp = (prop, booleanAttributes = false) => ({
|
|
814
|
+
name: prop.name,
|
|
815
|
+
required: prop.required ?? false,
|
|
816
|
+
transform: isTransformedProp(prop, booleanAttributes) ? true : undefined,
|
|
817
|
+
});
|
|
818
|
+
/**
|
|
819
|
+
* Whether any of a component's properties will declare an input transform, which determines
|
|
820
|
+
* whether the transform needs to be imported.
|
|
821
|
+
*
|
|
822
|
+
* @param components The components in the generated file.
|
|
823
|
+
* @param booleanAttributes Whether boolean properties should declare an input transform.
|
|
824
|
+
* @returns `true` when at least one property is transformed.
|
|
825
|
+
*/
|
|
826
|
+
const usesInputTransform = (components, booleanAttributes) => components.some((cmpMeta) => (cmpMeta.properties ?? []).filter(filterInternalProps).some((prop) => isTransformedProp(prop, booleanAttributes)));
|
|
777
827
|
async function angularDirectiveProxyOutput(compilerCtx, outputTarget, components, config) {
|
|
778
828
|
const filteredComponents = getFilteredComponents(outputTarget.excludeComponents, components);
|
|
779
829
|
const rootDir = config.rootDir;
|
|
@@ -871,6 +921,14 @@ function generateProxies(components, pkgData, outputTarget, rootDir, config) {
|
|
|
871
921
|
* The collection of named imports from the angular-component-lib/utils.
|
|
872
922
|
*/
|
|
873
923
|
const componentLibImports = ['ProxyCmp'];
|
|
924
|
+
/**
|
|
925
|
+
* The input transform lives in its own module so that it carries no runtime imports.
|
|
926
|
+
*/
|
|
927
|
+
let transformImport = '';
|
|
928
|
+
const booleanAttributes = outputTarget.booleanAttributes === true;
|
|
929
|
+
if (usesInputTransform(components, booleanAttributes)) {
|
|
930
|
+
transformImport = `\n${createImportStatement([INPUT_TRANSFORM_FUNCTION], './angular-component-lib/boolean-attribute')}`;
|
|
931
|
+
}
|
|
874
932
|
if (includeSingleComponentAngularModules) {
|
|
875
933
|
angularCoreImports.push('NgModule');
|
|
876
934
|
}
|
|
@@ -878,7 +936,7 @@ function generateProxies(components, pkgData, outputTarget, rootDir, config) {
|
|
|
878
936
|
/* auto-generated angular directive proxies */
|
|
879
937
|
${createImportStatement(angularCoreImports, '@angular/core')}
|
|
880
938
|
|
|
881
|
-
${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n`;
|
|
939
|
+
${createImportStatement(componentLibImports, './angular-component-lib/utils')}${transformImport}\n`;
|
|
882
940
|
/**
|
|
883
941
|
* Generate JSX import type from correct location.
|
|
884
942
|
* When using custom elements build, we need to import from
|
|
@@ -907,12 +965,6 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n
|
|
|
907
965
|
sourceImports = cmpImports.join('\n');
|
|
908
966
|
}
|
|
909
967
|
const proxyFileOutput = [];
|
|
910
|
-
const filterInternalProps = (prop) => !prop.internal;
|
|
911
|
-
// Ensure that virtual properties has required as false.
|
|
912
|
-
const mapInputProp = (prop) => ({
|
|
913
|
-
name: prop.name,
|
|
914
|
-
required: prop.required ?? false,
|
|
915
|
-
});
|
|
916
968
|
const { componentCorePackage, customElementsDir } = outputTarget;
|
|
917
969
|
for (let cmpMeta of components) {
|
|
918
970
|
const tagNameAsPascal = dashToPascalCase(cmpMeta.tagName);
|
|
@@ -920,9 +972,9 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n
|
|
|
920
972
|
if (cmpMeta.properties) {
|
|
921
973
|
internalProps.push(...cmpMeta.properties.filter(filterInternalProps));
|
|
922
974
|
}
|
|
923
|
-
const inputs = internalProps.map(mapInputProp);
|
|
975
|
+
const inputs = internalProps.map((prop) => mapInputProp(prop, booleanAttributes));
|
|
924
976
|
if (cmpMeta.virtualProperties) {
|
|
925
|
-
inputs.push(...cmpMeta.virtualProperties.map(mapInputProp));
|
|
977
|
+
inputs.push(...cmpMeta.virtualProperties.map((prop) => mapInputProp(prop)));
|
|
926
978
|
}
|
|
927
979
|
const orderedInputs = sortBy(inputs, (cip) => cip.name);
|
|
928
980
|
const methods = [];
|
|
@@ -969,11 +1021,18 @@ function generateComponentProxy(cmpMeta, pkgData, outputTarget, rootDir, config)
|
|
|
969
1021
|
if (includeSingleComponentAngularModules) {
|
|
970
1022
|
angularCoreImports.push('NgModule');
|
|
971
1023
|
}
|
|
1024
|
+
const booleanAttributes = outputTarget.booleanAttributes === true;
|
|
1025
|
+
const componentLibImports = ['ProxyCmp'];
|
|
1026
|
+
// The input transform lives in its own module so that it carries no runtime imports.
|
|
1027
|
+
let transformImport = '';
|
|
1028
|
+
if (usesInputTransform([cmpMeta], booleanAttributes)) {
|
|
1029
|
+
transformImport = `\n${createImportStatement([INPUT_TRANSFORM_FUNCTION], './angular-component-lib/boolean-attribute')}`;
|
|
1030
|
+
}
|
|
972
1031
|
const imports = `/* tslint:disable */
|
|
973
1032
|
/* auto-generated angular directive proxies */
|
|
974
1033
|
${createImportStatement(angularCoreImports, '@angular/core')}
|
|
975
1034
|
|
|
976
|
-
${createImportStatement(
|
|
1035
|
+
${createImportStatement(componentLibImports, './angular-component-lib/utils')}${transformImport}\n`;
|
|
977
1036
|
// Type imports
|
|
978
1037
|
const importLocation = componentCorePackage
|
|
979
1038
|
? getPathToComponentTypes(config, outputTarget)
|
|
@@ -985,18 +1044,13 @@ ${createImportStatement(['ProxyCmp'], './angular-component-lib/utils')}\n`;
|
|
|
985
1044
|
sourceImport = `import { defineCustomElement as define${tagNameAsPascal} } from '${normalizePath(componentCorePackage)}/${customElementsDir}/${cmpMeta.tagName}.js';\n`;
|
|
986
1045
|
}
|
|
987
1046
|
// Generate component definition
|
|
988
|
-
const filterInternalProps = (prop) => !prop.internal;
|
|
989
|
-
const mapInputProp = (prop) => ({
|
|
990
|
-
name: prop.name,
|
|
991
|
-
required: prop.required ?? false,
|
|
992
|
-
});
|
|
993
1047
|
const internalProps = [];
|
|
994
1048
|
if (cmpMeta.properties) {
|
|
995
1049
|
internalProps.push(...cmpMeta.properties.filter(filterInternalProps));
|
|
996
1050
|
}
|
|
997
|
-
const inputs = internalProps.map(mapInputProp);
|
|
1051
|
+
const inputs = internalProps.map((prop) => mapInputProp(prop, booleanAttributes));
|
|
998
1052
|
if (cmpMeta.virtualProperties) {
|
|
999
|
-
inputs.push(...cmpMeta.virtualProperties.map(mapInputProp));
|
|
1053
|
+
inputs.push(...cmpMeta.virtualProperties.map((prop) => mapInputProp(prop)));
|
|
1000
1054
|
}
|
|
1001
1055
|
const orderedInputs = sortBy(inputs, (cip) => cip.name);
|
|
1002
1056
|
const methods = [];
|
package/dist/output-angular.js
CHANGED
|
@@ -3,11 +3,49 @@ import { fileURLToPath } from 'url';
|
|
|
3
3
|
const __filename = fileURLToPath(import.meta.url);
|
|
4
4
|
const __dirname = path.dirname(__filename);
|
|
5
5
|
import { relativeImport, normalizePath, sortBy, readPackageJson, dashToPascalCase, createImportStatement, isOutputTypeCustomElementsBuild, OutputTypes, mapPropName, } from './utils';
|
|
6
|
-
import { createAngularComponentDefinition, createComponentTypeDefinition } from './generate-angular-component';
|
|
6
|
+
import { createAngularComponentDefinition, createComponentTypeDefinition, INPUT_TRANSFORM_FUNCTION, } from './generate-angular-component';
|
|
7
7
|
import { generateAngularDirectivesFile } from './generate-angular-directives-file';
|
|
8
8
|
import generateValueAccessors from './generate-value-accessors';
|
|
9
9
|
import { generateAngularModuleForComponent } from './generate-angular-modules';
|
|
10
10
|
import { generateTransformTagScript } from './generate-transformtag-script';
|
|
11
|
+
const filterInternalProps = (prop) => !prop.internal;
|
|
12
|
+
/**
|
|
13
|
+
* Whether a property declares an input transform.
|
|
14
|
+
*
|
|
15
|
+
* This is the single definition of the rule. Both the generated declaration and the import that
|
|
16
|
+
* satisfies it are derived from it, so widening the rule can't leave a generated file referencing
|
|
17
|
+
* a transform it never imports.
|
|
18
|
+
*
|
|
19
|
+
* @param prop The property compiler metadata.
|
|
20
|
+
* @param booleanAttributes Whether boolean properties should declare an input transform.
|
|
21
|
+
* @returns `true` when the property should be transformed.
|
|
22
|
+
*/
|
|
23
|
+
const isTransformedProp = (prop, booleanAttributes) => booleanAttributes && prop.type === 'boolean';
|
|
24
|
+
/**
|
|
25
|
+
* Maps a Stencil property to an Angular input declaration.
|
|
26
|
+
*
|
|
27
|
+
* Virtual properties are mapped with `booleanAttributes` left at its default of `false`. They
|
|
28
|
+
* carry a free-form type string and no `required` flag, so they are never transformed and always
|
|
29
|
+
* come out as optional.
|
|
30
|
+
*
|
|
31
|
+
* @param prop The property compiler metadata.
|
|
32
|
+
* @param booleanAttributes Whether boolean properties should declare an input transform.
|
|
33
|
+
* @returns The Angular input declaration.
|
|
34
|
+
*/
|
|
35
|
+
const mapInputProp = (prop, booleanAttributes = false) => ({
|
|
36
|
+
name: prop.name,
|
|
37
|
+
required: prop.required ?? false,
|
|
38
|
+
transform: isTransformedProp(prop, booleanAttributes) ? true : undefined,
|
|
39
|
+
});
|
|
40
|
+
/**
|
|
41
|
+
* Whether any of a component's properties will declare an input transform, which determines
|
|
42
|
+
* whether the transform needs to be imported.
|
|
43
|
+
*
|
|
44
|
+
* @param components The components in the generated file.
|
|
45
|
+
* @param booleanAttributes Whether boolean properties should declare an input transform.
|
|
46
|
+
* @returns `true` when at least one property is transformed.
|
|
47
|
+
*/
|
|
48
|
+
const usesInputTransform = (components, booleanAttributes) => components.some((cmpMeta) => (cmpMeta.properties ?? []).filter(filterInternalProps).some((prop) => isTransformedProp(prop, booleanAttributes)));
|
|
11
49
|
export async function angularDirectiveProxyOutput(compilerCtx, outputTarget, components, config) {
|
|
12
50
|
const filteredComponents = getFilteredComponents(outputTarget.excludeComponents, components);
|
|
13
51
|
const rootDir = config.rootDir;
|
|
@@ -105,6 +143,14 @@ export function generateProxies(components, pkgData, outputTarget, rootDir, conf
|
|
|
105
143
|
* The collection of named imports from the angular-component-lib/utils.
|
|
106
144
|
*/
|
|
107
145
|
const componentLibImports = ['ProxyCmp'];
|
|
146
|
+
/**
|
|
147
|
+
* The input transform lives in its own module so that it carries no runtime imports.
|
|
148
|
+
*/
|
|
149
|
+
let transformImport = '';
|
|
150
|
+
const booleanAttributes = outputTarget.booleanAttributes === true;
|
|
151
|
+
if (usesInputTransform(components, booleanAttributes)) {
|
|
152
|
+
transformImport = `\n${createImportStatement([INPUT_TRANSFORM_FUNCTION], './angular-component-lib/boolean-attribute')}`;
|
|
153
|
+
}
|
|
108
154
|
if (includeSingleComponentAngularModules) {
|
|
109
155
|
angularCoreImports.push('NgModule');
|
|
110
156
|
}
|
|
@@ -112,7 +158,7 @@ export function generateProxies(components, pkgData, outputTarget, rootDir, conf
|
|
|
112
158
|
/* auto-generated angular directive proxies */
|
|
113
159
|
${createImportStatement(angularCoreImports, '@angular/core')}
|
|
114
160
|
|
|
115
|
-
${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n`;
|
|
161
|
+
${createImportStatement(componentLibImports, './angular-component-lib/utils')}${transformImport}\n`;
|
|
116
162
|
/**
|
|
117
163
|
* Generate JSX import type from correct location.
|
|
118
164
|
* When using custom elements build, we need to import from
|
|
@@ -141,12 +187,6 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n
|
|
|
141
187
|
sourceImports = cmpImports.join('\n');
|
|
142
188
|
}
|
|
143
189
|
const proxyFileOutput = [];
|
|
144
|
-
const filterInternalProps = (prop) => !prop.internal;
|
|
145
|
-
// Ensure that virtual properties has required as false.
|
|
146
|
-
const mapInputProp = (prop) => ({
|
|
147
|
-
name: prop.name,
|
|
148
|
-
required: prop.required ?? false,
|
|
149
|
-
});
|
|
150
190
|
const { componentCorePackage, customElementsDir } = outputTarget;
|
|
151
191
|
for (let cmpMeta of components) {
|
|
152
192
|
const tagNameAsPascal = dashToPascalCase(cmpMeta.tagName);
|
|
@@ -154,9 +194,9 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}\n
|
|
|
154
194
|
if (cmpMeta.properties) {
|
|
155
195
|
internalProps.push(...cmpMeta.properties.filter(filterInternalProps));
|
|
156
196
|
}
|
|
157
|
-
const inputs = internalProps.map(mapInputProp);
|
|
197
|
+
const inputs = internalProps.map((prop) => mapInputProp(prop, booleanAttributes));
|
|
158
198
|
if (cmpMeta.virtualProperties) {
|
|
159
|
-
inputs.push(...cmpMeta.virtualProperties.map(mapInputProp));
|
|
199
|
+
inputs.push(...cmpMeta.virtualProperties.map((prop) => mapInputProp(prop)));
|
|
160
200
|
}
|
|
161
201
|
const orderedInputs = sortBy(inputs, (cip) => cip.name);
|
|
162
202
|
const methods = [];
|
|
@@ -203,11 +243,18 @@ export function generateComponentProxy(cmpMeta, pkgData, outputTarget, rootDir,
|
|
|
203
243
|
if (includeSingleComponentAngularModules) {
|
|
204
244
|
angularCoreImports.push('NgModule');
|
|
205
245
|
}
|
|
246
|
+
const booleanAttributes = outputTarget.booleanAttributes === true;
|
|
247
|
+
const componentLibImports = ['ProxyCmp'];
|
|
248
|
+
// The input transform lives in its own module so that it carries no runtime imports.
|
|
249
|
+
let transformImport = '';
|
|
250
|
+
if (usesInputTransform([cmpMeta], booleanAttributes)) {
|
|
251
|
+
transformImport = `\n${createImportStatement([INPUT_TRANSFORM_FUNCTION], './angular-component-lib/boolean-attribute')}`;
|
|
252
|
+
}
|
|
206
253
|
const imports = `/* tslint:disable */
|
|
207
254
|
/* auto-generated angular directive proxies */
|
|
208
255
|
${createImportStatement(angularCoreImports, '@angular/core')}
|
|
209
256
|
|
|
210
|
-
${createImportStatement(
|
|
257
|
+
${createImportStatement(componentLibImports, './angular-component-lib/utils')}${transformImport}\n`;
|
|
211
258
|
// Type imports
|
|
212
259
|
const importLocation = componentCorePackage
|
|
213
260
|
? getPathToComponentTypes(config, outputTarget)
|
|
@@ -219,18 +266,13 @@ ${createImportStatement(['ProxyCmp'], './angular-component-lib/utils')}\n`;
|
|
|
219
266
|
sourceImport = `import { defineCustomElement as define${tagNameAsPascal} } from '${normalizePath(componentCorePackage)}/${customElementsDir}/${cmpMeta.tagName}.js';\n`;
|
|
220
267
|
}
|
|
221
268
|
// Generate component definition
|
|
222
|
-
const filterInternalProps = (prop) => !prop.internal;
|
|
223
|
-
const mapInputProp = (prop) => ({
|
|
224
|
-
name: prop.name,
|
|
225
|
-
required: prop.required ?? false,
|
|
226
|
-
});
|
|
227
269
|
const internalProps = [];
|
|
228
270
|
if (cmpMeta.properties) {
|
|
229
271
|
internalProps.push(...cmpMeta.properties.filter(filterInternalProps));
|
|
230
272
|
}
|
|
231
|
-
const inputs = internalProps.map(mapInputProp);
|
|
273
|
+
const inputs = internalProps.map((prop) => mapInputProp(prop, booleanAttributes));
|
|
232
274
|
if (cmpMeta.virtualProperties) {
|
|
233
|
-
inputs.push(...cmpMeta.virtualProperties.map(mapInputProp));
|
|
275
|
+
inputs.push(...cmpMeta.virtualProperties.map((prop) => mapInputProp(prop)));
|
|
234
276
|
}
|
|
235
277
|
const orderedInputs = sortBy(inputs, (cip) => cip.name);
|
|
236
278
|
const methods = [];
|
package/dist/types.d.ts
CHANGED
|
@@ -94,6 +94,43 @@ export interface OutputTargetAngular {
|
|
|
94
94
|
* @default false
|
|
95
95
|
*/
|
|
96
96
|
esModules?: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* When `true`, boolean properties are declared with an Angular input transform so they can
|
|
99
|
+
* be set using attribute presence syntax:
|
|
100
|
+
*
|
|
101
|
+
* ```html
|
|
102
|
+
* <!-- with booleanAttributes enabled -->
|
|
103
|
+
* <my-component disabled></my-component>
|
|
104
|
+
*
|
|
105
|
+
* <!-- otherwise the value has to be bound explicitly -->
|
|
106
|
+
* <my-component [disabled]="true"></my-component>
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* Without a transform Angular resolves the bare attribute to the empty string and reports
|
|
110
|
+
* `Type 'string' is not assignable to type 'boolean'` under `strictTemplates`.
|
|
111
|
+
*
|
|
112
|
+
* Compile-time checking of these bindings also requires `inlineProperties`. Without it the
|
|
113
|
+
* generated wrappers declare no typed members for their inputs, so Angular never checks them
|
|
114
|
+
* and there is no error for the transform to resolve.
|
|
115
|
+
*
|
|
116
|
+
* The transform still runs either way, because Angular applies it from the component
|
|
117
|
+
* definition's input map rather than from a declared class member. Enabling this on its own
|
|
118
|
+
* therefore moves boolean coercion from Stencil to the transform with nothing checking what
|
|
119
|
+
* callers pass, so prefer enabling `inlineProperties` alongside it.
|
|
120
|
+
*
|
|
121
|
+
* The transform coerces strings the same way Angular's `booleanAttribute` does, but passes
|
|
122
|
+
* `null` and `undefined` through instead of coercing them to `false`. Refer to
|
|
123
|
+
* `nullableBooleanAttribute`.
|
|
124
|
+
*
|
|
125
|
+
* Properties are only transformed when Stencil reports their type as exactly `boolean`.
|
|
126
|
+
* Virtual properties, properties declared as `any`, and types that union `boolean` with
|
|
127
|
+
* something else (such as `boolean | 'auto'`, reported as `any`) get no transform, and the
|
|
128
|
+
* property receives the attribute's empty string rather than `true`. Only the union case fails
|
|
129
|
+
* to compile; the other two are silent. Refer to the README for the differences.
|
|
130
|
+
*
|
|
131
|
+
* @default false
|
|
132
|
+
*/
|
|
133
|
+
booleanAttributes?: boolean;
|
|
97
134
|
}
|
|
98
135
|
export type ValueAccessorTypes = 'text' | 'radio' | 'select' | 'number' | 'boolean';
|
|
99
136
|
export interface ValueAccessorConfig {
|
|
@@ -108,4 +145,8 @@ export interface PackageJSON {
|
|
|
108
145
|
export interface ComponentInputProperty {
|
|
109
146
|
name: string;
|
|
110
147
|
required: boolean;
|
|
148
|
+
/**
|
|
149
|
+
* Whether to declare an input transform so the property can be set by attribute presence.
|
|
150
|
+
*/
|
|
151
|
+
transform?: boolean;
|
|
111
152
|
}
|
package/dist/wizard.d.ts
CHANGED
|
@@ -4,6 +4,6 @@ export declare const wizard: {
|
|
|
4
4
|
id: string;
|
|
5
5
|
displayName: string;
|
|
6
6
|
description: string;
|
|
7
|
-
run({ config, workspaceRoot, prompts, nypm }: WizardContext): Promise<void>;
|
|
7
|
+
run({ config, workspaceRoot, prompts, nypm, openStencilConfig }: WizardContext): Promise<void>;
|
|
8
8
|
};
|
|
9
9
|
};
|
package/dist/wizard.js
CHANGED
|
@@ -1,54 +1,6 @@
|
|
|
1
|
-
import { Project, SyntaxKind } from 'ts-morph';
|
|
2
1
|
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
2
|
import { join, relative, dirname } from 'node:path';
|
|
4
3
|
// ---------------------------------------------------------------------------
|
|
5
|
-
// ts-morph helpers
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
function amendStencilConfig(configPath, targetCode) {
|
|
8
|
-
const project = new Project({ skipAddingFilesFromTsConfig: true });
|
|
9
|
-
const src = project.addSourceFileAtPath(configPath);
|
|
10
|
-
if (!src.getImportDeclaration('@stencil/angular-output-target')) {
|
|
11
|
-
src.addImportDeclaration({
|
|
12
|
-
moduleSpecifier: '@stencil/angular-output-target',
|
|
13
|
-
namedImports: ['angularOutputTarget'],
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
const existingProp = src
|
|
17
|
-
.getDescendantsOfKind(SyntaxKind.PropertyAssignment)
|
|
18
|
-
.find((p) => p.getName() === 'outputTargets');
|
|
19
|
-
if (!existingProp) {
|
|
20
|
-
const configObj = src.getVariableDeclaration('config')?.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression) ??
|
|
21
|
-
src
|
|
22
|
-
.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression)
|
|
23
|
-
.find((obj) => obj.getProperty('namespace') !== undefined);
|
|
24
|
-
if (!configObj)
|
|
25
|
-
throw new Error('Could not find Stencil config object in stencil.config.ts');
|
|
26
|
-
configObj.addPropertyAssignment({ name: 'outputTargets', initializer: '[]' });
|
|
27
|
-
}
|
|
28
|
-
const prop = src.getDescendantsOfKind(SyntaxKind.PropertyAssignment).find((p) => p.getName() === 'outputTargets');
|
|
29
|
-
const arr = prop.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression);
|
|
30
|
-
if (!arr)
|
|
31
|
-
throw new Error('outputTargets is not an array literal in stencil.config.ts');
|
|
32
|
-
const existing = arr.getText();
|
|
33
|
-
const alreadyAdded = existing.includes('angularOutputTarget(');
|
|
34
|
-
// Filter out any existing angularOutputTarget call so it can be replaced cleanly
|
|
35
|
-
const elements = arr
|
|
36
|
-
.getElements()
|
|
37
|
-
.map((e) => e.getText().trim())
|
|
38
|
-
.filter((e) => !e.includes('angularOutputTarget('));
|
|
39
|
-
const hasStandalone = existing.includes("type: 'standalone'") ||
|
|
40
|
-
existing.includes('type: "standalone"') ||
|
|
41
|
-
existing.includes("type: 'dist-custom-elements'") ||
|
|
42
|
-
existing.includes('type: "dist-custom-elements"');
|
|
43
|
-
if (!hasStandalone)
|
|
44
|
-
elements.push("{ type: 'standalone' }");
|
|
45
|
-
elements.push(targetCode);
|
|
46
|
-
prop.setInitializer(`[\n${elements.map((e) => ` ${e}`).join(',\n')},\n]`);
|
|
47
|
-
src.formatText();
|
|
48
|
-
src.saveSync();
|
|
49
|
-
return !alreadyAdded;
|
|
50
|
-
}
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
4
|
// pnpm build approval
|
|
53
5
|
// ---------------------------------------------------------------------------
|
|
54
6
|
async function allowPnpmBuild(pkg, rootDir, workspaceRoot) {
|
|
@@ -194,19 +146,12 @@ export const wizard = {
|
|
|
194
146
|
id: '@stencil/angular-output-target',
|
|
195
147
|
displayName: 'Angular',
|
|
196
148
|
description: 'Angular component wrappers for your Stencil components',
|
|
197
|
-
async run({ config, workspaceRoot, prompts, nypm }) {
|
|
149
|
+
async run({ config, workspaceRoot, prompts, nypm, openStencilConfig }) {
|
|
198
150
|
const { intro, outro, text, select, confirm, spinner, isCancel, cancel, log } = prompts;
|
|
199
151
|
intro('Angular output target');
|
|
200
|
-
const stencilConfigPath = join(config.rootDir, 'stencil.config.ts');
|
|
201
152
|
// Guard: already configured?
|
|
202
|
-
const
|
|
203
|
-
const alreadyConfigured =
|
|
204
|
-
.addSourceFileAtPath(stencilConfigPath)
|
|
205
|
-
.getDescendantsOfKind(SyntaxKind.PropertyAssignment)
|
|
206
|
-
.find((p) => p.getName() === 'outputTargets')
|
|
207
|
-
?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression)
|
|
208
|
-
?.getText()
|
|
209
|
-
.includes('angularOutputTarget(');
|
|
153
|
+
const editor = await openStencilConfig();
|
|
154
|
+
const alreadyConfigured = editor.outputTargetsContains('angularOutputTarget(');
|
|
210
155
|
if (alreadyConfigured) {
|
|
211
156
|
const redo = await confirm({
|
|
212
157
|
message: 'angularOutputTarget is already in stencil.config.ts — reconfigure?',
|
|
@@ -219,18 +164,21 @@ export const wizard = {
|
|
|
219
164
|
}
|
|
220
165
|
// Where should the wrapper package live?
|
|
221
166
|
let wrapperDir;
|
|
222
|
-
|
|
167
|
+
let wrapperPackageName = `${config.fsNamespace}-angular`;
|
|
223
168
|
if (workspaceRoot) {
|
|
224
169
|
const rawName = await text({
|
|
225
170
|
message: 'Wrapper package name?',
|
|
226
|
-
placeholder:
|
|
227
|
-
defaultValue:
|
|
171
|
+
placeholder: wrapperPackageName,
|
|
172
|
+
defaultValue: wrapperPackageName,
|
|
228
173
|
});
|
|
229
174
|
if (isCancel(rawName)) {
|
|
230
175
|
cancel('Setup cancelled.');
|
|
231
176
|
return;
|
|
232
177
|
}
|
|
233
|
-
|
|
178
|
+
wrapperPackageName = rawName;
|
|
179
|
+
// Directory is already namespaced by living under packages/, so keep it
|
|
180
|
+
// short rather than repeating the fsNamespace prefix from the package name.
|
|
181
|
+
wrapperDir = join(dirname(config.rootDir), 'angular');
|
|
234
182
|
}
|
|
235
183
|
else {
|
|
236
184
|
const defaultRel = `../${config.fsNamespace}-angular`;
|
|
@@ -271,8 +219,15 @@ export const wizard = {
|
|
|
271
219
|
const s = spinner();
|
|
272
220
|
s.start(`Scaffolding wrapper package at ${relative(workspaceRoot ?? config.rootDir, wrapperDir)}`);
|
|
273
221
|
try {
|
|
274
|
-
|
|
275
|
-
|
|
222
|
+
// workspace:* only resolves under pnpm/yarn-berry/bun — npm and yarn classic reject
|
|
223
|
+
// the protocol outright (even for devDependencies that are never installed), so only
|
|
224
|
+
// use it when the detected package manager actually understands it.
|
|
225
|
+
const pm = workspaceRoot ? await nypm.detectPackageManager(workspaceRoot) : undefined;
|
|
226
|
+
const supportsWorkspaceProtocol = pm?.name === 'pnpm' || pm?.name === 'bun' || (pm?.name === 'yarn' && pm.majorVersion !== '1');
|
|
227
|
+
const corePkgVersion = supportsWorkspaceProtocol
|
|
228
|
+
? 'workspace:*'
|
|
229
|
+
: `file:${relative(wrapperDir, config.rootDir)}`;
|
|
230
|
+
await scaffoldWrapperPackage(wrapperDir, wrapperPackageName, componentCorePackage, corePkgVersion, config.fsNamespace, selectedOutputType);
|
|
276
231
|
s.stop('Wrapper package scaffolded');
|
|
277
232
|
}
|
|
278
233
|
catch (e) {
|
|
@@ -301,10 +256,20 @@ export const wizard = {
|
|
|
301
256
|
...(selectedOutputType !== 'standalone' ? [`outputType: '${selectedOutputType}'`] : []),
|
|
302
257
|
];
|
|
303
258
|
const targetCode = `angularOutputTarget({\n ${lines.join(',\n ')},\n})`;
|
|
304
|
-
// Amend stencil.config.ts
|
|
259
|
+
// Amend stencil.config.ts
|
|
305
260
|
try {
|
|
306
|
-
|
|
307
|
-
|
|
261
|
+
editor.addImport('@stencil/angular-output-target', ['angularOutputTarget']);
|
|
262
|
+
const hasStandalone = editor.outputTargetsContains("type: 'standalone'") ||
|
|
263
|
+
editor.outputTargetsContains('type: "standalone"') ||
|
|
264
|
+
editor.outputTargetsContains("type: 'dist-custom-elements'") ||
|
|
265
|
+
editor.outputTargetsContains('type: "dist-custom-elements"');
|
|
266
|
+
if (!hasStandalone)
|
|
267
|
+
editor.addOutputTarget("{ type: 'standalone' }");
|
|
268
|
+
if (!editor.replaceOutputTarget('angularOutputTarget(', targetCode)) {
|
|
269
|
+
editor.addOutputTarget(targetCode);
|
|
270
|
+
}
|
|
271
|
+
await editor.save();
|
|
272
|
+
log.success('stencil.config.ts updated');
|
|
308
273
|
}
|
|
309
274
|
catch (e) {
|
|
310
275
|
log.warn(`Could not automatically update stencil.config.ts (${e}). Add manually:\n\n` +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stencil/angular-output-target",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Angular output target for @stencil/core components.",
|
|
5
5
|
"main": "dist/index.cjs",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -55,23 +55,21 @@
|
|
|
55
55
|
"bugs": {
|
|
56
56
|
"url": "https://github.com/stenciljs/output-targets/issues"
|
|
57
57
|
},
|
|
58
|
-
"dependencies": {
|
|
59
|
-
"ts-morph": "^28.0.0"
|
|
60
|
-
},
|
|
61
58
|
"devDependencies": {
|
|
62
|
-
"@stencil/cli": "^5.0.0 || >=5.0.0-alpha.0 <5.0.0-next.0",
|
|
63
59
|
"@angular/core": "8.2.14",
|
|
64
60
|
"@angular/forms": "8.2.14",
|
|
61
|
+
"@stencil/cli": "^5.0.0 || >=5.0.0-alpha.17 <5.0.0-next.0",
|
|
65
62
|
"@stencil/core": "4.41.0",
|
|
66
|
-
"@types/node": "^18.
|
|
67
|
-
"npm-run-all2": "^6.2.
|
|
68
|
-
"rimraf": "^5.0.
|
|
69
|
-
"rollup": "^2.
|
|
70
|
-
"
|
|
71
|
-
"
|
|
63
|
+
"@types/node": "^18.19.130",
|
|
64
|
+
"npm-run-all2": "^6.2.6",
|
|
65
|
+
"rimraf": "^5.0.10",
|
|
66
|
+
"rollup": "^2.80.0",
|
|
67
|
+
"stencil-output-targets-shared": "workspace:*",
|
|
68
|
+
"typescript": "~5.7.3",
|
|
69
|
+
"vitest": "^2.1.9"
|
|
72
70
|
},
|
|
73
71
|
"peerDependencies": {
|
|
74
|
-
"@stencil/core": ">=2.0.0 || >=3 || >=4.0.0 || ^5.0.0 || >=5.0.0-alpha.
|
|
72
|
+
"@stencil/core": ">=2.0.0 || >=3 || >=4.0.0 || ^5.0.0 || >=5.0.0-alpha.17 <5.0.0-next.0"
|
|
75
73
|
},
|
|
76
74
|
"gitHead": "a3588e905186a0e86e7f88418fd5b2f9531b55e0"
|
|
77
75
|
}
|