Skip to content

Commit

Permalink
feat(compiler): add CustomElementExportBehavior to custom elements … (
Browse files Browse the repository at this point in the history
#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
  • Loading branch information
tanner-reits committed Sep 12, 2022
1 parent eba64cb commit c9a9366
Show file tree
Hide file tree
Showing 7 changed files with 278 additions and 65 deletions.
9 changes: 9 additions & 0 deletions src/compiler/config/outputs/validate-custom-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
OutputTargetCopy,
ValidatedConfig,
} from '../../../declarations';
import { CustomElementsExportBehaviorOptions } from '../../../declarations';
import { getAbsolutePath } from '../config-utils';
import { COPY, DIST_TYPES, isOutputTargetDistCustomElements } from '../../output-targets/output-utils';
import { validateCopy } from '../validate-copy';
Expand Down Expand Up @@ -39,6 +40,14 @@ export const validateCustomElement = (
if (!isBoolean(outputTarget.generateTypeDeclarations)) {
outputTarget.generateTypeDeclarations = true;
}
// Export behavior must be defined on the validated target config and must
// be one of the export behavior valid values
if (
outputTarget.customElementsExportBehavior == null ||
!CustomElementsExportBehaviorOptions.includes(outputTarget.customElementsExportBehavior)
) {
outputTarget.customElementsExportBehavior = 'default';
}

// unlike other output targets, Stencil does not allow users to define the output location of types at this time
if (outputTarget.generateTypeDeclarations) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,59 @@ describe('validate-output-dist-custom-element', () => {
empty: true,
externalRuntime: true,
generateTypeDeclarations: true,
customElementsExportBehavior: 'default',
},
]);
});

it('uses a provided export behavior over the default value', () => {
const outputTarget: d.OutputTargetDistCustomElements = {
type: DIST_CUSTOM_ELEMENTS,
customElementsExportBehavior: 'single-export-module',
};
userConfig.outputTargets = [outputTarget];

const { config } = validateConfig(userConfig, mockLoadConfigInit());
expect(config.outputTargets).toEqual([
{
type: DIST_TYPES,
dir: defaultDistDir,
typesDir: path.join(rootDir, 'dist', 'types'),
},
{
type: DIST_CUSTOM_ELEMENTS,
copy: [],
dir: defaultDistDir,
empty: true,
externalRuntime: true,
generateTypeDeclarations: true,
customElementsExportBehavior: 'single-export-module',
},
]);
});

