Skip to content

Commit

Permalink
feat: Support Webpack 5 (#22)
Browse files Browse the repository at this point in the history
* Parameterize the test suite to run tests on multiple webpack versions with describe.each

* Run the (failing) tests on Webpack 5

* Switch the tests to test by reading from the memory filesystem which works on both Webpack 4 and Webpack 5

The Sources returned in the stats.compilation on Webpack 5 don't support reading their contents. So, let's just read the written output, which is a slightly more thorough test anyways. The snapshots didn't need to be updated so I think this is working well.

* Add Webpack 5 support

Uses the new processAssets compiler hook to avoid deprecation warnings and undefined methods.

* refactor: added getFile util method for tests

* refactor: minor filter addition and WP5/4 comments

* chore: added peerDependencies that specify Webpack 4 and 5 requirement

* chore: changed webpack4 to webpack to satisfy peerdeps

* refactor: minify-plugin organization

* fix: flatMap to assert input is array

* feat: added hash update

* feat: add minimize to stats in webpack5

* style: prettier

Co-authored-by: Harry Brundage <harry.brundage@gmail.com>
  • Loading branch information
privatenumber and airhorns committed Aug 23, 2020
1 parent 3517c7e commit c651c4f
Show file tree
Hide file tree
Showing 9 changed files with 2,015 additions and 277 deletions.
5 changes: 3 additions & 2 deletions package.json
Expand Up @@ -17,7 +17,7 @@
"webpack-sources": "^1.4.3"
},
"peerDependencies": {
"webpack": "^4.40.0"
"webpack": "^4.40.0 || ^5.0.0"
},
"devDependencies": {
"@types/jest": "^26.0.9",
Expand All @@ -26,6 +26,7 @@
"prettier": "^2.0.5",
"typescript": "^3.8.3",
"unionfs": "^4.4.0",
"webpack": "^4.44.1"
"webpack": "^4.44.1",
"webpack5": "npm:webpack@^5.0.0-beta.26"
}
}
135 changes: 89 additions & 46 deletions src/minify-plugin.js
@@ -1,9 +1,14 @@
const { version } = require('../package')
const assert = require('assert')
const { RawSource, SourceMapSource } = require('webpack-sources')

const isJsFile = /\.js$/i
const pluginName = 'esbuild-minify'

const flatMap = (arr, cb) => arr.flatMap ? arr.flatMap(cb) : [].concat(...arr.map(cb))
const flatMap = (arr, cb) => {
assert(Array.isArray(arr), `arr is not an Array`)
return arr.flatMap ? arr.flatMap(cb) : [].concat(...arr.map(cb))
}

