fix(build): собирать deserialize.worker.cjs, перевести пакет на ESM-only — Piscina требует отдельный .cjs файл, а top-level await несовместим с CJS форматом

- tsup.config.ts: добавлен 3-й entry для src/workers/deserialize.worker.ts (CJS) → dist/deserialize.worker.cjs
- tsup: main index и CLI переведены на ESM-only (TLA в ioredis/piscina импортах несовместим с CJS)
- package.json exports/main/bin: указывают на .js (ESM) вместо .cjs
- WorkerPool.resolveWorkerPath(): ищет worker в dist/ относительно src/ при запуске из источников (нужно для integration тестов)
- ci.yml: добавлен step build @coopenomics/parser перед integration test — worker .cjs должен существовать
This commit is contained in:
coopops
2026-04-22 05:14:08 +00:00
parent 5387e64ed8
commit 57b3b66513
4 changed files with 58 additions and 29 deletions
+2
View File
@@ -79,6 +79,8 @@ jobs:
with: { node-version: '20', cache: 'pnpm' }
- run: pnpm install --frozen-lockfile
- run: pnpm --filter @coopenomics/coopos-ship-reader build
# Parser2 build: нужен dist/deserialize.worker.cjs для Piscina worker pool
- run: pnpm --filter @coopenomics/parser build
# Запускаем nodeos вручную через docker run: образ dicoop/blockchain:v5.1.0-dev
# не имеет CMD по умолчанию, и GitHub Actions services: не поддерживает
# передачу команды контейнеру. Монтируем config.ini (SHiP на 8080) и genesis.json.
+5 -6
View File
@@ -11,16 +11,15 @@
"type": "module",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"bin": {
"parser": "./dist/cli/index.cjs"
"parser": "./dist/cli/index.js"
},
"files": [
"dist",
+38 -21
View File
@@ -4,19 +4,50 @@
* Использует Piscina — высокопроизводительный worker pool для Node.js.
* Каждый worker держит собственный in-memory ABI-кэш, поэтому повторные
* задания с одним и тем же abiJson не перепарсивают его.
*
* Важно: Piscina загружается через динамический import() из-за отсутствия
* поля "exports" в package.json пакета — NodeNext resolution требует его.
* Поэтому используется топ-левел await + явное приведение типа.
*/
// Piscina: default import через dynamic import() — TS не может статически
// проверить тип, поэтому используем топ-левел await + приведение типа.
type PiscinaPool = {
run(task: unknown): Promise<unknown>
utilization: number
destroy(): Promise<void>
}
type PiscinaCtor = new (opts: { filename: string; maxThreads?: number }) => PiscinaPool
const { default: PiscinaClass } = await import('piscina') as unknown as { default: PiscinaCtor }
import { fileURLToPath } from 'node:url'
import { join, dirname } from 'node:path'
import { join, dirname, resolve } from 'node:path'
import { existsSync } from 'node:fs'
// __dirname не доступен в ESM — восстанавливаем через import.meta.url
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
/**
* Находит путь к скомпилированному worker'у.
*
* Сценарии:
* 1. Production (dist/index.cjs) — worker рядом: dist/deserialize.worker.cjs
* 2. Source mode (src/workers/WorkerPool.ts) — worker должен быть предварительно
* собран в dist/workers/deserialize.worker.cjs (для тестов: build перед test:integration)
*/
function resolveWorkerPath(): string {
const candidates = [
// Production: рядом с WorkerPool
join(__dirname, 'deserialize.worker.cjs'),
// Source mode (tests): относительный путь к dist/
resolve(__dirname, '../../dist/deserialize.worker.cjs'),
resolve(__dirname, '../../dist/workers/deserialize.worker.cjs'),
]
for (const path of candidates) {
if (existsSync(path)) return path
}
throw new Error(
`Could not find deserialize.worker.cjs. Tried: ${candidates.join(', ')}. ` +
`Run "pnpm build" first if running from source.`,
)
}
export interface DeserializeTask {
/** Сырые байты action data или table row для декодирования. */
rawBinary: Uint8Array
@@ -28,22 +59,8 @@ export interface DeserializeTask {
kind: 'action' | 'delta'
}
interface PiscinaOptions {
filename: string
maxThreads?: number
}
// CJS/ESM interop: у Piscina нет поля "exports" для NodeNext resolution,
// поэтому TypeScript не может статически проверить тип импорта
type PiscinaConstructor = new (opts: PiscinaOptions) => {
run(task: unknown): Promise<unknown>
utilization: number
destroy(): Promise<void>
}
const { default: PiscinaClass } = await import('piscina') as unknown as { default: PiscinaConstructor }
export class WorkerPool {
private pool: InstanceType<PiscinaConstructor>
private pool: PiscinaPool
/**
* @param maxThreads — максимум параллельных worker-потоков.
@@ -53,7 +70,7 @@ export class WorkerPool {
constructor(maxThreads = 2) {
// Загружаем CJS-сборку worker'а, т.к. Piscina нативно работает с CJS
this.pool = new PiscinaClass({
filename: join(__dirname, 'deserialize.worker.cjs'),
filename: resolveWorkerPath(),
maxThreads,
})
}
+13 -2
View File
@@ -3,7 +3,9 @@ import { defineConfig } from 'tsup'
export default defineConfig([
{
entry: { index: 'src/index.ts' },
format: ['esm', 'cjs'],
// ESM only: код использует top-level await (для динамической загрузки CJS-only
// пакетов ioredis и piscina). TLA несовместим с CJS форматом.
format: ['esm'],
dts: true,
sourcemap: true,
clean: true,
@@ -11,9 +13,18 @@ export default defineConfig([
},
{
entry: { 'cli/index': 'src/cli/index.ts' },
format: ['cjs'],
// ESM: CLI импортирует из src/ который использует top-level await
format: ['esm'],
sourcemap: true,
target: 'node20',
banner: { js: '#!/usr/bin/env node' },
},
// Piscina worker: отдельный CJS файл (Piscina не поддерживает ESM).
// Размещается в dist/deserialize.worker.cjs — WorkerPool ищет по этому пути.
{
entry: { 'deserialize.worker': 'src/workers/deserialize.worker.ts' },
format: ['cjs'],
sourcemap: true,
target: 'node20',
},
])