-
Notifications
You must be signed in to change notification settings - Fork 793
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(custom-elements): enable dist-custom-elements output
Create individual custom element files for each component, which extend HTMLElement.
- Loading branch information
1 parent
a08f3a8
commit fc70564
Showing
10 changed files
with
251 additions
and
62 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)[]); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
85 changes: 85 additions & 0 deletions
85
src/compiler/output-targets/dist-custom-elements/custom-elements-types.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
152
src/compiler/output-targets/dist-custom-elements/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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), | ||
]; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.