class ESBuildMinifyPlugin {
constructor(options) {
Expand All @@ -21,63 +26,101 @@ class ESBuildMinifyPlugin {
* @param {import('webpack').Compiler} compiler
*/
apply(compiler) {
const { options } = this

compiler.hooks.compilation.tap(pluginName, (compilation) => {
const service = compiler.$esbuildService

if (!service) {
if (!compiler.$esbuildService) {
throw new Error(
`[esbuild-loader] You need to add ESBuildPlugin to your webpack config first`
)
}

const { devtool } = compiler.options
const sourcemap =
options.sourcemap !== undefined
? options.sourcemap
: devtool && devtool.includes('source-map')

compilation.hooks.optimizeChunkAssets.tapPromise(
pluginName,
async (chunks) => {
const transforms = flatMap(chunks, (chunk) => {
return chunk.files
.filter((file) => isJsFile.test(file))
.map(async (file) => {
const assetSource = compilation.assets[file]
const { source, map } = assetSource.sourceAndMap()
const meta = JSON.stringify({
name: 'esbuild-loader',
version,
options: this.options,
})
compilation.hooks.chunkHash.tap(pluginName, (chunk, hash) =>
hash.update(meta)
)

const result = await service.transform(source, {
...options,
sourcemap,
sourcefile: file,
})
// Webpack 5
if (compilation.hooks.processAssets) {
compilation.hooks.processAssets.tapPromise(
{
name: pluginName,
stage: compilation.constructor.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE,
},
(assets) => this.transformAssets(compilation, Object.keys(assets))
)

compilation.updateAsset(file, () => {
if (sourcemap) {
return new SourceMapSource(
result.js || '',
file,
result.jsSourceMap,
source,
map,
true
)
} else {
return new RawSource(result.js || '')
}
})
})
})
compilation.hooks.statsPrinter.tap(pluginName, (stats) => {
stats.hooks.print
.for('asset.info.minimized')
.tap(pluginName, (minimized, { green, formatFlag }) =>
minimized ? green(formatFlag('minimized')) : undefined
)
})
}

if (transforms.length) {
await Promise.all(transforms)
// Webpack 4
else {
compilation.hooks.optimizeChunkAssets.tapPromise(
pluginName,
async (chunks) => {
return this.transformAssets(
compilation,
flatMap(chunks, (chunk) => chunk.files)
)
}
}
)
)
}
})
}

async transformAssets(compilation, assetNames) {
const {
options: { devtool },
$esbuildService,
} = compilation.compiler

const sourcemap =
this.options.sourcemap !== undefined
? this.options.sourcemap
: devtool && devtool.includes('source-map')

const transforms = assetNames
.filter((assetName) => isJsFile.test(assetName))
.map((assetName) => [assetName, compilation.getAsset(assetName)])
.map(async ([assetName, { info, source: assetSource }]) => {
const { source, map } = assetSource.sourceAndMap()
const result = await $esbuildService.transform(source, {
...this.options,
sourcemap,
devtool,
sourcefile: assetName,
})

compilation.updateAsset(
assetName,
sourcemap
? new SourceMapSource(
result.js || '',
assetName,
result.jsSourceMap,
source,
map,
true
)
: new RawSource(result.js || ''),
{
...info,
minimized: true,
}
)
})
if (transforms.length) {
await Promise.all(transforms)
}
}
}

module.exports = ESBuildMinifyPlugin
9 changes: 3 additions & 6 deletions src/plugin.js
Expand Up @@ -14,12 +14,9 @@ class ESBuildPlugin {
}

compiler.hooks.thisCompilation.tap('esbuild', (compilation) => {
compilation.hooks.childCompiler.tap(
'esbuild',
(childCompiler) => {
childCompiler.$esbuildService = compiler.$esbuildService
}
)
compilation.hooks.childCompiler.tap('esbuild', (childCompiler) => {
childCompiler.$esbuildService = compiler.$esbuildService
})
})

compiler.hooks.run.tapPromise('esbuild', async () => {
Expand Down
1,163 changes: 1,152 additions & 11 deletions test/__snapshots__/loader.test.js.snap

Large diffs are not rendered by default.

417 changes: 333 additions & 84 deletions test/__snapshots__/minify.test.js.snap

Large diffs are not rendered by default.

134 changes: 68 additions & 66 deletions test/loader.test.js
@@ -1,91 +1,93 @@
const webpack = require('webpack')
const build = require('./build')
const { ESBuildMinifyPlugin } = require('../src')
const webpack4 = require('webpack')
const webpack5 = require('webpack5')
const { build, getFile } = require('./utils')
const fixtures = require('./fixtures')

describe('Loader', () => {
test('js', async () => {
const stats = await build(fixtures.js)
describe.each([
['Webpack 4', webpack4],
['Webpack 5', webpack5],
])('%s', (_name, webpack) => {
describe('Loader', () => {
test('js', async () => {
const stats = await build(webpack, fixtures.js)

const { assets } = stats.compilation
expect(assets['index.js'].source()).toMatchSnapshot()
})

test('tsx', async () => {
const stats = await build(fixtures.tsx, (config) => {
config.module.rules.push({
test: /\.tsx$/,
loader: 'esbuild-loader',
options: {
loader: 'tsx',
},
})
expect(getFile(stats, '/dist/index.js')).toMatchSnapshot()
})

const { assets } = stats.compilation
expect(assets['index.js'].source()).toMatchSnapshot()
})
})
test('tsx', async () => {
const stats = await build(webpack, fixtures.tsx, (config) => {
config.module.rules.push({
test: /\.tsx$/,
loader: 'esbuild-loader',
options: {
loader: 'tsx',
},
})
})

// Targets
test('target', async () => {
const stats = await build(fixtures.target, (config) => {
config.module.rules[0].options = {
target: 'es2015',
}
expect(getFile(stats, '/dist/index.js')).toMatchSnapshot()
})
})

const { assets } = stats.compilation
expect(assets['index.js'].source()).toMatchSnapshot()
})

describe('Source-map', () => {
test('source-map eval', async () => {
const stats = await build(fixtures.js, (config) => {
config.devtool = 'eval-source-map'
// Targets
test('target', async () => {
const stats = await build(webpack, fixtures.target, (config) => {
config.module.rules[0].options = {
target: 'es2015',
}
})

const { assets } = stats.compilation
expect(assets['index.js'].source()).toMatchSnapshot()
expect(getFile(stats, '/dist/index.js')).toMatchSnapshot()
})

test('source-map inline', async () => {
const stats = await build(fixtures.js, (config) => {
config.devtool = 'inline-source-map'
describe('Source-map', () => {
test('source-map eval', async () => {
const stats = await build(webpack, fixtures.js, (config) => {
config.devtool = 'eval-source-map'
})

const { assets } = stats.compilation
expect(getFile(stats, '/dist/index.js')).toMatchSnapshot()
})

const { assets } = stats.compilation
expect(assets['index.js'].source()).toMatchSnapshot()
})
test('source-map inline', async () => {
const stats = await build(webpack, fixtures.js, (config) => {
config.devtool = 'inline-source-map'
})

test('source-map file', async () => {
const stats = await build(fixtures.js, (config) => {
config.devtool = 'source-map'
const { assets } = stats.compilation
expect(getFile(stats, '/dist/index.js')).toMatchSnapshot()
})

const { assets } = stats.compilation
expect(assets['index.js'].source()).toMatchSnapshot()
expect(assets['index.js.map'].source()).toMatchSnapshot()
})
test('source-map file', async () => {
const stats = await build(webpack, fixtures.js, (config) => {
config.devtool = 'source-map'
})

test('source-map plugin', async () => {
const stats = await build(fixtures.js, (config) => {
delete config.devtool
config.plugins.push(new webpack.SourceMapDevToolPlugin({}))
const { assets } = stats.compilation
expect(getFile(stats, '/dist/index.js')).toMatchSnapshot()
expect(getFile(stats, '/dist/index.js.map')).toMatchSnapshot()
})

const { assets } = stats.compilation
expect(assets['index.js'].source()).toMatchSnapshot()
test('source-map plugin', async () => {
const stats = await build(webpack, fixtures.js, (config) => {
delete config.devtool
config.plugins.push(new webpack.SourceMapDevToolPlugin({}))
})

expect(getFile(stats, '/dist/index.js')).toMatchSnapshot()
})
})
})

test('webpack magic comments', async () => {
const stats = await build(fixtures.webpackMagicComments)
test('webpack magic comments', async () => {
const stats = await build(webpack, fixtures.webpackMagicComments)

const { assets } = stats.compilation
expect(assets['index.js'].source()).toMatchSnapshot()
expect(assets).toHaveProperty(['named-chunk-foo.js'])
expect(assets['named-chunk-foo.js'].source()).toMatchSnapshot()
expect(assets).toHaveProperty(['named-chunk-bar.js'])
expect(assets['named-chunk-bar.js'].source()).toMatchSnapshot()
const { assets } = stats.compilation
expect(getFile(stats, '/dist/index.js')).toMatchSnapshot()
expect(assets).toHaveProperty(['named-chunk-foo.js'])
expect(getFile(stats, '/dist/named-chunk-foo.js')).toMatchSnapshot()
expect(assets).toHaveProperty(['named-chunk-bar.js'])
expect(getFile(stats, '/dist/named-chunk-bar.js')).toMatchSnapshot()
})
})

0 comments on commit c651c4f

Please sign in to comment.