diff --git a/src/compiler/config/outputs/index.ts b/src/compiler/config/outputs/index.ts index 0871395b046..797016441fd 100644 --- a/src/compiler/config/outputs/index.ts +++ b/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'; @@ -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(', ')}`; } }); diff --git a/src/compiler/config/outputs/validate-custom-element.ts b/src/compiler/config/outputs/validate-custom-element.ts index 7eaf3853254..b39ca3f3c7f 100644 --- a/src/compiler/config/outputs/validate-custom-element.ts +++ b/src/compiler/config/outputs/validate-custom-element.ts @@ -1,10 +1,11 @@ -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'), @@ -12,6 +13,20 @@ export const validateCustomElement = (config: d.Config, userOutputs: d.OutputTar 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)[]); +}; \ No newline at end of file diff --git a/src/compiler/output-targets/dist-custom-elements-bundle/custom-elements-types.ts b/src/compiler/output-targets/dist-custom-elements-bundle/custom-elements-bundle-types.ts similarity index 93% rename from src/compiler/output-targets/dist-custom-elements-bundle/custom-elements-types.ts rename to src/compiler/output-targets/dist-custom-elements-bundle/custom-elements-bundle-types.ts index 7198c3cbe63..c7888b8eeee 100644 --- a/src/compiler/output-targets/dist-custom-elements-bundle/custom-elements-types.ts +++ b/src/compiler/output-targets/dist-custom-elements-bundle/custom-elements-bundle-types.ts @@ -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))); @@ -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), `/**`, @@ -51,7 +51,7 @@ const generateCustomElementsTypesOutput = async ( ` */`, `export declare const setAssetPath: (path: string) => void;`, ``, - `export { Components, JSX };`, + `export type { Components, JSX };`, `` ]; diff --git a/src/compiler/output-targets/dist-custom-elements-bundle/index.ts b/src/compiler/output-targets/dist-custom-elements-bundle/index.ts index 793292c6710..14a36393c4c 100644 --- a/src/compiler/output-targets/dist-custom-elements-bundle/index.ts +++ b/src/compiler/output-targets/dist-custom-elements-bundle/index.ts @@ -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') { diff --git a/src/compiler/output-targets/dist-custom-elements/custom-elements-types.ts b/src/compiler/output-targets/dist-custom-elements/custom-elements-types.ts new file mode 100644 index 00000000000..6ea464015ca --- /dev/null +++ b/src/compiler/output-targets/dist-custom-elements/custom-elements-types.ts @@ -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', '')); +}; diff --git a/src/compiler/output-targets/dist-custom-elements/index.ts b/src/compiler/output-targets/dist-custom-elements/index.ts index f439e53dd23..a07203a44bb 100644 --- a/src/compiler/output-targets/dist-custom-elements/index.ts +++ b/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), + ]; }; diff --git a/src/compiler/output-targets/index.ts b/src/compiler/output-targets/index.ts index cd2af0f0277..b6c0e8c29ee 100644 --- a/src/compiler/output-targets/index.ts +++ b/src/compiler/output-targets/index.ts @@ -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), diff --git a/src/compiler/output-targets/output-utils.ts b/src/compiler/output-targets/output-utils.ts index ca3c2c754b0..f11b65994e4 100644 --- a/src/compiler/output-targets/output-utils.ts +++ b/src/compiler/output-targets/output-utils.ts @@ -133,29 +133,11 @@ export const STATS = `stats`; export const WWW = `www`; export const VALID_TYPES = [ - ANGULAR, - COPY, - CUSTOM, - DIST, - DIST_COLLECTION, - DIST_CUSTOM_ELEMENTS, - DIST_GLOBAL_STYLES, - DIST_HYDRATE_SCRIPT, - DIST_LAZY, - DOCS_JSON, - DOCS_README, - DOCS_VSCODE, - DOCS_CUSTOM, - STATS, - WWW, -]; - -export const VALID_TYPES_NEXT = [ // DIST WWW, DIST, DIST_COLLECTION, - // DIST_CUSTOM_ELEMENTS, + DIST_CUSTOM_ELEMENTS, DIST_CUSTOM_ELEMENTS_BUNDLE, DIST_LAZY, DIST_HYDRATE_SCRIPT, diff --git a/src/compiler/types/generate-types.ts b/src/compiler/types/generate-types.ts index f2f2c9b2d5a..918cdd98a96 100644 --- a/src/compiler/types/generate-types.ts +++ b/src/compiler/types/generate-types.ts @@ -2,7 +2,8 @@ import type * as d from '../../declarations'; import { copyStencilCoreDts, updateStencilTypesImports } from './stencil-types'; import { join, relative } from 'path'; import { generateAppTypes } from './generate-app-types'; -import { generateCustomElementsTypes } from '../output-targets/dist-custom-elements-bundle/custom-elements-types'; +import { generateCustomElementsBundleTypes } from '../output-targets/dist-custom-elements-bundle/custom-elements-bundle-types'; +import { generateCustomElementsTypes } from '../output-targets/dist-custom-elements/custom-elements-types'; import { isDtsFile } from '@utils'; export const generateTypes = async (config: d.Config, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx, outputTarget: d.OutputTargetDistTypes) => { @@ -37,5 +38,6 @@ const generateTypesOutput = async (config: d.Config, compilerCtx: d.CompilerCtx, if (distDtsFilePath) { await generateCustomElementsTypes(config, compilerCtx, buildCtx, distDtsFilePath); + await generateCustomElementsBundleTypes(config, compilerCtx, buildCtx, distDtsFilePath); } }; diff --git a/src/declarations/stencil-public-compiler.ts b/src/declarations/stencil-public-compiler.ts index 3fbdd9299dd..e570944c1f6 100644 --- a/src/declarations/stencil-public-compiler.ts +++ b/src/declarations/stencil-public-compiler.ts @@ -1862,7 +1862,11 @@ export interface OutputTargetBaseNext { export interface OutputTargetDistCustomElements extends OutputTargetBaseNext { type: 'dist-custom-elements'; empty?: boolean; + externalRuntime?: boolean; copy?: CopyTask[]; + inlineDynamicImports?: boolean; + includeGlobalScripts?: boolean; + minify?: boolean; } export interface OutputTargetDistCustomElementsBundle extends OutputTargetBaseNext { @@ -1872,6 +1876,7 @@ export interface OutputTargetDistCustomElementsBundle extends OutputTargetBaseNe copy?: CopyTask[]; inlineDynamicImports?: boolean; includeGlobalScripts?: boolean; + minify?: boolean; } export interface OutputTargetBase {