diff --git a/.github/workflows/build-templates.yml b/.github/workflows/build-templates.yml index 43f185d55..e316a148f 100644 --- a/.github/workflows/build-templates.yml +++ b/.github/workflows/build-templates.yml @@ -200,8 +200,6 @@ jobs: working-directory: ${{ env.work_dir }} run: | yarn typecheck - # FIXME: Remove this once we fix the typecheck errors - continue-on-error: true - name: Test library working-directory: ${{ env.work_dir }} diff --git a/docs/pages/build.md b/docs/pages/build.md index 8e0b334d5..a3c07190e 100644 --- a/docs/pages/build.md +++ b/docs/pages/build.md @@ -73,11 +73,17 @@ yarn add --dev react-native-builder-bob 1. Configure the appropriate entry points: ```json - "main": "lib/commonjs/index.js", - "module": "lib/module/index.js", - "react-native": "src/index.ts", - "types": "lib/typescript/src/index.d.ts", - "source": "src/index.ts", + "source": "./src/index.tsx", + "main": "./lib/commonjs/index.cjs", + "module": "./lib/module/index.mjs", + "types": "./lib/typescript/src/index.d.ts", + "exports": { + ".": { + "types": "./typescript/src/index.d.ts", + "require": "./commonjs/index.cjs", + "import": "./module/index.mjs" + } + }, "files": [ "lib", "src" @@ -88,7 +94,6 @@ yarn add --dev react-native-builder-bob - `main`: The entry point for the commonjs build. This is used by Node - such as tests, SSR etc. - `module`: The entry point for the ES module build. This is used by bundlers such as webpack. - - `react-native`: The entry point for the React Native apps. This is used by Metro. It's common to point to the source code here as it can make debugging easier. - `types`: The entry point for the TypeScript definitions. This is used by TypeScript to type check the code using your library. - `source`: The path to the source code. It is used by `react-native-builder-bob` to detect the correct output files and provide better error messages. - `files`: The files to include in the package when publishing with `npm`. @@ -150,7 +155,7 @@ Various targets to build for. The available targets are: Enable compiling source files with Babel and use commonjs module system. -This is useful for running the code in Node (SSR, tests etc.). The output file should be referenced in the `main` field of `package.json`. +This is useful for running the code in Node (SSR, tests etc.). The output file should be referenced in the `main` field and `exports['.'].require` field of `package.json`. By default, the code is compiled to support last 2 versions of modern browsers. It also strips TypeScript and Flow annotations, and compiles JSX. You can customize the environments to compile for by using a [browserslist config](https://github.com/browserslist/browserslist#config-file). @@ -174,7 +179,7 @@ Example: Enable compiling source files with Babel and use ES module system. This is essentially same as the `commonjs` target and accepts the same options, but leaves the `import`/`export` statements in your code. -This is useful for bundlers which understand ES modules and can tree-shake. The output file should be referenced in the `module` field of `package.json`. +This is useful for bundlers which understand ES modules and can tree-shake. The output file should be referenced in the `module` field and `exports['.'].import` field of `package.json`. Example: @@ -198,6 +203,8 @@ Example: ["typescript", { "project": "tsconfig.build.json" }] ``` +The output file should be referenced in the `types` field or `exports['.'].types` field of `package.json`. + ## Commands The `bob` CLI exposes the following commands: diff --git a/packages/create-react-native-library/src/index.ts b/packages/create-react-native-library/src/index.ts index 77566886d..f4424c392 100644 --- a/packages/create-react-native-library/src/index.ts +++ b/packages/create-react-native-library/src/index.ts @@ -14,7 +14,7 @@ import generateExampleApp, { import { spawn } from './utils/spawn'; import { version } from '../package.json'; -const FALLBACK_BOB_VERSION = '0.20.0'; +const FALLBACK_BOB_VERSION = '0.25.0'; const BINARIES = [ /(gradlew|\.(jar|keystore|png|jpg|gif))$/, diff --git a/packages/create-react-native-library/templates/common-example/example/src/App.tsx b/packages/create-react-native-library/templates/common-example/example/src/App.tsx index 38db6fe50..acc5c7d17 100644 --- a/packages/create-react-native-library/templates/common-example/example/src/App.tsx +++ b/packages/create-react-native-library/templates/common-example/example/src/App.tsx @@ -1,9 +1,10 @@ -import * as React from 'react'; - <% if (project.view) { -%> import { StyleSheet, View } from 'react-native'; import { <%- project.name -%>View } from '<%- project.slug -%>'; <% } else { -%> +<% if (project.arch !== 'new') { -%> +import { useState, useEffect } from 'react'; +<% } -%> import { StyleSheet, View, Text } from 'react-native'; import { multiply } from '<%- project.slug -%>'; <% } -%> @@ -28,9 +29,9 @@ export default function App() { } <% } else { -%> export default function App() { - const [result, setResult] = React.useState(); + const [result, setResult] = useState(); - React.useEffect(() => { + useEffect(() => { multiply(3, 7).then(setResult); }, []); diff --git a/packages/create-react-native-library/templates/common/$package.json b/packages/create-react-native-library/templates/common/$package.json index f0388daa4..85f078c19 100644 --- a/packages/create-react-native-library/templates/common/$package.json +++ b/packages/create-react-native-library/templates/common/$package.json @@ -2,11 +2,17 @@ "name": "<%- project.slug -%>", "version": "0.1.0", "description": "<%- project.description %>", - "main": "lib/commonjs/index", - "module": "lib/module/index", - "types": "lib/typescript/src/index.d.ts", - "react-native": "src/index", - "source": "src/index", + "source": "./src/index.tsx", + "main": "./lib/commonjs/index.cjs", + "module": "./lib/module/index.mjs", + "types": "./lib/typescript/src/index.d.ts", + "exports": { + ".": { + "types": "./lib/typescript/src/index.d.ts", + "import": "./lib/module/index.mjs", + "require": "./lib/commonjs/index.cjs" + } + }, "files": [ "src", "lib", @@ -130,6 +136,7 @@ "prettier" ], "rules": { + "react/react-in-jsx-scope": "off", "prettier/prettier": [ "error", { diff --git a/packages/create-react-native-library/templates/common/tsconfig.json b/packages/create-react-native-library/templates/common/tsconfig.json index cd3d623ba..7472abcad 100644 --- a/packages/create-react-native-library/templates/common/tsconfig.json +++ b/packages/create-react-native-library/templates/common/tsconfig.json @@ -8,10 +8,10 @@ "allowUnusedLabels": false, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, - "jsx": "react", - "lib": ["esnext"], - "module": "esnext", - "moduleResolution": "node", + "jsx": "react-jsx", + "lib": ["ESNext"], + "module": "ESNext", + "moduleResolution": "Bundler", "noFallthroughCasesInSwitch": true, "noImplicitReturns": true, "noImplicitUseStrict": false, @@ -22,7 +22,7 @@ "resolveJsonModule": true, "skipLibCheck": true, "strict": true, - "target": "esnext", + "target": "ESNext", "verbatimModuleSyntax": true } } diff --git a/packages/react-native-builder-bob/babel-preset.js b/packages/react-native-builder-bob/babel-preset.js index f664c15b1..2b725e98d 100644 --- a/packages/react-native-builder-bob/babel-preset.js +++ b/packages/react-native-builder-bob/babel-preset.js @@ -3,6 +3,8 @@ const browserslist = require('browserslist'); module.exports = function (api, options, cwd) { + const cjs = options.modules === 'commonjs'; + return { presets: [ [ @@ -24,12 +26,25 @@ module.exports = function (api, options, cwd) { node: '18', }, useBuiltIns: false, - modules: options.modules || false, + modules: cjs ? 'commonjs' : false, + }, + ], + [ + require.resolve('@babel/preset-react'), + { + runtime: 'automatic', }, ], - require.resolve('@babel/preset-react'), require.resolve('@babel/preset-typescript'), require.resolve('@babel/preset-flow'), ], + plugins: [ + [ + require.resolve('./lib/babel'), + { + extension: cjs ? 'cjs' : 'mjs', + }, + ], + ], }; }; diff --git a/packages/react-native-builder-bob/src/index.ts b/packages/react-native-builder-bob/src/index.ts index b66859f91..4baff1af5 100644 --- a/packages/react-native-builder-bob/src/index.ts +++ b/packages/react-native-builder-bob/src/index.ts @@ -28,7 +28,7 @@ yargs const { shouldContinue } = await prompts({ type: 'confirm', name: 'shouldContinue', - message: `The working directory is not clean. You should commit or stash your changes before configuring bob. Continue anyway?`, + message: `The working directory is not clean.\n You should commit or stash your changes before configuring bob.\n Continue anyway?`, initial: false, }); @@ -41,7 +41,7 @@ yargs if (!(await fs.pathExists(pak))) { logger.exit( - `Couldn't find a 'package.json' file in '${root}'. Are you in a project folder?` + `Couldn't find a 'package.json' file in '${root}'.\n Are you in a project folder?` ); } @@ -52,7 +52,7 @@ yargs const { shouldContinue } = await prompts({ type: 'confirm', name: 'shouldContinue', - message: `The project seems to be already configured with bob. Do you want to overwrite the existing configuration?`, + message: `The project seems to be already configured with bob.\n Do you want to overwrite the existing configuration?`, initial: false, }); @@ -81,7 +81,7 @@ yargs if (!entryFile) { logger.exit( - `Couldn't find a 'index.js'. 'index.ts' or 'index.tsx' file under '${source}'. Please re-run the CLI after creating it.` + `Couldn't find a 'index.js'. 'index.ts' or 'index.tsx' file under '${source}'.\n Please re-run the CLI after creating it.` ); return; } @@ -147,26 +147,34 @@ yargs ? targets[0] : undefined; - const entries: { [key: string]: string } = { - 'main': target - ? path.join(output, target, 'index.js') - : path.join(source, entryFile), - 'react-native': path.join(source, entryFile), - 'source': path.join(source, entryFile), + const entries: { + [key in 'source' | 'main' | 'module' | 'types']?: string; + } = { + source: `./${path.join(source, entryFile)}`, + main: `./${ + target + ? path.join(output, target, 'index.cjs') + : path.join(source, entryFile) + }`, }; if (targets.includes('module')) { - entries.module = path.join(output, 'module', 'index.js'); + entries.module = `./${path.join(output, 'module', 'index.mjs')}`; } if (targets.includes('typescript')) { - entries.types = path.join(output, 'typescript', source, 'index.d.ts'); + entries.types = `./${path.join( + output, + 'typescript', + source, + 'index.d.ts' + )}`; if (!(await fs.pathExists(path.join(root, 'tsconfig.json')))) { const { tsconfig } = await prompts({ type: 'confirm', name: 'tsconfig', - message: `You have enabled 'typescript' compilation, but we couldn't find a 'tsconfig.json' in project root. Generate one?`, + message: `You have enabled 'typescript' compilation, but we couldn't find a 'tsconfig.json' in project root.\n Generate one?`, initial: true, }); @@ -180,10 +188,10 @@ yargs allowUnusedLabels: false, esModuleInterop: true, forceConsistentCasingInFileNames: true, - jsx: 'react', - lib: ['esnext'], - module: 'esnext', - moduleResolution: 'node', + jsx: 'react-jsx', + lib: ['ESNext'], + module: 'ESNext', + moduleResolution: 'Bundler', noFallthroughCasesInSwitch: true, noImplicitReturns: true, noImplicitUseStrict: false, @@ -194,7 +202,7 @@ yargs resolveJsonModule: true, skipLibCheck: true, strict: true, - target: 'esnext', + target: 'ESNext', verbatimModuleSyntax: true, }, }, @@ -214,13 +222,13 @@ yargs ]; for (const key in entries) { - const entry = entries[key]; + const entry = entries[key as keyof typeof entries]; if (pkg[key] && pkg[key] !== entry) { const { replace } = await prompts({ type: 'confirm', name: 'replace', - message: `Your package.json has the '${key}' field set to '${pkg[key]}'. Do you want to replace it with '${entry}'?`, + message: `Your package.json has the '${key}' field set to '${pkg[key]}'.\n Do you want to replace it with '${entry}'?`, initial: true, }); @@ -232,11 +240,60 @@ yargs } } + if (Object.values(entries).some((entry) => entry.endsWith('.mjs'))) { + let replace = false; + + const exports = { + '.': { + ...(entries.types ? { types: entries.types } : null), + ...(entries.module ? { import: entries.module } : null), + ...(entries.main ? { require: entries.main } : null), + }, + }; + + if ( + pkg.exports && + JSON.stringify(pkg.exports) !== JSON.stringify(exports) + ) { + replace = ( + await prompts({ + type: 'confirm', + name: 'replace', + message: `Your package.json has 'exports' field set.\n Do you want to replace it?`, + initial: true, + }) + ).replace; + } else { + replace = true; + } + + if (replace) { + pkg.exports = exports; + } + } + + if ( + pkg['react-native'] && + (pkg['react-native'].startsWith(source) || + pkg['react-native'].startsWith(`./${source}`)) + ) { + const { remove } = await prompts({ + type: 'confirm', + name: 'remove', + message: `Your package.json has the 'react-native' field pointing to source code.\n This can cause problems when customizing babel configuration.\n Do you want to remove it?`, + initial: true, + }); + + if (remove) { + delete pkg['react-native']; + } + } + if (pkg.scripts?.prepare && pkg.scripts.prepare !== prepare) { const { replace } = await prompts({ type: 'confirm', name: 'replace', - message: `Your package.json has the 'scripts.prepare' field set to '${pkg.scripts.prepare}'. Do you want to replace it with '${prepare}'?`, + message: `Your package.json has the 'scripts.prepare' field set to '${pkg.scripts.prepare}'.\n Do you want to replace it with '${prepare}'?`, initial: true, }); @@ -256,7 +313,7 @@ yargs const { update } = await prompts({ type: 'confirm', name: 'update', - message: `Your package.json already has a 'files' field. Do you want to update it?`, + message: `Your package.json already has a 'files' field.\n Do you want to update it?`, initial: true, }); diff --git a/packages/react-native-builder-bob/src/targets/commonjs.ts b/packages/react-native-builder-bob/src/targets/commonjs.ts index 2092906cd..35c9de57f 100644 --- a/packages/react-native-builder-bob/src/targets/commonjs.ts +++ b/packages/react-native-builder-bob/src/targets/commonjs.ts @@ -36,6 +36,5 @@ export default async function build({ exclude, modules: 'commonjs', report, - field: 'main', }); } diff --git a/packages/react-native-builder-bob/src/targets/module.ts b/packages/react-native-builder-bob/src/targets/module.ts index e6cee46c7..5ad1534a6 100644 --- a/packages/react-native-builder-bob/src/targets/module.ts +++ b/packages/react-native-builder-bob/src/targets/module.ts @@ -36,6 +36,5 @@ export default async function build({ exclude, modules: false, report, - field: 'module', }); } diff --git a/packages/react-native-builder-bob/src/targets/typescript.ts b/packages/react-native-builder-bob/src/targets/typescript.ts index 77ec303d1..9d2d9e1a7 100644 --- a/packages/react-native-builder-bob/src/targets/typescript.ts +++ b/packages/react-native-builder-bob/src/targets/typescript.ts @@ -217,43 +217,56 @@ export default async function build({ return null; }; - if ('types' in pkg) { - const typesPath = path.join(root, pkg.types); - - if (!(await fs.pathExists(typesPath))) { - const generatedTypesPath = await getGeneratedTypesPath(); - - if (!generatedTypesPath) { - report.warn( - `Failed to detect the entry point for the generated types. Make sure you have a valid ${kleur.blue( - 'source' - )} field in your ${kleur.blue('package.json')}.` - ); - } - - report.error( - `The ${kleur.blue('types')} field in ${kleur.blue( - 'package.json' - )} points to a non-existent file: ${kleur.blue( - pkg.types - )}.\nVerify the path points to the correct file under ${kleur.blue( - path.relative(root, output) - )}${ - generatedTypesPath - ? ` (found ${kleur.blue(generatedTypesPath)}).` - : '.' - }` - ); + const fields = [ + { name: 'types', value: pkg.types }, + { name: "exports['.'].types", value: pkg.exports?.['.']?.types }, + ]; + + if (fields.some((field) => field.value)) { + await Promise.all( + fields.map(async ({ name, value }) => { + if (!value) { + return; + } - throw new Error("Found incorrect path in 'types' field."); - } + const typesPath = path.join(root, value); + + if (!(await fs.pathExists(typesPath))) { + const generatedTypesPath = await getGeneratedTypesPath(); + + if (!generatedTypesPath) { + report.warn( + `Failed to detect the entry point for the generated types. 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) + )}${ + generatedTypesPath + ? ` (found ${kleur.blue(generatedTypesPath)}).` + : '.' + }` + ); + + throw new Error(`Found incorrect path in '${name}' field.`); + } + }) + ); } else { const generatedTypesPath = await getGeneratedTypesPath(); report.warn( - `No ${kleur.blue('types')} field found in ${kleur.blue( - 'package.json' - )}.\nConsider ${ + `No ${kleur.blue( + fields.map((field) => field.name).join(' or ') + )} field found in ${kleur.blue('package.json')}.\nConsider ${ generatedTypesPath ? `pointing it to ${kleur.blue(generatedTypesPath)}` : 'adding it' diff --git a/packages/react-native-builder-bob/src/utils/compile.ts b/packages/react-native-builder-bob/src/utils/compile.ts index 3e0cc8e8c..52a2bd9b6 100644 --- a/packages/react-native-builder-bob/src/utils/compile.ts +++ b/packages/react-native-builder-bob/src/utils/compile.ts @@ -11,7 +11,6 @@ type Options = Input & { sourceMaps?: boolean; copyFlow?: boolean; modules: 'commonjs' | false; - field: 'main' | 'module'; exclude: string; }; @@ -26,7 +25,6 @@ export default async function compile({ copyFlow, sourceMaps = true, report, - field, }: Options) { const files = glob.sync('**/*', { cwd: source, @@ -65,11 +63,13 @@ export default async function compile({ } } + const outputExtension = modules === 'commonjs' ? '.cjs' : '.mjs'; + await Promise.all( files.map(async (filepath) => { const outputFilename = path .join(output, path.relative(source, filepath)) - .replace(/\.(jsx?|tsx?)$/, '.js'); + .replace(/\.(jsx?|tsx?)$/, outputExtension); await fs.mkdirp(path.dirname(outputFilename)); @@ -125,7 +125,8 @@ export default async function compile({ const getGeneratedEntryPath = async () => { if (pkg.source) { const indexName = - path.basename(pkg.source).replace(/\.(jsx?|tsx?)$/, '') + '.js'; + path.basename(pkg.source).replace(/\.(jsx?|tsx?)$/, '') + + outputExtension; const potentialPath = path.join( output, @@ -141,52 +142,71 @@ export default async function compile({ return null; }; - if (field in pkg) { - try { - require.resolve(path.join(root, pkg[field])); - } catch (e: unknown) { - if ( - e != null && - typeof e === 'object' && - 'code' in e && - e.code === 'MODULE_NOT_FOUND' - ) { - const generatedEntryPath = await getGeneratedEntryPath(); - - 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')}.` - ); + const fields = + modules === 'commonjs' + ? [ + { name: 'main', value: pkg.main }, + { name: "exports['.'].require", value: pkg.exports?.['.']?.require }, + ] + : [ + { name: 'module', value: pkg.module }, + { name: "exports['.'].import", value: pkg.exports?.['.']?.import }, + ]; + + if (fields.some((field) => field.value)) { + await Promise.all( + fields.map(async ({ name, value }) => { + if (!value) { + return; } - report.error( - `The ${kleur.blue(field)} field in ${kleur.blue( - 'package.json' - )} points to a non-existent file: ${kleur.blue( - pkg[field] - )}.\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 '${field}' field.`); - } - - throw e; - } + try { + require.resolve(path.join(root, value)); + } catch (e: unknown) { + if ( + e != null && + typeof e === 'object' && + 'code' in e && + e.code === 'MODULE_NOT_FOUND' + ) { + const generatedEntryPath = await getGeneratedEntryPath(); + + 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.`); + } + + throw e; + } + }) + ); } else { const generatedEntryPath = await getGeneratedEntryPath(); report.warn( - `No ${kleur.blue(field)} field found in ${kleur.blue( - 'package.json' - )}. Consider ${ + `No ${kleur.blue( + fields.map((field) => field.name).join(' or ') + )} field found in ${kleur.blue('package.json')}. Consider ${ generatedEntryPath ? `pointing it to ${kleur.blue(generatedEntryPath)}` : 'adding it' diff --git a/tsconfig.json b/tsconfig.json index 90faea635..642896b15 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ "allowUnusedLabels": false, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, - "jsx": "react", + "jsx": "react-jsx", "lib": ["esnext", "dom"], "module": "esnext", "moduleResolution": "node",