Skip to content

Commit 00383bf

Browse files
tanner-reitsrwaskiewicz
authored andcommitted
feat(compiler): add CustomElementExportBehavior to custom elements … (#3562)
This commit adds a config option to the `dist-custom-elements` output target that will control the behavior for re-exporting class definitions and (eventually) defining them as custom elements. Essentially, when this option is not set, this output target will revert to the previous export behavior from v2.16. Start of addressing STENCIL-500
1 parent 03cfdfb commit 00383bf

File tree

7 files changed

+281
-66
lines changed

7 files changed

+281
-66
lines changed

src/compiler/config/outputs/validate-custom-element.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ export const validateCustomElement = (
4040
if (!isBoolean(outputTarget.generateTypeDeclarations)) {
4141
outputTarget.generateTypeDeclarations = true;
4242
}
43+
// Export behavior must be defined on the validated target config and must
44+
// be one of the export behavior valid values
45+
if (
46+
outputTarget.customElementsExportBehavior == null ||
47+
!CustomElementsExportBehaviorOptions.includes(outputTarget.customElementsExportBehavior)
48+
) {
49+
outputTarget.customElementsExportBehavior = 'default';
50+
}
4351

4452
// unlike other output targets, Stencil does not allow users to define the output location of types at this time
4553
if (outputTarget.generateTypeDeclarations) {

src/compiler/config/test/validate-output-dist-custom-element.spec.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,59 @@ describe('validate-output-dist-custom-element', () => {
3636
empty: true,
3737
externalRuntime: true,
3838
generateTypeDeclarations: true,
39+
customElementsExportBehavior: 'default',
40+
},
41+
]);
42+
});
43+
44+
it('uses a provided export behavior over the default value', () => {
45+
const outputTarget: d.OutputTargetDistCustomElements = {
46+
type: DIST_CUSTOM_ELEMENTS,
47+
customElementsExportBehavior: 'single-export-module',
48+
};
49+
userConfig.outputTargets = [outputTarget];
50+
51+
const { config } = validateConfig(userConfig, mockLoadConfigInit());
52+
expect(config.outputTargets).toEqual([
53+
{
54+
type: DIST_TYPES,
55+
dir: defaultDistDir,
56+
typesDir: path.join(rootDir, 'dist', 'types'),
57+
},
58+
{
59+
type: DIST_CUSTOM_ELEMENTS,
60+
copy: [],
61+
dir: defaultDistDir,
62+
empty: true,
63+
externalRuntime: true,
64+
generateTypeDeclarations: true,
65+
customElementsExportBehavior: 'single-export-module',
66+
},
67+
]);
68+
});
69+
70+
it('uses the default export behavior if the specified value is invalid', () => {
71+
const outputTarget: d.OutputTargetDistCustomElements = {
72+
type: DIST_CUSTOM_ELEMENTS,
73+
customElementsExportBehavior: 'not-a-valid-option' as d.CustomElementsExportBehavior,
74+
};
75+
userConfig.outputTargets = [outputTarget];
76+
77+
const { config } = validateConfig(userConfig, mockLoadConfigInit());
78+
expect(config.outputTargets).toEqual([
79+
{
80+
type: DIST_TYPES,
81+
dir: defaultDistDir,
82+
typesDir: path.join(rootDir, 'dist', 'types'),
83+
},
84+
{
85+
type: DIST_CUSTOM_ELEMENTS,
86+
copy: [],
87+
dir: defaultDistDir,
88+
empty: true,
89+
externalRuntime: true,
90+
generateTypeDeclarations: true,
91+
customElementsExportBehavior: 'default',
3992
},
4093
]);
4194
});
@@ -57,6 +110,7 @@ describe('validate-output-dist-custom-element', () => {
57110
empty: true,
58111
externalRuntime: true,
59112
generateTypeDeclarations: false,
113+
customElementsExportBehavior: 'default',
60114
},
61115
]);
62116
});
@@ -79,6 +133,7 @@ describe('validate-output-dist-custom-element', () => {
79133
empty: true,
80134
externalRuntime: false,
81135
generateTypeDeclarations: false,
136+
customElementsExportBehavior: 'default',
82137
},
83138
]);
84139
});
@@ -101,6 +156,7 @@ describe('validate-output-dist-custom-element', () => {
101156
empty: true,
102157
externalRuntime: false,
103158
generateTypeDeclarations: false,
159+
customElementsExportBehavior: 'default',
104160
},
105161
]);
106162
});
@@ -124,6 +180,7 @@ describe('validate-output-dist-custom-element', () => {
124180
empty: false,
125181
externalRuntime: true,
126182
generateTypeDeclarations: false,
183+
customElementsExportBehavior: 'default',
127184
},
128185
]);
129186
});
@@ -146,6 +203,7 @@ describe('validate-output-dist-custom-element', () => {
146203
empty: false,
147204
externalRuntime: true,
148205
generateTypeDeclarations: false,
206+
customElementsExportBehavior: 'default',
149207
},
150208
]);
151209
});
@@ -173,6 +231,7 @@ describe('validate-output-dist-custom-element', () => {
173231
empty: false,
174232
externalRuntime: true,
175233
generateTypeDeclarations: true,
234+
customElementsExportBehavior: 'default',
176235
},
177236
]);
178237
});
@@ -199,6 +258,7 @@ describe('validate-output-dist-custom-element', () => {
199258
empty: false,
200259
externalRuntime: true,
201260
generateTypeDeclarations: true,
261+
customElementsExportBehavior: 'default',
202262
},
203263
]);
204264
});
@@ -226,6 +286,7 @@ describe('validate-output-dist-custom-element', () => {
226286
empty: false,
227287
externalRuntime: false,
228288
generateTypeDeclarations: true,
289+
customElementsExportBehavior: 'default',
229290
},
230291
]);
231292
});
@@ -254,6 +315,7 @@ describe('validate-output-dist-custom-element', () => {
254315
empty: false,
255316
externalRuntime: false,
256317
generateTypeDeclarations: true,
318+
customElementsExportBehavior: 'default',
257319
},
258320
]);
259321
});
@@ -276,6 +338,7 @@ describe('validate-output-dist-custom-element', () => {
276338
empty: false,
277339
externalRuntime: false,
278340
generateTypeDeclarations: false,
341+
customElementsExportBehavior: 'default',
279342
},
280343
]);
281344
});
@@ -316,6 +379,7 @@ describe('validate-output-dist-custom-element', () => {
316379
empty: false,
317380
externalRuntime: false,
318381
generateTypeDeclarations: false,
382+
customElementsExportBehavior: 'default',
319383
},
320384
]);
321385
});

