-
-
Notifications
You must be signed in to change notification settings - Fork 517
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(plugin-webpack): improve native asset relocation without forking…
… Vercel loader (#2320) Co-authored-by: Mark Lee <electronjs@lazymalevolence.com>
- Loading branch information
Showing
21 changed files
with
436 additions
and
97 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,3 +25,4 @@ packages/plugin/**/README.md | |
packages/api/cli/README.md | ||
!packages/**/base/README.md | ||
!/README.md | ||
.webpack |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { Chunk, Compiler } from 'webpack'; | ||
|
||
export default class AssetRelocatorPatch { | ||
private readonly isProd: boolean; | ||
|
||
constructor(isProd: boolean) { | ||
this.isProd = isProd; | ||
} | ||
|
||
public apply(compiler: Compiler) { | ||
compiler.hooks.compilation.tap( | ||
'asset-relocator-forge-patch', | ||
(compilation) => { | ||
// We intercept the Vercel loader code injection and replace __dirname with | ||
// code that works with Electron Forge | ||
// | ||
// Here is where the injection occurs: | ||
// https://github.com/vercel/webpack-asset-relocator-loader/blob/4710a018fc8fb64ad51efb368759616fb273618f/src/asset-relocator.js#L331-L339 | ||
compilation.mainTemplate.hooks.requireExtensions.intercept({ | ||
register: (tapInfo) => { | ||
if (tapInfo.name === 'asset-relocator-loader') { | ||
const originalFn = tapInfo.fn as (source: string, chunk: Chunk) => string; | ||
|
||
tapInfo.fn = (source: string, chunk: Chunk) => { | ||
const originalInjectCode = originalFn(source, chunk); | ||
|
||
// Since this is not a public API of the Vercel loader, it could | ||
// change on patch versions and break things. | ||
// | ||
// If the injected code changes substantially, we throw an error | ||
if (!originalInjectCode.includes('__webpack_require__.ab = __dirname + ')) { | ||
throw new Error('The installed version of @vercel/webpack-asset-relocator-loader does not appear to be compatible with Forge'); | ||
} | ||
|
||
return originalInjectCode.replace( | ||
'__dirname', | ||
this.isProd | ||
// In production the assets are found one directory up from | ||
// __dirname | ||
// | ||
// __dirname cannot be used directly until this PR lands | ||
// https://github.com/jantimon/html-webpack-plugin/pull/1650 | ||
? 'require("path").resolve(require("path").dirname(__filename), "..")' | ||
// In development, the app is loaded via webpack-dev-server | ||
// so __dirname is useless because it points to Electron | ||
// internal code. Instead we hard-code the absolute path to | ||
// the webpack output. | ||
: JSON.stringify(compiler.options.output.path), | ||
); | ||
}; | ||
} | ||
|
||
return tapInfo; | ||
}, | ||
}); | ||
}, | ||
); | ||
} | ||
} |
228 changes: 228 additions & 0 deletions
228
packages/plugin/webpack/test/AssetRelocatorPatch_spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
import { Configuration, webpack } from 'webpack'; | ||
import path from 'path'; | ||
import { expect } from 'chai'; | ||
import http from 'http'; | ||
import { pathExists, readFile } from 'fs-extra'; | ||
import { spawn } from '@malept/cross-spawn-promise'; | ||
import { WebpackPluginConfig } from '../src/Config'; | ||
import WebpackConfigGenerator from '../src/WebpackConfig'; | ||
|
||
type Closeable = { | ||
close: () => void; | ||
} | ||
|
||
let servers: Closeable[] = []; | ||
|
||
const nativePathSuffix = 'build/Release/hello_world.node'; | ||
const appPath = path.join(__dirname, 'fixtures', 'apps', 'native-modules'); | ||
|
||
async function asyncWebpack(config: Configuration): Promise<void> { | ||
return new Promise((resolve, reject) => { | ||
webpack(config, (err, stats) => { | ||
if (err) { | ||
reject(err); | ||
return; | ||
} | ||
|
||
if (stats?.compilation?.errors?.length) { | ||
reject(stats.compilation.errors); | ||
return; | ||
} | ||
|
||
if (stats?.compilation?.warnings?.length) { | ||
reject(stats.compilation.warnings); | ||
return; | ||
} | ||
|
||
resolve(); | ||
}); | ||
}); | ||
} | ||
|
||
/** | ||
* Webpack dev server doesn't like to exit, outputs logs so instead we just create a | ||
* basic server. | ||
*/ | ||
function createSimpleDevServer(rendererOut: string): http.Server { | ||
return http.createServer(async (req, res) => { | ||
const url = (req.url || ''); | ||
const file = url.endsWith('main_window') ? path.join(url, '/index.html') : url; | ||
const fullPath = path.join(rendererOut, file); | ||
try { | ||
const data = await readFile(fullPath); | ||
res.writeHead(200); | ||
res.end(data); | ||
} catch (err) { | ||
res.writeHead(404); | ||
res.end(JSON.stringify(err)); | ||
} | ||
}).listen(3000); | ||
} | ||
|
||
type ExpectNativeModulePathOptions = { | ||
outDir: string, | ||
jsPath: string, | ||
nativeModulesString: string, | ||
nativePathString: string | ||
}; | ||
|
||
async function expectOutputFileToHaveTheCorrectNativeModulePath({ | ||
outDir, | ||
jsPath, | ||
nativeModulesString, | ||
nativePathString, | ||
}: ExpectNativeModulePathOptions): Promise<void> { | ||
const nativePath = `native_modules/${nativePathSuffix}`; | ||
expect(await pathExists(path.join(outDir, nativePath))).to.equal(true); | ||
|
||
const jsContents = await readFile(jsPath, { encoding: 'utf8' }); | ||
expect(jsContents).to.contain(nativeModulesString); | ||
expect(jsContents).to.contain(nativePathString); | ||
} | ||
|
||
async function yarnStart(): Promise<string> { | ||
return spawn('yarn', ['start'], { | ||
cwd: appPath, | ||
shell: true, | ||
env: { | ||
...process.env, | ||
ELECTRON_ENABLE_LOGGING: 'true', | ||
}, | ||
}); | ||
} | ||
|
||
describe('AssetRelocatorPatch', () => { | ||
const rendererOut = path.join(appPath, '.webpack/renderer'); | ||
const mainOut = path.join(appPath, '.webpack/main'); | ||
|
||
before(async () => { | ||
await spawn('yarn', [], { cwd: appPath, shell: true }); | ||
}); | ||
|
||
after(() => { | ||
for (const server of servers) { | ||
server.close(); | ||
} | ||
servers = []; | ||
}); | ||
|
||
const config = { | ||
mainConfig: './webpack.main.config.js', | ||
renderer: { | ||
config: './webpack.renderer.config.js', | ||
entryPoints: [ | ||
{ | ||
name: 'main_window', | ||
html: './src/index.html', | ||
js: './src/renderer.js', | ||
preload: { | ||
js: './src/preload.js', | ||
}, | ||
}, | ||
], | ||
}, | ||
} as WebpackPluginConfig; | ||
|
||
describe('Development', () => { | ||
const generator = new WebpackConfigGenerator(config, appPath, false, 3000); | ||
|
||
it('builds main', async () => { | ||
await asyncWebpack(generator.getMainConfig()); | ||
|
||
await expectOutputFileToHaveTheCorrectNativeModulePath({ | ||
outDir: mainOut, | ||
jsPath: path.join(mainOut, 'index.js'), | ||
nativeModulesString: '__webpack_require__.ab = __dirname + "/native_modules/"', | ||
nativePathString: `require(__webpack_require__.ab + "${nativePathSuffix}")`, | ||
}); | ||
}); | ||
|
||
it('builds preload', async () => { | ||
const entryPoint = config.renderer.entryPoints[0]; | ||
const preloadConfig = await generator.getPreloadRendererConfig( | ||
entryPoint, entryPoint.preload!, | ||
); | ||
await asyncWebpack(preloadConfig); | ||
|
||
await expectOutputFileToHaveTheCorrectNativeModulePath({ | ||
outDir: path.join(rendererOut, 'main_window'), | ||
jsPath: path.join(rendererOut, 'main_window/preload.js'), | ||
nativeModulesString: '__webpack_require__.ab = __dirname + "/native_modules/"', | ||
nativePathString: `require(__webpack_require__.ab + \\"${nativePathSuffix}\\")`, | ||
}); | ||
}); | ||
|
||
it('builds renderer', async () => { | ||
const rendererConfig = await generator.getRendererConfig(config.renderer.entryPoints); | ||
await asyncWebpack(rendererConfig); | ||
|
||
await expectOutputFileToHaveTheCorrectNativeModulePath({ | ||
outDir: rendererOut, | ||
jsPath: path.join(rendererOut, 'main_window/index.js'), | ||
nativeModulesString: `__webpack_require__.ab = ${JSON.stringify(rendererOut)} + "/native_modules/"`, | ||
nativePathString: `require(__webpack_require__.ab + \\"${nativePathSuffix}\\")`, | ||
}); | ||
}); | ||
|
||
it('runs the app with the native module', async () => { | ||
servers.push(createSimpleDevServer(rendererOut)); | ||
|
||
const output = await yarnStart(); | ||
|
||
expect(output).to.contain('Hello, world! from the main'); | ||
expect(output).to.contain('Hello, world! from the preload'); | ||
expect(output).to.contain('Hello, world! from the renderer'); | ||
}); | ||
}); | ||
|
||
describe('Production', () => { | ||
const generator = new WebpackConfigGenerator(config, appPath, true, 3000); | ||
|
||
it('builds main', async () => { | ||
const mainConfig = generator.getMainConfig(); | ||
await asyncWebpack(mainConfig); | ||
|
||
await expectOutputFileToHaveTheCorrectNativeModulePath({ | ||
outDir: mainOut, | ||
jsPath: path.join(mainOut, 'index.js'), | ||
nativeModulesString: '.ab=__dirname+"/native_modules/"', | ||
nativePathString: `.ab+"${nativePathSuffix}"`, | ||
}); | ||
}); | ||
|
||
it('builds preload', async () => { | ||
const entryPoint = config.renderer.entryPoints[0]; | ||
const preloadConfig = await generator.getPreloadRendererConfig( | ||
entryPoint, entryPoint.preload!, | ||
); | ||
await asyncWebpack(preloadConfig); | ||
|
||
await expectOutputFileToHaveTheCorrectNativeModulePath({ | ||
outDir: path.join(rendererOut, 'main_window'), | ||
jsPath: path.join(rendererOut, 'main_window/preload.js'), | ||
nativeModulesString: '.ab=__dirname+"/native_modules/"', | ||
nativePathString: `.ab+"${nativePathSuffix}"`, | ||
}); | ||
}); | ||
|
||
it('builds renderer', async () => { | ||
const rendererConfig = await generator.getRendererConfig(config.renderer.entryPoints); | ||
await asyncWebpack(rendererConfig); | ||
|
||
await expectOutputFileToHaveTheCorrectNativeModulePath({ | ||
outDir: rendererOut, | ||
jsPath: path.join(rendererOut, 'main_window/index.js'), | ||
nativeModulesString: '.ab=require("path").resolve(require("path").dirname(__filename),"..")+"/native_modules/"', | ||
nativePathString: `.ab+"${nativePathSuffix}"`, | ||
}); | ||
}); | ||
|
||
it('runs the app with the native module', async () => { | ||
const output = await yarnStart(); | ||
|
||
expect(output).to.contain('Hello, world! from the main'); | ||
expect(output).to.contain('Hello, world! from the preload'); | ||
expect(output).to.contain('Hello, world! from the renderer'); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.