diff --git a/node.d.ts b/node.d.ts new file mode 100644 index 0000000..1a59335 --- /dev/null +++ b/node.d.ts @@ -0,0 +1,5 @@ +// node worker +declare module '*?nodeWorker' { + import { Worker, WorkerOptions } from 'node:worker_threads' + export default function (options: WorkerOptions): Worker +} diff --git a/package.json b/package.json index 6ccc87b..850b8b1 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ }, "files": [ "bin", - "dist" + "dist", + "node.d.ts" ], "engines": { "node": ">=12.2.0" @@ -83,6 +84,7 @@ "@babel/plugin-transform-arrow-functions": "^7.18.6", "cac": "^6.7.14", "esbuild": "^0.15.12", + "magic-string": "^0.26.7", "picocolors": "^1.0.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 95af9cb..51ebe3b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,7 @@ specifiers: eslint-plugin-prettier: ^4.2.1 fs-extra: ^10.1.0 lint-staged: ^13.0.3 + magic-string: ^0.26.7 picocolors: ^1.0.0 prettier: ^2.7.1 rollup: ^2.79.1 @@ -30,6 +31,7 @@ dependencies: '@babel/plugin-transform-arrow-functions': 7.18.6_@babel+core@7.19.6 cac: 6.7.14 esbuild: 0.15.12 + magic-string: 0.26.7 picocolors: 1.0.0 devDependencies: @@ -1916,6 +1918,13 @@ packages: yallist: 4.0.0 dev: true + /magic-string/0.26.7: + resolution: {integrity: sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==} + engines: {node: '>=12'} + dependencies: + sourcemap-codec: 1.4.8 + dev: false + /merge-stream/2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} dev: true @@ -2277,6 +2286,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /sourcemap-codec/1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + dev: false + /sprintf-js/1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} dev: true diff --git a/src/config.ts b/src/config.ts index fb4b648..ea621fd 100644 --- a/src/config.ts +++ b/src/config.ts @@ -16,6 +16,7 @@ import { import { build } from 'esbuild' import { electronMainVitePlugin, electronPreloadVitePlugin, electronRendererVitePlugin } from './plugin' +import workerPlugin from './plugins/worker' import { isObject, dynamicImport } from './utils' export { defineConfig as defineViteConfig } from 'vite' @@ -129,7 +130,7 @@ export async function resolveConfig( resetOutDir(mainViteConfig, outDir, 'main') } - mergePlugins(mainViteConfig, electronMainVitePlugin({ root })) + mergePlugins(mainViteConfig, [...electronMainVitePlugin({ root }), workerPlugin()]) loadResult.config.main = mainViteConfig loadResult.config.main.configFile = false diff --git a/src/plugins/worker.ts b/src/plugins/worker.ts new file mode 100644 index 0000000..3dea1cd --- /dev/null +++ b/src/plugins/worker.ts @@ -0,0 +1,69 @@ +import path from 'node:path' +import type { Plugin } from 'vite' +import type { SourceMapInput } from 'rollup' +import MagicString from 'magic-string' +import { cleanUrl, parseRequest } from '../utils' + +const nodeWorkerAssetUrlRE = /__VITE_NODE_WORKER_ASSET__([a-z\d]{8})__/g + +/** + * Resolve `?nodeWorker` import and automatically generate `Worker` wrapper. + */ +export default function workerPlugin(): Plugin { + let sourcemap: boolean | 'inline' | 'hidden' = false + return { + name: 'vite:node-worker', + apply: 'build', + enforce: 'pre', + configResolved(config): void { + sourcemap = config.build.sourcemap + }, + resolveId(id, importer): string | void { + const query = parseRequest(id) + if (query && typeof query.nodeWorker === 'string') { + return id + `&importer=${importer}` + } + }, + load(id): string | void { + const query = parseRequest(id) + if (query && typeof query.nodeWorker === 'string' && typeof query.importer === 'string') { + const cleanPath = cleanUrl(id) + const hash = this.emitFile({ + type: 'chunk', + id: cleanPath, + importer: query.importer + }) + const assetRefId = `__VITE_NODE_WORKER_ASSET__${hash}__` + return ` + import { Worker } from 'node:worker_threads'; + export default function (options) { return new Worker(require.resolve(${assetRefId}), options); }` + } + }, + renderChunk(code, chunk): { code: string; map: SourceMapInput } | null { + if (code.match(nodeWorkerAssetUrlRE)) { + let match: RegExpExecArray | null + const s = new MagicString(code) + + while ((match = nodeWorkerAssetUrlRE.exec(code))) { + const [full, hash] = match + const filename = this.getFileName(hash) + let outputFilepath = path.posix.relative(path.dirname(chunk.fileName), filename) + if (!outputFilepath.startsWith('.')) { + outputFilepath = './' + outputFilepath + } + const replacement = JSON.stringify(outputFilepath) + s.overwrite(match.index, match.index + full.length, replacement, { + contentOnly: true + }) + } + + return { + code: s.toString(), + map: sourcemap ? s.generateMap({ hires: true }) : null + } + } + + return null + } + } +} diff --git a/src/utils.ts b/src/utils.ts index 0bab821..5e2037f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,5 @@ +import { URL, URLSearchParams } from 'node:url' + export function isObject(value: unknown): value is Record { return Object.prototype.toString.call(value) === '[object Object]' } @@ -9,3 +11,16 @@ export const wildcardHosts = new Set(['0.0.0.0', '::', '0000:0000:0000:0000:0000 export function resolveHostname(optionsHost: string | boolean | undefined): string { return typeof optionsHost === 'string' && !wildcardHosts.has(optionsHost) ? optionsHost : 'localhost' } + +export const queryRE = /\?.*$/s +export const hashRE = /#.*$/s + +export const cleanUrl = (url: string): string => url.replace(hashRE, '').replace(queryRE, '') + +export function parseRequest(id: string): Record | null { + const { search } = new URL(id, 'file:') + if (!search) { + return null + } + return Object.fromEntries(new URLSearchParams(search)) +}