src/compiler/output-targets/dist-custom-elements/custom-elements-types.ts

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ const generateCustomElementsTypesOutput = async (
4343
typesDir: string,
4444
outputTarget: d.OutputTargetDistCustomElements
4545
) => {
46+
const isBarrelExport = outputTarget.customElementsExportBehavior === 'single-export-module';
47+
4648
// the path where we're going to write the typedef for the whole dist-custom-elements output
4749
const customElementsDtsPath = join(outputTarget.dir!, 'index.d.ts');
4850
// the directory where types for the individual components are written
@@ -51,24 +53,32 @@ const generateCustomElementsTypesOutput = async (
5153
const components = buildCtx.components.filter((m) => !m.isCollectionDependency);
5254

5355
const code = [
54-
`/* ${config.namespace} custom elements */`,
55-
...components.map((component) => {
56-
const exportName = dashToPascalCase(component.tagName);
57-
const importName = component.componentClassName;
58-
// typedefs for individual components can be found under paths like
59-
// $TYPES_DIR/components/my-component/my-component.d.ts
60-
//
61-
// To construct this path we:
62-
//
63-
// - get the relative path to the component's source file from the source directory
64-
// - join that relative path to the relative path from the `index.d.ts` file to the
65-
// directory where typedefs are saved
66-
const componentSourceRelPath = relative(config.srcDir, component.sourceFilePath).replace('.tsx', '');
67-
const componentDTSPath = join(componentsTypeDirectoryRelPath, componentSourceRelPath);
68-
69-
return `export { ${importName} as ${exportName} } from '${componentDTSPath}';`;
70-
}),
71-
``,
56+
// To mirror the index.js file and only export the typedefs for the
57+
// entities exported there, we will re-export the typedefs iff
58+
// the `customElementsExportBehavior` is set to barrel component exports
59+
...(isBarrelExport
60+
? [
61+
`/* ${config.namespace} custom elements */`,
62+
...components.map((component) => {
63+
const exportName = dashToPascalCase(component.tagName);
64+
const importName = component.componentClassName;
65+
66+
// typedefs for individual components can be found under paths like
67+
// $TYPES_DIR/components/my-component/my-component.d.ts
68+
//
69+
// To construct this path we:
70+
//
71+
// - get the relative path to the component's source file from the source directory
72+
// - join that relative path to the relative path from the `index.d.ts` file to the
73+
// directory where typedefs are saved
74+
const componentSourceRelPath = relative(config.srcDir, component.sourceFilePath).replace('.tsx', '');
75+
const componentDTSPath = join(componentsTypeDirectoryRelPath, componentSourceRelPath);
76+
77+
return `export { ${importName} as ${exportName} } from '${componentDTSPath}';`;
78+
}),
79+
``,
80+
]
81+
: []),
7282
`/**`,
7383
` * Used to manually set the base path where assets can be found.`,
7484
` * If the script is used as "module", it's recommended to use "import.meta.url",`,
@@ -100,13 +110,18 @@ const generateCustomElementsTypesOutput = async (
100110

101111
const componentsDtsRelPath = relDts(outputTarget.dir!, join(typesDir, 'components.d.ts'));
102112

103-
const usersIndexJsPath = join(config.srcDir, 'index.ts');
104-
const hasUserIndex = await compilerCtx.fs.access(usersIndexJsPath);
105-
if (hasUserIndex) {
106-
const userIndexRelPath = normalizePath(dirname(componentsDtsRelPath));
107-
code.push(`export * from '${userIndexRelPath}';`);
108-
} else {
109-
code.push(`export * from '${componentsDtsRelPath}';`);
113+
// To mirror the index.js file and only export the typedefs for the
114+
// entities exported there, we will re-export the typedefs iff
115+
// the `customElementsExportBehavior` is set to barrel component exports
116+
if (isBarrelExport) {
117+
const usersIndexJsPath = join(config.srcDir, 'index.ts');
118+
const hasUserIndex = await compilerCtx.fs.access(usersIndexJsPath);
119+
if (hasUserIndex) {
120+
const userIndexRelPath = normalizePath(dirname(componentsDtsRelPath));
121+
code.push(`export * from '${userIndexRelPath}';`);
122+
} else {
123+
code.push(`export * from '${componentsDtsRelPath}';`);
124+
}
110125
}
111126

112127
await compilerCtx.fs.writeFile(customElementsDtsPath, code.join('\n') + `\n`, {

src/compiler/output-targets/dist-custom-elements/index.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export const bundleCustomElements = async (
113113
try {
114114
const bundleOpts = getBundleOptions(config, buildCtx, compilerCtx, outputTarget);
115115

116-
addCustomElementInputs(buildCtx, bundleOpts);
116+
addCustomElementInputs(buildCtx, bundleOpts, outputTarget);
117117

118118
const build = await bundleOutput(config, compilerCtx, buildCtx, bundleOpts);
119119

@@ -181,8 +181,13 @@ export const bundleCustomElements = async (
181181
* Create the virtual modules/input modules for the `dist-custom-elements` output target.
182182
* @param buildCtx the context for the current build
183183
* @param bundleOpts the bundle options to store the virtual modules under. acts as an output parameter
184+
* @param outputTarget the configuration for the custom element output target
184185
*/
185-
export const addCustomElementInputs = (buildCtx: d.BuildCtx, bundleOpts: BundleOptions): void => {
186+
export const addCustomElementInputs = (
187+
buildCtx: d.BuildCtx,
188+
bundleOpts: BundleOptions,
189+
outputTarget: d.OutputTargetDistCustomElements
190+
): void => {
186191
const components = buildCtx.components;
187192
// an array to store the imports of these modules that we're going to add to our entry chunk
188193
const indexImports: string[] = [];
@@ -220,7 +225,10 @@ export const addCustomElementInputs = (buildCtx: d.BuildCtx, bundleOpts: BundleO
220225
bundleOpts.loader![coreKey] = exp.join('\n');
221226
});
222227

223-
bundleOpts.loader!['\0core'] += indexImports.join('\n');
228+
// Only re-export component definitions if the barrel export behavior is set
229+
if (outputTarget.customElementsExportBehavior === 'single-export-module') {
230+
bundleOpts.loader!['\0core'] += indexImports.join('\n');
231+
}
224232
};
225233

226234
/**

src/compiler/output-targets/test/custom-elements-types.spec.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const setup = () => {
4040
};
4141

4242
describe('Custom Elements Typedef generation', () => {
43-
it('should generate an index.d.ts file corresponding to the index.js file', async () => {
43+
it('should generate an index.d.ts file corresponding to the index.js file when barrel export behavior is enabled', async () => {
4444
// this component tests the 'happy path' of a component's filename coinciding with its
4545
// tag name
4646
const componentOne = stubComponentCompilerMeta({
@@ -55,6 +55,7 @@ describe('Custom Elements Typedef generation', () => {
5555
tagName: 'my-best-component',
5656
});
5757
const { config, compilerCtx, buildCtx } = setup();
58+
(config.outputTargets[0] as d.OutputTargetDistCustomElements).customElementsExportBehavior = 'single-export-module';
5859
buildCtx.components = [componentOne, componentTwo];
5960

6061
const writeFileSpy = jest.spyOn(compilerCtx.fs, 'writeFile');
@@ -109,4 +110,54 @@ describe('Custom Elements Typedef generation', () => {
109110

110111
writeFileSpy.mockRestore();
111112
});
113+
114+
it('should generate an index.d.ts file corresponding to the index.js file when barrel export behavior is disabled', async () => {
115+
// this component tests the 'happy path' of a component's filename coinciding with its
116+
// tag name
117+
const componentOne = stubComponentCompilerMeta({
118+
tagName: 'my-component',
119+
sourceFilePath: '/src/components/my-component/my-component.tsx',
120+
});
121+
// this component tests that we correctly resolve its path when the component tag does
122+
// not match its filename
123+
const componentTwo = stubComponentCompilerMeta({
124+
sourceFilePath: '/src/components/the-other-component/my-real-best-component.tsx',
125+
componentClassName: 'MyBestComponent',
126+
tagName: 'my-best-component',
127+
});
128+
const { config, compilerCtx, buildCtx } = setup();
129+
buildCtx.components = [componentOne, componentTwo];
130+
131+
const writeFileSpy = jest.spyOn(compilerCtx.fs, 'writeFile');
132+
133+
await generateCustomElementsTypes(config, compilerCtx, buildCtx, 'types_dir');
134+
135+
const expectedTypedefOutput = [
136+
'/**',
137+
' * Used to manually set the base path where assets can be found.',
138+
' * If the script is used as "module", it\'s recommended to use "import.meta.url",',
139+
' * such as "setAssetPath(import.meta.url)". Other options include',
140+
' * "setAssetPath(document.currentScript.src)", or using a bundler\'s replace plugin to',
141+
' * dynamically set the path at build time, such as "setAssetPath(process.env.ASSET_PATH)".',
142+
' * But do note that this configuration depends on how your script is bundled, or lack of',
143+
' * bundling, and where your assets can be loaded from. Additionally custom bundling',
144+
' * will have to ensure the static assets are copied to its build directory.',
145+
' */',
146+
'export declare const setAssetPath: (path: string) => void;',
147+
'',
148+
'export interface SetPlatformOptions {',
149+
' raf?: (c: FrameRequestCallback) => number;',
150+
' ael?: (el: EventTarget, eventName: string, listener: EventListenerOrEventListenerObject, options: boolean | AddEventListenerOptions) => void;',
151+
' rel?: (el: EventTarget, eventName: string, listener: EventListenerOrEventListenerObject, options: boolean | AddEventListenerOptions) => void;',
152+
'}',
153+
'export declare const setPlatformOptions: (opts: SetPlatformOptions) => void;',
154+
'',
155+
].join('\n');
156+
157+
expect(compilerCtx.fs.writeFile).toBeCalledWith(join('my-best-dir', 'index.d.ts'), expectedTypedefOutput, {
158+
outputTargetType: DIST_CUSTOM_ELEMENTS,
159+
});
160+
161+
writeFileSpy.mockRestore();
162+
});
112163
});

0 commit comments

Comments
 (0)