Skip to content

Commit 093015d

Browse files
authored
feat(template-compiler): dynamic components (#3337)
1 parent 0666b16 commit 093015d

File tree

231 files changed

+9330
-133
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

231 files changed

+9330
-133
lines changed

packages/@lwc/compiler/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export {
1111
TransformOptions,
1212
StylesheetConfig,
1313
CustomPropertiesResolution,
14-
DynamicComponentConfig,
14+
DynamicImportConfig,
1515
OutputConfig,
1616
} from './options';
1717

packages/@lwc/compiler/src/options.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const DEFAULT_OPTIONS = {
1919
disableSyntheticShadowSupport: false,
2020
};
2121

22-
const DEFAULT_DYNAMIC_CMP_CONFIG: Required<DynamicComponentConfig> = {
22+
const DEFAULT_DYNAMIC_IMPORT_CONFIG: Required<DynamicImportConfig> = {
2323
loader: '',
2424
strictSpecifier: true,
2525
};
@@ -56,7 +56,7 @@ export interface OutputConfig {
5656
minify?: boolean;
5757
}
5858

59-
export interface DynamicComponentConfig {
59+
export interface DynamicImportConfig {
6060
loader?: string;
6161
strictSpecifier?: boolean;
6262
}
@@ -65,7 +65,13 @@ export interface TransformOptions {
6565
name?: string;
6666
namespace?: string;
6767
stylesheetConfig?: StylesheetConfig;
68-
experimentalDynamicComponent?: DynamicComponentConfig;
68+
// TODO [#3331]: deprecate / rename this compiler option in 246
69+
/* Config applied in usage of dynamic import statements in javascript */
70+
experimentalDynamicComponent?: DynamicImportConfig;
71+
/* Flag to enable usage of dynamic component(lwc:dynamic) directive in HTML template */
72+
experimentalDynamicDirective?: boolean;
73+
/* Flag to enable usage of dynamic component(lwc:is) directive in HTML template */
74+
enableDynamicComponents?: boolean;
6975
outputConfig?: OutputConfig;
7076
isExplicitImport?: boolean;
7177
preserveHtmlComments?: boolean;
@@ -85,6 +91,9 @@ type RequiredTransformOptions = Omit<
8591
| 'customRendererConfig'
8692
| 'enableLwcSpread'
8793
| 'enableScopedSlots'
94+
| 'enableDynamicComponents'
95+
| 'experimentalDynamicDirective'
96+
| 'experimentalDynamicComponent'
8897
>;
8998
export interface NormalizedTransformOptions extends RecursiveRequired<RequiredTransformOptions> {
9099
name?: string;
@@ -93,6 +102,9 @@ export interface NormalizedTransformOptions extends RecursiveRequired<RequiredTr
93102
customRendererConfig?: CustomRendererConfig;
94103
enableLwcSpread?: boolean;
95104
enableScopedSlots?: boolean;
105+
enableDynamicComponents?: boolean;
106+
experimentalDynamicDirective?: boolean;
107+
experimentalDynamicComponent?: DynamicImportConfig;
96108
}
97109

98110
export function validateTransformOptions(options: TransformOptions): NormalizedTransformOptions {
@@ -165,8 +177,8 @@ function normalizeOptions(options: TransformOptions): NormalizedTransformOptions
165177
},
166178
};
167179

168-
const experimentalDynamicComponent: Required<DynamicComponentConfig> = {
169-
...DEFAULT_DYNAMIC_CMP_CONFIG,
180+
const experimentalDynamicComponent: Required<DynamicImportConfig> = {
181+
...DEFAULT_DYNAMIC_IMPORT_CONFIG,
170182
...options.experimentalDynamicComponent,
171183
};
172184

packages/@lwc/compiler/src/transformers/__tests__/transform-html.spec.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,69 @@ describe('transformSync', () => {
123123
expect(warnings).toHaveLength(0);
124124
expect(code).not.toMatch('renderer: renderer');
125125
});
126+
127+
describe('dynamic components', () => {
128+
it('should allow dynamic components when enableDynamicComponents is set to true', () => {
129+
const template = `
130+
<template>
131+
<lwc:component lwc:is={ctor}></lwc:component>
132+
</template>
133+
`;
134+
const { code, warnings } = transformSync(template, 'foo.html', {
135+
enableDynamicComponents: true,
136+
...TRANSFORMATION_OPTIONS,
137+
});
138+
139+
expect(warnings).toHaveLength(0);
140+
expect(code).toContain('api_dynamic_component');
141+
});
142+
143+
it('should not allow dynamic components when enableDynamicComponents is set to false', () => {
144+
const template = `
145+
<template>
146+
<lwc:component lwc:is={ctor}></lwc:component>
147+
</template>
148+
`;
149+
expect(() =>
150+
transformSync(template, 'foo.html', {
151+
enableDynamicComponents: false,
152+
...TRANSFORMATION_OPTIONS,
153+
})
154+
).toThrow('LWC1188: Invalid dynamic components usage');
155+
});
156+
157+
it('should allow deprecated dynamic components when experimentalDynamicDirective is set to true', () => {
158+
const template = `
159+
<template>
160+
<x-dynamic lwc:dynamic={ctor}></x-dynamic>
161+
</template>
162+
`;
163+
const { code, warnings } = transformSync(template, 'foo.html', {
164+
experimentalDynamicDirective: true,
165+
...TRANSFORMATION_OPTIONS,
166+
});
167+
168+
expect(warnings).toHaveLength(1);
169+
expect(warnings?.[0]).toMatchObject({
170+
message: expect.stringContaining('lwc:dynamic directive is deprecated'),
171+
});
172+
expect(code).toContain('api_deprecated_dynamic_component');
173+
});
174+
175+
it('should not allow dynamic components when experimentalDynamicDirective is set to false', () => {
176+
const template = `
177+
<template>
178+
<x-dynamic lwc:dynamic={ctor}></x-dynamic>
179+
</template>
180+
`;
181+
expect(() =>
182+
transformSync(template, 'foo.html', {
183+
experimentalDynamicDirective: false,
184+
...TRANSFORMATION_OPTIONS,
185+
})
186+
).toThrowErrorMatchingInlineSnapshot(
187+
'"LWC1128: Invalid lwc:dynamic usage. The LWC dynamic directive must be enabled in order to use this feature."'
188+
);
189+
});
190+
});
126191
});

packages/@lwc/compiler/src/transformers/javascript.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export default function scriptTransform(
3636
babelrc: false,
3737
configFile: false,
3838

39-
// Force Babel to generate new line and whitespaces. This prevent Babel from generating
39+
// Force Babel to generate new line and white spaces. This prevent Babel from generating
4040
// an error when the generated code is over 500KB.
4141
compact: false,
4242

packages/@lwc/compiler/src/transformers/template.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,11 @@ export default function templateTransform(
3434
customRendererConfig,
3535
enableLwcSpread,
3636
enableScopedSlots,
37+
enableDynamicComponents,
38+
experimentalDynamicDirective: deprecatedDynamicDirective,
3739
} = options;
38-
const experimentalDynamicDirective = Boolean(experimentalDynamicComponent);
40+
const experimentalDynamicDirective =
41+
deprecatedDynamicDirective ?? Boolean(experimentalDynamicComponent);
3942

4043
let result;
4144
try {
@@ -46,6 +49,7 @@ export default function templateTransform(
4649
customRendererConfig,
4750
enableLwcSpread,
4851
enableScopedSlots,
52+
enableDynamicComponents,
4953
});
5054
} catch (e) {
5155
throw normalizeToCompilerError(TransformerErrors.HTML_TRANSFORMER_ERROR, e, { filename });

packages/@lwc/engine-core/src/framework/api.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -533,9 +533,11 @@ function fid(url: string | undefined | null): string | null | undefined {
533533
}
534534

535535
/**
536-
* create a dynamic component via `<x-foo lwc:dynamic={Ctor}>`
536+
* [ddc] - create a (deprecated) dynamic component via `<x-foo lwc:dynamic={Ctor}>`
537+
*
538+
* TODO [#3331]: remove usage of lwc:dynamic in 246
537539
*/
538-
function dc(
540+
function ddc(
539541
sel: string,
540542
Ctor: LightningElementConstructor | null | undefined,
541543
data: VElementData,
@@ -550,7 +552,7 @@ function dc(
550552
);
551553
}
552554
// null or undefined values should produce a null value in the VNodes
553-
if (Ctor == null) {
555+
if (isNull(Ctor) || isUndefined(Ctor)) {
554556
return null;
555557
}
556558
if (!isComponentConstructor(Ctor)) {
@@ -560,6 +562,35 @@ function dc(
560562
return c(sel, Ctor, data, children);
561563
}
562564

565+
/**
566+
* [dc] - create a dynamic component via `<lwc:component lwc:is={Ctor}>`
567+
*/
568+
function dc(
569+
Ctor: LightningElementConstructor | null | undefined,
570+
data: VElementData,
571+
children: VNodes = EmptyArray
572+
): VCustomElement | null {
573+
if (process.env.NODE_ENV !== 'production') {
574+
assert.isTrue(isObject(data), `dc() 2nd argument data must be an object.`);
575+
assert.isTrue(
576+
arguments.length === 3 || isArray(children),
577+
`dc() 3rd argument data must be an array.`
578+
);
579+
}
580+
// null or undefined values should produce a null value in the VNodes
581+
if (isNull(Ctor) || isUndefined(Ctor)) {
582+
return null;
583+
}
584+
585+
if (!isComponentConstructor(Ctor)) {
586+
throw new Error(
587+
`Invalid constructor ${toString(Ctor)} is not a LightningElement constructor.`
588+
);
589+
}
590+
591+
return null;
592+
}
593+
563594
/**
564595
* slow children collection marking mechanism. this API allows the compiler to signal
565596
* to the engine that a particular collection of children must be diffed using the slow
@@ -628,6 +659,7 @@ const api = ObjectFreeze({
628659
fid,
629660
shc,
630661
ssf,
662+
ddc,
631663
});
632664

633665
export default api;

packages/@lwc/engine-server/src/__tests__/fixtures.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import fs from 'fs';
99
import path from 'path';
1010

11-
import { rollup } from 'rollup';
11+
import { rollup, RollupWarning } from 'rollup';
1212
// @ts-ignore
1313
import lwcRollupPlugin from '@lwc/rollup-plugin';
1414
import { isVoidElement, HTML_NAMESPACE } from '@lwc/shared';
@@ -29,6 +29,8 @@ jest.setTimeout(10_000 /* 10 seconds */);
2929
async function compileFixture({ input, dirname }: { input: string; dirname: string }) {
3030
const modulesDir = path.resolve(dirname, './modules');
3131
const outputFile = path.resolve(dirname, './dist/compiled.js');
32+
// TODO [#3331]: this is only needed to silence warnings on lwc:dynamic, remove in 246.
33+
const warnings: RollupWarning[] = [];
3234

3335
const bundle = await rollup({
3436
input,
@@ -43,6 +45,9 @@ async function compileFixture({ input, dirname }: { input: string; dirname: stri
4345
],
4446
}),
4547
],
48+
onwarn(warning) {
49+
warnings.push(warning);
50+
},
4651
});
4752

4853
await bundle.write({

packages/@lwc/errors/src/compiler/error-info/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
66
*/
77
/**
8-
* Next error code: 1183
8+
* Next error code: 1189
99
*/
1010

1111
export * from './compiler';

packages/@lwc/errors/src/compiler/error-info/template-transform.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,7 @@ export const ParserDiagnostics = {
471471
INVALID_OPTS_LWC_DYNAMIC: {
472472
code: 1128,
473473
message:
474-
'Invalid lwc:dynamic usage. The LWC dynamic Directive must be enabled in order to use this feature.',
474+
'Invalid lwc:dynamic usage. The LWC dynamic directive must be enabled in order to use this feature.',
475475
level: DiagnosticLevel.Error,
476476
url: '',
477477
},
@@ -548,7 +548,7 @@ export const ParserDiagnostics = {
548548
LWC_INNER_HTML_INVALID_CUSTOM_ELEMENT: {
549549
code: 1140,
550550
message:
551-
'Invalid lwc:inner-html usage on element "{0}". The directive can\'t be used on a custom element.',
551+
'Invalid lwc:inner-html usage on element "{0}". The directive can\'t be used on a custom element or special LWC managed elements denoted with lwc:*.',
552552
level: DiagnosticLevel.Error,
553553
url: '',
554554
},
@@ -846,4 +846,49 @@ export const ParserDiagnostics = {
846846
level: DiagnosticLevel.Warning,
847847
url: '',
848848
},
849+
850+
LWC_COMPONENT_TAG_WITHOUT_IS_DIRECTIVE: {
851+
code: 1183,
852+
message: `<lwc:component> must have an 'lwc:is' attribute.`,
853+
level: DiagnosticLevel.Error,
854+
url: '',
855+
},
856+
857+
UNSUPPORTED_LWC_TAG_NAME: {
858+
code: 1184,
859+
message: '{0} is not a special LWC tag name and will be treated as an HTML element.',
860+
level: DiagnosticLevel.Warning,
861+
url: '',
862+
},
863+
864+
INVALID_LWC_IS_DIRECTIVE_VALUE: {
865+
code: 1185,
866+
message:
867+
'Invalid lwc:is usage for value {0}. The value assigned to lwc:is must be an expression.',
868+
level: DiagnosticLevel.Error,
869+
url: '',
870+
},
871+
872+
LWC_IS_INVALID_ELEMENT: {
873+
code: 1186,
874+
message:
875+
'Invalid lwc:is usage for element {0}. The directive can only be used with <lwc:component>',
876+
level: DiagnosticLevel.Error,
877+
url: '',
878+
},
879+
880+
DEPRECATED_LWC_DYNAMIC_ATTRIBUTE: {
881+
code: 1187,
882+
message: `The lwc:dynamic directive is deprecated and will be removed in a future release. Please use lwc:is instead.`,
883+
level: DiagnosticLevel.Warning,
884+
url: '',
885+
},
886+
887+
INVALID_OPTS_LWC_ENABLE_DYNAMIC_COMPONENTS: {
888+
code: 1188,
889+
message:
890+
'Invalid dynamic components usage, lwc:component and lwc:is can only be used when dynamic components have been enabled.',
891+
level: DiagnosticLevel.Error,
892+
url: '',
893+
},
849894
};

packages/@lwc/rollup-plugin/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ export default {
2929
- `modules` (type: `ModuleRecord[]`, default: `[]`) - The [module resolution](https://lwc.dev/guide/es_modules#module-resolution) overrides passed to the `@lwc/module-resolver`.
3030
- `stylesheetConfig` (type: `object`, default: `{}`) - The stylesheet compiler configuration to pass to the `@lwc/style-compiler`.
3131
- `preserveHtmlComments` (type: `boolean`, default: `false`) - The configuration to pass to the `@lwc/template-compiler`.
32-
- `experimentalDynamicComponent` (type: `DynamicComponentConfig`, default: `null`) - The configuration to pass to `@lwc/compiler`.
32+
- `experimentalDynamicComponent` (type: `DynamicImportConfig`, default: `null`) - The configuration to pass to `@lwc/compiler`.
33+
- `experimentalDynamicDirective` (type: `boolean`, default: `false`) - The configuration to pass to `@lwc/template-compiler` to enable deprecated dynamic components.
34+
- `enableDynamicComponents` (type: `boolean`, default: `false`) - The configuration to pass to `@lwc/template-compiler` to enable dynamic components.
3335
- `enableLwcSpread` (type: `boolean`, default: `false`) - The configuration to pass to the `@lwc/template-compiler`.
3436
- `enableScopedSlots` (type: `boolean`, default: `false`) - The configuration to pass to `@lwc/template-compiler` to enable scoped slots feature.
3537
- `disableSyntheticShadowSupport` (type: `boolean`, default: `false`) - Set to true if synthetic shadow DOM support is not needed, which can result in smaller output.

0 commit comments

Comments
 (0)