UNPKG

@jsenv/core

Version:

Tool to develop, test and build js projects

186 lines (179 loc) 5.95 kB
/* * Some code uses globals specific to Node.js in code meant to run in browsers... * This plugin will replace some node globals to things compatible with web: * - process.env.NODE_ENV * - __filename * - __dirname * - global */ import { applyBabelPlugins } from "@jsenv/ast"; import { createMagicSource } from "@jsenv/sourcemap"; export const jsenvPluginCommonJsGlobals = () => { const transformCommonJsGlobals = async (urlInfo) => { if ( !urlInfo.content.includes("process.env.NODE_ENV") && !urlInfo.content.includes("__filename") && !urlInfo.content.includes("__dirname") ) { return null; } const isJsModule = urlInfo.type === "js_module"; const replaceMap = { "process.env.NODE_ENV": `("${ urlInfo.context.dev ? "development" : "production" }")`, "global": "globalThis", "__filename": isJsModule ? `import.meta.url.slice('file:///'.length)` : `document.currentScript.src`, "__dirname": isJsModule ? `import.meta.url.slice('file:///'.length).replace(/[\\\/\\\\][^\\\/\\\\]*$/, '')` : `new URL('./', document.currentScript.src).href`, }; const { metadata } = await applyBabelPlugins({ babelPlugins: [ [ babelPluginMetadataExpressionPaths, { replaceMap, allowConflictingReplacements: true, }, ], ], input: urlInfo.content, inputIsJsModule: urlInfo.type === "js_module", inputUrl: urlInfo.originalUrl, outputUrl: urlInfo.generatedUrl, }); const { expressionPaths } = metadata; const keys = Object.keys(expressionPaths); if (keys.length === 0) { return null; } const magicSource = createMagicSource(urlInfo.content); keys.forEach((key) => { expressionPaths[key].forEach((path) => { magicSource.replace({ start: path.node.start, end: path.node.end, replacement: replaceMap[key], }); }); }); return magicSource.toContentAndSourcemap(); }; return { name: "jsenv:commonjs_globals", appliesDuring: "*", transformUrlContent: { js_classic: transformCommonJsGlobals, js_module: transformCommonJsGlobals, }, }; }; // heavily inspired from https://github.com/jviide/babel-plugin-transform-replace-expressions // last known commit: 57b608e0eeb8807db53d1c68292621dfafb5599c const babelPluginMetadataExpressionPaths = ( babel, { replaceMap = {}, allowConflictingReplacements = false }, ) => { const { traverse, parse, types } = babel; const replacementMap = new Map(); const valueExpressionSet = new Set(); return { name: "metadata-replace", pre: (state) => { // https://github.com/babel/babel/blob/d50e78d45b608f6e0f6cc33aeb22f5db5027b153/packages/babel-traverse/src/path/replacement.js#L93 const parseExpression = (value) => { const expressionNode = parse(value, state.opts).program.body[0] .expression; traverse.removeProperties(expressionNode); return expressionNode; }; Object.keys(replaceMap).forEach((key) => { const keyExpressionNode = parseExpression(key); const candidateArray = replacementMap.get(keyExpressionNode.type) || []; const value = replaceMap[key]; const valueExpressionNode = parseExpression(value); const equivalentKeyExpressionIndex = candidateArray.findIndex( (candidate) => types.isNodesEquivalent( candidate.keyExpressionNode, keyExpressionNode, ), ); if ( !allowConflictingReplacements && equivalentKeyExpressionIndex > -1 ) { throw new Error( `Expressions ${candidateArray[equivalentKeyExpressionIndex].key} and ${key} conflict`, ); } const newCandidate = { key, value, keyExpressionNode, valueExpressionNode, }; if (equivalentKeyExpressionIndex > -1) { candidateArray[equivalentKeyExpressionIndex] = newCandidate; } else { candidateArray.push(newCandidate); } replacementMap.set(keyExpressionNode.type, candidateArray); }); replacementMap.forEach((candidateArray) => { candidateArray.forEach((candidate) => { valueExpressionSet.add(candidate.valueExpressionNode); }); }); }, visitor: { Program: (programPath, state) => { const expressionPaths = {}; programPath.traverse({ Expression(path) { if (valueExpressionSet.has(path.node)) { path.skip(); return; } const candidateArray = replacementMap.get(path.node.type); if (!candidateArray) { return; } const candidateFound = candidateArray.find((candidate) => { return types.isNodesEquivalent( candidate.keyExpressionNode, path.node, ); }); if (candidateFound) { try { types.validate( path.parent, path.key, candidateFound.valueExpressionNode, ); } catch (err) { if (err instanceof TypeError) { path.skip(); return; } throw err; } const paths = expressionPaths[candidateFound.key]; if (paths) { expressionPaths[candidateFound.key] = [...paths, path]; } else { expressionPaths[candidateFound.key] = [path]; } return; } }, }); state.file.metadata.expressionPaths = expressionPaths; }, }, }; };