Skip to content

Commit

Permalink
fix!: drop watchChange hook for esbuild (#284)
Browse files Browse the repository at this point in the history
  • Loading branch information
sxzz authored Mar 11, 2023
1 parent 34bc653 commit 095849b
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 89 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Currently supports:
| `transformInclude`<sup>2</sup> ||||||
| [`transform`](https://rollupjs.org/guide/en/#transformers) ||||| ✅ <sup>3</sup> |
| [`watchChange`](https://rollupjs.org/guide/en/#watchchange) ||||||
| [`buildEnd`](https://rollupjs.org/guide/en/#buildend) ||||| |
| [`buildEnd`](https://rollupjs.org/guide/en/#buildend) ||||| |
| [`writeBundle`](https://rollupjs.org/guide/en/#writebundle)<sup>4</sup> ||||||

1. Rollup and esbuild do not support using `enforce` to control the order of plugins. Users need to maintain the order manually.
Expand All @@ -43,9 +43,9 @@ Currently supports:
| Hook | Rollup | Vite | Webpack 4 | Webpack 5 | esbuild |
| ---- | :----: | :--: | :-------: | :-------: | :-----: |
| [`this.parse`](https://rollupjs.org/guide/en/#thisparse) ||||||
| [`this.addWatchFile`](https://rollupjs.org/guide/en/#thisaddwatchfile) ||||| |
| [`this.addWatchFile`](https://rollupjs.org/guide/en/#thisaddwatchfile) ||||| |
| [`this.emitFile`](https://rollupjs.org/guide/en/#thisemitfile)<sup>5</sup> ||||||
| [`this.getWatchFiles`](https://rollupjs.org/guide/en/#thisgetwatchfiles) ||||| |
| [`this.getWatchFiles`](https://rollupjs.org/guide/en/#thisgetwatchfiles) ||||| |
| [`this.warn`](https://rollupjs.org/guide/en/#thiswarn) ||||||
| [`this.error`](https://rollupjs.org/guide/en/#thiserror) ||||||

Expand Down
80 changes: 7 additions & 73 deletions src/esbuild/index.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,16 @@
import fs from 'fs'
import path from 'path'
import chokidar from 'chokidar'
import type { PartialMessage } from 'esbuild'
import type { SourceMap } from 'rollup'
import { Parser } from 'acorn'
import type { RawSourceMap } from '@ampproject/remapping'
import type { EsbuildPlugin, UnpluginBuildContext, UnpluginContext, UnpluginContextMeta, UnpluginFactory, UnpluginInstance, UnpluginOptions } from '../types'
import { combineSourcemaps, fixSourceMap, guessLoader, toArray } from './utils'

const watchListRecord: Record<string, chokidar.FSWatcher> = {}
const watchList: Set<string> = new Set()
import { combineSourcemaps, createEsbuildContext, guessLoader, processCodeWithSourceMap, toArray } from './utils'

let i = 0

export function getEsbuildPlugin<UserOptions = {}>(
factory: UnpluginFactory<UserOptions>,
): UnpluginInstance<UserOptions>['esbuild'] {
function processCodeWithSourceMap(map: SourceMap | null | undefined, code: string) {
if (map) {
if (!map.sourcesContent || map.sourcesContent.length === 0)
map.sourcesContent = [code]

map = fixSourceMap(map as RawSourceMap)
code += `\n//# sourceMappingURL=${map.toUrl()}`
}
return code
}

return (userOptions?: UserOptions): EsbuildPlugin => {
const meta: UnpluginContextMeta = {
framework: 'esbuild',
Expand All @@ -35,75 +19,25 @@ export function getEsbuildPlugin<UserOptions = {}>(

const setup = (plugin: UnpluginOptions): EsbuildPlugin['setup'] =>
plugin.esbuild?.setup
?? ((pluginBuild) => {
const { onStart, onEnd, onResolve, onLoad, initialOptions, esbuild: { build } } = pluginBuild
meta.build = pluginBuild
?? ((build) => {
meta.build = build
const { onStart, onEnd, onResolve, onLoad, initialOptions } = build

const onResolveFilter = plugin.esbuild?.onResolveFilter ?? /.*/
const onLoadFilter = plugin.esbuild?.onLoadFilter ?? /.*/

const context: UnpluginBuildContext = {
parse(code: string, opts: any = {}) {
return Parser.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest',
locations: true,
...opts,
})
},
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 [...watchList]
},
}

// Ensure output directory exists for this.emitFile
if (initialOptions.outdir && !fs.existsSync(initialOptions.outdir))
fs.mkdirSync(initialOptions.outdir, { recursive: true })
const context: UnpluginBuildContext = createEsbuildContext(initialOptions)

if (plugin.buildStart)
onStart(() => plugin.buildStart!.call(context))

if (plugin.buildEnd || plugin.writeBundle || initialOptions.watch) {
const rebuild = () => build({
...initialOptions,
watch: false,
})

if (plugin.buildEnd || plugin.writeBundle) {
onEnd(async () => {
if (plugin.buildEnd)
await plugin.buildEnd.call(context)

if (plugin.writeBundle)
await plugin.writeBundle()

if (initialOptions.watch) {
Object.keys(watchListRecord).forEach((id) => {
if (!watchList.has(id)) {
watchListRecord[id].close()
delete watchListRecord[id]
}
})
watchList.forEach((id) => {
if (!Object.keys(watchListRecord).includes(id)) {
watchListRecord[id] = chokidar.watch(id)
watchListRecord[id].on('change', async () => {
await plugin.watchChange?.call(context, id, { event: 'update' })
rebuild()
})
watchListRecord[id].on('unlink', async () => {
await plugin.watchChange?.call(context, id, { event: 'delete' })
rebuild()
})
}
})
}
})
}

Expand Down
64 changes: 51 additions & 13 deletions src/esbuild/utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { extname } from 'path'
import fs from 'fs'
import path from 'path'
import remapping from '@ampproject/remapping'
import type {
DecodedSourceMap,
RawSourceMap,
} from '@ampproject/remapping/dist/types/types'
import type { Loader } from 'esbuild'
import { Parser } from 'acorn'
import type { DecodedSourceMap, EncodedSourceMap } from '@ampproject/remapping'
import type { BuildOptions, Loader } from 'esbuild'
import type { SourceMap } from 'rollup'
import type { UnpluginBuildContext } from '../types'

export * from '../utils'

Expand All @@ -28,12 +28,12 @@ const ExtToLoader: Record<string, Loader> = {
}

export function guessLoader(id: string): Loader {
return ExtToLoader[extname(id).toLowerCase()] || 'js'
return ExtToLoader[path.extname(id).toLowerCase()] || 'js'
}

// `load` and `transform` may return a sourcemap without toString and toUrl,
// but esbuild needs them, we fix the two methods
export function fixSourceMap(map: RawSourceMap): SourceMap {
export function fixSourceMap(map: EncodedSourceMap): SourceMap {
if (!('toString' in map)) {
Object.defineProperty(map, 'toString', {
enumerable: false,
Expand All @@ -54,16 +54,16 @@ export function fixSourceMap(map: RawSourceMap): SourceMap {
}

// taken from https://github.com/vitejs/vite/blob/71868579058512b51991718655e089a78b99d39c/packages/vite/src/node/utils.ts#L525
const nullSourceMap: RawSourceMap = {
const nullSourceMap: EncodedSourceMap = {
names: [],
sources: [],
mappings: '',
version: 3,
}
export function combineSourcemaps(
filename: string,
sourcemapList: Array<DecodedSourceMap | RawSourceMap>,
): RawSourceMap {
sourcemapList: Array<DecodedSourceMap | EncodedSourceMap>,
): EncodedSourceMap {
sourcemapList = sourcemapList.filter(m => m.sources)

if (
Expand All @@ -72,7 +72,7 @@ export function combineSourcemaps(
)
return { ...nullSourceMap }

// We don't declare type here so we can convert/fake/map as RawSourceMap
// We don't declare type here so we can convert/fake/map as EncodedSourceMap
let map // : SourceMap
let mapIndex = 1
const useArrayInterface
Expand All @@ -95,5 +95,43 @@ export function combineSourcemaps(
if (!map.file)
delete map.file

return map as RawSourceMap
return map as EncodedSourceMap
}

export function createEsbuildContext(initialOptions: BuildOptions): UnpluginBuildContext {
return {
parse(code: string, opts: any = {}) {
return Parser.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest',
locations: true,
...opts,
})
},
addWatchFile() {
},
emitFile(emittedFile) {
// Ensure output directory exists for this.emitFile
if (initialOptions.outdir && !fs.existsSync(initialOptions.outdir))
fs.mkdirSync(initialOptions.outdir, { recursive: true })

const outFileName = emittedFile.fileName || emittedFile.name
if (initialOptions.outdir && emittedFile.source && outFileName)
fs.writeFileSync(path.resolve(initialOptions.outdir, outFileName), emittedFile.source)
},
getWatchFiles() {
return []
},
}
}

export function processCodeWithSourceMap(map: SourceMap | null | undefined, code: string) {
if (map) {
if (!map.sourcesContent || map.sourcesContent.length === 0)
map.sourcesContent = [code]

map = fixSourceMap(map as EncodedSourceMap)
code += `\n//# sourceMappingURL=${map.toUrl()}`
}
return code
}

0 comments on commit 095849b

Please sign in to comment.