UNPKG

react-native-builder-bob

Version:

CLI to build JavaScript files for React Native libraries

211 lines 10.2 kB
import { createRequire } from 'node:module'; import path from 'node:path'; import fs from 'fs-extra'; import kleur from 'kleur'; import * as babel from '@babel/core'; import { glob } from 'glob'; import { isCodegenSpec } from "./isCodegenSpec.js"; const require = createRequire(import.meta.url); const sourceExt = /\.([cm])?[jt]sx?$/; export default async function compile({ root, source, output, esm = false, babelrc = false, configFile = false, exclude, modules, copyFlow, sourceMaps = true, report, jsxRuntime = 'automatic', variants, }) { const files = await glob('**/*', { cwd: source, absolute: true, nodir: true, ignore: exclude, }); report.info(`Compiling ${kleur.blue(String(files.length))} files in ${kleur.blue(path.relative(root, source))} with ${kleur.blue('babel')}`); const pkg = JSON.parse(await fs.readFile(path.join(root, 'package.json'), 'utf-8')); if (copyFlow) { if (!Object.keys(pkg.devDependencies || {}).includes('flow-bin')) { report.warn(`The ${kleur.blue('copyFlow')} option was specified, but couldn't find ${kleur.blue('flow-bin')} in ${kleur.blue('package.json')}.\nIf the project is using ${kleur.blue('flow')}, then make sure you have added ${kleur.blue('flow-bin')} to your ${kleur.blue('devDependencies')}, otherwise remove the ${kleur.blue('copyFlow')} option.`); } } await fs.mkdirp(output); // Imports are not rewritten to include the extension if `esm` is not enabled // Ideally we should always treat ESM syntax as CommonJS if `esm` is not enabled // This would maintain compatibility for legacy setups where `import`/`export` didn't require file extensions // However NextJS has non-standard behavior and breaks if we add `type: 'commonjs'` for code with import/export // So we skip generating `package.json` if `esm` is not enabled and `modules` is not `commonjs` // This means that user can't use `type: 'module'` in root `package.json` without enabling `esm` for `module` target if (esm || modules === 'commonjs') { await fs.writeJSON(path.join(output, 'package.json'), { type: modules === 'commonjs' ? 'commonjs' : 'module', }); } await Promise.all(files.map(async (filepath) => { const outputFilename = path .join(output, path.relative(source, filepath)) .replace(sourceExt, '.$1js'); await fs.mkdirp(path.dirname(outputFilename)); if (!sourceExt.test(filepath)) { // Copy files which aren't source code await fs.copy(filepath, outputFilename); return; } const content = await fs.readFile(filepath, 'utf-8'); // If codegen is used in the app, then we need to preserve TypeScript source // So we copy the file as is instead of transforming it const codegenEnabled = 'codegenConfig' in pkg; if (codegenEnabled && isCodegenSpec(filepath)) { await fs.copy(filepath, path.join(output, path.relative(source, filepath))); return; } const result = await babel.transformAsync(content, { caller: { name: 'react-native-builder-bob', supportsStaticESM: /\.m[jt]s$/.test(filepath) || // If a file is explicitly marked as ESM, then preserve the syntax modules === 'preserve' ? true : false, rewriteImportExtensions: esm, jsxRuntime, codegenEnabled, }, cwd: root, babelrc: babelrc, configFile: configFile, sourceMaps, sourceRoot: path.relative(path.dirname(outputFilename), source), sourceFileName: path.relative(source, filepath), filename: filepath, ...(babelrc || configFile ? null : { presets: [require.resolve('../configs/babel-preset.cjs')], }), }); if (result == null) { throw new Error('Output code was null'); } let code = result.code || ''; if (sourceMaps && result.map) { const mapFilename = outputFilename + '.map'; code += '\n//# sourceMappingURL=' + path.basename(mapFilename); // Don't inline the source code, it can be retrieved from the source file result.map.sourcesContent = undefined; await fs.writeJSON(mapFilename, result.map); } await fs.writeFile(outputFilename, code); if (copyFlow) { await fs.copy(filepath, outputFilename + '.flow'); } })); report.success(`Wrote files to ${kleur.blue(path.relative(root, output))}`); const getGeneratedEntryPath = async () => { if (pkg.source) { for (const ext of ['.js', '.cjs', '.mjs']) { const indexName = // The source field may not have an extension, so we add it instead of replacing directly path.basename(pkg.source).replace(sourceExt, '') + ext; const potentialPath = path.join(output, path.dirname(path.relative(source, path.join(root, pkg.source))), indexName); if (await fs.pathExists(potentialPath)) { return path.relative(root, potentialPath); } } } return null; }; const fields = []; if (variants.commonjs && variants.module) { if (modules === 'commonjs') { fields.push({ name: 'main', value: pkg.main }); } else { fields.push({ name: 'module', value: pkg.module }); } } else { fields.push({ name: 'main', value: pkg.main }); } if (esm) { if (variants.commonjs && variants.module) { if (modules === 'commonjs') { fields.push(typeof pkg.exports?.['.']?.require === 'string' ? { name: "exports['.'].require", value: pkg.exports?.['.']?.require, } : { name: "exports['.'].require.default", value: pkg.exports?.['.']?.require?.default, }); } else { fields.push(typeof pkg.exports?.['.']?.import === 'string' ? { name: "exports['.'].import", value: pkg.exports?.['.']?.import, } : { name: "exports['.'].import.default", value: pkg.exports?.['.']?.import?.default, }); } } else { fields.push({ name: "exports['.'].default", value: pkg.exports?.['.']?.default, }); } } else { if (modules === 'commonjs' && pkg.exports?.['.']?.require) { report.warn(`The ${kleur.blue('esm')} option is disabled, but the ${kleur.blue("exports['.'].require")} field is set in ${kleur.blue('package.json')}. This is likely a mistake.`); } else if (modules === 'preserve' && pkg.exports?.['.']?.import) { report.warn(`The ${kleur.blue('esm')} option is disabled, but the ${kleur.blue("exports['.'].import")} field is set in ${kleur.blue('package.json')}. This is likely a mistake.`); } } const generatedEntryPath = await getGeneratedEntryPath(); if (fields.some((field) => field.value)) { for (const { name, value } of fields) { if (!value) { continue; } if (name.startsWith('exports') && value && !/^\.\//.test(value)) { report.error(`The ${kleur.blue(name)} field in ${kleur.blue(`package.json`)} should be a relative path starting with ${kleur.blue('./')}. Found: ${kleur.blue(value)}`); throw new Error(`Found incorrect path in '${name}' field.`); } try { require.resolve(path.join(root, value)); } catch (e) { if (e != null && typeof e === 'object' && 'code' in e && e.code === 'MODULE_NOT_FOUND') { if (!generatedEntryPath) { report.warn(`Failed to detect the entry point for the generated files. Make sure you have a valid ${kleur.blue('source')} field in your ${kleur.blue('package.json')}.`); } report.error(`The ${kleur.blue(name)} field in ${kleur.blue('package.json')} points to a non-existent file: ${kleur.blue(value)}.\nVerify the path points to the correct file under ${kleur.blue(path.relative(root, output))}${generatedEntryPath ? ` (found ${kleur.blue(generatedEntryPath)}).` : '.'}`); throw new Error(`Found incorrect path in '${name}' field.`, { cause: e, }); } throw e; } } if (generatedEntryPath) { if (modules === 'commonjs' && pkg.exports?.['.']?.import === `./${generatedEntryPath}`) { report.warn(`The the ${kleur.blue("exports['.'].import")} field points to a CommonJS module. This is likely a mistake.`); } else if (modules === 'preserve' && pkg.exports?.['.']?.require === `./${generatedEntryPath}`) { report.warn(`The the ${kleur.blue("exports['.'].import")} field points to a ES module. This is likely a mistake.`); } } } else { report.warn(`No ${fields .map((field) => kleur.blue(field.name)) .join(' or ')} field found in ${kleur.blue('package.json')}. Consider ${generatedEntryPath ? `pointing to ${kleur.blue(generatedEntryPath)}` : `adding ${fields.length > 1 ? 'them' : 'it'}`} so that consumers of your package can import your package.`); } } //# sourceMappingURL=compile.js.map