astro/packages/astro/snowpack-plugin-jsx.cjs
2021-07-28 18:32:50 +00:00

190 lines
5.8 KiB
JavaScript

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);
}
// The `tsx` loader in esbuild will remove unused imports, so we need to
// be careful about esbuild not treating h, React, Fragment, etc. as unused.
const PREVENT_UNUSED_IMPORTS = ';;(React,Fragment,h);';
/**
* @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) {
throw new Error(`${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.`);
}
// 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 here, so we can use `h` and `Fragment` as placeholders
// NOTE(fks, matthewp): Make sure that you're transforming the original contents here.
const { code: codeToScan } = await esbuild.transform(contents + PREVENT_UNUSED_IMPORTS, {
loader,
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,
});