feat: add svelte support

This commit is contained in:
EGOIST 2021-02-27 13:34:12 +08:00
parent 40a38dda88
commit e18e1c8a39
7 changed files with 232 additions and 61 deletions

View File

@ -54,6 +54,7 @@
"ts-essentials": "^7.0.1",
"ts-jest": "^26.5.1",
"tsup": "^4.2.0",
"typescript": "^4.1.5"
"typescript": "^4.1.5",
"svelte": "3.34.0"
}
}

View File

@ -17,6 +17,7 @@ import glob from 'globby'
import { PrettyError } from './errors'
import { postcssPlugin } from './plugins/postcss'
import { externalPlugin } from './plugins/external'
import { sveltePlugin } from './plugins/svelte'
const textDecoder = new TextDecoder('utf-8')
@ -75,7 +76,10 @@ export type Options = {
splitting?: boolean
}
export type NormalizedOptions = MarkRequired<Options, 'entryPoints' | 'format'>
export type NormalizedOptions = MarkRequired<
Options,
'entryPoints' | 'format' | 'outDir'
>
const services: Map<string, Service> = new Map()
@ -103,8 +107,8 @@ const getOutputExtensionMap = (
}
export async function runEsbuild(
options: Options,
{ format }: { format: Format }
options: NormalizedOptions,
{ format, css }: { format: Format; css?: Set<string> }
): Promise<BuildResult | undefined> {
let service = services.get(format)
if (!service) {
@ -114,7 +118,7 @@ export async function runEsbuild(
const pkg = await loadPkg(process.cwd())
const deps = await getDeps(process.cwd())
const external = [...deps, ...(options.external || [])]
const outDir = options.outDir || 'dist'
const outDir = options.outDir
const outExtension = getOutputExtensionMap(pkg.type, format)
const env: { [k: string]: string } = {
@ -149,7 +153,8 @@ export async function runEsbuild(
// esbuild's `external` option doesn't support RegExp
// So here we use a custom plugin to implement it
externalPlugin(external),
postcssPlugin,
postcssPlugin(),
sveltePlugin({ css }),
],
define: {
...options.define,
@ -286,6 +291,8 @@ const normalizeOptions = async (
)
}
options.outDir = options.outDir || 'dist'
// Build in cjs format by default
if (!options.format) {
options.format = ['cjs']
@ -346,10 +353,25 @@ export async function build(_options: Options) {
})
}
const buildAll = () =>
Promise.all([
...options.format.map((format) => runEsbuild(options, { format })),
const buildAll = async () => {
const css: Set<string> = new Set()
await Promise.all([
...options.format.map((format, index) =>
runEsbuild(options, { format, css: index === 0 ? css : undefined })
),
])
if (css.size > 0) {
let cssCode = ''
for (const cssPart of css) {
cssCode += cssPart
}
await fs.promises.writeFile(
join(options.outDir, 'styles.css'),
cssCode,
'utf8'
)
}
}
try {
console.log(makeLabel('CLI', 'info'), `Target: ${options.target}`)

View File

@ -3,67 +3,69 @@ import path from 'path'
import { Plugin } from 'esbuild'
import { getPostcss } from '../utils'
export const postcssPlugin: Plugin = {
name: 'postcss',
export const postcssPlugin = (): Plugin => {
return {
name: 'postcss',
setup(build) {
const configCache = new Map()
setup(build) {
const configCache = new Map()
const getPostcssConfig = async (file: string) => {
const loadConfig = require('postcss-load-config')
const getPostcssConfig = async (file: string) => {
const loadConfig = require('postcss-load-config')
if (configCache.has(file)) {
return configCache.get(file)
}
try {
const result = await loadConfig({}, path.dirname(file))
configCache.set(file, result)
return result
} catch (error) {
if (error.message.includes('No PostCSS Config found in')) {
const result = { plugins: [], options: {} }
return result
if (configCache.has(file)) {
return configCache.get(file)
}
try {
const result = await loadConfig({}, path.dirname(file))
configCache.set(file, result)
return result
} catch (error) {
if (error.message.includes('No PostCSS Config found in')) {
const result = { plugins: [], options: {} }
return result
}
throw error
}
throw error
}
}
build.onLoad({ filter: /\.css$/ }, async (args) => {
const contents = await fs.promises.readFile(args.path, 'utf8')
build.onLoad({ filter: /\.css$/ }, async (args) => {
const contents = await fs.promises.readFile(args.path, 'utf8')
// Load postcss config
const { plugins, options } = await getPostcssConfig(args.path)
// Load postcss config
const { plugins, options } = await getPostcssConfig(args.path)
// Return if no postcss plugins are supplied
if (!plugins || plugins.length === 0) {
return {
contents,
loader: 'css',
}
}
// Load postcss
const postcss = getPostcss()
if (!postcss) {
return {
errors: [
{
text: `postcss is not installed`,
},
],
}
}
// Tranform CSS
const result = await postcss
?.default(plugins)
.process(contents, { ...options, from: args.path })
// Return if no postcss plugins are supplied
if (!plugins || plugins.length === 0) {
return {
contents,
contents: result.css,
loader: 'css',
}
}
// Load postcss
const postcss = getPostcss()
if (!postcss) {
return {
errors: [
{
text: `postcss is not installed`,
},
],
}
}
// Tranform CSS
const result = await postcss
?.default(plugins)
.process(contents, { ...options, from: args.path })
return {
contents: result.css,
loader: 'css',
}
})
},
})
},
}
}

61
src/plugins/svelte.ts Normal file
View File

@ -0,0 +1,61 @@
import fs from 'fs'
import path from 'path'
import { Plugin } from 'esbuild'
import { localRequire } from '../utils'
export const sveltePlugin = ({ css }: { css?: Set<string> }): Plugin => {
return {
name: 'svelte',
setup(build) {
let svelte: typeof import('svelte/compiler')
build.onLoad({ filter: /\.svelte$/ }, async (args) => {
svelte = svelte || localRequire('svelte/compiler')
if (!svelte) {
return {
errors: [{ text: `You need to install "svelte" in your project` }],
}
}
// This converts a message in Svelte's format to esbuild's format
let convertMessage = ({ message, start, end }: any) => {
let location
if (start && end) {
let lineText = source.split(/\r\n|\r|\n/g)[start.line - 1]
let lineEnd = start.line === end.line ? end.column : lineText.length
location = {
file: filename,
line: start.line,
column: start.column,
length: lineEnd - start.column,
lineText,
}
}
return { text: message, location }
}
// Load the file from the file system
let source = await fs.promises.readFile(args.path, 'utf8')
let filename = path.relative(process.cwd(), args.path)
// Convert Svelte syntax to JavaScript
try {
const result = svelte.compile(source, {
filename,
css: false,
})
if (css && result.css) {
css.add(result.css.code)
}
let contents =
result.js.code + `//# sourceMappingURL=` + result.js.map.toUrl()
return { contents, warnings: result.warnings.map(convertMessage) }
} catch (e) {
return { errors: [convertMessage(e)] }
}
})
},
}
}

View File

@ -119,6 +119,11 @@ export function getPostcss(): null | typeof import('postcss') {
return p && require(p)
}
export function localRequire(moduleName: string) {
const p = resovleFrom.silent(process.cwd(), moduleName)
return p && require(p)
}
export function pathExists(p: string) {
return new Promise((resolve) => {
fs.access(p, (err) => {

View File

@ -416,3 +416,78 @@ test('disable code splitting to get proper module.exports =', async () => {
"
`)
})
test('bundle svelte', async () => {
const { output, getFileContent } = await run(
getTestName(),
{
'input.ts': `import App from './App.svelte'
export { App }
`,
'App.svelte': `
<script>
let msg = 'hello svelte'
</script>
<span>{msg}</span>
<style>
span {color: red}
</style>
`,
},
{
// To make the snapshot leaner
flags: ['--external', 'svelte/internal'],
}
)
expect(output).toMatchInlineSnapshot(`
"\\"use strict\\";Object.defineProperty(exports, \\"__esModule\\", {value: true});// App.svelte
var _internal = require('svelte/internal');
function create_fragment(ctx) {
let span;
return {
c() {
span = _internal.element.call(void 0, \\"span\\");
span.textContent = \`\${msg}\`;
_internal.attr.call(void 0, span, \\"class\\", \\"svelte-1jo4k3z\\");
},
m(target, anchor) {
_internal.insert.call(void 0, target, span, anchor);
},
p: _internal.noop,
i: _internal.noop,
o: _internal.noop,
d(detaching) {
if (detaching)
_internal.detach.call(void 0, span);
}
};
}
var msg = \\"hello svelte\\";
var App = class extends _internal.SvelteComponent {
constructor(options) {
super();
_internal.init.call(void 0, this, options, null, create_fragment, _internal.safe_not_equal, {});
}
};
var App_default = App;
exports.App = App_default;
"
`)
expect(await getFileContent('dist/styles.css')).toMatchInlineSnapshot(
`"span.svelte-1jo4k3z{color:red}"`
)
})

View File

@ -3822,6 +3822,11 @@ supports-hyperlinks@^2.0.0:
has-flag "^4.0.0"
supports-color "^7.0.0"
svelte@3.34.0:
version "3.34.0"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.34.0.tgz#a0195a0db0305d78df87520711317b99e6fb8a26"
integrity sha512-xWcaQ/J4Yd5k0UWz+ef6i5RW5WP3hNpluEI2qtTTKlMOXERHpVL509O9lIw7sgEn1JjJgTOS+lnnDj99vQ3YqQ==
symbol-tree@^3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"