From d73280949d51cfabfed11ef8bee12d0965ab00e9 Mon Sep 17 00:00:00 2001 From: Johnwesley R Date: Thu, 10 Mar 2022 18:28:16 -0500 Subject: [PATCH 1/3] feat: add context functions to more hooks, fix async hooks --- README.md | 7 +++- src/esbuild/index.ts | 55 +++++++++++++++------------ src/types.ts | 8 ++-- src/webpack/genContext.ts | 38 +++++++++++++++++++ src/webpack/index.ts | 64 +++++++++----------------------- src/webpack/loaders/load.ts | 3 +- src/webpack/loaders/transform.ts | 3 +- 7 files changed, 99 insertions(+), 79 deletions(-) create mode 100644 src/webpack/genContext.ts diff --git a/README.md b/README.md index 23d56e84..eb181103 100644 --- a/README.md +++ b/README.md @@ -31,16 +31,18 @@ Currently supports: 2. Rollup and esbuild do not support using `enforce` to control the order of plugins. Users need to maintain the order manually. 3. Although esbuild can handle both JavaScript and CSS and many other file formats, you can only return JavaScript in `load` and `transform` results. -### [`buildStart`](https://rollupjs.org/guide/en/#buildstart) Context +### Hook Context ([`buildStart`](https://rollupjs.org/guide/en/#buildstart), [`buildEnd`](https://rollupjs.org/guide/en/#buildend), [`transform`](https://rollupjs.org/guide/en/#transformers), [`load`](https://rollupjs.org/guide/en/#load), and [`watchChange`](https://rollupjs.org/guide/en/#watchchange)) ###### Supported | Hook | Rollup | Vite | Webpack 4 | Webpack 5 | esbuild | | ---- | :----: | :--: | :-------: | :-------: | :-----: | | [`this.addWatchFile`](https://rollupjs.org/guide/en/#thisaddwatchfile) | ✅ | ✅ | ✅ | ✅ | ✅ | -| [`this.emitFile`](https://rollupjs.org/guide/en/#thisemitfile) | ✅ | ✅ | ✅ | ✅ | ✅ | +| [`this.emitFile`](https://rollupjs.org/guide/en/#thisemitfile)4 | ✅ | ✅ | ✅ | ✅ | ✅ | | [`this.getWatchFiles`](https://rollupjs.org/guide/en/#thisgetwatchfiles) | ✅ | ✅ | ✅ | ✅ | ✅ | +4. Currently, [`this.emitFile`](https://rollupjs.org/guide/en/#thisemitfile) only supports the `EmittedAsset` variant. + ## Usage ```ts @@ -170,6 +172,7 @@ export const unplugin = createUnplugin((options: UserOptions, meta) => { - [unplugin-icons](https://github.com/antfu/unplugin-icons) - [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components) - [unplugin-upload-cdn](https://github.com/zenotsai/unplugin-upload-cdn) +- [unplugin-web-ext](https://github.com/jwr12135/unplugin-web-ext) ## License diff --git a/src/esbuild/index.ts b/src/esbuild/index.ts index 972883ef..de19304f 100644 --- a/src/esbuild/index.ts +++ b/src/esbuild/index.ts @@ -1,10 +1,10 @@ -import fs from 'fs' +import fs, { existsSync, mkdirSync } from 'fs' import path from 'path' import chokidar from 'chokidar' import type { PartialMessage } from 'esbuild' import type { SourceMap } from 'rollup' import type { RawSourceMap } from '@ampproject/remapping/dist/types/types' -import type { UnpluginContext, UnpluginContextMeta, UnpluginFactory, UnpluginInstance } from '../types' +import type { UnpluginBuildContext, UnpluginContext, UnpluginContextMeta, UnpluginFactory, UnpluginInstance } from '../types' import { combineSourcemaps, fixSourceMap, guessLoader } from './utils' const watchListRecord: Record = {} @@ -27,21 +27,28 @@ export function getEsbuildPlugin ( const onResolveFilter = plugin.esbuild?.onResolveFilter ?? /.*/ const onLoadFilter = plugin.esbuild?.onLoadFilter ?? /.*/ - if (plugin.buildStart) { - onStart(() => plugin.buildStart!.call({ - addWatchFile (id) { - watchList.add(path.resolve(id)) - }, - emitFile (emittedFile) { - const outFileName = emittedFile.fileName || emittedFile.name - if (initialOptions.outdir && emittedFile.source && outFileName) { - fs.writeFileSync(path.resolve(initialOptions.outdir, outFileName), emittedFile.source) - } - }, - getWatchFiles () { - return Array.from(watchList) + const context:UnpluginBuildContext = { + addWatchFile (id) { + watchList.add(path.resolve(id)) + }, + emitFile (emittedFile) { + const outFileName = emittedFile.fileName || emittedFile.name + if (initialOptions.outdir && emittedFile.source && outFileName) { + fs.writeFileSync(path.resolve(initialOptions.outdir, outFileName), emittedFile.source) } - })) + }, + getWatchFiles () { + return Array.from(watchList) + } + } + + // Ensure output directory exists for this.emitFile + if (initialOptions.outdir && !existsSync(initialOptions.outdir)) { + mkdirSync(initialOptions.outdir, { recursive: true }) + } + + if (plugin.buildStart) { + onStart(() => plugin.buildStart!.call(context)) } if (plugin.buildEnd || initialOptions.watch) { @@ -50,8 +57,8 @@ export function getEsbuildPlugin ( watch: false }) - onEnd(() => { - plugin.buildEnd?.() + onEnd(async () => { + await plugin.buildEnd!.call(context) if (initialOptions.watch) { Object.keys(watchListRecord).forEach((id) => { if (!watchList.has(id)) { @@ -62,12 +69,12 @@ export function getEsbuildPlugin ( watchList.forEach((id) => { if (!Object.keys(watchListRecord).includes(id)) { watchListRecord[id] = chokidar.watch(id) - watchListRecord[id].on('change', () => { - plugin.watchChange?.(id, { event: 'update' }) + watchListRecord[id].on('change', async () => { + await plugin.watchChange?.call(context, id, { event: 'update' }) rebuild() }) - watchListRecord[id].on('unlink', () => { - plugin.watchChange?.(id, { event: 'delete' }) + watchListRecord[id].on('unlink', async () => { + await plugin.watchChange?.call(context, id, { event: 'delete' }) rebuild() }) } @@ -102,7 +109,7 @@ export function getEsbuildPlugin ( let code: string | undefined, map: SourceMap | null | undefined if (plugin.load) { - const result = await plugin.load.call(pluginContext, args.path) + const result = await plugin.load.call(Object.assign(context, pluginContext), args.path) if (typeof result === 'string') { code = result } else if (typeof result === 'object' && result !== null) { @@ -134,7 +141,7 @@ export function getEsbuildPlugin ( code = await fs.promises.readFile(args.path, 'utf8') } - const result = await plugin.transform.call(pluginContext, code, args.path) + const result = await plugin.transform.call(Object.assign(context, pluginContext), code, args.path) if (typeof result === 'string') { code = result } else if (typeof result === 'object' && result !== null) { diff --git a/src/types.ts b/src/types.ts index c76b1e1b..8cc06681 100644 --- a/src/types.ts +++ b/src/types.ts @@ -27,12 +27,12 @@ export interface UnpluginOptions { name: string; enforce?: 'post' | 'pre' | undefined; buildStart?: (this: UnpluginBuildContext) => Promise | void; - buildEnd?: () => Promise | void; + buildEnd?: (this: UnpluginBuildContext) => Promise | void; transformInclude?: (id: string) => boolean; - transform?: (this: UnpluginContext, code: string, id: string) => Thenable; - load?: (this: UnpluginContext, id: string) => Thenable + transform?: (this: UnpluginBuildContext & UnpluginContext, code: string, id: string) => Thenable; + load?: (this: UnpluginBuildContext & UnpluginContext, id: string) => Thenable resolveId?: (id: string, importer?: string) => Thenable - watchChange?: (id: string, change: {event: 'create' | 'update' | 'delete'}) => void + watchChange?: (this: UnpluginBuildContext, id: string, change: {event: 'create' | 'update' | 'delete'}) => void // framework specify extends rollup?: Partial diff --git a/src/webpack/genContext.ts b/src/webpack/genContext.ts new file mode 100644 index 00000000..d195d223 --- /dev/null +++ b/src/webpack/genContext.ts @@ -0,0 +1,38 @@ +import { resolve } from 'path' +import { sources } from 'webpack' +import type { Compilation } from 'webpack' +import type { UnpluginBuildContext } from 'src' + +export default function genContext (compilation: Compilation):UnpluginBuildContext { + return { + addWatchFile (id) { + (compilation.fileDependencies ?? compilation.compilationDependencies).add( + resolve(process.cwd(), id) + ) + }, + emitFile (emittedFile) { + const outFileName = emittedFile.fileName || emittedFile.name + if (emittedFile.source && outFileName) { + compilation.emitAsset( + outFileName, + // @ts-ignore + sources + ? new sources.RawSource( + typeof emittedFile.source === 'string' + ? emittedFile.source + : Buffer.from(emittedFile.source) + ) + : { + source: () => emittedFile.source, + size: () => emittedFile.source!.length + } + ) + } + }, + getWatchFiles () { + return Array.from( + compilation.fileDependencies ?? compilation.compilationDependencies + ) + } + } +} diff --git a/src/webpack/index.ts b/src/webpack/index.ts index deb48f1c..0d57e2f6 100644 --- a/src/webpack/index.ts +++ b/src/webpack/index.ts @@ -5,7 +5,7 @@ import VirtualModulesPlugin from 'webpack-virtual-modules' import type { Resolver, ResolveRequest } from 'enhanced-resolve' import type { UnpluginContextMeta, UnpluginInstance, UnpluginFactory, WebpackCompiler, ResolvedUnpluginOptions } from '../types' import { slash, backSlash } from './utils' - +import genContext from './genContext' const _dirname = typeof __dirname !== 'undefined' ? __dirname : dirname(fileURLToPath(import.meta.url)) const TRANSFORM_LOADER = resolve(_dirname, 'webpack/loaders/transform.js') const LOAD_LOADER = resolve(_dirname, 'webpack/loaders/load.js') @@ -152,58 +152,28 @@ export function getWebpackPlugin ( plugin.webpack(compiler) } - compiler.hooks.thisCompilation.tap(plugin.name, (compilation) => { - plugin.buildStart?.call({ - addWatchFile (id) { - (compilation.fileDependencies ?? compilation.compilationDependencies).add( - resolve(process.cwd(), id) - ) - }, - emitFile (emittedFile) { - const outFileName = emittedFile.fileName || emittedFile.name - if (emittedFile.source && outFileName) { - compilation.emitAsset( - outFileName, - // @ts-ignore - compiler.webpack?.sources - ? new compiler.webpack.sources.RawSource( - typeof emittedFile.source === 'string' - ? emittedFile.source - : Buffer.from(emittedFile.source) - ) - : { - source: () => emittedFile.source, - size: () => emittedFile.source!.length - } - ) - } - }, - getWatchFiles () { - return Array.from( - compilation.fileDependencies ?? compilation.compilationDependencies - ) - } - }) - }) - - if (plugin.watchChange) { - compiler.hooks.watchRun.tap(plugin.name, (compilation) => { - if (compilation.modifiedFiles) { - compilation.modifiedFiles.forEach(file => - plugin.watchChange!(file, { event: 'update' }) + compiler.hooks.make.tapPromise(plugin.name, async (compilation) => { + if (plugin.watchChange && (compiler.modifiedFiles || compiler.removedFiles)) { + const promises:Promise[] = [] + if (compiler.modifiedFiles) { + compiler.modifiedFiles.forEach(file => + promises.push(Promise.resolve(plugin.watchChange!(file, { event: 'update' }))) ) } - if (compilation.removedFiles) { - compilation.removedFiles.forEach(file => - plugin.watchChange!(file, { event: 'delete' }) + if (compiler.removedFiles) { + compiler.removedFiles.forEach(file => + promises.push(Promise.resolve(plugin.watchChange!(file, { event: 'delete' }))) ) } - }) - } + await Promise.all(promises) + } + + return await plugin.buildStart!.call(genContext(compilation)) + }) if (plugin.buildEnd) { - compiler.hooks.done.tapPromise(plugin.name, async () => { - await plugin.buildEnd!() + compiler.hooks.emit.tapPromise(plugin.name, async (compilation) => { + await plugin.buildEnd!.call(genContext(compilation)) }) } } diff --git a/src/webpack/loaders/load.ts b/src/webpack/loaders/load.ts index b9cc7ff6..34d304a0 100644 --- a/src/webpack/loaders/load.ts +++ b/src/webpack/loaders/load.ts @@ -1,5 +1,6 @@ import type { LoaderContext } from 'webpack' import { UnpluginContext } from '../../types' +import genContext from '../genContext' import { slash } from '../utils' export default async function load (this: LoaderContext, source: string, map: any) { @@ -21,7 +22,7 @@ export default async function load (this: LoaderContext, source: string, ma id = id.slice(plugin.__virtualModulePrefix.length) } - const res = await plugin.load.call(context, slash(id)) + const res = await plugin.load.call(Object.assign(this._compilation && genContext(this._compilation), context), slash(id)) if (res == null) { callback(null, source, map) diff --git a/src/webpack/loaders/transform.ts b/src/webpack/loaders/transform.ts index 57e92030..16fcd3ca 100644 --- a/src/webpack/loaders/transform.ts +++ b/src/webpack/loaders/transform.ts @@ -1,5 +1,6 @@ import type { LoaderContext } from 'webpack' import { UnpluginContext } from '../../types' +import genContext from '../genContext' export default async function transform (this: LoaderContext, source: string, map: any) { const callback = this.async() @@ -14,7 +15,7 @@ export default async function transform (this: LoaderContext, source: strin error: error => this.emitError(typeof error === 'string' ? new Error(error) : error), warn: error => this.emitWarning(typeof error === 'string' ? new Error(error) : error) } - const res = await plugin.transform.call(context, source, this.resource) + const res = await plugin.transform.call(Object.assign(this._compilation && genContext(this._compilation), context), source, this.resource) if (res == null) { callback(null, source, map) From d8a7a8fe9b701c48ad825f1ca4d2c5d36e5e2346 Mon Sep 17 00:00:00 2001 From: Johnwesley R Date: Thu, 10 Mar 2022 18:49:50 -0500 Subject: [PATCH 2/3] fix: webpack watchChange context & buildStart required --- src/webpack/index.ts | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/webpack/index.ts b/src/webpack/index.ts index 0d57e2f6..1ceb5005 100644 --- a/src/webpack/index.ts +++ b/src/webpack/index.ts @@ -152,24 +152,29 @@ export function getWebpackPlugin ( plugin.webpack(compiler) } - compiler.hooks.make.tapPromise(plugin.name, async (compilation) => { - if (plugin.watchChange && (compiler.modifiedFiles || compiler.removedFiles)) { - const promises:Promise[] = [] - if (compiler.modifiedFiles) { - compiler.modifiedFiles.forEach(file => - promises.push(Promise.resolve(plugin.watchChange!(file, { event: 'update' }))) - ) - } - if (compiler.removedFiles) { - compiler.removedFiles.forEach(file => - promises.push(Promise.resolve(plugin.watchChange!(file, { event: 'delete' }))) - ) + if (plugin.watchChange || plugin.buildStart) { + compiler.hooks.make.tapPromise(plugin.name, async (compilation) => { + const context = genContext(compilation) + if (plugin.watchChange && (compiler.modifiedFiles || compiler.removedFiles)) { + const promises:Promise[] = [] + if (compiler.modifiedFiles) { + compiler.modifiedFiles.forEach(file => + promises.push(Promise.resolve(plugin.watchChange!.call(context, file, { event: 'update' }))) + ) + } + if (compiler.removedFiles) { + compiler.removedFiles.forEach(file => + promises.push(Promise.resolve(plugin.watchChange!.call(context, file, { event: 'delete' }))) + ) + } + await Promise.all(promises) } - await Promise.all(promises) - } - return await plugin.buildStart!.call(genContext(compilation)) - }) + if (plugin.buildStart) { + return await plugin.buildStart.call(context) + } + }) + } if (plugin.buildEnd) { compiler.hooks.emit.tapPromise(plugin.name, async (compilation) => { From 8eb4eb603d3e9584db97084f242f3eae9a51c956 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Fri, 11 Mar 2022 08:41:22 +0800 Subject: [PATCH 3/3] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eb181103..cb0658a2 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Currently supports: 2. Rollup and esbuild do not support using `enforce` to control the order of plugins. Users need to maintain the order manually. 3. Although esbuild can handle both JavaScript and CSS and many other file formats, you can only return JavaScript in `load` and `transform` results. -### Hook Context ([`buildStart`](https://rollupjs.org/guide/en/#buildstart), [`buildEnd`](https://rollupjs.org/guide/en/#buildend), [`transform`](https://rollupjs.org/guide/en/#transformers), [`load`](https://rollupjs.org/guide/en/#load), and [`watchChange`](https://rollupjs.org/guide/en/#watchchange)) +### Hook Context ###### Supported