Created
January 8, 2026 11:45
-
-
Save composite/2d9a408ad6bafe10cd73601c4411573a to your computer and use it in GitHub Desktop.
node.js `Worker` with `.ts` in dev, real life usage with Bree, `?modulePath` plugin for Vite and howto... you can make all for one!
This file contains hidden or 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
| import { parentPort } from 'node:worker_threads'; | |
| const heavyTask = (): string => { | |
| return "Task Completed in TypeScript! and it also works after built Javascript!"; | |
| }; | |
| parentPort?.postMessage(heavyTask()); |
This file contains hidden or 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
| import { fileURLToPath } from 'node:url'; | |
| import { dirname, join } from 'node:path'; | |
| import Bree from 'bree'; | |
| import devplugin from './devplugin'; | |
| import childpath from './bree.child?modulePath'; | |
| const __filename = fileURLToPath(import.meta.url); | |
| const __dirname = dirname(__filename); | |
| const DEV = import.meta.env.DEV; | |
| Bree.extend(devplugin); | |
| const bree = new Bree({ | |
| jobs: [ | |
| { | |
| name: 'child-worker', | |
| path: childpath, | |
| cron: '*/15 * * * *', | |
| }, | |
| ], | |
| }); |
This file contains hidden or 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
| import { dirname, resolve } from 'node:path'; | |
| import { fileURLToPath } from 'node:url'; | |
| import { Worker } from 'node:worker_threads'; | |
| import type { PluginFunc } from 'bree'; | |
| const __filename = fileURLToPath(import.meta.url); | |
| const __dirname = dirname(__filename); | |
| const devplugin: PluginFunc = (opts, Bree: any) => { | |
| if (import.meta.env.DEV) { | |
| const init = Bree.prototype.init; | |
| Bree.prototype.init = async function () { | |
| if (!this.config.acceptedExtensions.includes('.ts')) | |
| this.config.acceptedExtensions.push('.ts'); | |
| return init.call(this); | |
| }; | |
| const createWorker = Bree.prototype.createWorker; | |
| Bree.prototype.createWorker = function (filename, options) { | |
| if (filename.endsWith('.ts')) { | |
| console.log('Delegating worker with filename "' + filename + '"'); | |
| options.workerData.JOB__PATH = filename; | |
| return new Worker(resolve(__dirname, 'devworker.mjs'), options); | |
| } | |
| return createWorker(filename, options); | |
| }; | |
| } | |
| }; | |
| export default devplugin; |
This file contains hidden or 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
| import { workerData } from 'node:worker_threads'; | |
| import { createServer } from 'vite'; | |
| import { ViteNodeRunner } from 'vite-node/client'; | |
| import { ViteNodeServer } from 'vite-node/server'; | |
| import { installSourcemapsSupport } from 'vite-node/source-map'; | |
| // create vite server | |
| const server = await createServer({ | |
| optimizeDeps: { | |
| // It's recommended to disable deps optimization | |
| noDiscovery: true, | |
| include: undefined, | |
| }, | |
| }); | |
| // create vite-node server | |
| const node = new ViteNodeServer(server); | |
| // fixes stacktraces in Errors | |
| installSourcemapsSupport({ | |
| getSourceMap: (source) => node.getSourceMap(source), | |
| }); | |
| // create vite-node runner | |
| const runner = new ViteNodeRunner({ | |
| root: server.config.root, | |
| base: server.config.base, | |
| // when having the server and runner in a different context, | |
| // you will need to handle the communication between them | |
| // and pass to this function | |
| fetchModule(id) { | |
| return node.fetchModule(id); | |
| }, | |
| resolveId(id, importer) { | |
| return node.resolveId(id, importer); | |
| }, | |
| }); | |
| // execute the file | |
| await runner.executeFile(workerData.JOB__PATH); | |
| // close the vite server | |
| await server.close(); |
This file contains hidden or 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
| /// <reference types="vite/client" /> | |
| declare module '*?modulePath' { | |
| const src: string; | |
| export default src; | |
| } |
This file contains hidden or 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
| import MagicString from 'magic-string'; | |
| import { URL, URLSearchParams } from 'node:url'; | |
| import path from 'node:path'; | |
| import fs from 'node:fs/promises'; | |
| import { createHash } from 'node:crypto'; | |
| import type { Plugin, InlineConfig, Rolldown } from 'vite'; | |
| import { build as viteBuild, mergeConfig } from 'vite'; | |
| function toRelativePath(filename: string, importer: string): string { | |
| const relPath = path.posix.relative(path.dirname(importer), filename); | |
| return relPath.startsWith('.') ? relPath : `./${relPath}`; | |
| } | |
| function parseRequest(id: string): Record<string, string> | null { | |
| try { | |
| const { search } = new URL(id, 'file:'); | |
| if (!search) { | |
| return null; | |
| } | |
| return Object.fromEntries(new URLSearchParams(search)); | |
| } catch { | |
| return null; | |
| } | |
| } | |
| function getHash(text: string): string { | |
| return createHash('sha256').update(text).digest('hex').substring(0, 8); | |
| } | |
| const queryRE = /\?.*$/s; | |
| const hashRE = /#.*$/s; | |
| const cleanUrl = (url: string): string => | |
| url.replace(hashRE, '').replace(queryRE, ''); | |
| const modulePathRE = /__VITE_MODULE_PATH__([\w$]+)__/g; | |
| // ESM shim for __dirname | |
| const CJSShim = ` | |
| import __cjs_url__ from 'node:url'; | |
| import __cjs_path__ from 'node:path'; | |
| const __filename = __cjs_url__.fileURLToPath(import.meta.url); | |
| const __dirname = __cjs_path__.dirname(__filename); | |
| `; | |
| interface ModulePathPluginOptions { | |
| /** | |
| * Vite configuration to use for bundling modules | |
| */ | |
| bundleConfig?: InlineConfig; | |
| } | |
| interface EmittedChunk { | |
| fileName: string; | |
| code: string; | |
| referenceId: string; | |
| } | |
| /** | |
| * Resolve `?modulePath` import and return the module bundle path. | |
| */ | |
| export default function modulePathPlugin( | |
| options: ModulePathPluginOptions = {} | |
| ): Plugin { | |
| let sourcemap = false; | |
| let viteConfig: InlineConfig; | |
| let rootDir: string; | |
| let outDir: string; | |
| let isDev = false; | |
| const assetCache = new Map<string, string>(); // original path -> hashed fileName | |
| const chunksToWrite = new Map<string, EmittedChunk>(); // fileName -> chunk data | |
| const chunkShims = new Set<string>(); | |
| const fileNameMap = new Map<string, string>(); // original fileName -> hashed fileName | |
| return { | |
| name: 'vite:module-path', | |
| enforce: 'pre', | |
| configResolved(config) { | |
| isDev = config.command === 'serve'; | |
| sourcemap = !!config.build.sourcemap; | |
| rootDir = config.root; | |
| outDir = path.resolve(rootDir, config.build.outDir); | |
| if (!isDev) { | |
| viteConfig = { | |
| root: config.root, | |
| mode: config.mode, | |
| configFile: false, | |
| logLevel: 'warn', | |
| ...options.bundleConfig, | |
| build: { | |
| ...options.bundleConfig?.build, | |
| write: false, | |
| lib: undefined, | |
| rollupOptions: { | |
| ...options.bundleConfig?.build?.rollupOptions, | |
| output: { | |
| format: 'es', | |
| entryFileNames: '[name].js', | |
| chunkFileNames: '[name]-[hash].js', | |
| assetFileNames: '[name]-[hash][extname]', | |
| ...(options.bundleConfig?.build?.rollupOptions?.output as any), | |
| }, | |
| }, | |
| }, | |
| }; | |
| console.log(`[module-path] Output directory: ${outDir}`); | |
| } | |
| }, | |
| buildStart() { | |
| if (!isDev) { | |
| assetCache.clear(); | |
| chunksToWrite.clear(); | |
| chunkShims.clear(); | |
| fileNameMap.clear(); | |
| } | |
| }, | |
| async resolveId(id, importer) { | |
| const query = parseRequest(id); | |
| if (query && typeof query.modulePath === 'string') { | |
| // dev 모드에서는 절대 경로로 변환 | |
| if (isDev) { | |
| const cleanPath = cleanUrl(id); | |
| // 절대 경로가 아니면 importer 기준으로 resolve | |
| let resolvedPath: string; | |
| if (path.isAbsolute(cleanPath)) { | |
| resolvedPath = cleanPath; | |
| } else if (importer) { | |
| resolvedPath = path.resolve(path.dirname(importer), cleanPath); | |
| } else { | |
| resolvedPath = path.resolve(rootDir, cleanPath); | |
| } | |
| console.log(`[module-path] Resolved: ${id} -> ${resolvedPath}`); | |
| // 절대 경로에 플래그 추가 | |
| return resolvedPath + '?__modulePath__=true'; | |
| } | |
| } | |
| return null; | |
| }, | |
| async load(id) { | |
| const query = parseRequest(id); | |
| // Dev 모드: 파일을 실행하지 않고 경로만 반환 | |
| if (isDev && query && query.__modulePath__ === 'true') { | |
| const cleanPath = cleanUrl(id); | |
| console.log( | |
| `[module-path] Dev mode: returning file path for ${cleanPath}` | |
| ); | |
| // 절대 경로를 그대로 반환 (dev 모드에서는 소스 파일 직접 사용) | |
| return `export default ${JSON.stringify(cleanPath)}`; | |
| } | |
| // Build 모드: 번들링 수행 | |
| if (!isDev && query && typeof query.modulePath === 'string') { | |
| const cleanPath = cleanUrl(id); | |
| // 이미 처리된 파일인지 확인 | |
| if (assetCache.has(cleanPath)) { | |
| const hashedFileName = assetCache.get(cleanPath)!; | |
| const referenceId = chunksToWrite.get(hashedFileName)?.referenceId; | |
| if (referenceId) { | |
| console.log( | |
| `[module-path] Reusing cached bundle: ${cleanPath} -> ${hashedFileName}` | |
| ); | |
| const refId = `__VITE_MODULE_PATH__${referenceId}__`; | |
| return ` | |
| import { fileURLToPath } from 'node:url' | |
| import { dirname, join } from 'node:path' | |
| const __filename = fileURLToPath(import.meta.url) | |
| const __dirname = dirname(__filename) | |
| export default join(__dirname, ${refId})`; | |
| } | |
| } | |
| console.log(`[module-path] Bundling module: ${cleanPath}`); | |
| try { | |
| const { outputChunks } = await bundleModule(cleanPath, viteConfig); | |
| if (outputChunks.length === 0) { | |
| throw new Error(`No output chunks generated for: ${cleanPath}`); | |
| } | |
| console.log( | |
| `[module-path] Generated ${outputChunks.length} chunk(s)` | |
| ); | |
| // 메인 청크에 대해 고유한 파일명 생성 | |
| const mainChunk = outputChunks[0]; | |
| const mainChunkHash = getHash(cleanPath + mainChunk.code); | |
| const ext = path.extname(mainChunk.fileName); | |
| const baseName = path.basename(mainChunk.fileName, ext); | |
| const hashedMainFileName = `${baseName}-${mainChunkHash}${ext}`; | |
| // 파일명 매핑 저장 | |
| fileNameMap.set(mainChunk.fileName, hashedMainFileName); | |
| console.log( | |
| `[module-path] Main chunk: ${mainChunk.fileName} -> ${hashedMainFileName}` | |
| ); | |
| let mainReferenceId: string | undefined; | |
| // 모든 청크를 저장 | |
| for (const chunk of outputChunks) { | |
| const originalFileName = chunk.fileName; | |
| const isMainChunk = chunk === mainChunk; | |
| // 메인 청크는 이미 해시된 이름 사용, 나머지는 원본 이름 또는 이미 해시된 이름 사용 | |
| const fileName = isMainChunk | |
| ? hashedMainFileName | |
| : fileNameMap.get(originalFileName) || originalFileName; | |
| // 동일한 파일이 이미 처리되었는지 확인 | |
| if (chunksToWrite.has(fileName)) { | |
| console.log(`[module-path] Skipping duplicate: ${fileName}`); | |
| if (isMainChunk) { | |
| mainReferenceId = chunksToWrite.get(fileName)!.referenceId; | |
| } | |
| continue; | |
| } | |
| // 청크의 import 경로를 수정 (다른 청크를 참조하는 경우) | |
| let code = | |
| chunk.type === 'chunk' ? chunk.code : (chunk.source as string); | |
| if (chunk.type === 'chunk') { | |
| // import 문에서 다른 청크를 참조하는 경우 해시된 이름으로 변경 | |
| for (const [original, hashed] of fileNameMap) { | |
| if (original !== hashed) { | |
| const importRegex = new RegExp( | |
| `(from\\s+['"]\\.\\/)(${original.replace('.', '\\.')})(['"])`, | |
| 'g' | |
| ); | |
| code = code.replace(importRegex, `$1${hashed}$3`); | |
| } | |
| } | |
| } | |
| // emitFile로 참조 ID 생성 | |
| const referenceId = this.emitFile({ | |
| type: 'asset', | |
| fileName: fileName, | |
| source: code, | |
| }); | |
| if (isMainChunk) { | |
| mainReferenceId = referenceId; | |
| assetCache.set(cleanPath, fileName); | |
| } | |
| // 나중에 직접 쓰기 위해 저장 | |
| chunksToWrite.set(fileName, { | |
| fileName, | |
| code, | |
| referenceId, | |
| }); | |
| if (chunk.type === 'chunk') { | |
| chunkShims.add(fileName); | |
| } | |
| console.log(`[module-path] Processed: ${fileName}`); | |
| } | |
| if (!mainReferenceId) { | |
| throw new Error('Failed to process main chunk'); | |
| } | |
| const refId = `__VITE_MODULE_PATH__${mainReferenceId}__`; | |
| return ` | |
| import { fileURLToPath } from 'node:url' | |
| import { dirname, join } from 'node:path' | |
| const __filename = fileURLToPath(import.meta.url) | |
| const __dirname = dirname(__filename) | |
| export default join(__dirname, ${refId})`; | |
| } catch (error) { | |
| console.error(`[module-path] Bundle error:`, error); | |
| throw error; | |
| } | |
| } | |
| }, | |
| renderChunk(code, chunk) { | |
| if (isDev) return null; | |
| let modified = false; | |
| let s: MagicString | undefined; | |
| if (code.match(modulePathRE)) { | |
| s = new MagicString(code); | |
| let match: RegExpExecArray | null; | |
| modulePathRE.lastIndex = 0; | |
| while ((match = modulePathRE.exec(code))) { | |
| const [full, hash] = match; | |
| const filename = this.getFileName(hash); | |
| const outputFilepath = toRelativePath(filename, chunk.fileName); | |
| const replacement = JSON.stringify(outputFilepath); | |
| s.overwrite(match.index, match.index + full.length, replacement, { | |
| contentOnly: true, | |
| }); | |
| modified = true; | |
| } | |
| } | |
| if (chunkShims.has(chunk.fileName) && !code.includes('const __dirname')) { | |
| s = s || new MagicString(code); | |
| const importMatches = Array.from( | |
| code.matchAll(/import\s+.*?from\s+['"][^'"]+['"]\s*;?/g) | |
| ); | |
| const lastImport = importMatches[importMatches.length - 1]; | |
| const insertPos = lastImport | |
| ? lastImport.index! + lastImport[0].length | |
| : 0; | |
| s.appendRight(insertPos, CJSShim); | |
| modified = true; | |
| } | |
| if (modified && s) { | |
| return { | |
| code: s.toString(), | |
| map: sourcemap ? s.generateMap({ hires: 'boundary' }) : null, | |
| }; | |
| } | |
| return null; | |
| }, | |
| async writeBundle(options, bundle) { | |
| if (isDev) return; | |
| console.log( | |
| `[module-path] Writing ${chunksToWrite.size} additional chunk(s)` | |
| ); | |
| for (const [fileName, chunkData] of chunksToWrite) { | |
| const outputPath = path.resolve(outDir, fileName); | |
| try { | |
| await fs.mkdir(path.dirname(outputPath), { recursive: true }); | |
| await fs.writeFile(outputPath, chunkData.code, 'utf-8'); | |
| console.log(`[module-path] Written: ${fileName}`); | |
| } catch (error) { | |
| console.error(`[module-path] Failed to write ${fileName}:`, error); | |
| throw error; | |
| } | |
| } | |
| console.log(`[module-path] All chunks written successfully`); | |
| }, | |
| }; | |
| } | |
| async function bundleModule( | |
| input: string, | |
| baseConfig: InlineConfig | |
| ): Promise<{ outputChunks: any[] }> { | |
| const entryName = path.basename(input, path.extname(input)); | |
| const buildConfig = mergeConfig(baseConfig, { | |
| build: { | |
| write: false, | |
| emptyOutDir: false, | |
| rollupOptions: { | |
| input: { | |
| [entryName]: input, | |
| }, | |
| preserveEntrySignatures: 'strict', | |
| }, | |
| }, | |
| }) as InlineConfig; | |
| try { | |
| const bundles = (await viteBuild(buildConfig)) as Rolldown.RolldownOutput; | |
| if (!bundles || !bundles.output) { | |
| throw new Error('No output from vite build'); | |
| } | |
| const outputChunks = bundles.output; | |
| return { outputChunks }; | |
| } catch (error) { | |
| console.error('[module-path] Bundle error:', error); | |
| throw error; | |
| } | |
| } |
This file contains hidden or 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
| export default defineConfig(({ mode, command }) => { | |
| return { | |
| plugins: [ | |
| modulePathPlugin({ | |
| bundleConfig: { | |
| build: { | |
| target: 'node20', | |
| minify: false, | |
| ssr: true, // SSR mode should be enabled! | |
| rollupOptions: { | |
| external: [/* CommonJS, native related dependencies here. */], | |
| }, | |
| }, | |
| ssr: { | |
| external: [/* CommonJS, native related dependencies here. */], | |
| } | |
| }, | |
| }), | |
| // ... other plugins ... | |
| ], | |
| build: { | |
| target: 'esnext', | |
| copyPublicDir: false, | |
| rollupOptions: { | |
| output: { | |
| format: 'es', | |
| } | |
| }, | |
| }, | |
| resolve, | |
| }; | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment