@stencil/angular-output-target 1.5.2 → 1.6.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 +46 -0
- package/angular-component-lib/utils.ts +69 -4
- package/dist/generate-angular-component.d.ts +13 -1
- package/dist/generate-angular-component.js +14 -2
- package/dist/index.cjs +75 -8
- package/dist/index.js +75 -8
- package/dist/output-angular.js +61 -6
- package/dist/types.d.ts +14 -0
- package/dist/wizard.js +47 -6
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -58,6 +58,52 @@ export const config: Config = {
|
|
|
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
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
|
+
| `ssrPropModule` | The component library's `ssr-prop` module (e.g. `'component-library/ssr-prop'`), from Stencil's `ssr` output target. Set it when apps server-render the wrappers with Angular SSR. Refer to [Server-side rendering](#server-side-rendering). |
|
|
62
|
+
|
|
63
|
+
## Server-side rendering
|
|
64
|
+
|
|
65
|
+
With Stencil's `ssr` output target, an Angular SSR app passes each rendered page through the
|
|
66
|
+
library's `renderToString()`, which renders the components into it. That needs the wrappers'
|
|
67
|
+
inputs in the HTML, but Angular's server DOM never writes properties to the HTML, so
|
|
68
|
+
`<my-chart [data]="points">` would reach `renderToString()` with no `data`.
|
|
69
|
+
|
|
70
|
+
Set `ssrPropModule` to the library's `ssr-prop` module:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
angularOutputTarget({
|
|
74
|
+
componentCorePackage: 'component-library',
|
|
75
|
+
directivesProxyFile: '../component-library-angular/src/proxies.ts',
|
|
76
|
+
ssrPropModule: 'component-library/ssr-prop',
|
|
77
|
+
}),
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
A wrapper rendering on the server then writes each input into the element's attributes, under
|
|
81
|
+
the prop's attribute name:
|
|
82
|
+
|
|
83
|
+
| Input value | Written as |
|
|
84
|
+
|---|---|
|
|
85
|
+
| a string or number | the attribute's value |
|
|
86
|
+
| `true` | an empty attribute |
|
|
87
|
+
| `null` or `undefined` | no attribute |
|
|
88
|
+
| `false`, an object, an array, a function, or any value for a prop with no attribute or not typed exactly `string`, `number` or `boolean` | an `ssrProp()` reference |
|
|
89
|
+
|
|
90
|
+
Strings, numbers and `true` stay in the HTML, so the components' first render in the browser
|
|
91
|
+
sees them too. A reference hands `renderToString()` the exact value and is removed from the HTML.
|
|
92
|
+
(`false` can't be an attribute: Stencil reads any attribute on a boolean prop as `true`. And
|
|
93
|
+
Stencil only converts an attribute for a prop of one of those three types: for a `string | boolean`
|
|
94
|
+
prop, `""` would stay a string.)
|
|
95
|
+
|
|
96
|
+
In the browser, inputs are set as properties, as usual. Some things to know:
|
|
97
|
+
|
|
98
|
+
- To keep an object or array after the components hydrate, the component has to carry it in its
|
|
99
|
+
attribute, for example with `@Prop({ serialize: 'json' })`. A function never reaches the
|
|
100
|
+
browser this way, but Angular sets it as a property when the app hydrates.
|
|
101
|
+
- The attributes written on the server stay on the element after it hydrates. Setting the input
|
|
102
|
+
later updates the property, not the attribute.
|
|
103
|
+
- Reading an input back on the server returns the value you set.
|
|
104
|
+
|
|
105
|
+
For a working setup, see the [Angular example](https://github.com/stenciljs/ssr-examples/tree/main/examples/angular)
|
|
106
|
+
in the Stencil SSR examples.
|
|
61
107
|
|
|
62
108
|
## Boolean attributes
|
|
63
109
|
|
|
@@ -1,8 +1,67 @@
|
|
|
1
1
|
/* eslint-disable */
|
|
2
2
|
/* tslint:disable */
|
|
3
|
+
import { inject, PLATFORM_ID } from '@angular/core';
|
|
3
4
|
import { fromEvent } from 'rxjs';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Server rendering options (the output target's `ssrPropModule`).
|
|
8
|
+
*/
|
|
9
|
+
interface SsrOptions {
|
|
10
|
+
/** The component library's `ssrProp()`, which hands a value to Stencil's server render. */
|
|
11
|
+
ssrProp: (value: any) => any;
|
|
12
|
+
/** Inputs whose attribute isn't Stencil's dash-cased prop name. */
|
|
13
|
+
attributes?: Record<string, string>;
|
|
14
|
+
/** Inputs with no attribute: every value goes through `ssrProp()`. */
|
|
15
|
+
refs?: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const serverInstances = new WeakSet<object>();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Records a wrapper created during server rendering. Call from the constructor.
|
|
22
|
+
*/
|
|
23
|
+
export const markServer = (instance: object) => {
|
|
24
|
+
// what `isPlatformServer()` from `@angular/common` checks
|
|
25
|
+
if (inject(PLATFORM_ID) === 'server') {
|
|
26
|
+
serverInstances.add(instance);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Stencil's own prop name to attribute name conversion (`prop1` -> `prop-1`).
|
|
32
|
+
*/
|
|
33
|
+
const toDashCase = (str: string): string =>
|
|
34
|
+
str
|
|
35
|
+
.replace(/([A-Z0-9])/g, (match) => ` ${match[0]}`)
|
|
36
|
+
.trim()
|
|
37
|
+
.split(' ')
|
|
38
|
+
.join('-')
|
|
39
|
+
.toLowerCase();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Angular's server DOM never writes properties to the HTML, so on the server an input becomes an
|
|
43
|
+
* attribute, which Stencil's `renderToString()` reads:
|
|
44
|
+
* - a string or number as itself, and `true` as an empty attribute, so the component's first
|
|
45
|
+
* render in the browser sees them too;
|
|
46
|
+
* - `false`, anything that isn't a primitive, and any value for a prop with no attribute through
|
|
47
|
+
* `ssrProp()`, which gives the server render the exact value and leaves no attribute behind.
|
|
48
|
+
*/
|
|
49
|
+
const setServerAttribute = (el: any, item: string, val: any, ssr: SsrOptions) => {
|
|
50
|
+
const name = ssr.attributes?.[item] ?? toDashCase(item);
|
|
51
|
+
if (val == null) {
|
|
52
|
+
el.removeAttribute(name);
|
|
53
|
+
} else if (
|
|
54
|
+
ssr.refs?.includes(item) ||
|
|
55
|
+
val === false ||
|
|
56
|
+
(typeof val !== 'string' && typeof val !== 'number' && val !== true)
|
|
57
|
+
) {
|
|
58
|
+
el.setAttribute(name, ssr.ssrProp(val));
|
|
59
|
+
} else {
|
|
60
|
+
el.setAttribute(name, val === true ? '' : String(val));
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const proxyInputs = (Cmp: any, inputs: string[], ssr?: SsrOptions) => {
|
|
6
65
|
const Prototype = Cmp.prototype;
|
|
7
66
|
inputs.forEach((item) => {
|
|
8
67
|
Object.defineProperty(Prototype, item, {
|
|
@@ -10,6 +69,12 @@ export const proxyInputs = (Cmp: any, inputs: string[]) => {
|
|
|
10
69
|
return this.el[item];
|
|
11
70
|
},
|
|
12
71
|
set(val: any) {
|
|
72
|
+
if (ssr && serverInstances.has(this)) {
|
|
73
|
+
// kept on the element too, so reading the input back on the server works
|
|
74
|
+
this.el[item] = val;
|
|
75
|
+
setServerAttribute(this.el, item, val, ssr);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
13
78
|
this.z.runOutsideAngular(() => (this.el[item] = val));
|
|
14
79
|
},
|
|
15
80
|
/**
|
|
@@ -45,16 +110,16 @@ export const defineCustomElement = (tagName: string, customElement: any) => {
|
|
|
45
110
|
};
|
|
46
111
|
|
|
47
112
|
// tslint:disable-next-line: only-arrow-functions
|
|
48
|
-
export function ProxyCmp(opts: { defineCustomElementFn?: () => void; inputs?: any; methods?: any }) {
|
|
113
|
+
export function ProxyCmp(opts: { defineCustomElementFn?: () => void; inputs?: any; methods?: any; ssr?: SsrOptions }) {
|
|
49
114
|
const decorator = function (cls: any) {
|
|
50
|
-
const { defineCustomElementFn, inputs, methods } = opts;
|
|
115
|
+
const { defineCustomElementFn, inputs, methods, ssr } = opts;
|
|
51
116
|
|
|
52
117
|
if (defineCustomElementFn !== undefined) {
|
|
53
118
|
defineCustomElementFn();
|
|
54
119
|
}
|
|
55
120
|
|
|
56
121
|
if (inputs) {
|
|
57
|
-
proxyInputs(cls, inputs);
|
|
122
|
+
proxyInputs(cls, inputs, ssr);
|
|
58
123
|
}
|
|
59
124
|
if (methods) {
|
|
60
125
|
proxyMethods(cls, methods);
|
|
@@ -5,6 +5,15 @@ import type { ComponentInputProperty, OutputType } from './types';
|
|
|
5
5
|
* `angular-component-lib/boolean-attribute`.
|
|
6
6
|
*/
|
|
7
7
|
export declare const INPUT_TRANSFORM_FUNCTION = "nullableBooleanAttribute";
|
|
8
|
+
/**
|
|
9
|
+
* What a wrapper needs to write its inputs as attributes during server rendering.
|
|
10
|
+
*/
|
|
11
|
+
export interface SsrInputs {
|
|
12
|
+
/** Inputs whose attribute isn't the dash-cased prop name, mapped to that attribute. */
|
|
13
|
+
attributes: Record<string, string>;
|
|
14
|
+
/** Inputs whose every value goes through `ssrProp()`: no attribute, or not a primitive type. */
|
|
15
|
+
refs: string[];
|
|
16
|
+
}
|
|
8
17
|
/**
|
|
9
18
|
* Creates an Angular component declaration from formatted Stencil compiler metadata.
|
|
10
19
|
*
|
|
@@ -15,9 +24,12 @@ export declare const INPUT_TRANSFORM_FUNCTION = "nullableBooleanAttribute";
|
|
|
15
24
|
* @param standalone Whether to define the component as a standalone component.
|
|
16
25
|
* @param inlineComponentProps List of properties that should be inlined into the component definition.
|
|
17
26
|
* @param events The events of the Stencil component for generating outputs.
|
|
27
|
+
* @param ssr When set, the wrapper writes its inputs as attributes during server rendering.
|
|
28
|
+
* `attributes` maps the inputs whose attribute isn't the dash-cased prop name to that attribute,
|
|
29
|
+
* and `refs` lists the inputs whose every value goes through `ssrProp()`.
|
|
18
30
|
* @returns The component declaration as a string.
|
|
19
31
|
*/
|
|
20
|
-
export declare const createAngularComponentDefinition: (tagName: string, inputs: readonly ComponentInputProperty[], methods: readonly string[], includeImportCustomElements?: boolean, standalone?: boolean, inlineComponentProps?: readonly ComponentCompilerProperty[], events?: readonly ComponentCompilerEvent[]) => string;
|
|
32
|
+
export declare const createAngularComponentDefinition: (tagName: string, inputs: readonly ComponentInputProperty[], methods: readonly string[], includeImportCustomElements?: boolean, standalone?: boolean, inlineComponentProps?: readonly ComponentCompilerProperty[], events?: readonly ComponentCompilerEvent[], ssr?: SsrInputs) => string;
|
|
21
33
|
/**
|
|
22
34
|
* Creates the component interface type definition.
|
|
23
35
|
* @param outputType The output type.
|
|
@@ -65,9 +65,12 @@ function formatInputs(inputs) {
|
|
|
65
65
|
* @param standalone Whether to define the component as a standalone component.
|
|
66
66
|
* @param inlineComponentProps List of properties that should be inlined into the component definition.
|
|
67
67
|
* @param events The events of the Stencil component for generating outputs.
|
|
68
|
+
* @param ssr When set, the wrapper writes its inputs as attributes during server rendering.
|
|
69
|
+
* `attributes` maps the inputs whose attribute isn't the dash-cased prop name to that attribute,
|
|
70
|
+
* and `refs` lists the inputs whose every value goes through `ssrProp()`.
|
|
68
71
|
* @returns The component declaration as a string.
|
|
69
72
|
*/
|
|
70
|
-
export const createAngularComponentDefinition = (tagName, inputs, methods, includeImportCustomElements = false, standalone = false, inlineComponentProps = [], events = []) => {
|
|
73
|
+
export const createAngularComponentDefinition = (tagName, inputs, methods, includeImportCustomElements = false, standalone = false, inlineComponentProps = [], events = [], ssr) => {
|
|
71
74
|
const tagNameAsPascal = dashToPascalCase(tagName);
|
|
72
75
|
const outputs = events.filter((event) => !event.internal).map((event) => event.name);
|
|
73
76
|
const hasInputs = inputs.length > 0;
|
|
@@ -93,6 +96,15 @@ export const createAngularComponentDefinition = (tagName, inputs, methods, inclu
|
|
|
93
96
|
if (hasMethods) {
|
|
94
97
|
proxyCmpOptions.push(`\n methods: [${formattedMethods}]`);
|
|
95
98
|
}
|
|
99
|
+
if (ssr && hasInputs) {
|
|
100
|
+
const ssrOptions = ['ssrProp'];
|
|
101
|
+
const attributes = Object.entries(ssr.attributes).map(([prop, attr]) => `'${prop}': '${attr}'`);
|
|
102
|
+
if (attributes.length)
|
|
103
|
+
ssrOptions.push(`attributes: { ${attributes.join(', ')} }`);
|
|
104
|
+
if (ssr.refs.length)
|
|
105
|
+
ssrOptions.push(`refs: [${formatToQuotedList(ssr.refs)}]`);
|
|
106
|
+
proxyCmpOptions.push(`\n ssr: { ${ssrOptions.join(', ')} }`);
|
|
107
|
+
}
|
|
96
108
|
let standaloneOption = '';
|
|
97
109
|
if (!standalone) {
|
|
98
110
|
standaloneOption = `\n standalone: false`;
|
|
@@ -129,7 +141,7 @@ export class ${tagNameAsPascal} {
|
|
|
129
141
|
${propertiesDeclarationText}
|
|
130
142
|
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
|
|
131
143
|
c.detach();
|
|
132
|
-
this.el = r.nativeElement;
|
|
144
|
+
this.el = r.nativeElement;${ssr && hasInputs ? '\n markServer(this);' : ''}
|
|
133
145
|
}
|
|
134
146
|
}`;
|
|
135
147
|
return output;
|
package/dist/index.cjs
CHANGED
|
@@ -219,9 +219,12 @@ function formatInputs(inputs) {
|
|
|
219
219
|
* @param standalone Whether to define the component as a standalone component.
|
|
220
220
|
* @param inlineComponentProps List of properties that should be inlined into the component definition.
|
|
221
221
|
* @param events The events of the Stencil component for generating outputs.
|
|
222
|
+
* @param ssr When set, the wrapper writes its inputs as attributes during server rendering.
|
|
223
|
+
* `attributes` maps the inputs whose attribute isn't the dash-cased prop name to that attribute,
|
|
224
|
+
* and `refs` lists the inputs whose every value goes through `ssrProp()`.
|
|
222
225
|
* @returns The component declaration as a string.
|
|
223
226
|
*/
|
|
224
|
-
const createAngularComponentDefinition = (tagName, inputs, methods, includeImportCustomElements = false, standalone = false, inlineComponentProps = [], events = []) => {
|
|
227
|
+
const createAngularComponentDefinition = (tagName, inputs, methods, includeImportCustomElements = false, standalone = false, inlineComponentProps = [], events = [], ssr) => {
|
|
225
228
|
const tagNameAsPascal = dashToPascalCase(tagName);
|
|
226
229
|
const outputs = events.filter((event) => !event.internal).map((event) => event.name);
|
|
227
230
|
const hasInputs = inputs.length > 0;
|
|
@@ -247,6 +250,15 @@ const createAngularComponentDefinition = (tagName, inputs, methods, includeImpor
|
|
|
247
250
|
if (hasMethods) {
|
|
248
251
|
proxyCmpOptions.push(`\n methods: [${formattedMethods}]`);
|
|
249
252
|
}
|
|
253
|
+
if (ssr && hasInputs) {
|
|
254
|
+
const ssrOptions = ['ssrProp'];
|
|
255
|
+
const attributes = Object.entries(ssr.attributes).map(([prop, attr]) => `'${prop}': '${attr}'`);
|
|
256
|
+
if (attributes.length)
|
|
257
|
+
ssrOptions.push(`attributes: { ${attributes.join(', ')} }`);
|
|
258
|
+
if (ssr.refs.length)
|
|
259
|
+
ssrOptions.push(`refs: [${formatToQuotedList(ssr.refs)}]`);
|
|
260
|
+
proxyCmpOptions.push(`\n ssr: { ${ssrOptions.join(', ')} }`);
|
|
261
|
+
}
|
|
250
262
|
let standaloneOption = '';
|
|
251
263
|
if (!standalone) {
|
|
252
264
|
standaloneOption = `\n standalone: false`;
|
|
@@ -283,7 +295,7 @@ export class ${tagNameAsPascal} {
|
|
|
283
295
|
${propertiesDeclarationText}
|
|
284
296
|
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
|
|
285
297
|
c.detach();
|
|
286
|
-
this.el = r.nativeElement;
|
|
298
|
+
this.el = r.nativeElement;${ssr && hasInputs ? '\n markServer(this);' : ''}
|
|
287
299
|
}
|
|
288
300
|
}`;
|
|
289
301
|
return output;
|
|
@@ -801,6 +813,47 @@ stencilSetTagTransformer(\${transformerArg});
|
|
|
801
813
|
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)));
|
|
802
814
|
const __dirname$1 = path__default["default"].dirname(__filename$1);
|
|
803
815
|
const filterInternalProps = (prop) => !prop.internal;
|
|
816
|
+
/**
|
|
817
|
+
* Stencil's own prop name to attribute name conversion (`prop1` -> `prop-1`).
|
|
818
|
+
*/
|
|
819
|
+
const toDashCase = (str) => str
|
|
820
|
+
.replace(/([A-Z0-9])/g, (match) => ` ${match[0]}`)
|
|
821
|
+
.trim()
|
|
822
|
+
.split(' ')
|
|
823
|
+
.join('-')
|
|
824
|
+
.toLowerCase();
|
|
825
|
+
/**
|
|
826
|
+
* With `ssrPropModule` set, what the wrapper needs to write its inputs as attributes during
|
|
827
|
+
* server rendering. Otherwise `undefined`.
|
|
828
|
+
*
|
|
829
|
+
* Stencil only converts an attribute for a prop typed exactly `string`, `number` or `boolean`. Any
|
|
830
|
+
* other prop (`any`, which covers mixed unions like `string | boolean`, or `unknown`) gets the
|
|
831
|
+
* attribute's string as it is, so it's a ref: every value goes through `ssrProp()`. So is a prop
|
|
832
|
+
* with no attribute, which Stencil never reads.
|
|
833
|
+
*/
|
|
834
|
+
const LITERAL_ATTRIBUTE_TYPES = ['string', 'number', 'boolean'];
|
|
835
|
+
const getSsrInputs = (outputTarget, props) => {
|
|
836
|
+
if (!outputTarget.ssrPropModule)
|
|
837
|
+
return undefined;
|
|
838
|
+
const ssr = { attributes: {}, refs: [] };
|
|
839
|
+
for (const prop of props) {
|
|
840
|
+
if (!prop.attribute || !LITERAL_ATTRIBUTE_TYPES.includes(prop.type))
|
|
841
|
+
ssr.refs.push(prop.name);
|
|
842
|
+
else if (prop.attribute !== toDashCase(prop.name))
|
|
843
|
+
ssr.attributes[prop.name] = prop.attribute;
|
|
844
|
+
}
|
|
845
|
+
return ssr;
|
|
846
|
+
};
|
|
847
|
+
/**
|
|
848
|
+
* Whether a component's wrapper has inputs: its properties and virtual properties.
|
|
849
|
+
*/
|
|
850
|
+
const hasInputs = (cmp) => (cmp.properties ?? []).some(filterInternalProps) || (cmp.virtualProperties ?? []).length > 0;
|
|
851
|
+
/**
|
|
852
|
+
* The extra imports a proxy file needs for `ssrPropModule`.
|
|
853
|
+
*/
|
|
854
|
+
const getSsrImports = (outputTarget, components) => outputTarget.ssrPropModule && components.some(hasInputs)
|
|
855
|
+
? { componentLib: ['markServer'], source: `import { ssrProp } from '${outputTarget.ssrPropModule}';\n` }
|
|
856
|
+
: { componentLib: [], source: '' };
|
|
804
857
|
/**
|
|
805
858
|
* Whether a property declares an input transform.
|
|
806
859
|
*
|
|
@@ -949,7 +1002,8 @@ function generateProxies(components, pkgData, outputTarget, rootDir, config) {
|
|
|
949
1002
|
/**
|
|
950
1003
|
* The collection of named imports from the angular-component-lib/utils.
|
|
951
1004
|
*/
|
|
952
|
-
const
|
|
1005
|
+
const ssrImports = getSsrImports(outputTarget, components);
|
|
1006
|
+
const componentLibImports = ['ProxyCmp', ...ssrImports.componentLib];
|
|
953
1007
|
/**
|
|
954
1008
|
* The input transform lives in its own module so that it carries no runtime imports.
|
|
955
1009
|
*/
|
|
@@ -1017,7 +1071,7 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}${
|
|
|
1017
1071
|
* 2. Optionally the @NgModule decorated class (if includeSingleComponentAngularModules is true)
|
|
1018
1072
|
* 3. The component interface (using declaration merging for types).
|
|
1019
1073
|
*/
|
|
1020
|
-
const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, orderedInputs, methods, isCustomElementsBuild, isStandaloneBuild, inlineComponentProps, cmpMeta.events || []);
|
|
1074
|
+
const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, orderedInputs, methods, isCustomElementsBuild, isStandaloneBuild, inlineComponentProps, cmpMeta.events || [], getSsrInputs(outputTarget, internalProps));
|
|
1021
1075
|
const moduleDefinition = generateAngularModuleForComponent(cmpMeta.tagName);
|
|
1022
1076
|
const componentTypeDefinition = createComponentTypeDefinition(outputType, tagNameAsPascal, cmpMeta.events, componentCorePackage, customElementsDir, getEventTypesModule(config, outputTarget));
|
|
1023
1077
|
proxyFileOutput.push(componentDefinition, '\n');
|
|
@@ -1026,7 +1080,13 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}${
|
|
|
1026
1080
|
}
|
|
1027
1081
|
proxyFileOutput.push(componentTypeDefinition, '\n');
|
|
1028
1082
|
}
|
|
1029
|
-
const final = [
|
|
1083
|
+
const final = [
|
|
1084
|
+
imports,
|
|
1085
|
+
typeImports,
|
|
1086
|
+
sourceImports,
|
|
1087
|
+
...(ssrImports.source ? [ssrImports.source] : []),
|
|
1088
|
+
...proxyFileOutput,
|
|
1089
|
+
];
|
|
1030
1090
|
return final.join('\n') + '\n';
|
|
1031
1091
|
}
|
|
1032
1092
|
/**
|
|
@@ -1048,7 +1108,8 @@ function generateComponentProxy(cmpMeta, pkgData, outputTarget, rootDir, config)
|
|
|
1048
1108
|
angularCoreImports.push('NgModule');
|
|
1049
1109
|
}
|
|
1050
1110
|
const booleanAttributes = outputTarget.booleanAttributes === true;
|
|
1051
|
-
const
|
|
1111
|
+
const ssrImports = getSsrImports(outputTarget, [cmpMeta]);
|
|
1112
|
+
const componentLibImports = ['ProxyCmp', ...ssrImports.componentLib];
|
|
1052
1113
|
// The input transform lives in its own module so that it carries no runtime imports.
|
|
1053
1114
|
let transformImport = '';
|
|
1054
1115
|
if (usesInputTransform([cmpMeta], booleanAttributes)) {
|
|
@@ -1084,7 +1145,7 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}${
|
|
|
1084
1145
|
methods.push(...cmpMeta.methods.filter(filterInternalProps).map(mapPropName));
|
|
1085
1146
|
}
|
|
1086
1147
|
const inlineComponentProps = outputTarget.inlineProperties ? internalProps : [];
|
|
1087
|
-
const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, orderedInputs, methods, isCustomElementsBuild, isStandaloneBuild, inlineComponentProps, cmpMeta.events || []);
|
|
1148
|
+
const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, orderedInputs, methods, isCustomElementsBuild, isStandaloneBuild, inlineComponentProps, cmpMeta.events || [], getSsrInputs(outputTarget, internalProps));
|
|
1088
1149
|
const moduleDefinition = generateAngularModuleForComponent(cmpMeta.tagName);
|
|
1089
1150
|
const componentTypeDefinition = createComponentTypeDefinition(outputType, tagNameAsPascal, cmpMeta.events, componentCorePackage, customElementsDir, getEventTypesModule(config, outputTarget));
|
|
1090
1151
|
const proxyFileOutput = [componentDefinition, '\n'];
|
|
@@ -1092,7 +1153,13 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}${
|
|
|
1092
1153
|
proxyFileOutput.push(moduleDefinition, '\n');
|
|
1093
1154
|
}
|
|
1094
1155
|
proxyFileOutput.push(componentTypeDefinition, '\n');
|
|
1095
|
-
const final = [
|
|
1156
|
+
const final = [
|
|
1157
|
+
imports,
|
|
1158
|
+
typeImports,
|
|
1159
|
+
sourceImport,
|
|
1160
|
+
...(ssrImports.source ? [ssrImports.source] : []),
|
|
1161
|
+
...proxyFileOutput,
|
|
1162
|
+
];
|
|
1096
1163
|
return final.join('\n') + '\n';
|
|
1097
1164
|
}
|
|
1098
1165
|
/**
|
package/dist/index.js
CHANGED
|
@@ -211,9 +211,12 @@ function formatInputs(inputs) {
|
|
|
211
211
|
* @param standalone Whether to define the component as a standalone component.
|
|
212
212
|
* @param inlineComponentProps List of properties that should be inlined into the component definition.
|
|
213
213
|
* @param events The events of the Stencil component for generating outputs.
|
|
214
|
+
* @param ssr When set, the wrapper writes its inputs as attributes during server rendering.
|
|
215
|
+
* `attributes` maps the inputs whose attribute isn't the dash-cased prop name to that attribute,
|
|
216
|
+
* and `refs` lists the inputs whose every value goes through `ssrProp()`.
|
|
214
217
|
* @returns The component declaration as a string.
|
|
215
218
|
*/
|
|
216
|
-
const createAngularComponentDefinition = (tagName, inputs, methods, includeImportCustomElements = false, standalone = false, inlineComponentProps = [], events = []) => {
|
|
219
|
+
const createAngularComponentDefinition = (tagName, inputs, methods, includeImportCustomElements = false, standalone = false, inlineComponentProps = [], events = [], ssr) => {
|
|
217
220
|
const tagNameAsPascal = dashToPascalCase(tagName);
|
|
218
221
|
const outputs = events.filter((event) => !event.internal).map((event) => event.name);
|
|
219
222
|
const hasInputs = inputs.length > 0;
|
|
@@ -239,6 +242,15 @@ const createAngularComponentDefinition = (tagName, inputs, methods, includeImpor
|
|
|
239
242
|
if (hasMethods) {
|
|
240
243
|
proxyCmpOptions.push(`\n methods: [${formattedMethods}]`);
|
|
241
244
|
}
|
|
245
|
+
if (ssr && hasInputs) {
|
|
246
|
+
const ssrOptions = ['ssrProp'];
|
|
247
|
+
const attributes = Object.entries(ssr.attributes).map(([prop, attr]) => `'${prop}': '${attr}'`);
|
|
248
|
+
if (attributes.length)
|
|
249
|
+
ssrOptions.push(`attributes: { ${attributes.join(', ')} }`);
|
|
250
|
+
if (ssr.refs.length)
|
|
251
|
+
ssrOptions.push(`refs: [${formatToQuotedList(ssr.refs)}]`);
|
|
252
|
+
proxyCmpOptions.push(`\n ssr: { ${ssrOptions.join(', ')} }`);
|
|
253
|
+
}
|
|
242
254
|
let standaloneOption = '';
|
|
243
255
|
if (!standalone) {
|
|
244
256
|
standaloneOption = `\n standalone: false`;
|
|
@@ -275,7 +287,7 @@ export class ${tagNameAsPascal} {
|
|
|
275
287
|
${propertiesDeclarationText}
|
|
276
288
|
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
|
|
277
289
|
c.detach();
|
|
278
|
-
this.el = r.nativeElement;
|
|
290
|
+
this.el = r.nativeElement;${ssr && hasInputs ? '\n markServer(this);' : ''}
|
|
279
291
|
}
|
|
280
292
|
}`;
|
|
281
293
|
return output;
|
|
@@ -793,6 +805,47 @@ stencilSetTagTransformer(\${transformerArg});
|
|
|
793
805
|
const __filename = fileURLToPath(import.meta.url);
|
|
794
806
|
const __dirname = path.dirname(__filename);
|
|
795
807
|
const filterInternalProps = (prop) => !prop.internal;
|
|
808
|
+
/**
|
|
809
|
+
* Stencil's own prop name to attribute name conversion (`prop1` -> `prop-1`).
|
|
810
|
+
*/
|
|
811
|
+
const toDashCase = (str) => str
|
|
812
|
+
.replace(/([A-Z0-9])/g, (match) => ` ${match[0]}`)
|
|
813
|
+
.trim()
|
|
814
|
+
.split(' ')
|
|
815
|
+
.join('-')
|
|
816
|
+
.toLowerCase();
|
|
817
|
+
/**
|
|
818
|
+
* With `ssrPropModule` set, what the wrapper needs to write its inputs as attributes during
|
|
819
|
+
* server rendering. Otherwise `undefined`.
|
|
820
|
+
*
|
|
821
|
+
* Stencil only converts an attribute for a prop typed exactly `string`, `number` or `boolean`. Any
|
|
822
|
+
* other prop (`any`, which covers mixed unions like `string | boolean`, or `unknown`) gets the
|
|
823
|
+
* attribute's string as it is, so it's a ref: every value goes through `ssrProp()`. So is a prop
|
|
824
|
+
* with no attribute, which Stencil never reads.
|
|
825
|
+
*/
|
|
826
|
+
const LITERAL_ATTRIBUTE_TYPES = ['string', 'number', 'boolean'];
|
|
827
|
+
const getSsrInputs = (outputTarget, props) => {
|
|
828
|
+
if (!outputTarget.ssrPropModule)
|
|
829
|
+
return undefined;
|
|
830
|
+
const ssr = { attributes: {}, refs: [] };
|
|
831
|
+
for (const prop of props) {
|
|
832
|
+
if (!prop.attribute || !LITERAL_ATTRIBUTE_TYPES.includes(prop.type))
|
|
833
|
+
ssr.refs.push(prop.name);
|
|
834
|
+
else if (prop.attribute !== toDashCase(prop.name))
|
|
835
|
+
ssr.attributes[prop.name] = prop.attribute;
|
|
836
|
+
}
|
|
837
|
+
return ssr;
|
|
838
|
+
};
|
|
839
|
+
/**
|
|
840
|
+
* Whether a component's wrapper has inputs: its properties and virtual properties.
|
|
841
|
+
*/
|
|
842
|
+
const hasInputs = (cmp) => (cmp.properties ?? []).some(filterInternalProps) || (cmp.virtualProperties ?? []).length > 0;
|
|
843
|
+
/**
|
|
844
|
+
* The extra imports a proxy file needs for `ssrPropModule`.
|
|
845
|
+
*/
|
|
846
|
+
const getSsrImports = (outputTarget, components) => outputTarget.ssrPropModule && components.some(hasInputs)
|
|
847
|
+
? { componentLib: ['markServer'], source: `import { ssrProp } from '${outputTarget.ssrPropModule}';\n` }
|
|
848
|
+
: { componentLib: [], source: '' };
|
|
796
849
|
/**
|
|
797
850
|
* Whether a property declares an input transform.
|
|
798
851
|
*
|
|
@@ -941,7 +994,8 @@ function generateProxies(components, pkgData, outputTarget, rootDir, config) {
|
|
|
941
994
|
/**
|
|
942
995
|
* The collection of named imports from the angular-component-lib/utils.
|
|
943
996
|
*/
|
|
944
|
-
const
|
|
997
|
+
const ssrImports = getSsrImports(outputTarget, components);
|
|
998
|
+
const componentLibImports = ['ProxyCmp', ...ssrImports.componentLib];
|
|
945
999
|
/**
|
|
946
1000
|
* The input transform lives in its own module so that it carries no runtime imports.
|
|
947
1001
|
*/
|
|
@@ -1009,7 +1063,7 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}${
|
|
|
1009
1063
|
* 2. Optionally the @NgModule decorated class (if includeSingleComponentAngularModules is true)
|
|
1010
1064
|
* 3. The component interface (using declaration merging for types).
|
|
1011
1065
|
*/
|
|
1012
|
-
const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, orderedInputs, methods, isCustomElementsBuild, isStandaloneBuild, inlineComponentProps, cmpMeta.events || []);
|
|
1066
|
+
const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, orderedInputs, methods, isCustomElementsBuild, isStandaloneBuild, inlineComponentProps, cmpMeta.events || [], getSsrInputs(outputTarget, internalProps));
|
|
1013
1067
|
const moduleDefinition = generateAngularModuleForComponent(cmpMeta.tagName);
|
|
1014
1068
|
const componentTypeDefinition = createComponentTypeDefinition(outputType, tagNameAsPascal, cmpMeta.events, componentCorePackage, customElementsDir, getEventTypesModule(config, outputTarget));
|
|
1015
1069
|
proxyFileOutput.push(componentDefinition, '\n');
|
|
@@ -1018,7 +1072,13 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}${
|
|
|
1018
1072
|
}
|
|
1019
1073
|
proxyFileOutput.push(componentTypeDefinition, '\n');
|
|
1020
1074
|
}
|
|
1021
|
-
const final = [
|
|
1075
|
+
const final = [
|
|
1076
|
+
imports,
|
|
1077
|
+
typeImports,
|
|
1078
|
+
sourceImports,
|
|
1079
|
+
...(ssrImports.source ? [ssrImports.source] : []),
|
|
1080
|
+
...proxyFileOutput,
|
|
1081
|
+
];
|
|
1022
1082
|
return final.join('\n') + '\n';
|
|
1023
1083
|
}
|
|
1024
1084
|
/**
|
|
@@ -1040,7 +1100,8 @@ function generateComponentProxy(cmpMeta, pkgData, outputTarget, rootDir, config)
|
|
|
1040
1100
|
angularCoreImports.push('NgModule');
|
|
1041
1101
|
}
|
|
1042
1102
|
const booleanAttributes = outputTarget.booleanAttributes === true;
|
|
1043
|
-
const
|
|
1103
|
+
const ssrImports = getSsrImports(outputTarget, [cmpMeta]);
|
|
1104
|
+
const componentLibImports = ['ProxyCmp', ...ssrImports.componentLib];
|
|
1044
1105
|
// The input transform lives in its own module so that it carries no runtime imports.
|
|
1045
1106
|
let transformImport = '';
|
|
1046
1107
|
if (usesInputTransform([cmpMeta], booleanAttributes)) {
|
|
@@ -1076,7 +1137,7 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}${
|
|
|
1076
1137
|
methods.push(...cmpMeta.methods.filter(filterInternalProps).map(mapPropName));
|
|
1077
1138
|
}
|
|
1078
1139
|
const inlineComponentProps = outputTarget.inlineProperties ? internalProps : [];
|
|
1079
|
-
const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, orderedInputs, methods, isCustomElementsBuild, isStandaloneBuild, inlineComponentProps, cmpMeta.events || []);
|
|
1140
|
+
const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, orderedInputs, methods, isCustomElementsBuild, isStandaloneBuild, inlineComponentProps, cmpMeta.events || [], getSsrInputs(outputTarget, internalProps));
|
|
1080
1141
|
const moduleDefinition = generateAngularModuleForComponent(cmpMeta.tagName);
|
|
1081
1142
|
const componentTypeDefinition = createComponentTypeDefinition(outputType, tagNameAsPascal, cmpMeta.events, componentCorePackage, customElementsDir, getEventTypesModule(config, outputTarget));
|
|
1082
1143
|
const proxyFileOutput = [componentDefinition, '\n'];
|
|
@@ -1084,7 +1145,13 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}${
|
|
|
1084
1145
|
proxyFileOutput.push(moduleDefinition, '\n');
|
|
1085
1146
|
}
|
|
1086
1147
|
proxyFileOutput.push(componentTypeDefinition, '\n');
|
|
1087
|
-
const final = [
|
|
1148
|
+
const final = [
|
|
1149
|
+
imports,
|
|
1150
|
+
typeImports,
|
|
1151
|
+
sourceImport,
|
|
1152
|
+
...(ssrImports.source ? [ssrImports.source] : []),
|
|
1153
|
+
...proxyFileOutput,
|
|
1154
|
+
];
|
|
1088
1155
|
return final.join('\n') + '\n';
|
|
1089
1156
|
}
|
|
1090
1157
|
/**
|
package/dist/output-angular.js
CHANGED
|
@@ -9,6 +9,47 @@ import generateValueAccessors from './generate-value-accessors';
|
|
|
9
9
|
import { generateAngularModuleForComponent } from './generate-angular-modules';
|
|
10
10
|
import { generateTransformTagScript } from './generate-transformtag-script';
|
|
11
11
|
const filterInternalProps = (prop) => !prop.internal;
|
|
12
|
+
/**
|
|
13
|
+
* Stencil's own prop name to attribute name conversion (`prop1` -> `prop-1`).
|
|
14
|
+
*/
|
|
15
|
+
const toDashCase = (str) => str
|
|
16
|
+
.replace(/([A-Z0-9])/g, (match) => ` ${match[0]}`)
|
|
17
|
+
.trim()
|
|
18
|
+
.split(' ')
|
|
19
|
+
.join('-')
|
|
20
|
+
.toLowerCase();
|
|
21
|
+
/**
|
|
22
|
+
* With `ssrPropModule` set, what the wrapper needs to write its inputs as attributes during
|
|
23
|
+
* server rendering. Otherwise `undefined`.
|
|
24
|
+
*
|
|
25
|
+
* Stencil only converts an attribute for a prop typed exactly `string`, `number` or `boolean`. Any
|
|
26
|
+
* other prop (`any`, which covers mixed unions like `string | boolean`, or `unknown`) gets the
|
|
27
|
+
* attribute's string as it is, so it's a ref: every value goes through `ssrProp()`. So is a prop
|
|
28
|
+
* with no attribute, which Stencil never reads.
|
|
29
|
+
*/
|
|
30
|
+
const LITERAL_ATTRIBUTE_TYPES = ['string', 'number', 'boolean'];
|
|
31
|
+
const getSsrInputs = (outputTarget, props) => {
|
|
32
|
+
if (!outputTarget.ssrPropModule)
|
|
33
|
+
return undefined;
|
|
34
|
+
const ssr = { attributes: {}, refs: [] };
|
|
35
|
+
for (const prop of props) {
|
|
36
|
+
if (!prop.attribute || !LITERAL_ATTRIBUTE_TYPES.includes(prop.type))
|
|
37
|
+
ssr.refs.push(prop.name);
|
|
38
|
+
else if (prop.attribute !== toDashCase(prop.name))
|
|
39
|
+
ssr.attributes[prop.name] = prop.attribute;
|
|
40
|
+
}
|
|
41
|
+
return ssr;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Whether a component's wrapper has inputs: its properties and virtual properties.
|
|
45
|
+
*/
|
|
46
|
+
const hasInputs = (cmp) => (cmp.properties ?? []).some(filterInternalProps) || (cmp.virtualProperties ?? []).length > 0;
|
|
47
|
+
/**
|
|
48
|
+
* The extra imports a proxy file needs for `ssrPropModule`.
|
|
49
|
+
*/
|
|
50
|
+
const getSsrImports = (outputTarget, components) => outputTarget.ssrPropModule && components.some(hasInputs)
|
|
51
|
+
? { componentLib: ['markServer'], source: `import { ssrProp } from '${outputTarget.ssrPropModule}';\n` }
|
|
52
|
+
: { componentLib: [], source: '' };
|
|
12
53
|
/**
|
|
13
54
|
* Whether a property declares an input transform.
|
|
14
55
|
*
|
|
@@ -157,7 +198,8 @@ export function generateProxies(components, pkgData, outputTarget, rootDir, conf
|
|
|
157
198
|
/**
|
|
158
199
|
* The collection of named imports from the angular-component-lib/utils.
|
|
159
200
|
*/
|
|
160
|
-
const
|
|
201
|
+
const ssrImports = getSsrImports(outputTarget, components);
|
|
202
|
+
const componentLibImports = ['ProxyCmp', ...ssrImports.componentLib];
|
|
161
203
|
/**
|
|
162
204
|
* The input transform lives in its own module so that it carries no runtime imports.
|
|
163
205
|
*/
|
|
@@ -225,7 +267,7 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}${
|
|
|
225
267
|
* 2. Optionally the @NgModule decorated class (if includeSingleComponentAngularModules is true)
|
|
226
268
|
* 3. The component interface (using declaration merging for types).
|
|
227
269
|
*/
|
|
228
|
-
const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, orderedInputs, methods, isCustomElementsBuild, isStandaloneBuild, inlineComponentProps, cmpMeta.events || []);
|
|
270
|
+
const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, orderedInputs, methods, isCustomElementsBuild, isStandaloneBuild, inlineComponentProps, cmpMeta.events || [], getSsrInputs(outputTarget, internalProps));
|
|
229
271
|
const moduleDefinition = generateAngularModuleForComponent(cmpMeta.tagName);
|
|
230
272
|
const componentTypeDefinition = createComponentTypeDefinition(outputType, tagNameAsPascal, cmpMeta.events, componentCorePackage, customElementsDir, getEventTypesModule(config, outputTarget));
|
|
231
273
|
proxyFileOutput.push(componentDefinition, '\n');
|
|
@@ -234,7 +276,13 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}${
|
|
|
234
276
|
}
|
|
235
277
|
proxyFileOutput.push(componentTypeDefinition, '\n');
|
|
236
278
|
}
|
|
237
|
-
const final = [
|
|
279
|
+
const final = [
|
|
280
|
+
imports,
|
|
281
|
+
typeImports,
|
|
282
|
+
sourceImports,
|
|
283
|
+
...(ssrImports.source ? [ssrImports.source] : []),
|
|
284
|
+
...proxyFileOutput,
|
|
285
|
+
];
|
|
238
286
|
return final.join('\n') + '\n';
|
|
239
287
|
}
|
|
240
288
|
/**
|
|
@@ -256,7 +304,8 @@ export function generateComponentProxy(cmpMeta, pkgData, outputTarget, rootDir,
|
|
|
256
304
|
angularCoreImports.push('NgModule');
|
|
257
305
|
}
|
|
258
306
|
const booleanAttributes = outputTarget.booleanAttributes === true;
|
|
259
|
-
const
|
|
307
|
+
const ssrImports = getSsrImports(outputTarget, [cmpMeta]);
|
|
308
|
+
const componentLibImports = ['ProxyCmp', ...ssrImports.componentLib];
|
|
260
309
|
// The input transform lives in its own module so that it carries no runtime imports.
|
|
261
310
|
let transformImport = '';
|
|
262
311
|
if (usesInputTransform([cmpMeta], booleanAttributes)) {
|
|
@@ -292,7 +341,7 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}${
|
|
|
292
341
|
methods.push(...cmpMeta.methods.filter(filterInternalProps).map(mapPropName));
|
|
293
342
|
}
|
|
294
343
|
const inlineComponentProps = outputTarget.inlineProperties ? internalProps : [];
|
|
295
|
-
const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, orderedInputs, methods, isCustomElementsBuild, isStandaloneBuild, inlineComponentProps, cmpMeta.events || []);
|
|
344
|
+
const componentDefinition = createAngularComponentDefinition(cmpMeta.tagName, orderedInputs, methods, isCustomElementsBuild, isStandaloneBuild, inlineComponentProps, cmpMeta.events || [], getSsrInputs(outputTarget, internalProps));
|
|
296
345
|
const moduleDefinition = generateAngularModuleForComponent(cmpMeta.tagName);
|
|
297
346
|
const componentTypeDefinition = createComponentTypeDefinition(outputType, tagNameAsPascal, cmpMeta.events, componentCorePackage, customElementsDir, getEventTypesModule(config, outputTarget));
|
|
298
347
|
const proxyFileOutput = [componentDefinition, '\n'];
|
|
@@ -300,7 +349,13 @@ ${createImportStatement(componentLibImports, './angular-component-lib/utils')}${
|
|
|
300
349
|
proxyFileOutput.push(moduleDefinition, '\n');
|
|
301
350
|
}
|
|
302
351
|
proxyFileOutput.push(componentTypeDefinition, '\n');
|
|
303
|
-
const final = [
|
|
352
|
+
const final = [
|
|
353
|
+
imports,
|
|
354
|
+
typeImports,
|
|
355
|
+
sourceImport,
|
|
356
|
+
...(ssrImports.source ? [ssrImports.source] : []),
|
|
357
|
+
...proxyFileOutput,
|
|
358
|
+
];
|
|
304
359
|
return final.join('\n') + '\n';
|
|
305
360
|
}
|
|
306
361
|
/**
|
package/dist/types.d.ts
CHANGED
|
@@ -51,6 +51,20 @@ export interface OutputTargetAngular {
|
|
|
51
51
|
* to type-check and show jsdocs when using the components in html-templates.
|
|
52
52
|
*/
|
|
53
53
|
inlineProperties?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* The component library's `ssr-prop` module, e.g. `'component-library/ssr-prop'`, from the
|
|
56
|
+
* `ssr` output target. Set it when apps render the wrappers with Angular SSR and pass the page
|
|
57
|
+
* through the library's `renderToString()`.
|
|
58
|
+
*
|
|
59
|
+
* Angular's server DOM never writes properties to the HTML, so `[value]="..."` bindings would
|
|
60
|
+
* be missing from the server render. With this set, a wrapper rendering on the server writes
|
|
61
|
+
* each input into the element's attributes instead. For a prop typed `string`, `number` or
|
|
62
|
+
* `boolean`, a string or number is written as is and `true` as an empty attribute. Everything
|
|
63
|
+
* else goes through `ssrProp()`: `false`, objects, arrays, functions, and any value for a prop
|
|
64
|
+
* of another type or with no attribute. In the browser, inputs are still set as properties.
|
|
65
|
+
* See the README's "Server-side rendering" section.
|
|
66
|
+
*/
|
|
67
|
+
ssrPropModule?: string;
|
|
54
68
|
/**
|
|
55
69
|
* Use `transformTag` to generate a build-time script.
|
|
56
70
|
* This script patches Angular `@Component` selectors in any installed Angular wrapper library
|
package/dist/wizard.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { join, relative, dirname } from 'node:path';
|
|
2
|
+
import { join, relative, dirname, isAbsolute } from 'node:path';
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
// pnpm build approval
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
@@ -77,7 +77,22 @@ function toPascalCase(s) {
|
|
|
77
77
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
78
78
|
.join('');
|
|
79
79
|
}
|
|
80
|
-
|
|
80
|
+
/**
|
|
81
|
+
* The module the lazy loader's `defineCustomElements()` is imported from: the `./loader` entry
|
|
82
|
+
* when the project generates an `exports` map, otherwise its place in the `loader-bundle` output.
|
|
83
|
+
*/
|
|
84
|
+
function getLoaderModule(config, corePackageName) {
|
|
85
|
+
if (config.generateExportMaps === true) {
|
|
86
|
+
return `${corePackageName}/loader`;
|
|
87
|
+
}
|
|
88
|
+
const target = config.outputTargets?.find((o) => o.type === 'loader-bundle');
|
|
89
|
+
const rawDir = target?.dir ?? 'dist/loader-bundle';
|
|
90
|
+
const dir = (isAbsolute(rawDir) ? relative(config.rootDir, rawDir) : rawDir).replace(/\\/g, '/');
|
|
91
|
+
return `${corePackageName}/${dir}/${target?.loaderPath || 'loader'}`;
|
|
92
|
+
}
|
|
93
|
+
async function scaffoldWrapperPackage(wrapperDir, packageName, corePackageName, corePkgVersion, namespace, outputType,
|
|
94
|
+
/** For the 'component' type: the module to import the loader's `defineCustomElements()` from. */
|
|
95
|
+
loaderModule) {
|
|
81
96
|
const libDir = join(wrapperDir, 'src', 'lib');
|
|
82
97
|
await mkdir(libDir, { recursive: true });
|
|
83
98
|
const className = toPascalCase(namespace);
|
|
@@ -125,12 +140,23 @@ async function scaffoldWrapperPackage(wrapperDir, packageName, corePackageName,
|
|
|
125
140
|
// NgModule file — only needed for 'component' outputType
|
|
126
141
|
let ngModuleTs;
|
|
127
142
|
if (outputType === 'component') {
|
|
143
|
+
// The 'component' type's wrappers register nothing themselves: this module does it, with the
|
|
144
|
+
// lazy loader, when the consuming app starts. It's written once and never regenerated.
|
|
128
145
|
ngModuleTs =
|
|
129
|
-
`import { NgModule } from '@angular/core';\n` +
|
|
146
|
+
`import { NgModule, provideAppInitializer } from '@angular/core';\n` +
|
|
147
|
+
`import { defineCustomElements } from '${loaderModule}';\n` +
|
|
130
148
|
`import { DIRECTIVES } from './directives.array';\n\n` +
|
|
149
|
+
`/**\n` +
|
|
150
|
+
` * Declares every component, and registers the custom elements when the app starts.\n` +
|
|
151
|
+
` *\n` +
|
|
152
|
+
` * This file is yours to edit - Stencil doesn't regenerate it. Do any setup your library\n` +
|
|
153
|
+
` * needs before the components are defined in the initializer, e.g. \`setNonce()\` or\n` +
|
|
154
|
+
` * \`setTagTransformer()\` (both exported by the loader).\n` +
|
|
155
|
+
` */\n` +
|
|
131
156
|
`@NgModule({\n` +
|
|
132
157
|
` declarations: [...DIRECTIVES],\n` +
|
|
133
158
|
` exports: [...DIRECTIVES],\n` +
|
|
159
|
+
` providers: [provideAppInitializer(() => defineCustomElements())],\n` +
|
|
134
160
|
`})\n` +
|
|
135
161
|
`export class ${className}Module {}\n`;
|
|
136
162
|
}
|
|
@@ -240,7 +266,7 @@ export const wizard = {
|
|
|
240
266
|
const corePkgVersion = supportsWorkspaceProtocol
|
|
241
267
|
? 'workspace:*'
|
|
242
268
|
: `file:${relative(wrapperDir, config.rootDir)}`;
|
|
243
|
-
await scaffoldWrapperPackage(wrapperDir, wrapperPackageName, componentCorePackage, corePkgVersion, config.fsNamespace, selectedOutputType);
|
|
269
|
+
await scaffoldWrapperPackage(wrapperDir, wrapperPackageName, componentCorePackage, corePkgVersion, config.fsNamespace, selectedOutputType, selectedOutputType === 'component' ? getLoaderModule(config, componentCorePackage) : undefined);
|
|
244
270
|
s.stop('Wrapper package scaffolded');
|
|
245
271
|
}
|
|
246
272
|
catch (e) {
|
|
@@ -263,7 +289,8 @@ export const wizard = {
|
|
|
263
289
|
// Build target config
|
|
264
290
|
const lines = [
|
|
265
291
|
`componentCorePackage: '${componentCorePackage}'`,
|
|
266
|
-
`
|
|
292
|
+
// only the types built on the `standalone` output import from it
|
|
293
|
+
...(selectedOutputType !== 'component' ? [`customElementsDir: '${customElementsDir}'`] : []),
|
|
267
294
|
`directivesProxyFile: '${directivesProxyFile}'`,
|
|
268
295
|
...(directivesArrayFile ? [`directivesArrayFile: '${directivesArrayFile}'`] : []),
|
|
269
296
|
...(selectedOutputType !== 'standalone' ? [`outputType: '${selectedOutputType}'`] : []),
|
|
@@ -276,8 +303,18 @@ export const wizard = {
|
|
|
276
303
|
editor.outputTargetsContains('type: "standalone"') ||
|
|
277
304
|
editor.outputTargetsContains("type: 'dist-custom-elements'") ||
|
|
278
305
|
editor.outputTargetsContains('type: "dist-custom-elements"');
|
|
279
|
-
|
|
306
|
+
const hasLoaderBundle = editor.outputTargetsContains("type: 'loader-bundle'") ||
|
|
307
|
+
editor.outputTargetsContains('type: "loader-bundle"') ||
|
|
308
|
+
editor.outputTargetsContains("type: 'dist'") ||
|
|
309
|
+
editor.outputTargetsContains('type: "dist"');
|
|
310
|
+
// 'component' wrappers rely on the lazy loader; the other types import from `standalone`
|
|
311
|
+
if (selectedOutputType === 'component') {
|
|
312
|
+
if (!hasLoaderBundle)
|
|
313
|
+
editor.addOutputTarget("{ type: 'loader-bundle' }");
|
|
314
|
+
}
|
|
315
|
+
else if (!hasStandalone) {
|
|
280
316
|
editor.addOutputTarget("{ type: 'standalone' }");
|
|
317
|
+
}
|
|
281
318
|
if (!editor.replaceOutputTarget('angularOutputTarget(', targetCode)) {
|
|
282
319
|
editor.addOutputTarget(targetCode);
|
|
283
320
|
}
|
|
@@ -306,6 +343,10 @@ export const wizard = {
|
|
|
306
343
|
], { cwd: wrapperDir, dev: true });
|
|
307
344
|
log.success('Angular dependencies installed');
|
|
308
345
|
}
|
|
346
|
+
if (selectedOutputType === 'component' && !shouldScaffold) {
|
|
347
|
+
log.info(`NgModule components: your Angular package's module needs to call defineCustomElements() ` +
|
|
348
|
+
`from '${getLoaderModule(config, componentCorePackage)}' when the app starts, e.g. with provideAppInitializer().`);
|
|
349
|
+
}
|
|
309
350
|
outro('Angular output target configured');
|
|
310
351
|
},
|
|
311
352
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stencil/angular-output-target",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Angular output target for @stencil/core components.",
|
|
5
5
|
"main": "dist/index.cjs",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"npm-run-all2": "^6.2.6",
|
|
65
65
|
"rimraf": "^5.0.10",
|
|
66
66
|
"rollup": "^2.80.0",
|
|
67
|
+
"rxjs": "^7.8.2",
|
|
67
68
|
"stencil-output-targets-shared": "workspace:*",
|
|
68
69
|
"typescript": "~5.7.3",
|
|
69
70
|
"vitest": "^2.1.9"
|