import { readable, Stores } from 'svelte/store' import { importFromStringSync } from 'module-from-string' import { sveltePreprocess } from 'svelte-preprocess' import type { Warning } from 'svelte/types/compiler/interfaces' import * as recast from 'recast' import { rollup } from 'rollup'; import svelte from "rollup-plugin-svelte" import alias from '@rollup/plugin-alias'; import commonjs from '@rollup/plugin-commonjs'; import { nodeResolve } from '@rollup/plugin-node-resolve'; import virtual from '@rollup/plugin-virtual'; import swc from '@rollup/plugin-swc'; import path from "node:path" export interface BuildSuccess { client: string server: { html: string css: string head: string } | null compiled: { client: string server: string | undefined } } export interface BuildError { error: any } export type BuildResult = BuildSuccess | BuildError /** * Builder is a class that builds, compiles and bundles Svelte components into a nice object for the template handler */ class Builder { path: string props: Stores locals: object compiled: { client: string | null server: string | null } ssr: boolean workingDir: string preprocess: object pathAliases?: object root: string constructor ( path: string, root: string, props: object, locals: object, client: string | null, server: string | null, ssr: boolean, workingDir: string, preprocess: object, pathAliases: object ) { this.path = path this.root = root this.props = readable(Object.assign(props, locals, props)) this.locals = locals this.compiled = { client, server } this.ssr = ssr this.workingDir = workingDir this.preprocess = preprocess this.pathAliases = pathAliases } async bundle (generate: 'ssr' | 'dom'): Promise { const bundle = (await (await rollup({ input: 'entry', output: { format: "esm", sourcemap: true }, watch: { skipWrite: true }, plugins: [ // @ts-expect-error see https://github.com/rollup/plugins/issues/1662 virtual({ entry: `import App from '${this.path}'; export default App` }), // @ts-expect-error see https://github.com/rollup/plugins/issues/1662 svelte({ compilerOptions: { generate, css: 'injected', hydratable: true, }, emitCss: false, preprocess: sveltePreprocess(this.preprocess), onwarn: (warning: Warning, handler: Function) => { if ( warning.code === 'missing-declaration' && warning.message.includes("'props'") ) return throw new Error(warning.message); } }), // @ts-expect-error see https://github.com/rollup/plugins/issues/1662 alias({ entries: Object.entries(this.pathAliases || {}).map((k, v) => { return new Object({ find: k, replacement: v }) }) }), // @ts-expect-error see https://github.com/rollup/plugins/issues/1662 commonjs(), nodeResolve({ browser: true, exportConditions: ['svelte'], extensions: ['.svelte'] }), // @ts-expect-error see https://github.com/rollup/plugins/issues/1662 swc({ swc: { jsc: { target: "es6", minify: { format: { comments: 'all' }, } }, minify: true, sourceMaps: true, inlineSourcesContent: true } }) ] })).generate({ format: "esm", sourcemap: true })).output bundle[0].map!.sources = bundle[0].map!.sources.map((el) => { return path.relative(this.path, this.root) + "/" + path.relative(this.root, el) }) return `//# sourceMappingURL=${bundle[0].map!.toUrl()}\n//# sourceURL=${path.relative(this.path, this.root) + "/" + path.relative(this.root, this.path) }\n${bundle[0].code}`.trim() } async client (): Promise { return ( this.compiled?.client || this.standardizeClient(await this.bundle("dom")) // eslint-disable-line ) } standardizeClient (code: string): string { const ast = recast.parse(code) let name: string | undefined recast.visit(ast, { visitExportNamedDeclaration: (path) => { const stagingName: any = path.node?.specifiers?.[0].local?.name name = typeof stagingName !== 'string' ? '' : stagingName path.prune() return false } }) recast.visit(ast, { visitIdentifier: (path) => { if (path.node.name === name) { path.node.name = 'App' } return false } }) return recast.print(ast).code } async server (): Promise<{ output: string, html: string, css: string, head: string }> { const output = this.compiled?.server || (await this.bundle('ssr')) // eslint-disable-line const Component = importFromStringSync(output, { globals: { props: this.props } }).default const { html, css, head } = await Component.render(this.locals) return { output, html, head, css: css.code } } async build (): Promise { try { const serv = this.ssr ? await this.server() : null const cli = await this.client() const comp = { client: cli, server: serv?.output } return { client: cli, server: serv, compiled: comp } } catch (e) { return { error: e } } } } export default Builder