Skip to content

Commit

Permalink
feat(custom-elements): enable dist-custom-elements output
Browse files Browse the repository at this point in the history
Create individual custom element files for each component, which extend HTMLElement.
  • Loading branch information
adamdbradley committed Dec 17, 2020
1 parent a08f3a8 commit fc70564
Show file tree
Hide file tree
Showing 10 changed files with 251 additions and 62 deletions.
6 changes: 3 additions & 3 deletions src/compiler/config/outputs/index.ts
@@ -1,6 +1,6 @@
import type * as d from '../../../declarations';
import { buildError } from '@utils';
import { VALID_TYPES_NEXT } from '../../output-targets/output-utils';
import { VALID_TYPES } from '../../output-targets/output-utils';
import { validateCollection } from './validate-collection';
import { validateCustomElement } from './validate-custom-element';
import { validateCustomOutput } from './validate-custom-output';
Expand All @@ -17,11 +17,11 @@ export const validateOutputTargets = (config: d.Config, diagnostics: d.Diagnosti
const userOutputs = (config.outputTargets || []).slice();

userOutputs.forEach(outputTarget => {
if (!VALID_TYPES_NEXT.includes(outputTarget.type)) {
if (!VALID_TYPES.includes(outputTarget.type)) {
const err = buildError(diagnostics);
err.messageText = `Invalid outputTarget type "${
outputTarget.type
}". Valid outputTarget types include: ${VALID_TYPES_NEXT.map(t => `"${t}"`).join(', ')}`;
}". Valid outputTarget types include: ${VALID_TYPES.map(t => `"${t}"`).join(', ')}`;
}
});

Expand Down
29 changes: 22 additions & 7 deletions src/compiler/config/outputs/validate-custom-element.ts
@@ -1,17 +1,32 @@
import type * as d from '../../../declarations';
import type { Config, OutputTarget, OutputTargetDistCustomElements, OutputTargetCopy } from '../../../declarations';
import { getAbsolutePath } from '../config-utils';
import { isBoolean } from '@utils';
import { isOutputTargetDistCustomElements } from '../../output-targets/output-utils';
import { COPY, isOutputTargetDistCustomElements } from '../../output-targets/output-utils';
import { validateCopy } from '../validate-copy';

export const validateCustomElement = (config: d.Config, userOutputs: d.OutputTarget[]) => {
return userOutputs.filter(isOutputTargetDistCustomElements).map(o => {
export const validateCustomElement = (config: Config, userOutputs: OutputTarget[]) => {
return userOutputs.filter(isOutputTargetDistCustomElements).reduce((arr, o) => {
const outputTarget = {
...o,
dir: getAbsolutePath(config, o.dir || 'dist/components'),
};
if (!isBoolean(outputTarget.empty)) {
outputTarget.empty = true;
}
return outputTarget;
});
};
if (!isBoolean(outputTarget.externalRuntime)) {
outputTarget.externalRuntime = true;
}
outputTarget.copy = validateCopy(outputTarget.copy, []);

if (outputTarget.copy.length > 0) {
arr.push({
type: COPY,
dir: config.rootDir,
copy: [...outputTarget.copy],
});
}
arr.push(outputTarget);

return arr;
}, [] as (OutputTargetDistCustomElements | OutputTargetCopy)[]);
};
Expand Up @@ -3,7 +3,7 @@ import { isOutputTargetDistCustomElementsBundle } from '../output-utils';
import { dirname, join, relative } from 'path';
import { normalizePath, dashToPascalCase } from '@utils';

export const generateCustomElementsTypes = async (config: d.Config, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx, distDtsFilePath: string) => {
export const generateCustomElementsBundleTypes = async (config: d.Config, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx, distDtsFilePath: string) => {
const outputTargets = config.outputTargets.filter(isOutputTargetDistCustomElementsBundle);

await Promise.all(outputTargets.map(outputTarget => generateCustomElementsTypesOutput(config, compilerCtx, buildCtx, distDtsFilePath, outputTarget)));
Expand All @@ -24,7 +24,7 @@ const generateCustomElementsTypesOutput = async (
const code = [
`/* ${config.namespace} custom elements bundle */`,
``,
`import { Components, JSX } from "${componentsDtsRelPath}";`,
`import type { Components, JSX } from "${componentsDtsRelPath}";`,
``,
...components.map(generateCustomElementType),
`/**`,
Expand All @@ -51,7 +51,7 @@ const generateCustomElementsTypesOutput = async (
` */`,
`export declare const setAssetPath: (path: string) => void;`,
``,
`export { Components, JSX };`,
`export type { Components, JSX };`,
``
];

Expand Down
Expand Up @@ -66,13 +66,15 @@ const bundleCustomElements = async (
hoistTransitiveImports: false,
preferConst: true,
});

const minify = outputTarget.externalRuntime || outputTarget.minify !== true ? false : config.minifyJs;
const files = rollupOutput.output.map(async bundle => {
if (bundle.type === 'chunk') {
let code = bundle.code;
const optimizeResults = await optimizeModule(config, compilerCtx, {
input: code,
isCore: bundle.isEntry,
minify: outputTarget.externalRuntime ? false : config.minifyJs,
minify,
});
buildCtx.diagnostics.push(...optimizeResults.diagnostics);
if (!hasError(optimizeResults.diagnostics) && typeof optimizeResults.output === 'string') {
Expand Down
@@ -0,0 +1,85 @@
import type * as d from '../../../declarations';
import { isOutputTargetDistCustomElements } from '../output-utils';
import { dirname, join, relative } from 'path';
import { normalizePath, dashToPascalCase } from '@utils';

export const generateCustomElementsTypes = async (config: d.Config, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx, distDtsFilePath: string) => {
const outputTargets = config.outputTargets.filter(isOutputTargetDistCustomElements);

await Promise.all(outputTargets.map(outputTarget => generateCustomElementsTypesOutput(config, compilerCtx, buildCtx, distDtsFilePath, outputTarget)));
};

export const generateCustomElementsTypesOutput = async (
config: d.Config,
compilerCtx: d.CompilerCtx,
buildCtx: d.BuildCtx,
distDtsFilePath: string,
outputTarget: d.OutputTargetDistCustomElementsBundle | d.OutputTargetDistCustomElements,
) => {
const customElementsDtsPath = join(outputTarget.dir, 'index.d.ts');
const componentsDtsRelPath = relDts(outputTarget.dir, distDtsFilePath);

const code = [
`/* ${config.namespace} custom elements */`,
``,
`import type { Components, JSX } from "${componentsDtsRelPath}";`,
``,
`/**`,
` * 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`,
` * bunding, 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 type { Components, JSX };`,
``
];

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`, { outputTargetType: outputTarget.type });

const components = buildCtx.components.filter(m => !m.isCollectionDependency);
await Promise.all(components.map(async cmp => {
const dtsCode = generateCustomElementType(componentsDtsRelPath, cmp);
const fileName = `${cmp.tagName}.d.ts`;
const filePath = join(outputTarget.dir, fileName);
await compilerCtx.fs.writeFile(filePath, dtsCode, { outputTargetType: outputTarget.type });
}));
};

const generateCustomElementType = (componentsDtsRelPath: string, cmp: d.ComponentCompilerMeta) => {
const tagNameAsPascal = dashToPascalCase(cmp.tagName);
const o: string[] = [
`import type { Components, JSX } from "${componentsDtsRelPath}";`,
``,
`interface ${tagNameAsPascal} extends Components.${tagNameAsPascal}, HTMLElement {}`,
`export const ${tagNameAsPascal}: {`,
` prototype: ${tagNameAsPascal};`,
` new (): ${tagNameAsPascal};`,
`};`,
``,
];

return o.join('\n');
};

const relDts = (fromPath: string, dtsPath: string) => {
dtsPath = relative(fromPath, dtsPath);
if (!dtsPath.startsWith('.')) {
dtsPath = '.' + dtsPath;
}
return normalizePath(dtsPath.replace('.d.ts', ''));
};
152 changes: 125 additions & 27 deletions src/compiler/output-targets/dist-custom-elements/index.ts
@@ -1,54 +1,152 @@
import type * as d from '../../../declarations';
import { catchError } from '@utils';
import type { BundleOptions } from '../../bundle/bundle-interface';
import { bundleOutput } from '../../bundle/bundle-output';
import { catchError, dashToPascalCase, formatComponentRuntimeMeta, hasError, stringifyRuntimeData } from '@utils';
import { getCustomElementsBuildConditionals } from '../dist-custom-elements-bundle/custom-elements-build-conditionals';
import { isOutputTargetDistCustomElements } from '../output-utils';
import { join } from 'path';
import { nativeComponentTransform } from '../../transformers/component-native/tranform-to-native-component';
import { optimizeModule } from '../../optimize/optimize-module';
import { removeCollectionImports } from '../../transformers/remove-collection-imports';
import { STENCIL_CORE_ID } from '../../bundle/entry-alias-ids';
import { STENCIL_INTERNAL_CLIENT_ID, USER_INDEX_ENTRY_ID, STENCIL_APP_GLOBALS_ID } from '../../bundle/entry-alias-ids';
import { updateStencilCoreImports } from '../../transformers/update-stencil-core-import';
import { join, relative } from 'path';
import ts from 'typescript';

export const outputCustomElements = async (config: d.Config, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx, changedModuleFiles: d.Module[]) => {
export const outputCustomElements = async (
config: d.Config,
compilerCtx: d.CompilerCtx,
buildCtx: d.BuildCtx,
) => {
if (!config.buildDist) {
return;
}

const outputTargets = config.outputTargets.filter(isOutputTargetDistCustomElements);
if (outputTargets.length === 0) {
return;
}

const timespan = buildCtx.createTimeSpan(`generate custom elements started`, true);
const printer = ts.createPrinter();
const timespan = buildCtx.createTimeSpan(`generate custom elements started`);

await Promise.all(outputTargets.map(o => bundleCustomElements(config, compilerCtx, buildCtx, o)));

timespan.finish(`generate custom elements finished`);
};

const bundleCustomElements = async (
config: d.Config,
compilerCtx: d.CompilerCtx,
buildCtx: d.BuildCtx,
outputTarget: d.OutputTargetDistCustomElements,
) => {
try {
await Promise.all(
changedModuleFiles.map(async mod => {
const transformResults = ts.transform(mod.staticSourceFile, getCustomElementTransformer(config, compilerCtx));
const transformed = transformResults.transformed[0];
const code = printer.printFile(transformed);

await Promise.all(
outputTargets.map(async o => {
const relPath = relative(config.srcDir, mod.jsFilePath);
const filePath = join(o.dir, relPath);
await compilerCtx.fs.writeFile(filePath, code, { outputTargetType: o.type });
}),
);
}),
);
const bundleOpts: BundleOptions = {
id: 'customElements',
platform: 'client',
conditionals: getCustomElementsBuildConditionals(config, buildCtx.components),
customTransformers: getCustomElementBundleCustomTransformer(config, compilerCtx),
externalRuntime: !!outputTarget.externalRuntime,
inlineWorkers: true,
inputs: {
index: '\0core',
},
loader: {
'\0core': generateEntryPoint(outputTarget, buildCtx),
},
inlineDynamicImports: outputTarget.inlineDynamicImports,
preserveEntrySignatures: 'allow-extension',
};

addCustomElementInputs(outputTarget, buildCtx, bundleOpts);

const build = await bundleOutput(config, compilerCtx, buildCtx, bundleOpts);
if (build) {
const rollupOutput = await build.generate({
format: 'esm',
sourcemap: config.sourceMap,
chunkFileNames: outputTarget.externalRuntime || !config.hashFileNames ? '[name].js' : 'p-[hash].js',
entryFileNames: '[name].js',
hoistTransitiveImports: false,
preferConst: true,
});

const minify = outputTarget.externalRuntime || outputTarget.minify !== true ? false : config.minifyJs;
const files = rollupOutput.output.map(async bundle => {
if (bundle.type === 'chunk') {
let code = bundle.code;
const optimizeResults = await optimizeModule(config, compilerCtx, {
input: code,
isCore: bundle.isEntry,
minify,
});
buildCtx.diagnostics.push(...optimizeResults.diagnostics);
if (!hasError(optimizeResults.diagnostics) && typeof optimizeResults.output === 'string') {
code = optimizeResults.output;
}
await compilerCtx.fs.writeFile(join(outputTarget.dir, bundle.fileName), code, {
outputTargetType: outputTarget.type,
});
}
});
await Promise.all(files);
}
} catch (e) {
catchError(buildCtx.diagnostics, e);
}
};

timespan.finish(`generate custom elements finished`);
const addCustomElementInputs = (_outputTarget: d.OutputTargetDistCustomElements, buildCtx: d.BuildCtx, bundleOpts: BundleOptions) => {
const components = buildCtx.components;
components.forEach(cmp => {
const exp: string[] = [];
const exportName = dashToPascalCase(cmp.tagName);
const importName = cmp.componentClassName;
const importAs = `$Cmp${exportName}`;
const coreKey = `\0${exportName}`

if (cmp.isPlain) {
exp.push(`export { ${importName} as ${exportName} } from '${cmp.sourceFilePath}';`);
} else {
const meta = stringifyRuntimeData(formatComponentRuntimeMeta(cmp, false));

exp.push(`import { proxyCustomElement } from '${STENCIL_INTERNAL_CLIENT_ID}';`);
exp.push(`import { ${importName} as ${importAs} } from '${cmp.sourceFilePath}';`);
exp.push(`export const ${exportName} = /*@__PURE__*/proxyCustomElement(${importAs}, ${meta});`);
}

bundleOpts.inputs[cmp.tagName] = coreKey;
bundleOpts.loader[coreKey] = exp.join('\n');
});
}

const generateEntryPoint = (outputTarget: d.OutputTargetDistCustomElements, _buildCtx: d.BuildCtx) => {
const imp: string[] = [];
const exp: string[] = [];

imp.push(
`export { setAssetPath } from '${STENCIL_INTERNAL_CLIENT_ID}';`,
`export * from '${USER_INDEX_ENTRY_ID}';`,
);

if (outputTarget.includeGlobalScripts !== false) {
imp.push(`import { globalScripts } from '${STENCIL_APP_GLOBALS_ID}';`, `globalScripts();`);
}

return [...imp, ...exp].join('\n') + '\n';
};

const getCustomElementTransformer = (config: d.Config, compilerCtx: d.CompilerCtx) => {
const getCustomElementBundleCustomTransformer = (config: d.Config, compilerCtx: d.CompilerCtx) => {
const transformOpts: d.TransformOptions = {
coreImportPath: STENCIL_CORE_ID,
coreImportPath: STENCIL_INTERNAL_CLIENT_ID,
componentExport: null,
componentMetadata: null,
currentDirectory: config.sys.getCurrentDirectory(),
module: 'esm',
proxy: null,
style: 'static',
styleImportData: 'queryparams',
};
return [updateStencilCoreImports(transformOpts.coreImportPath), nativeComponentTransform(compilerCtx, transformOpts), removeCollectionImports(compilerCtx)];
return [
updateStencilCoreImports(transformOpts.coreImportPath),
nativeComponentTransform(compilerCtx, transformOpts),
removeCollectionImports(compilerCtx),
];
};
2 changes: 1 addition & 1 deletion src/compiler/output-targets/index.ts
Expand Up @@ -27,7 +27,7 @@ export const generateOutputTargets = async (config: d.Config, compilerCtx: d.Com
outputAngular(config, compilerCtx, buildCtx),
outputCopy(config, compilerCtx, buildCtx),
outputCollection(config, compilerCtx, buildCtx, changedModuleFiles),
outputCustomElements(config, compilerCtx, buildCtx, changedModuleFiles),
outputCustomElements(config, compilerCtx, buildCtx),
outputCustomElementsBundle(config, compilerCtx, buildCtx),
outputHydrateScript(config, compilerCtx, buildCtx),
outputLazyLoader(config, compilerCtx),
Expand Down

0 comments on commit fc70564

Please sign in to comment.