Skip to content

Instantly share code, notes, and snippets.

@hyrious
Created December 12, 2021 10:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hyrious/5546487637ac86ffb0ec14f754f87d15 to your computer and use it in GitHub Desktop.
Save hyrious/5546487637ac86ffb0ec14f754f87d15 to your computer and use it in GitHub Desktop.
possible plugin api through esm loaders
// plugin.mjs
import path from "path"
export default function test() {
return {
name: 'test',
setup({ onStart, onResolve, onLoad }) {
onStart(() => {
console.log('started --', import.meta.url)
})
onResolve({ filter: /()/ }, args => {
if (args.path.includes('--')) {
return {
path: path.join(
args.resolveDir,
args.path.slice(0, args.path.indexOf('-')) + ".ts"
)
}
}
})
onLoad({ filter: /()/ }, args => {
if (args.path.endsWith('b.ts')) {
return {
contents: `console.log(
import.meta.url,
'has been hooked by',
${JSON.stringify(import.meta.url)}
)`
}
}
})
}
}
}
import "./b-----?" // will be edited from plugin's onResolve
const message = "Hello, world!"
throw new Error(message as string)
// loader.mjs
const plugins = []
for (const arg of JSON.parse(process.env.__esbuild_plugins__)) {
let plugin
if (arg.startsWith('{')) {
plugin = new Function('return ' + arg)
} else {
plugin = await import(arg)
}
if (plugin.default) {
plugin = plugin.default
} else {
let key = Object.keys(plugin).filter(e => e.startsWith('_'))[0]
if (key) {
plugin = plugin[key]
} else {
throw new Error(`can not load plugin ${JSON.stringify(arg)}`)
}
}
if (typeof plugin === 'function') {
plugin = plugin()
}
plugins.push(plugin)
}
const onStart = fn => fn()
const onEnd = fn => undefined // never end, because never build
const resolvers = []
const onResolve = (options, callback) => {
resolvers.push([options, callback])
}
const loaders = []
const onLoad = (options, callback) => {
loaders.push([options, callback])
}
for (const plugin of plugins) {
plugin.setup({ onStart, onEnd, onResolve, onLoad, esbuild })
}
import path from "path"
import { fileURLToPath, pathToFileURL } from "url"
export async function resolve(id, context, defaultResolve) {
for (const [{ filter }, callback] of resolvers) {
if (filter.test(id)) {
const result = await callback({
path: id,
importer: context.parentURL,
namespace: 'file',
resolveDir: context.parentURL ? path.dirname(fileURLToPath(context.parentURL)) : process.cwd(),
kind: 'import-statement'
})
if (result?.path) {
return { url: result.path, format: 'module' }
}
}
}
let url
try {
url = new URL(id)
} catch (e) {
if (e instanceof TypeError) {
if (id[0] === '.') {
let result
await esbuild.build({
stdin: {
contents: `import ${JSON.stringify(id)}`,
resolveDir: path.dirname(fileURLToPath(context.parentURL))
},
write: false,
bundle: true,
platform: 'node',
plugins: [{
name: 'resolve',
setup({ onLoad }) {
onLoad({ filter: /.*/ }, (args) => {
result = args.path
return { contents: '' }
})
},
}],
})
if (result) {
url = pathToFileURL(result)
}
}
} else {
throw e
}
}
if (url) {
return { url: url.href, format: 'module' }
}
return defaultResolve(id, context, defaultResolve)
}
import fs from "fs"
import esbuild from "esbuild"
// node16.12
export async function load(url, context, defaultLoad) {
for (const [{ filter }, callback] of loaders) {
if (filter.test(url)) {
const result = await callback({
path: url,
})
if (result?.contents) {
// todo: transform contents with result.loader
return { format: 'module', source: result.contents }
}
}
}
if (!url.startsWith('file://')) {
url = pathToFileURL(url)
} else {
url = new URL(url)
}
const source = await fs.promises.readFile(url, 'utf-8')
const { code, warnings } = await esbuild.transform(source, {
sourcefile: url.pathname,
sourcemap: 'inline',
loader: path.extname(new URL(url).pathname).slice(1),
target: `node${process.versions.node}`,
format: context.format === 'module' ? 'esm' : 'cjs',
})
// todo: print warnings
return { format: 'module', source: code }
return defaultLoad(url, context, defaultLoad)
}
console.log("b --", import.meta.url)
// esmo.mjs
import { spawnSync } from 'child_process'
process.exit(spawnSync(
process.argv0,
[
'--no-warnings',
'--experimental-loader',
new URL('./b.mjs', import.meta.url).toString(),
process.argv.slice(2)
],
{
stdio: 'inherit',
env: {
...process.env,
// todo: make id more unique, like appending '_' if already exist
'__esbuild_plugins__': JSON.stringify([
'./a.mjs'
]),
}
}
).status)
@hyrious
Copy link
Author

hyrious commented Dec 12, 2021

Usage:

> node c.mjs a.ts
started -- file:///C:/Users/hyrious/Desktop/Aqua/a.mjs
C:\Users\hyrious\Desktop\Aqua\b.ts has been hooked by file:///C:/Users/hyrious/Desktop/Aqua/a.mjs
file:///C:/Users/hyrious/Desktop/Aqua/a.ts:3
throw new Error(message);
      ^

Error: Hello, world!
    at file:///C:/Users/hyrious/Desktop/Aqua/a.ts:3:7

More todo:

support namespace and pluginData. This shouldn't be too hard.

@hyrious
Copy link
Author

hyrious commented Dec 12, 2021

The realworld implementation should refer to https://github.com/evanw/esbuild/blob/master/lib/shared/common.ts#L630

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment