UNPKG

@revolist/svelte-output-target

Version:

Svelte output target for @stencil/core components.

259 lines (239 loc) 11.1 kB
'use strict'; var path = require('path'); var util = require('util'); var fs = require('fs'); /* eslint-disable no-param-reassign */ util.promisify(fs.readFile); const EXTENDED_PATH_REGEX = /^\\\\\?\\/; const SLASH_REGEX = /\\/g; // eslint-disable-next-line no-control-regex const NON_ASCII_REGEX = /[^\x00-\x80]+/; const toLowerCase = (str) => str.toLowerCase(); const dashToPascalCase = (str) => toLowerCase(str) .split('-') .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) .join(''); const sortBy = (array, prop) => array.slice().sort((a, b) => { const nameA = prop(a); const nameB = prop(b); if (nameA < nameB) return -1; if (nameA > nameB) return 1; return 0; }); function normalizePath(str) { if (typeof str !== 'string') { throw new Error('invalid path to normalize'); } str = str.trim(); if (EXTENDED_PATH_REGEX.test(str) || NON_ASCII_REGEX.test(str)) { return str; } str = str.replace(SLASH_REGEX, '/'); // always remove the trailing / // this makes our file cache look ups consistent if (str.charAt(str.length - 1) === '/') { const colonIndex = str.indexOf(':'); if (colonIndex > -1) { if (colonIndex < str.length - 2) { str = str.substring(0, str.length - 1); } } else if (str.length > 1) { str = str.substring(0, str.length - 1); } } return str; } const createComponentDefinition = (cmpMeta, bindingsConfig) => { const { tagName, properties, methods, events } = cmpMeta; const bindings = bindingsConfig === null || bindingsConfig === void 0 ? void 0 : bindingsConfig.filter((c) => (Array.isArray(c.elements) ? c.elements.includes(tagName) : c.elements === tagName)).filter((c1, index, self) => index === self.findIndex((c2) => c1.event === c2.event && c1.targetProp === c2.targetProp)).map((c) => `if (e.type === '${c.event}') { ${c.targetProp} = e.detail; }`).join('\n '); return ` <script> import { createEventDispatcher, onMount } from 'svelte'; let __ref; let __mounted = false; const dispatch = createEventDispatcher(); ${properties.map((prop) => `export let ${prop.name}${!prop.required ? ' = undefined' : ''};`).join('\n')} ${methods.map((method) => `export const ${method.name} = (...args) => __ref.${method.name}(...args);`).join('\n')} export const getWebComponent = () => __ref; onMount(() => { __mounted = true; }); const setProp = (prop, value) => { if (__ref) __ref[prop] = value; }; ${properties .filter((prop) => !prop.attribute) .map((prop) => `$: if (__mounted) setProp('${prop.name}', ${prop.name});`) .join('\n')} const onEvent = (e) => { e.stopPropagation(); dispatch(e.type, e.detail);${bindings ? `\n ${bindings}` : ''} }; </script> <${tagName} ${properties .filter((prop) => !!prop.attribute) .map((prop) => `${prop.attribute}={${prop.name}}`) .join('\n ')} ${events.map((event) => `on:${event.name}={onEvent}`).join('\n ')} bind:this={__ref} > <slot></slot> </${tagName}> `; }; const generateTypings = (meta) => { const name = dashToPascalCase(meta.tagName); const jsxEventName = (eventName) => `on${eventName.charAt(0).toUpperCase() + eventName.slice(1)}`; const types = ` interface ${name}Props { ${meta.properties .map((prop) => `\n /** ${prop.docs.text} */\n ${prop.name}?: Components.${name}["${prop.name}"]`) .join('\n ')} } interface ${name}Events { ${meta.events .map((event) => `\n /** ${event.docs.text} */\n ${event.name}: Parameters<JSX.${name}["${jsxEventName(event.name)}"]>[0]`) .join('\n ')} } interface ${name}Slots { default: any } `; return types; }; const generate$$TypeDefs = (meta, source) => { const name = dashToPascalCase(meta.tagName); const inject = [ 'extends SvelteComponent {', // For some reason n-1 $ signs appear in output...? `$$$prop_def: ${name}Props;`, `$$$events_def: ${name}Events;`, `$$$slot_def: ${name}Slots;\n`, `$on<K extends keyof ${name}Events>(type: K, callback: (e: ${name}Events[K]) => any): () => void {\n\t return super.$on(type, callback);\n\t}\n`, `$set($$$props: Partial<${name}Props>): void {\n\t super.$set($$$props);\n\t}\n`, ].join('\n '); return source.replace('extends SvelteComponent {', inject); }; const replaceMethodDefs = (meta, source) => { const name = dashToPascalCase(meta.tagName); let newSource = source; newSource = source.replace('get getWebComponent() {', `get getWebComponent(): HTML${name}Element | undefined {`); meta.methods.forEach((method) => { newSource = newSource.replace(`get ${method.name}() {`, `\n /** ${method.docs.text} */\n get ${method.name}(): Components.${name}["${method.name}"] {`); }); return newSource; }; const svelte = require('svelte/compiler'); const REGISTER_CUSTOM_ELEMENTS = 'defineCustomElements'; const APPLY_POLYFILLS = 'applyPolyfills'; const DEFAULT_LOADER_DIR = '/dist/loader'; const ignoreChecks = () => ['/* eslint-disable */', '/* tslint:disable */', '// @ts-nocheck'].join('\n'); const getFilteredComponents = (excludeComponents = [], cmps) => sortBy(cmps, (cmp) => cmp.tagName).filter((c) => !excludeComponents.includes(c.tagName) && !c.internal); const getPathToCorePackageLoader = (config, outputTarget) => { var _a; const basePkg = outputTarget.componentCorePackage || ''; const distOutputTarget = (_a = config.outputTargets) === null || _a === void 0 ? void 0 : _a.find((o) => o.type === 'dist'); const distAbsEsmLoaderPath = (distOutputTarget === null || distOutputTarget === void 0 ? void 0 : distOutputTarget.esmLoaderPath) && path.isAbsolute(distOutputTarget.esmLoaderPath) ? distOutputTarget.esmLoaderPath : null; const distRelEsmLoaderPath = config.rootDir && distAbsEsmLoaderPath ? path.relative(config.rootDir, distAbsEsmLoaderPath) : null; const loaderDir = outputTarget.loaderDir || distRelEsmLoaderPath || DEFAULT_LOADER_DIR; return normalizePath(path.join(basePkg, loaderDir)); }; function generateProxies(config, components, outputTarget) { const pathToCorePackageLoader = getPathToCorePackageLoader(config, outputTarget); let sourceImports = `import '${pathToCorePackageLoader}';`; let registerCustomElements = ''; if (outputTarget.includePolyfills && outputTarget.includeDefineCustomElements) { sourceImports = `import { ${APPLY_POLYFILLS}, ${REGISTER_CUSTOM_ELEMENTS} } from '${pathToCorePackageLoader}';\n`; registerCustomElements = `${APPLY_POLYFILLS}().then(() => ${REGISTER_CUSTOM_ELEMENTS}());`; } else if (!outputTarget.includePolyfills && outputTarget.includeDefineCustomElements) { sourceImports = `import { ${REGISTER_CUSTOM_ELEMENTS} } from '${pathToCorePackageLoader}';\n`; registerCustomElements = `${REGISTER_CUSTOM_ELEMENTS}();`; } const fileName = (c) => dashToPascalCase(c.tagName); const buildImports = (dir, ext = '') => components.map((c) => `import ${fileName(c)} from '${dir}/${fileName(c)}${ext}';`).join('\n'); const buildExports = () => components.map((c) => `export { ${fileName(c)} };`).join('\n'); const entry = [ ignoreChecks(), sourceImports, registerCustomElements, buildImports('./components'), buildExports(), ].join('\n'); const uncompiledFiles = components.map((c) => ({ name: fileName(c), meta: c, content: createComponentDefinition(c, outputTarget.componentBindings), })); const uncompiledEntry = [ignoreChecks(), buildImports('.', '.svelte'), buildExports()].join('\n'); const compiledFiles = uncompiledFiles.map((file) => ({ name: file.name, meta: file.meta, content: svelte.compile(file.content, { // legacy: outputTarget.legacy, // css: false, name: file.name, accessors: outputTarget.accessors, preserveComments: true, outputFilename: file.name, }).js.code, })); return { entry, uncompiledEntry, uncompiledFiles, compiledFiles, }; } const svelteProxyOutput = async (config, compilerCtx, outputTarget, components) => { const filteredComponents = getFilteredComponents(outputTarget.excludeComponents, components); const output = generateProxies(config, filteredComponents, outputTarget); await compilerCtx.fs.writeFile(outputTarget.proxiesFile, output.entry); const outputDir = path.dirname(outputTarget.proxiesFile); const uncompiledDir = path.resolve(outputDir, 'svelte'); const compiledDir = path.resolve(outputDir, 'components'); await compilerCtx.fs.writeFile(path.resolve(uncompiledDir, 'index.js'), output.uncompiledEntry); await Promise.all(output.uncompiledFiles.map((file) => { const filePath = path.resolve(uncompiledDir, `${file.name}.svelte`); return compilerCtx.fs.writeFile(filePath, file.content); })); await Promise.all(output.compiledFiles.map((file) => { const filePath = path.resolve(compiledDir, `${file.name}.ts`); const { content, meta } = file; return compilerCtx.fs.writeFile(filePath, [ ignoreChecks(), `import type { Components, JSX } from '${outputTarget.componentCorePackage}';\n`, generateTypings(meta), replaceMethodDefs(meta, generate$$TypeDefs(meta, content)), ].join('\n')); })); }; const normalizeOutputTarget = (config, outputTarget) => { var _a, _b; const results = Object.assign(Object.assign({}, outputTarget), { excludeComponents: outputTarget.excludeComponents || [], componentModels: outputTarget.componentModels || [], includePolyfills: (_a = outputTarget.includePolyfills) !== null && _a !== void 0 ? _a : true, includeDefineCustomElements: (_b = outputTarget.includeDefineCustomElements) !== null && _b !== void 0 ? _b : true }); if (config.rootDir == null) { throw new Error('rootDir is not set and it should be set by stencil itself'); } if (outputTarget.proxiesFile == null) { throw new Error('proxiesFile is required'); } if (outputTarget.directivesProxyFile && !path.isAbsolute(outputTarget.directivesProxyFile)) { results.proxiesFile = normalizePath(path.join(config.rootDir, outputTarget.proxiesFile)); } return results; }; const svelteOutputTarget = (outputTarget) => ({ type: 'custom', name: 'svelte-library', validate(config) { return normalizeOutputTarget(config, outputTarget); }, async generator(config, compilerCtx, buildCtx) { const timespan = buildCtx.createTimeSpan('generate svelte started', true); await svelteProxyOutput(config, compilerCtx, outputTarget, buildCtx.components); timespan.finish('generate svelte finished'); }, }); exports.svelteOutputTarget = svelteOutputTarget;