@web/dev-server-esbuild
Version:
Plugin for using esbuild in @web/dev-server
149 lines • 6.69 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.EsbuildPlugin = void 0;
const dev_server_core_1 = require("@web/dev-server-core");
const esbuild_1 = require("esbuild");
const util_1 = require("util");
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const dom5_1 = require("@web/dev-server-core/dist/dom5");
const parse5_1 = require("parse5");
const getEsbuildTarget_js_1 = require("./getEsbuildTarget.js");
const filteredWarnings = ['Unsupported source map comment'];
async function fileExists(path) {
try {
await (0, util_1.promisify)(fs_1.default.access)(path);
return true;
}
catch (_a) {
return false;
}
}
class EsbuildPlugin {
constructor(esbuildConfig) {
this.transformedHtmlFiles = [];
this.name = 'esbuild';
this.esbuildConfig = esbuildConfig;
}
async serverStart({ config, logger }) {
this.config = config;
this.logger = logger;
if (this.esbuildConfig.tsconfig) {
this.tsconfigRaw = await (0, util_1.promisify)(fs_1.default.readFile)(this.esbuildConfig.tsconfig, 'utf8');
}
}
resolveMimeType(context) {
const fileExtension = path_1.default.posix.extname(context.path);
if (this.esbuildConfig.handledExtensions.includes(fileExtension)) {
return 'js';
}
}
async resolveImport({ source, context }) {
const fileExtension = path_1.default.posix.extname(context.path);
if (!this.esbuildConfig.tsFileExtensions.includes(fileExtension)) {
// only handle typescript files
return;
}
if (!source.endsWith('.js') || !source.startsWith('.')) {
// only handle relative imports
return;
}
// a TS file imported a .js file relatively, but they might intend to import a .ts file instead
// check if the .ts file exists, and rewrite it in that case
const filePath = (0, dev_server_core_1.getRequestFilePath)(context.url, this.config.rootDir);
const fileDir = path_1.default.dirname(filePath);
const importAsTs = source.substring(0, source.length - 3) + '.ts';
const importedTsFilePath = path_1.default.join(fileDir, importAsTs);
if (!(await fileExists(importedTsFilePath))) {
return;
}
return importAsTs;
}
transformCacheKey(context) {
// the transformed files are cached per esbuild transform target
const target = (0, getEsbuildTarget_js_1.getEsbuildTarget)(this.esbuildConfig.target, context.headers['user-agent']);
return Array.isArray(target) ? target.join('_') : target;
}
async transform(context) {
let loader;
if (context.response.is('html')) {
// we are transforming inline scripts
loader = 'js';
}
else {
const fileExtension = path_1.default.posix.extname(context.path);
loader = this.esbuildConfig.loaders[fileExtension];
}
if (!loader) {
// we are not handling this file
return;
}
const target = (0, getEsbuildTarget_js_1.getEsbuildTarget)(this.esbuildConfig.target, context.headers['user-agent']);
if (target === 'esnext' && loader === 'js') {
// no need run esbuild, this happens when compile target is set to auto and the user is on a modern browser
return;
}
const filePath = (0, dev_server_core_1.getRequestFilePath)(context.url, this.config.rootDir);
if (context.response.is('html')) {
this.transformedHtmlFiles.push(context.path);
return this.__transformHtml(context, filePath, loader, target);
}
return this.__transformCode(context.body, filePath, loader, target);
}
async __transformHtml(context, filePath, loader, target) {
const documentAst = (0, parse5_1.parse)(context.body);
const inlineScripts = (0, dom5_1.queryAll)(documentAst, dom5_1.predicates.AND(dom5_1.predicates.hasTagName('script'), dom5_1.predicates.NOT(dom5_1.predicates.hasAttr('src')), dom5_1.predicates.OR(dom5_1.predicates.NOT(dom5_1.predicates.hasAttr('type')), dom5_1.predicates.hasAttrValue('type', 'module'))));
if (inlineScripts.length === 0) {
return;
}
for (const node of inlineScripts) {
const code = (0, dom5_1.getTextContent)(node);
const transformedCode = await this.__transformCode(code, filePath, loader, target);
(0, dom5_1.setTextContent)(node, transformedCode);
}
return (0, parse5_1.serialize)(documentAst);
}
async __transformCode(code, filePath, loader, target) {
try {
const transformOptions = {
sourcefile: filePath,
sourcemap: 'inline',
loader,
target,
// don't set any format for JS-like formats, otherwise esbuild reformats the code unnecessarily
format: ['js', 'jsx', 'ts', 'tsx'].includes(loader) ? undefined : 'esm',
jsxFactory: this.esbuildConfig.jsxFactory,
jsxFragment: this.esbuildConfig.jsxFragment,
define: this.esbuildConfig.define,
tsconfigRaw: this.tsconfigRaw,
banner: this.esbuildConfig.banner,
footer: this.esbuildConfig.footer,
};
const { code: transformedCode, warnings } = await (0, esbuild_1.transform)(code, transformOptions);
if (warnings) {
const relativePath = path_1.default.relative(process.cwd(), filePath);
for (const warning of warnings) {
if (!filteredWarnings.some(w => warning.text.includes(w))) {
this.logger.warn(`[/test-runner-esbuild] Warning while transforming ${relativePath}: ${warning.text}`);
}
}
}
return transformedCode;
}
catch (e) {
if (Array.isArray(e.errors)) {
const msg = e.errors[0];
if (msg.location) {
throw new dev_server_core_1.PluginSyntaxError(msg.text, filePath, code, msg.location.line, msg.location.column);
}
throw new Error(msg.text);
}
throw e;
}
}
}
exports.EsbuildPlugin = EsbuildPlugin;
//# sourceMappingURL=EsbuildPlugin.js.map