🏸
NodejsFunctionでjsdom使おうとしたら「"./xhr-sync-worker.js" should be..」エラー
副題
プラグインを導入したesbuildを用いたNodejsFunctionを使いたい
ポイント
- esbuildでjsdomのトランスパイルはできず、esbuildのpluginの作成が必要
- NodejsFunctionではesbuildの プラグイン設定が行えない
- pluginを仕込んだ自前esbuildを叩くNodejsFunctionもどきを作る必要がある
環境
aws-cdk@2.84.0
jsdom@22.1.0
背景
JSDOMを叩くNodeランタイムのlambdaをAWS CDK (NodejsFunction)で構築しようとした
lambdaコード
exports.handler = (event, _context) => {
import { JSDOM } from 'jsdom'
const dom = new JSDOM(html)
...
}
CDKコード
import * as nodeLambda from 'aws-cdk-lib/aws-lambda-nodejs'
const function = nodeLambda.NodejsFunction(scope, id, {
entry: path.join(__dirname, './lambda/index.ts'),
handler: 'handler',
...
})
結果、esbuildによるビルド時に以下エラーが発生した
Warning: G] "./xhr-sync-worker.js" should be marked as external for use with "require.resolve" [require-resolve-not-external]
node_modules/jsdom/lib/jsdom/living/xhr/XMLHttpRequest-impl.js:31:57:
31 │ ... require.resolve ? require.resolve("./xhr-sync-worker.js") : null;
external moduleに指定しろと言っているが、そういう問題ではなさそう
warningなんで試しに無視して動かしてみたが、案の定runtime error
原因
JSDOM中の以下記述における、相対パスのrequire.resolveにesbuildが対応できない
const syncWorkerFile = require.resolve ? require.resolve("./xhr-sync-worker.js") : null;
対策
1. jsdomPatchPlugin.cjs を作成
const fs = require('fs')
const jsdomPatch = {
name: 'jsdom-patch',
setup(build) {
// jsdomのバージョンアップに伴い、以下のfilterの正規表現、置換後の文字列を変更する必要がある
build.onLoad({ filter: /XMLHttpRequest-impl\.js$/ }, async (args) => {
let contents = await fs.promises.readFile(args.path, 'utf8')
contents = contents.replace(
'const syncWorkerFile = require.resolve ? require.resolve("./xhr-sync-worker.js") : null;',
`const syncWorkerFile = "${require.resolve('jsdom/lib/jsdom/living/xhr/xhr-sync-worker.js')}";`
)
return { contents, loader: 'js' }
})
}
}
module.exports = jsdomPatch
2. build.mjs を作成
import esbuild from 'esbuild'
import jsdomPatch from './jsdomPatchPlugin.cjs'
await esbuild
.build({
// jsdomPatch用のpluginを使うように設定
plugins: [jsdomPatch],
})
.catch((error) => {
console.log(error)
process.exit(1)
})
3. customEsbuildFunction.ts を作成 (lambda.Functionを継承したカスタムコンストラクタ)
import { IBuildProvider, ProviderBuildOptions, TypeScriptCode } from '@mrgrain/cdk-esbuild'
import * as lambda from 'aws-cdk-lib/aws-lambda'
import { spawnSync } from 'child_process'
import { Construct } from 'constructs'
import * as path from 'path'
interface CustomEsbuildFunctionProps {
code: string
functionProps: Omit<lambda.FunctionProps, 'code'>
}
class BuildScriptProvider implements IBuildProvider {
constructor(public readonly scriptPath: string) {}
buildSync(options: ProviderBuildOptions): void {
// spawnSyncでプラグイン導入したesbuildを実行
const result = spawnSync('node', [this.scriptPath, JSON.stringify(options)], {
stdio: ['inherit', 'pipe']
})
if (result.stderr.byteLength > 0) {
throw new Error(`${result.stderr.toString()}`)
}
}
}
export class CustomEsbuildFunction extends lambda.Function {
constructor(scope: Construct, id: string, { code, functionProps }: CustomEsbuildFunctionProps) {
super(scope, id, {
...functionProps,
// カスタムesbuildでトランスパイル&バンドルしたJSコードをlambdaのコードとして指定する
// 細かいところは全てTypeScriptCodeが実行してくれる
code: new TypeScriptCode(code, { buildProvider: new BuildScriptProvider(path.join(__dirname, 'build.mjs')) })
})
}
}
4. CustomEsbuildFunctionを呼び出す
export class CdkStack extends Stack {
constructor(scope: Construct, id: string, props: RemsStackProps) {
super(scope, id, props)
const lambdaFunction = new CustomEsbuildFunction(this, 'id', {
code: path.join(__dirname, './lambda/index.ts'),
functionProps: {
runtime: lambda.Runtime.NODEJS_18_x,
handler: 'index.handler',
...
}
})
}
}
なにやってるの?
相対パスで記述された部分をesbuildのプリプロセスでフルパスの記述に置き換えています
let contents = await fs.promises.readFile(args.path, 'utf8')
contents = contents.replace(
'const syncWorkerFile = require.resolve ? require.resolve("./xhr-sync-worker.js") : null;',
`const syncWorkerFile = "${require.resolve('jsdom/lib/jsdom/living/xhr/xhr-sync-worker.js')}";`
)
JSDOMのバージョンによってこの記述は変わる可能性があります。
(事実、ちょいちょい変わってます)
補足
- esbuildのプラグインについてはこちらのブログが参考になりました
- cjs, mjsが入り乱れているのは海よりも深い理由があります
- そんな深くないですが、mjs統一に失敗したので諦めました。
- TypeScript化はもっと諦めています(spawnSyncで別途build.ts, plugin.tsもトランスパイルするプロセスを追加すればいけるかも?)
- 4-5時間溶かした結果です。どなたかの救いになれば。
Discussion