@revolist/svelte-output-target
Version:
Svelte output target for @stencil/core components.
259 lines (239 loc) • 11.1 kB
JavaScript
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;
;