-
-
Save ShogunPanda/752cce88659a09bff827ef8d2ecf8c80 to your computer and use it in GitHub Desktop.
| const { dirname, sep, join, resolve } = require('path') | |
| const { build } = require('esbuild') | |
| const { readFile } = require('fs/promises') | |
| // TODO: Check how to solve when [dir] or [hash] are used | |
| function pinoPlugin(options) { | |
| options = { transports: [], ...options } | |
| return { | |
| name: 'pino', | |
| setup(currentBuild) { | |
| const pino = dirname(require.resolve('pino')) | |
| const threadStream = dirname(require.resolve('thread-stream')) | |
| // Adjust entrypoints if it is an array | |
| let entrypoints = currentBuild.initialOptions.entryPoints | |
| if (Array.isArray(entrypoints)) { | |
| let outbase = currentBuild.initialOptions.outbase | |
| // Find the outbase | |
| if (!outbase) { | |
| const hierarchy = entrypoints[0].split(sep) | |
| let i = 0 | |
| outbase = '' | |
| let nextOutbase = '' | |
| do { | |
| outbase = nextOutbase | |
| i++ | |
| nextOutbase = hierarchy.slice(0, i).join(sep) | |
| } while (entrypoints.every(e => e.startsWith(`${nextOutbase}/`))) | |
| } | |
| const newEntrypoints = {} | |
| for (const entrypoint of entrypoints) { | |
| const destination = (outbase ? entrypoint.replace(`${outbase}/`, '') : entrypoint).replace(/.js$/, '') | |
| newEntrypoints[destination] = entrypoint | |
| } | |
| entrypoints = newEntrypoints | |
| } | |
| // Now add our endpoints | |
| const userEntrypoints = Object.entries(entrypoints) | |
| const customEntrypoints = { | |
| 'thread-stream-worker': join(threadStream, 'lib/worker.js'), | |
| 'pino-worker': join(pino, 'lib/worker.js'), | |
| 'pino-pipeline-worker': join(pino, 'lib/worker-pipeline.js'), | |
| 'pino-file': join(pino, 'file.js') | |
| } | |
| // TODO: Add files in options.transport as well using require.resolve | |
| currentBuild.initialOptions.entryPoints = { ...entrypoints, ...customEntrypoints } | |
| // // Add a loader for all entrypoints to add the banner | |
| currentBuild.onResolve({ filter: /\.js$/ }, args => { | |
| if (args.kind === 'entry-point') { | |
| const absolutePath = resolve(process.cwd(), args.path) | |
| // Find in the entrypoints the one which has this definition in order to get the folder | |
| const destination = userEntrypoints.find(pair => resolve(process.cwd(), pair[1]) === absolutePath) | |
| if (destination) { | |
| return { path: join(args.resolveDir, args.path), pluginData: { pinoBundlerOverride: destination[0] } } | |
| } | |
| } | |
| return undefined | |
| }) | |
| // Prepend our overrides | |
| const banner = `/* Start of pino-webpack-bundler additions */ | |
| function pinoWebpackBundlerAbsolutePath(p) { | |
| try { | |
| return require('path').join(__dirname, p) | |
| } catch(e) { | |
| // This is needed not to trigger a warning if we try to use within CJS - Do we have another way? | |
| const f = new Function('p', 'return new URL(p, import.meta.url).pathname'); | |
| return f(p) | |
| } | |
| } | |
| ` | |
| currentBuild.onLoad({ filter: /\.js$/ }, async args => { | |
| if (!args.pluginData || !args.pluginData.pinoBundlerOverride) { | |
| return undefined | |
| } | |
| const contents = await readFile(args.path, 'utf8') | |
| // Find how much the asset is nested | |
| const prefix = | |
| args.pluginData.pinoBundlerOverride | |
| .split(sep) | |
| .slice(0, -1) | |
| .map(() => '..') | |
| .join(sep) || '.' | |
| const declarations = Object.keys(customEntrypoints) | |
| .map( | |
| id => | |
| `'${id === 'pino-file' ? 'pino/file' : id}': pinoWebpackBundlerAbsolutePath('${prefix}${sep}${id}.js')` | |
| ) | |
| .join(',') | |
| const overrides = `\nglobalThis.pinoBundlerOverrides = {${declarations}};\n/* End of pino-webpack-bundler additions */\n\n` | |
| return { | |
| contents: banner + overrides + contents | |
| } | |
| }) | |
| } | |
| } | |
| } | |
| build({ | |
| entryPoints: { | |
| main: 'src/index.js', | |
| }, | |
| bundle: true, | |
| platform: 'node', | |
| outdir: 'dist', | |
| plugins: [pinoPlugin({ transport: 'pino-pretty' })] | |
| }).catch(() => process.exit(1)) |
Nice solution! Glad to have been helpful!
I made a few changes to support different OS & typescript based on @scorsi's version:
do {
outbase = nextOutbase
i++
nextOutbase = hierarchy.slice(0, i).join(sep)
- } while (entrypoints.every((e) => e.startsWith(`${nextOutbase}/`)))
+ } while (entrypoints.every((e) => e.startsWith(`${nextOutbase}${sep}`)))for (const entrypoint of entrypoints) {
- const destination = (outbase ? entrypoint.replace(`${outbase}/`, '') : entrypoint).replace(/.js$/, '')
+ const destination = (outbase ? entrypoint.replace(`${outbase}${sep}`, '') : entrypoint).replace(/.(js|ts)$/, '')
newEntrypoints[destination] = entrypoint
}Here is the code:
const pinoPlugin = (options) => ({
name: 'pino',
setup(currentBuild) {
const pino = dirname(require.resolve('pino'))
const threadStream = dirname(require.resolve('thread-stream'))
let entrypoints = currentBuild.initialOptions.entryPoints
if (Array.isArray(entrypoints)) {
let outbase = currentBuild.initialOptions.outbase
if (!outbase) {
const hierarchy = entrypoints[0].split(sep)
let i = 0
outbase = ''
let nextOutbase = ''
do {
outbase = nextOutbase
i++
nextOutbase = hierarchy.slice(0, i).join(sep)
} while (entrypoints.every((e) => e.startsWith(`${nextOutbase}${sep}`)))
}
const newEntrypoints = {}
for (const entrypoint of entrypoints) {
const destination = (
outbase ? entrypoint.replace(`${outbase}${sep}`, '') : entrypoint
).replace(/.(js|ts)$/, '')
newEntrypoints[destination] = entrypoint
}
entrypoints = newEntrypoints
}
const customEntrypoints = {
'thread-stream-worker': join(threadStream, 'lib/worker.js'),
'pino-worker': join(pino, 'lib/worker.js'),
'pino-pipeline-worker': join(pino, 'lib/worker-pipeline.js'),
'pino-file': join(pino, 'file.js')
}
const transportsEntrypoints = Object.fromEntries(
(options.transports || []).map((t) => [
t,
join(dirname(require.resolve(t)), 'index.js')
])
)
currentBuild.initialOptions.entryPoints = {
...entrypoints,
...customEntrypoints,
...transportsEntrypoints
}
let pinoBundlerRan = false
currentBuild.onEnd(() => {
pinoBundlerRan = false
})
currentBuild.onLoad({ filter: /pino\.js$/ }, async (args) => {
if (pinoBundlerRan) return
pinoBundlerRan = true
const contents = await readFile(args.path, 'utf8')
const functionDeclaration = `
function pinoBundlerAbsolutePath(p) {
try {
return require('path').join(__dirname, p)
} catch(e) {
const f = new Function('p', 'return new URL(p, import.meta.url).pathname');
return f(p)
}
}
`
const pinoOverrides = Object.keys(customEntrypoints)
.map(
(id) =>
`'${
id === 'pino-file' ? 'pino/file' : id
}': pinoBundlerAbsolutePath('./${id}.js')`
)
.join(',')
const globalThisDeclaration = `
globalThis.__bundlerPathsOverrides =
globalThis.__bundlerPathsOverrides
? {...globalThis.__bundlerPathsOverrides, ${pinoOverrides}}
: {${pinoOverrides}};
`
const code = functionDeclaration + globalThisDeclaration
return {
contents: code + contents
}
})
}
})It works like a charm and kudos to @ShogunPanda & @scorsi!
Maybe it's great to have an esbuild plugin package just like pino-webpack-plugin.
@davipon Looks amazing dude! Nice work!
Maybe it's great to have an esbuild plugin package just like pino-webpack-plugin.
That would be nice. Do you want to write one?
Yeah, I'd like to write one.
While I'm relatively new to tooling unit tests, it might take some time ๐.
I will update it here once it's ready.
Thanks again for your work @ShogunPanda.
No problem sir! :)
Let me know when you have the package. I'll love to see it in action!
Just released the first version of the plugin: esbuild-plugin-pino.
I learned so much from your work, and I appreciate that! ๐
It was my first time working on a plugin. Please kindly let me know if there are any problems or suggestions.
@ShogunPanda would you mind if I create a PR to add my plugin to this page? https://github.com/pinojs/pino/blob/master/docs/bundling.md
I finally found how to correctly implement that esbuild plugin, here's the code:
I add the overrides on the first
pino.jsfile imports (I have two import of that lib (fastify dep + my own), instead of the entry point of the bundling. I use a globalpinoBundlerRanvariable to avoid running the code twice and ifglobalThis.__bundlerPathsOverridesis already defined I append the overrides to the object instead of redefining it.Thanks to @ShogunPanda for the first code which helps me a lot !