it('uses the default export behavior if the specified value is invalid', () => {
const outputTarget: d.OutputTargetDistCustomElements = {
type: DIST_CUSTOM_ELEMENTS,
customElementsExportBehavior: 'not-a-valid-option' as d.CustomElementsExportBehavior,
};
userConfig.outputTargets = [outputTarget];

const { config } = validateConfig(userConfig, mockLoadConfigInit());
expect(config.outputTargets).toEqual([
{
type: DIST_TYPES,
dir: defaultDistDir,
typesDir: path.join(rootDir, 'dist', 'types'),
},
{
type: DIST_CUSTOM_ELEMENTS,
copy: [],
dir: defaultDistDir,
empty: true,
externalRuntime: true,
generateTypeDeclarations: true,
customElementsExportBehavior: 'default',
},
]);
});
Expand All @@ -56,6 +109,7 @@ describe('validate-output-dist-custom-element', () => {
empty: true,
externalRuntime: true,
generateTypeDeclarations: false,
customElementsExportBehavior: 'default',
},
]);
});
Expand All @@ -78,6 +132,7 @@ describe('validate-output-dist-custom-element', () => {
empty: true,
externalRuntime: false,
generateTypeDeclarations: false,
customElementsExportBehavior: 'default',
},
]);
});
Expand All @@ -100,6 +155,7 @@ describe('validate-output-dist-custom-element', () => {
empty: true,
externalRuntime: false,
generateTypeDeclarations: false,
customElementsExportBehavior: 'default',
},
]);
});
Expand All @@ -123,6 +179,7 @@ describe('validate-output-dist-custom-element', () => {
empty: false,
externalRuntime: true,
generateTypeDeclarations: false,
customElementsExportBehavior: 'default',
},
]);
});
Expand All @@ -145,6 +202,7 @@ describe('validate-output-dist-custom-element', () => {
empty: false,
externalRuntime: true,
generateTypeDeclarations: false,
customElementsExportBehavior: 'default',
},
]);
});
Expand Down Expand Up @@ -172,6 +230,7 @@ describe('validate-output-dist-custom-element', () => {
empty: false,
externalRuntime: true,
generateTypeDeclarations: true,
customElementsExportBehavior: 'default',
},
]);
});
Expand All @@ -198,6 +257,7 @@ describe('validate-output-dist-custom-element', () => {
empty: false,
externalRuntime: true,
generateTypeDeclarations: true,
customElementsExportBehavior: 'default',
},
]);
});
Expand Down Expand Up @@ -225,6 +285,7 @@ describe('validate-output-dist-custom-element', () => {
empty: false,
externalRuntime: false,
generateTypeDeclarations: true,
customElementsExportBehavior: 'default',
},
]);
});
Expand Down Expand Up @@ -253,6 +314,7 @@ describe('validate-output-dist-custom-element', () => {
empty: false,
externalRuntime: false,
generateTypeDeclarations: true,
customElementsExportBehavior: 'default',
},
]);
});
Expand All @@ -275,6 +337,7 @@ describe('validate-output-dist-custom-element', () => {
empty: false,
externalRuntime: false,
generateTypeDeclarations: false,
customElementsExportBehavior: 'default',
},
]);
});
Expand Down Expand Up @@ -315,6 +378,7 @@ describe('validate-output-dist-custom-element', () => {
empty: false,
externalRuntime: false,
generateTypeDeclarations: false,
customElementsExportBehavior: 'default',
},
]);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ const generateCustomElementsTypesOutput = async (
typesDir: string,
outputTarget: d.OutputTargetDistCustomElements
) => {
const isBarrelExport = outputTarget.customElementsExportBehavior === 'single-export-module';

// the path where we're going to write the typedef for the whole dist-custom-elements output
const customElementsDtsPath = join(outputTarget.dir!, 'index.d.ts');
// the directory where types for the individual components are written
Expand All @@ -50,24 +52,32 @@ const generateCustomElementsTypesOutput = async (
const components = buildCtx.components.filter((m) => !m.isCollectionDependency);

const code = [
`/* ${config.namespace} custom elements */`,
...components.map((component) => {
const exportName = dashToPascalCase(component.tagName);
const importName = component.componentClassName;
// typedefs for individual components can be found under paths like
// $TYPES_DIR/components/my-component/my-component.d.ts
//
// To construct this path we:
//
// - get the relative path to the component's source file from the source directory
// - join that relative path to the relative path from the `index.d.ts` file to the
// directory where typedefs are saved
const componentSourceRelPath = relative(config.srcDir, component.sourceFilePath).replace('.tsx', '');
const componentDTSPath = join(componentsTypeDirectoryRelPath, componentSourceRelPath);
// To mirror the index.js file and only export the typedefs for the
// entities exported there, we will re-export the typedefs iff
// the `customElementsExportBehavior` is set to barrel component exports
...(isBarrelExport
? [
`/* ${config.namespace} custom elements */`,
...components.map((component) => {
const exportName = dashToPascalCase(component.tagName);
const importName = component.componentClassName;

return `export { ${importName} as ${exportName} } from '${componentDTSPath}';`;
}),
``,
// typedefs for individual components can be found under paths like
// $TYPES_DIR/components/my-component/my-component.d.ts
//
// To construct this path we:
//
// - get the relative path to the component's source file from the source directory
// - join that relative path to the relative path from the `index.d.ts` file to the
// directory where typedefs are saved
const componentSourceRelPath = relative(config.srcDir, component.sourceFilePath).replace('.tsx', '');
const componentDTSPath = join(componentsTypeDirectoryRelPath, componentSourceRelPath);

return `export { ${importName} as ${exportName} } from '${componentDTSPath}';`;
}),
``,
]
: []),
`/**`,
` * Used to manually set the base path where assets can be found.`,
` * If the script is used as "module", it's recommended to use "import.meta.url",`,
Expand All @@ -90,13 +100,18 @@ const generateCustomElementsTypesOutput = async (

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

const usersIndexJsPath = join(config.srcDir, 'index.ts');
const hasUserIndex = await compilerCtx.fs.access(usersIndexJsPath);
if (hasUserIndex) {
const userIndexRelPath = normalizePath(dirname(componentsDtsRelPath));
code.push(`export * from '${userIndexRelPath}';`);
} else {
code.push(`export * from '${componentsDtsRelPath}';`);
// To mirror the index.js file and only export the typedefs for the
// entities exported there, we will re-export the typedefs iff
// the `customElementsExportBehavior` is set to barrel component exports
if (isBarrelExport) {
const usersIndexJsPath = join(config.srcDir, 'index.ts');
const hasUserIndex = await compilerCtx.fs.access(usersIndexJsPath);
if (hasUserIndex) {
const userIndexRelPath = normalizePath(dirname(componentsDtsRelPath));
code.push(`export * from '${userIndexRelPath}';`);
} else {
code.push(`export * from '${componentsDtsRelPath}';`);
}
}

await compilerCtx.fs.writeFile(customElementsDtsPath, code.join('\n') + `\n`, {
Expand Down
14 changes: 11 additions & 3 deletions src/compiler/output-targets/dist-custom-elements/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export const bundleCustomElements = async (
try {
const bundleOpts = getBundleOptions(config, buildCtx, compilerCtx, outputTarget);

addCustomElementInputs(buildCtx, bundleOpts);
addCustomElementInputs(buildCtx, bundleOpts, outputTarget);

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

Expand Down Expand Up @@ -180,8 +180,13 @@ export const bundleCustomElements = async (
* Create the virtual modules/input modules for the `dist-custom-elements` output target.
* @param buildCtx the context for the current build
* @param bundleOpts the bundle options to store the virtual modules under. acts as an output parameter
* @param outputTarget the configuration for the custom element output target
*/
export const addCustomElementInputs = (buildCtx: d.BuildCtx, bundleOpts: BundleOptions): void => {
export const addCustomElementInputs = (
buildCtx: d.BuildCtx,
bundleOpts: BundleOptions,
outputTarget: d.OutputTargetDistCustomElements
): void => {
const components = buildCtx.components;
// an array to store the imports of these modules that we're going to add to our entry chunk
const indexImports: string[] = [];
Expand Down Expand Up @@ -219,7 +224,10 @@ export const addCustomElementInputs = (buildCtx: d.BuildCtx, bundleOpts: BundleO
bundleOpts.loader![coreKey] = exp.join('\n');
});

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

/**
Expand Down
53 changes: 52 additions & 1 deletion src/compiler/output-targets/test/custom-elements-types.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const setup = () => {
};

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

const writeFileSpy = jest.spyOn(compilerCtx.fs, 'writeFile');
Expand Down Expand Up @@ -99,4 +100,54 @@ describe('Custom Elements Typedef generation', () => {

writeFileSpy.mockRestore();
});

it('should generate an index.d.ts file corresponding to the index.js file when barrel export behavior is disabled', async () => {
// this component tests the 'happy path' of a component's filename coinciding with its
// tag name
const componentOne = stubComponentCompilerMeta({
tagName: 'my-component',
sourceFilePath: '/src/components/my-component/my-component.tsx',
});
// this component tests that we correctly resolve its path when the component tag does
// not match its filename
const componentTwo = stubComponentCompilerMeta({
sourceFilePath: '/src/components/the-other-component/my-real-best-component.tsx',
componentClassName: 'MyBestComponent',
tagName: 'my-best-component',
});
const { config, compilerCtx, buildCtx } = setup();
buildCtx.components = [componentOne, componentTwo];

const writeFileSpy = jest.spyOn(compilerCtx.fs, 'writeFile');

await generateCustomElementsTypes(config, compilerCtx, buildCtx, 'types_dir');

const expectedTypedefOutput = [
'/**',
' * Used to manually set the base path where assets can be found.',
' * If the script is used as "module", it\'s recommended to use "import.meta.url",',
' * such as "setAssetPath(import.meta.url)". Other options include',
' * "setAssetPath(document.currentScript.src)", or using a bundler\'s replace plugin to',
' * dynamically set the path at build time, such as "setAssetPath(process.env.ASSET_PATH)".',
' * But do note that this configuration depends on how your script is bundled, or lack of',
' * bundling, and where your assets can be loaded from. Additionally custom bundling',
' * will have to ensure the static assets are copied to its build directory.',
' */',
'export declare const setAssetPath: (path: string) => void;',
'',
'export interface SetPlatformOptions {',
' raf?: (c: FrameRequestCallback) => number;',
' ael?: (el: EventTarget, eventName: string, listener: EventListenerOrEventListenerObject, options: boolean | AddEventListenerOptions) => void;',
' rel?: (el: EventTarget, eventName: string, listener: EventListenerOrEventListenerObject, options: boolean | AddEventListenerOptions) => void;',
'}',
'export declare const setPlatformOptions: (opts: SetPlatformOptions) => void;',
'',
].join('\n');

expect(compilerCtx.fs.writeFile).toBeCalledWith(join('my-best-dir', 'index.d.ts'), expectedTypedefOutput, {
outputTargetType: DIST_CUSTOM_ELEMENTS,
});

writeFileSpy.mockRestore();
});
});
Loading

0 comments on commit c9a9366

Please sign in to comment.