|
1 | | -import fs from 'node:fs' |
2 | | -import { createRequire } from 'node:module' |
3 | 1 | import path from 'node:path' |
4 | | -import { fileURLToPath } from 'node:url' |
5 | | -import vm from 'node:vm' |
6 | | -import { describe, expect, it, vi } from 'vitest' |
| 2 | +import { fileURLToPath, pathToFileURL } from 'node:url' |
| 3 | +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' |
7 | 4 |
|
8 | | -const requireFn = createRequire(import.meta.url) |
9 | 5 | const testDir = path.dirname(fileURLToPath(import.meta.url)) |
10 | | -const scriptPath = path.resolve(testDir, '../scripts/postinstall.cjs') |
11 | | -const builtScriptPath = path.resolve(testDir, '../dist/postinstall.cjs') |
12 | | -const scriptSource = fs.readFileSync(scriptPath, 'utf8') |
13 | | - |
14 | | -function executeLegacyPostinstall(hasBuiltScript: boolean) { |
15 | | - const existsSync = vi.fn(() => hasBuiltScript) |
16 | | - let distModuleLoads = 0 |
17 | | - const consoleLog = vi.fn() |
18 | | - |
19 | | - const contextRequire = (specifier: string) => { |
20 | | - if (specifier === 'node:fs') { |
21 | | - return { |
22 | | - existsSync, |
23 | | - } |
24 | | - } |
25 | | - if (specifier === 'node:path') { |
26 | | - return requireFn('node:path') |
27 | | - } |
28 | | - if (specifier === builtScriptPath) { |
29 | | - distModuleLoads += 1 |
30 | | - return {} |
31 | | - } |
32 | | - throw new Error(`Unexpected require: ${specifier}`) |
33 | | - } |
34 | | - |
35 | | - const context = { |
36 | | - require: contextRequire, |
37 | | - module: { exports: {} }, |
38 | | - exports: {}, |
39 | | - __dirname: path.dirname(scriptPath), |
40 | | - console: { log: consoleLog }, |
41 | | - } |
42 | | - |
43 | | - vm.runInNewContext(scriptSource, context, { filename: scriptPath }) |
44 | | - |
45 | | - return { |
46 | | - existsSync, |
47 | | - consoleLog, |
48 | | - distModuleLoads, |
49 | | - } |
| 6 | +const scriptPath = path.resolve(testDir, '../scripts/postinstall.mjs') |
| 7 | +const scriptUrl = pathToFileURL(scriptPath).href |
| 8 | +const esmBundlePath = path.resolve(testDir, '../dist/postinstall.js') |
| 9 | +const cjsBundlePath = path.resolve(testDir, '../dist/postinstall.cjs') |
| 10 | + |
| 11 | +interface Logger { |
| 12 | + log: (...args: unknown[]) => void |
| 13 | + error: (...args: unknown[]) => void |
| 14 | +} |
| 15 | + |
| 16 | +interface RunOptions { |
| 17 | + existsSync?: (file: string) => boolean |
| 18 | + loadEsm?: (file: string) => Promise<unknown> |
| 19 | + loadCjs?: (file: string) => unknown | Promise<unknown> |
| 20 | + logger?: Logger |
| 21 | +} |
| 22 | + |
| 23 | +const originalExitCode = process.exitCode |
| 24 | + |
| 25 | +async function runWithOptions(options?: RunOptions) { |
| 26 | + const { run } = await import(scriptUrl) as { run: (options?: RunOptions) => Promise<void> } |
| 27 | + await run(options) |
50 | 28 | } |
51 | 29 |
|
52 | | -describe('legacy postinstall entry', () => { |
53 | | - it('skips when built postinstall script is missing', () => { |
54 | | - const result = executeLegacyPostinstall(false) |
| 30 | +beforeEach(() => { |
| 31 | + process.exitCode = undefined |
| 32 | +}) |
| 33 | + |
| 34 | +afterEach(() => { |
| 35 | + vi.restoreAllMocks() |
| 36 | + process.exitCode = originalExitCode |
| 37 | +}) |
| 38 | + |
| 39 | +describe('postinstall bootstrap script', () => { |
| 40 | + it('prefers the ESM bundle when available', async () => { |
| 41 | + const existsSync = vi.fn((file: string) => file === esmBundlePath) |
| 42 | + const loadEsm = vi.fn(async () => {}) |
| 43 | + const loadCjs = vi.fn() |
| 44 | + const logger = { log: vi.fn(), error: vi.fn() } |
55 | 45 |
|
56 | | - expect(result.existsSync).toHaveBeenCalledWith(builtScriptPath) |
57 | | - expect(result.consoleLog).toHaveBeenCalledWith('postinstall.cjs not found') |
58 | | - expect(result.distModuleLoads).toBe(0) |
| 46 | + await runWithOptions({ existsSync, loadEsm, loadCjs, logger }) |
| 47 | + |
| 48 | + expect(loadEsm).toHaveBeenCalledWith(esmBundlePath) |
| 49 | + expect(loadCjs).not.toHaveBeenCalled() |
| 50 | + expect(logger.log).not.toHaveBeenCalled() |
| 51 | + expect(logger.error).not.toHaveBeenCalled() |
| 52 | + }) |
| 53 | + |
| 54 | + it('falls back to the CJS bundle when ESM is unavailable', async () => { |
| 55 | + const existsSync = vi.fn((file: string) => file === cjsBundlePath) |
| 56 | + const loadEsm = vi.fn() |
| 57 | + const loadCjs = vi.fn(async () => {}) |
| 58 | + const logger = { log: vi.fn(), error: vi.fn() } |
| 59 | + |
| 60 | + await runWithOptions({ existsSync, loadEsm, loadCjs, logger }) |
| 61 | + |
| 62 | + expect(loadEsm).not.toHaveBeenCalled() |
| 63 | + expect(loadCjs).toHaveBeenCalledWith(cjsBundlePath) |
| 64 | + expect(logger.log).not.toHaveBeenCalled() |
| 65 | + expect(logger.error).not.toHaveBeenCalled() |
59 | 66 | }) |
60 | 67 |
|
61 | | - it('loads built postinstall script when present', () => { |
62 | | - const result = executeLegacyPostinstall(true) |
| 68 | + it('logs when no bundle is present', async () => { |
| 69 | + const existsSync = vi.fn(() => false) |
| 70 | + const loadEsm = vi.fn() |
| 71 | + const loadCjs = vi.fn() |
| 72 | + const logger = { log: vi.fn(), error: vi.fn() } |
| 73 | + |
| 74 | + await runWithOptions({ existsSync, loadEsm, loadCjs, logger }) |
| 75 | + |
| 76 | + expect(loadEsm).not.toHaveBeenCalled() |
| 77 | + expect(loadCjs).not.toHaveBeenCalled() |
| 78 | + expect(logger.log).toHaveBeenCalledWith('postinstall bundle not found') |
| 79 | + expect(logger.error).not.toHaveBeenCalled() |
| 80 | + }) |
| 81 | + |
| 82 | + it('reports errors when both bundle loads fail', async () => { |
| 83 | + const existsSync = vi.fn((file: string) => file === esmBundlePath || file === cjsBundlePath) |
| 84 | + const esmError = new Error('esm failed') |
| 85 | + const cjsError = new Error('cjs failed') |
| 86 | + const loadEsm = vi.fn(async () => { |
| 87 | + throw esmError |
| 88 | + }) |
| 89 | + const loadCjs = vi.fn(async () => { |
| 90 | + throw cjsError |
| 91 | + }) |
| 92 | + const logger = { log: vi.fn(), error: vi.fn() } |
| 93 | + |
| 94 | + await runWithOptions({ existsSync, loadEsm, loadCjs, logger }) |
63 | 95 |
|
64 | | - expect(result.existsSync).toHaveBeenCalledWith(builtScriptPath) |
65 | | - expect(result.consoleLog).not.toHaveBeenCalled() |
66 | | - expect(result.distModuleLoads).toBe(1) |
| 96 | + expect(loadEsm).toHaveBeenCalledWith(esmBundlePath) |
| 97 | + expect(loadCjs).toHaveBeenCalledWith(cjsBundlePath) |
| 98 | + expect(logger.error).toHaveBeenCalledWith('Failed to load postinstall bundle.') |
| 99 | + expect(logger.error).toHaveBeenCalledWith(esmError) |
| 100 | + expect(logger.error).toHaveBeenCalledWith(cjsError) |
| 101 | + expect(process.exitCode).toBe(1) |
| 102 | + expect(logger.log).not.toHaveBeenCalled() |
67 | 103 | }) |
68 | 104 | }) |
0 commit comments