Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add context functions to more hooks, fix async hooks #57

Merged
merged 3 commits into from
Mar 15, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
antfu marked this conversation as resolved.
Show resolved Hide resolved

###### 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)<sup>4</sup> | ✅ | ✅ | ✅ | ✅ | ✅ |
| [`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
Expand Down Expand Up @@ -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

Expand Down
55 changes: 31 additions & 24 deletions src/esbuild/index.ts
Original file line number Diff line number Diff line change
@@ -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<string, chokidar.FSWatcher> = {}
Expand All @@ -27,21 +27,28 @@ export function getEsbuildPlugin <UserOptions = {}> (
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) {
Expand All @@ -50,8 +57,8 @@ export function getEsbuildPlugin <UserOptions = {}> (
watch: false
})

onEnd(() => {
plugin.buildEnd?.()
onEnd(async () => {
await plugin.buildEnd!.call(context)
if (initialOptions.watch) {
Object.keys(watchListRecord).forEach((id) => {
if (!watchList.has(id)) {
Expand All @@ -62,12 +69,12 @@ export function getEsbuildPlugin <UserOptions = {}> (
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()
})
}
Expand Down Expand Up @@ -102,7 +109,7 @@ export function getEsbuildPlugin <UserOptions = {}> (
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) {
Expand Down Expand Up @@ -134,7 +141,7 @@ export function getEsbuildPlugin <UserOptions = {}> (
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) {
Expand Down
8 changes: 4 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ export interface UnpluginOptions {
name: string;
enforce?: 'post' | 'pre' | undefined;
buildStart?: (this: UnpluginBuildContext) => Promise<void> | void;
buildEnd?: () => Promise<void> | void;
buildEnd?: (this: UnpluginBuildContext) => Promise<void> | void;
transformInclude?: (id: string) => boolean;
transform?: (this: UnpluginContext, code: string, id: string) => Thenable<TransformResult>;
load?: (this: UnpluginContext, id: string) => Thenable<TransformResult>
transform?: (this: UnpluginBuildContext & UnpluginContext, code: string, id: string) => Thenable<TransformResult>;
load?: (this: UnpluginBuildContext & UnpluginContext, id: string) => Thenable<TransformResult>
resolveId?: (id: string, importer?: string) => Thenable<string | ExternalIdResult | null | undefined>
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<RollupPlugin>
Expand Down
38 changes: 38 additions & 0 deletions src/webpack/genContext.ts
Original file line number Diff line number Diff line change
@@ -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
)
}
}
}
63 changes: 19 additions & 44 deletions src/webpack/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -152,58 +152,33 @@ export function getWebpackPlugin<UserOptions = {}> (
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
}
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<void>[] = []
if (compiler.modifiedFiles) {
compiler.modifiedFiles.forEach(file =>
promises.push(Promise.resolve(plugin.watchChange!.call(context, file, { event: 'update' })))
)
}
},
getWatchFiles () {
return Array.from(
compilation.fileDependencies ?? compilation.compilationDependencies
)
if (compiler.removedFiles) {
compiler.removedFiles.forEach(file =>
promises.push(Promise.resolve(plugin.watchChange!.call(context, file, { event: 'delete' })))
)
}
await Promise.all(promises)
}
})
})

if (plugin.watchChange) {
compiler.hooks.watchRun.tap(plugin.name, (compilation) => {
if (compilation.modifiedFiles) {
compilation.modifiedFiles.forEach(file =>
plugin.watchChange!(file, { event: 'update' })
)
}
if (compilation.removedFiles) {
compilation.removedFiles.forEach(file =>
plugin.watchChange!(file, { event: 'delete' })
)
if (plugin.buildStart) {
return await plugin.buildStart.call(context)
}
})
}

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))
})
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/webpack/loaders/load.ts
Original file line number Diff line number Diff line change
@@ -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<any>, source: string, map: any) {
Expand All @@ -21,7 +22,7 @@ export default async function load (this: LoaderContext<any>, 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)
Expand Down
3 changes: 2 additions & 1 deletion src/webpack/loaders/transform.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { LoaderContext } from 'webpack'
import { UnpluginContext } from '../../types'
import genContext from '../genContext'

export default async function transform (this: LoaderContext<any>, source: string, map: any) {
const callback = this.async()
Expand All @@ -14,7 +15,7 @@ export default async function transform (this: LoaderContext<any>, 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)
Expand Down