diff --git a/bundler/bundle.js b/bundler/bundle.js index 6c0c9b6..7140872 100644 --- a/bundler/bundle.js +++ b/bundler/bundle.js @@ -1,6 +1,6 @@ import assert from 'node:assert/strict' import { readFile } from 'node:fs/promises' -import { existsSync } from 'node:fs' +import { existsSync, readFileSync } from 'node:fs' import { fileURLToPath, pathToFileURL } from 'node:url' import { basename, dirname, extname, resolve, join } from 'node:path' import { createRequire } from 'node:module' @@ -165,6 +165,26 @@ export const build = async (...files) => { } const fsfiles = await getPackageFiles(filename ? dirname(resolve(filename)) : process.cwd()) + const fsFilesContents = new Map() + loadPipeline.push(async (source) => { + const cwd = process.cwd() + for (const re of [/readFileSync\('([^'\\]+)'[),]/gu, /readFileSync\("([^"\\]+)"[),]/gu]) { + for (const match of source.matchAll(re)) { + const file = match[1] + if (file && /^[a-z0-9@_./-]+$/iu.test(file)) { + if (!resolve(file).startsWith(`${cwd}/`)) continue + const data = readFileSync(file, 'base64') + if (fsFilesContents.has(file)) { + assert(fsFilesContents.get(file) === data) + } else { + fsFilesContents.set(file, data) + } + } + } + } + + return source + }) const hasBuffer = ['node', 'bun'].includes(options.platform) const api = (f) => resolveRequire(`./modules/${f}`) @@ -191,10 +211,7 @@ export const build = async (...files) => { zlib: resolveRequire('browserify-zlib'), } - const defines = {} - if (files.length === 1) defines['process.argv'] = stringify(['exodus-test', resolve(files[0])]) - - const res = await buildWrap({ + const config = { logLevel: 'silent', stdin: { contents: `(async function () {\n${main}\n})()`, @@ -206,7 +223,6 @@ export const build = async (...files) => { platform: 'neutral', mainFields: ['browser', 'module', 'main'], define: { - ...defines, 'process.env.FORCE_COLOR': stringify('0'), 'process.env.NO_COLOR': stringify('1'), 'process.env.NODE_ENV': stringify(process.env.NODE_ENV), @@ -233,6 +249,7 @@ export const build = async (...files) => { EXODUS_TEST_SNAPSHOTS: stringify(EXODUS_TEST_SNAPSHOTS), EXODUS_TEST_RECORDINGS: stringify(EXODUS_TEST_RECORDINGS), EXODUS_TEST_FSFILES: stringify(fsfiles), // TODO: can we safely use relative paths? + EXODUS_TEST_FSFILES_CONTENTS: stringify([...fsFilesContents.entries()]), }, alias: { // Jest, tape and node:test @@ -286,9 +303,21 @@ export const build = async (...files) => { }, }, ], - }) + } + + if (files.length === 1) + config.define['process.argv'] = stringify(['exodus-test', resolve(files[0])]) + + let res = await buildWrap(config) assert.equal(res instanceof Error, res.errors.length > 0) + if (fsFilesContents.size > 0) { + // re-run as we detected that tests depend on fsReadFileSync contents + config.define.EXODUS_TEST_FSFILES_CONTENTS = stringify([...fsFilesContents.entries()]) + res = await buildWrap(config) + assert.equal(res instanceof Error, res.errors.length > 0) + } + // if (res.errors.length === 0) require('fs').copyFileSync(outfile, 'tempout.cjs') // DEBUG // We treat warnings as errors, so just merge all them diff --git a/bundler/modules/fs.cjs b/bundler/modules/fs.cjs index a6b3a75..362937c 100644 --- a/bundler/modules/fs.cjs +++ b/bundler/modules/fs.cjs @@ -77,11 +77,33 @@ const promises = { ...stubsPromises, constants } // eslint-disable-next-line no-undef const fsFiles = typeof EXODUS_TEST_FSFILES === 'undefined' ? null : new Set(EXODUS_TEST_FSFILES) const existsSync = (file) => { - if (fsFiles.has(file)) return true + if (fsFiles?.has(file)) return true err('existsSync', file) } -const readFileSync = (file /*, options */) => { +const fsFilesContents = + // eslint-disable-next-line no-undef + typeof EXODUS_TEST_FSFILES_CONTENTS === 'undefined' ? null : new Map(EXODUS_TEST_FSFILES_CONTENTS) +const readFileSync = (file, options) => { + let encoding + if (typeof options === 'string') { + encoding = options + } else if (options !== undefined) { + if (typeof options !== 'object') throw new Error('Unexpected readFileSync options') + const { encoding: enc, ...rest } = options + if (enc !== undefined && typeof enc !== 'string') throw new Error('encoding should be a string') + encoding = enc + if (Object.keys(rest).length > 0) throw new Error('Unsupported readFileSync options') + } + + if (typeof file !== 'string') throw new Error('file argument should be string') + if (fsFilesContents?.has(file)) { + const data = Buffer.from(fsFilesContents.get(file), 'base64') + if (encoding?.toLowerCase().replace('-', '') === 'utf8') return data.toString('utf8') + if (encoding === undefined) return data + throw new Error('Unsupported encoding') + } + err('readFileSync', file) }