diff options
Diffstat (limited to 'packages/astro/snowpack-plugin-jsx.cjs')
-rw-r--r-- | packages/astro/snowpack-plugin-jsx.cjs | 189 |
1 files changed, 189 insertions, 0 deletions
diff --git a/packages/astro/snowpack-plugin-jsx.cjs b/packages/astro/snowpack-plugin-jsx.cjs new file mode 100644 index 000000000..f7b31b76f --- /dev/null +++ b/packages/astro/snowpack-plugin-jsx.cjs @@ -0,0 +1,189 @@ +const esbuild = require('esbuild'); +const colors = require('kleur/colors'); +const loggerPromise = import('./dist/logger.js'); +const { promises: fs } = require('fs'); + +const babel = require('@babel/core') +const eslexer = require('es-module-lexer'); +let error = (...args) => {}; + +/** + * @typedef {Object} PluginOptions - creates a new type named 'SpecialType' + * @prop {import('./src/config_manager').ConfigManager} configManager + * @prop {'development' | 'production'} mode + */ + +/** + * Returns esbuild loader for a given file + * @param filePath {string} + * @returns {import('esbuild').Loader} + */ +function getLoader(fileExt) { + /** @type {any} */ + return fileExt.substr(1); +} + +/** + * @type {import('snowpack').SnowpackPluginFactory<PluginOptions>} + */ +module.exports = function jsxPlugin(config, options = {}) { + const { + configManager, + logging, + } = options; + + let didInit = false; + return { + name: '@astrojs/snowpack-plugin-jsx', + resolve: { + input: ['.jsx', '.tsx'], + output: ['.js'], + }, + async load({ filePath, fileExt, ...transformContext }) { + if (!didInit) { + const logger = await loggerPromise; + error = logger.error; + await eslexer.init; + didInit = true; + } + + const contents = await fs.readFile(filePath, 'utf8'); + const loader = getLoader(fileExt); + + const { code, warnings } = await esbuild.transform(contents, { + loader, + jsx: 'preserve', + sourcefile: filePath, + sourcemap: config.buildOptions.sourcemap ? 'inline' : undefined, + charset: 'utf8', + sourcesContent: config.mode !== 'production', + }); + for (const warning of warnings) { + error(logging, 'renderer', `${colors.bold('!')} ${filePath} + ${warning.text}`); + } + + let renderers = await configManager.getRenderers(); + const importSources = new Set(renderers.map(({ jsxImportSource }) => jsxImportSource).filter(i => i)); + const getRenderer = (importSource) => renderers.find(({ jsxImportSource }) => jsxImportSource === importSource); + const getTransformOptions = async (importSource) => { + const { name } = getRenderer(importSource); + const { default: renderer } = await import(name); + return renderer.jsxTransformOptions(transformContext); + } + + if (importSources.size === 0) { + error(logging, 'renderer', `${colors.yellow(filePath)} +Unable to resolve a renderer that handles JSX transforms! Please include a \`renderer\` plugin which supports JSX in your \`astro.config.mjs\` file.`); + + return { + '.js': { + code: `(() => { + throw new Error("Hello world!"); + })()` + }, + } + } + + // If we only have a single renderer, we can skip a bunch of work! + if (importSources.size === 1) { + const result = transform(code, filePath, await getTransformOptions(Array.from(importSources)[0])) + + return { + '.js': { + code: result.code || '' + }, + }; + } + + // we need valid JS to scan for imports + // so let's just use `h` and `Fragment` as placeholders + const { code: codeToScan } = await esbuild.transform(code, { + loader: 'jsx', + jsx: 'transform', + jsxFactory: 'h', + jsxFragment: 'Fragment', + }); + + let imports = []; + if (/import/.test(codeToScan)) { + let [i] = eslexer.parse(codeToScan); + // @ts-ignore + imports = i; + } + + let importSource; + + if (imports.length > 0) { + for (let { n: name } of imports) { + if (name.indexOf('/') > -1) name = name.split('/')[0]; + if (importSources.has(name)) { + importSource = name; + break; + } + } + } + + if (!importSource) { + const multiline = contents.match(/\/\*\*[\S\s]*\*\//gm) || []; + + for (const comment of multiline) { + const [_, lib] = comment.match(/@jsxImportSource\s*(\S+)/) || []; + if (lib) { + importSource = lib; + break; + } + } + } + + if (!importSource) { + const importStatements = { + 'react': "import React from 'react'", + 'preact': "import { h } from 'preact'", + 'solid-js': "import 'solid-js/web'" + } + if (importSources.size > 1) { + const defaultRenderer = Array.from(importSources)[0]; + error(logging, 'renderer', `${colors.yellow(filePath)} +Unable to resolve a renderer that handles this file! With more than one renderer enabled, you should include an import or use a pragma comment. +Add ${colors.cyan(importStatements[defaultRenderer] || `import '${defaultRenderer}';`)} or ${colors.cyan(`/* jsxImportSource: ${defaultRenderer} */`)} to this file. +`); + } + + return { + '.js': { + code: contents + }, + } + } + + const result = transform(code, filePath, await getTransformOptions(importSource)); + + return { + '.js': { + code: result.code || '' + }, + }; + }, + cleanup() {}, + }; +} + +/** + * + * @param code {string} + * @param id {string} + * @param opts {{ plugins?: import('@babel/core').PluginItem[], presets?: import('@babel/core').PluginItem[] }|undefined} + */ +const transform = (code, id, { alias, plugins = [], presets = [] } = {}) => + babel.transformSync(code, { + presets, + plugins: [...plugins, alias ? ['babel-plugin-module-resolver', { root: process.cwd(), alias }] : undefined].filter(v => v), + cwd: process.cwd(), + filename: id, + ast: false, + compact: false, + sourceMaps: false, + configFile: false, + babelrc: false, + }); |