From a9bb07f4622ede5d1ec3d1c568d280155e30cc0c Mon Sep 17 00:00:00 2001 From: Rob Jacobson Date: Mon, 12 Oct 2026 16:44:09 -0700 Subject: [PATCH 1/2] fix(types): restore missing type exports (#4139) - Restore the four reported source type imports through explicit declaration exports - Expose expression, style, symbol layer, map payload, and press event types publicly - Verify built and packed outputs with generated package metadata and dependency types - Cover private internal paths and Node runtime export rejection --- __tests__/typeExports.test.js | 241 ++++++++++++++++++++++++++++++++++ package.json | 12 ++ src/Mapbox.native.ts | 11 +- 3 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 __tests__/typeExports.test.js diff --git a/__tests__/typeExports.test.js b/__tests__/typeExports.test.js new file mode 100644 index 000000000..f422e2c73 --- /dev/null +++ b/__tests__/typeExports.test.js @@ -0,0 +1,241 @@ +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { execFileSync } from 'child_process'; +import ts from 'typescript'; + +const legacyPaths = [ + '@rnmapbox/maps/src/utils/MapboxStyles', + '@rnmapbox/maps/src/components/SymbolLayer', + '@rnmapbox/maps/src/components/MapView', + '@rnmapbox/maps/src/types/OnPressEvent', +]; + +const consumerOptions = { + module: ts.ModuleKind.ESNext, + moduleResolution: ts.ModuleResolutionKind.Bundler, + target: ts.ScriptTarget.ESNext, + strict: true, + skipLibCheck: true, + noEmit: true, + esModuleInterop: true, + types: ['react', 'geojson'], +}; + +const consumerSource = ` +import type Mapbox from '@rnmapbox/maps'; +import type { + Expression, FilterExpression, SymbolLayerProps, SymbolLayerStyle, + SymbolLayerStyleProps, RegionPayload, GestureSettings, MapLoadingErrorPayload, + OnPressEvent, Translation, Transition, +} from '@rnmapbox/maps'; +import type { Expression as LegacyExpression } from '@rnmapbox/maps/src/utils/MapboxStyles'; +import type { Props as LegacySymbolLayerProps } from '@rnmapbox/maps/src/components/SymbolLayer'; +import type { RegionPayload as LegacyRegionPayload } from '@rnmapbox/maps/src/components/MapView'; +import type { OnPressEvent as LegacyOnPressEvent } from '@rnmapbox/maps/src/types/OnPressEvent'; + +type Equal = [A] extends [B] ? ([B] extends [A] ? true : false) : false; +type Assert = T; +export type CompatibleTypes = [ + Assert>, + Assert>, + Assert>, + Assert>, + Assert>, + Assert>, + Assert>, +]; + +export const expression: Expression = ['get', 'myProp']; +export const symbol: SymbolLayerProps = { id: 'symbols', style: { textField: expression } }; +export const gestures: GestureSettings = { panEnabled: false }; +export const loadingError: MapLoadingErrorPayload = { error: 'Missing style', type: 'style' }; +export const translation: Translation = [1, 2]; +export const transition: Transition = { duration: 300, delay: 0 }; + +// @ts-expect-error Expressions must start with a known operator. +export const invalidExpression: Expression = ['unknown-operator']; +// @ts-expect-error Symbol layer props require an id. +export const invalidSymbol: SymbolLayerProps = { style: {} }; +// @ts-expect-error Source press events include features, coordinates and a point. +export const invalidPress: OnPressEvent = {}; +// @ts-expect-error Press features must be GeoJSON features. +export const invalidFeature: OnPressEvent['features'][number] = { type: 'wrong' }; +// @ts-expect-error Symbol children must be React elements. +export const invalidChildren: SymbolLayerProps['children'] = 123; +`; + +describe('Published type exports', () => { + let consumerDir; + let packageDir; + + beforeAll(() => { + consumerDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'rnmapbox-type-exports-'), + ); + packageDir = path.join(consumerDir, 'node_modules/@rnmapbox/maps'); + fs.mkdirSync(packageDir, { recursive: true }); + fs.writeFileSync(path.join(consumerDir, 'index.ts'), consumerSource); + + const projectRoot = path.resolve(__dirname, '..'); + const buildDir = path.join(consumerDir, 'build'); + fs.mkdirSync(buildDir); + fs.cpSync(path.join(projectRoot, 'src'), path.join(buildDir, 'src'), { + recursive: true, + }); + for (const file of [ + 'package.json', + 'tsconfig.json', + 'tsconfig.build.json', + 'yarn.lock', + ]) { + fs.copyFileSync(path.join(projectRoot, file), path.join(buildDir, file)); + } + fs.mkdirSync(path.join(buildDir, 'example')); + fs.copyFileSync( + path.join(projectRoot, 'example/package.json'), + path.join(buildDir, 'example/package.json'), + ); + fs.writeFileSync( + path.join(buildDir, '.yarnrc.yml'), + 'nodeLinker: node-modules\n', + ); + fs.symlinkSync( + path.join(projectRoot, 'node_modules'), + path.join(buildDir, 'node_modules'), + 'junction', + ); + + // Use the real build targets and package file selection, including Bob's + // generated package metadata. Build outside the checkout to avoid stale + // output and interference with other checks. + const env = { ...process.env }; + delete env.npm_execpath; + const bob = require.resolve('react-native-builder-bob/bin/bob'); + for (const target of ['module', 'typescript']) { + execFileSync(process.execPath, [bob, 'build', '--target', target], { + cwd: buildDir, + env, + timeout: 60000, + }); + } + const archive = path.join(consumerDir, 'package.tgz'); + execFileSync( + process.execPath, + [ + path.join(projectRoot, '.yarn/releases/yarn-3.6.1.cjs'), + 'pack', + '--out', + archive, + ], + { cwd: buildDir, env, timeout: 30000 }, + ); + execFileSync('tar', [ + '-xzf', + archive, + '--strip-components=1', + '-C', + packageDir, + ]); + + // Resolve React and GeoJSON types as a consumer would, rather than letting + // missing declaration dependencies degrade to any under skipLibCheck. + for (const dependency of [ + '@types', + 'react', + 'react-native', + 'debounce', + '@turf', + ]) { + fs.symlinkSync( + path.join(projectRoot, 'node_modules', dependency), + path.join(consumerDir, 'node_modules', dependency), + 'junction', + ); + } + }, 60000); + + afterAll(() => { + if (consumerDir) { + fs.rmSync(consumerDir, { recursive: true, force: true }); + } + }); + + it.each([[''], ['.native', '']])( + 'resolves public and legacy types with moduleSuffixes %j', + (...moduleSuffixes) => { + const program = ts.createProgram([path.join(consumerDir, 'index.ts')], { + ...consumerOptions, + moduleSuffixes, + }); + const diagnostics = ts.getPreEmitDiagnostics(program); + expect( + diagnostics.map((diagnostic) => + ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'), + ), + ).toEqual([]); + }, + ); + + it('includes the generated package metadata and export targets', () => { + for (const output of ['module', 'typescript']) { + expect( + JSON.parse( + fs.readFileSync( + path.join(packageDir, 'lib', output, 'package.json'), + 'utf8', + ), + ), + ).toEqual({ type: 'module' }); + } + const manifest = JSON.parse( + fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8'), + ); + expect(fs.existsSync(path.join(packageDir, manifest.main))).toBe(true); + for (const entry of [ + '.', + ...legacyPaths.map((name) => '.' + name.slice('@rnmapbox/maps'.length)), + ]) { + expect( + fs.existsSync(path.join(packageDir, manifest.exports[entry].types)), + ).toBe(true); + } + }); + + it.each([ + '@rnmapbox/maps/src/specs/codegenUtils', + '@rnmapbox/maps/src/components/NativeBridgeComponent', + ])('keeps unrelated source path %s private', (specifier) => { + const resolution = ts.resolveModuleName( + specifier, + path.join(consumerDir, 'index.ts'), + consumerOptions, + ts.sys, + ); + expect(resolution.resolvedModule).toBeUndefined(); + }); + + it.each(legacyPaths)( + 'does not add a Node runtime export for %s', + (specifier) => { + // Run real Node resolution, outside Jest's module resolver. + const result = execFileSync( + process.execPath, + [ + '-e', + `const consumerRequire = require('node:module').createRequire(process.argv[1]); + try { + consumerRequire.resolve(process.argv[2]); + process.stdout.write('resolved'); + } catch (error) { + process.stdout.write(error.code); + }`, + path.join(consumerDir, 'index.ts'), + specifier, + ], + { encoding: 'utf8' }, + ); + expect(result).toBe('ERR_PACKAGE_PATH_NOT_EXPORTED'); + }, + ); +}); diff --git a/package.json b/package.json index 89bfd7a43..f414fd44d 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,18 @@ "types": "./lib/typescript/src/index.native.d.ts", "default": "./lib/module/index.js" }, + "./src/utils/MapboxStyles": { + "types": "./lib/typescript/src/utils/MapboxStyles.d.ts" + }, + "./src/components/SymbolLayer": { + "types": "./lib/typescript/src/components/SymbolLayer.d.ts" + }, + "./src/components/MapView": { + "types": "./lib/typescript/src/components/MapView.d.ts" + }, + "./src/types/OnPressEvent": { + "types": "./lib/typescript/src/types/OnPressEvent.d.ts" + }, "./package.json": "./package.json", "./app.plugin.js": "./app.plugin.js", "./setup-jest": "./setup-jest.js" diff --git a/src/Mapbox.native.ts b/src/Mapbox.native.ts index 852e29768..60b896510 100644 --- a/src/Mapbox.native.ts +++ b/src/Mapbox.native.ts @@ -13,7 +13,11 @@ export { Snow } from './components/Snow'; export { Rain } from './components/Rain'; export { default as MapView, + type Point, + type RegionPayload, + type GestureSettings, type MapState, + type MapLoadingErrorPayload, type ScreenPointPayload, } from './components/MapView'; export { default as Light } from './components/Light'; @@ -45,7 +49,10 @@ export { default as LineLayer } from './components/LineLayer'; export { default as CircleLayer } from './components/CircleLayer'; export { default as SkyLayer } from './components/SkyLayer'; export { default as ModelLayer } from './components/ModelLayer'; -export { SymbolLayer } from './components/SymbolLayer'; +export { + SymbolLayer, + type Props as SymbolLayerProps, +} from './components/SymbolLayer'; export { default as RasterLayer } from './components/RasterLayer'; export { default as RasterParticleLayer } from './components/RasterParticleLayer'; export { default as HillshadeLayer } from './components/HillshadeLayer'; @@ -82,6 +89,8 @@ export { default as Logger, type LogLevel } from './utils/Logger'; export { requestAndroidLocationPermissions } from './requestAndroidLocationPermissions'; export { getAnnotationsLayerID } from './utils/getAnnotationsLayerID'; export { normalizeLanguageTag } from './utils/normalizeLanguageTag'; +export type { OnPressEvent } from './types/OnPressEvent'; +export type * from './utils/MapboxStyles'; export type { FillLayerStyleProps as FillLayerStyle, LineLayerStyleProps as LineLayerStyle, From c3964f7ad62fb88b6f89b68b8ac541b5ab3a3051 Mon Sep 17 00:00:00 2001 From: Rob Jacobson Date: Mon, 12 Oct 2026 17:24:35 -0700 Subject: [PATCH 2/2] fix(expo): preserve config plugin resolution in packed builds - Disable Bob's strict ESM packaging metadata so legacy Expo resolvers find the root config plugin - Exercise legacy and current Expo plugin loaders and native/web Metro resolution against fresh npm archives - Share packed-artifact fixtures with the published type-export regression suite - Exclude generated library output from the general source type check --- __tests__/fixtures/packPackage.js | 99 ++++++++++++++++++ __tests__/fixtures/packageResolution.js | 132 ++++++++++++++++++++++++ __tests__/packageResolution.test.js | 87 ++++++++++++++++ __tests__/typeExports.test.js | 86 ++------------- package.json | 3 +- tsconfig.json | 1 + yarn.lock | 120 ++++++++++++++++++++- 7 files changed, 450 insertions(+), 78 deletions(-) create mode 100644 __tests__/fixtures/packPackage.js create mode 100644 __tests__/fixtures/packageResolution.js create mode 100644 __tests__/packageResolution.test.js diff --git a/__tests__/fixtures/packPackage.js b/__tests__/fixtures/packPackage.js new file mode 100644 index 000000000..ec1886e0e --- /dev/null +++ b/__tests__/fixtures/packPackage.js @@ -0,0 +1,99 @@ +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { execFileSync } from 'child_process'; + +// Build outside the checkout, then test the files selected by npm rather than +// stale lib output or Jest's @rnmapbox/maps source alias. +export function packPackage(targets = ['module', 'typescript']) { + const projectRoot = path.resolve(__dirname, '../..'); + const consumerDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'rnmapbox-package-'), + ); + const packageDir = path.join(consumerDir, 'node_modules/@rnmapbox/maps'); + const buildDir = path.join(consumerDir, 'build'); + const env = { ...process.env }; + delete env.npm_execpath; + env.npm_config_cache = path.join(consumerDir, 'npm-cache'); + + try { + fs.mkdirSync(packageDir, { recursive: true }); + fs.mkdirSync(buildDir); + for (const directory of ['src', 'plugin']) { + fs.cpSync( + path.join(projectRoot, directory), + path.join(buildDir, directory), + { + recursive: true, + }, + ); + } + // Compile the plugin from source too; a checked-in build must not mask a + // missing or broken published plugin. + fs.rmSync(path.join(buildDir, 'plugin/build'), { + recursive: true, + force: true, + }); + for (const file of [ + 'package.json', + 'app.plugin.js', + '.npmignore', + 'tsconfig.json', + 'tsconfig.build.json', + ]) { + fs.copyFileSync(path.join(projectRoot, file), path.join(buildDir, file)); + } + fs.symlinkSync( + path.join(projectRoot, 'node_modules'), + path.join(buildDir, 'node_modules'), + 'junction', + ); + + execFileSync( + process.execPath, + [require.resolve('typescript/bin/tsc'), '--build', 'plugin'], + { cwd: buildDir, env, timeout: 60000 }, + ); + const bob = require.resolve('react-native-builder-bob/bin/bob'); + for (const target of targets) { + execFileSync(process.execPath, [bob, 'build', '--target', target], { + cwd: buildDir, + env, + timeout: 60000, + }); + } + // npm 10 runs prepare even with --ignore-scripts. The real build targets + // already ran above; prevent a second build (and npx/network access) only + // in this disposable fixture. Keep exports and file selection unchanged. + const manifestPath = path.join(buildDir, 'package.json'); + const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + delete manifest.scripts.prepare; + fs.writeFileSync(manifestPath, JSON.stringify(manifest)); + const packed = JSON.parse( + execFileSync( + 'npm', + [ + 'pack', + '--ignore-scripts', + '--offline', + '--json', + '--pack-destination', + consumerDir, + ], + { cwd: buildDir, env, timeout: 30000, encoding: 'utf8' }, + ), + ); + execFileSync('tar', [ + '-xzf', + path.join(consumerDir, packed[0].filename), + '--strip-components=1', + '-C', + packageDir, + ]); + + return { consumerDir, packageDir, projectRoot }; + } catch (error) { + fs.rmSync(consumerDir, { recursive: true, force: true }); + throw error; + } +} diff --git a/__tests__/fixtures/packageResolution.js b/__tests__/fixtures/packageResolution.js new file mode 100644 index 000000000..39adfefcc --- /dev/null +++ b/__tests__/fixtures/packageResolution.js @@ -0,0 +1,132 @@ +const fs = require('fs'); +const path = require('path'); +const { createRequire } = require('module'); + +const [consumerDir, projectRoot, mode, ...options] = process.argv.slice(2); +const projectRequire = createRequire(path.join(projectRoot, 'package.json')); +const consumerRequire = createRequire(path.join(consumerDir, 'index.js')); +const packageDir = path.dirname( + consumerRequire.resolve('@rnmapbox/maps/package.json'), +); + +function packageRelativePath(file) { + return path.relative(packageDir, file).split(path.sep).join('/'); +} + +if (mode === 'plugin') { + const [resolver, reference] = options; + const { resolveConfigPluginFunctionWithInfo } = projectRequire( + `${resolver}/build/utils/plugin-resolver`, + ); + const { plugin, pluginFile, isPluginFile } = + resolveConfigPluginFunctionWithInfo(consumerDir, reference); + const config = plugin({ name: 'package-test', slug: 'package-test' }); + process.stdout.write( + JSON.stringify({ + pluginFile: packageRelativePath(pluginFile), + isPluginFile, + mods: Object.keys(config.mods).sort(), + }), + ); +} else { + const [platform, sourceCondition] = options; + const { resolve } = projectRequire('metro-resolver'); + const { parse } = projectRequire('@babel/parser'); + const context = { + allowHaste: false, + assetExts: new Set(['png', 'css']), + customResolverOptions: {}, + disableHierarchicalLookup: false, + isESMImport: true, + mainFields: ['react-native', 'browser', 'main'], + nodeModulesPaths: [], + preferNativePlatform: platform !== 'web', + sourceExts: ['js', 'jsx', 'ts', 'tsx'], + unstable_enablePackageExports: true, + unstable_conditionNames: sourceCondition === 'source' ? ['source'] : [], + unstable_conditionsByPlatform: { + ios: ['react-native'], + android: ['react-native'], + web: ['browser'], + }, + unstable_logWarning(message) { + throw new Error(message); + }, + resolveAsset() { + return null; + }, + doesFileExist(file) { + return fs.existsSync(file) && fs.statSync(file).isFile(); + }, + fileSystemLookup(file) { + if (!fs.existsSync(file)) { + return { exists: false }; + } + return { + exists: true, + type: fs.statSync(file).isDirectory() ? 'd' : 'f', + realPath: fs.realpathSync(file), + }; + }, + getPackage(file) { + return fs.existsSync(file) + ? JSON.parse(fs.readFileSync(file, 'utf8')) + : null; + }, + getPackageForModule(file) { + let directory = + fs.existsSync(file) && fs.statSync(file).isDirectory() + ? file + : path.dirname(file); + while (directory !== path.dirname(directory)) { + const packageJson = context.getPackage( + path.join(directory, 'package.json'), + ); + if (packageJson) { + return { + rootPath: directory, + packageJson, + packageRelativePath: path.relative(directory, file), + }; + } + directory = path.dirname(directory); + } + return null; + }, + }; + function resolveFrom(originModulePath, specifier) { + return resolve({ ...context, originModulePath }, specifier, platform) + .filePath; + } + function dependency(file, specifier) { + const ast = parse(fs.readFileSync(file, 'utf8'), { + sourceType: 'module', + plugins: ['typescript', 'jsx'], + }); + const declaration = ast.program.body.find( + (node) => + node.source?.value.replace(/(?:\/index)?\.js$/, '') === specifier, + ); + if (!declaration) { + throw new Error(`Missing ${specifier} dependency in ${file}`); + } + return resolveFrom(file, declaration.source.value); + } + const entry = resolveFrom( + path.join(consumerDir, 'index.js'), + '@rnmapbox/maps', + ); + const mapbox = dependency(entry, './Mapbox'); + const implementation = dependency( + mapbox, + platform === 'web' ? './web' : './RNMBXModule', + ); + const components = platform === 'web' ? implementation : mapbox; + const camera = dependency(components, './components/Camera'); + const mapView = dependency(components, './components/MapView'); + process.stdout.write( + JSON.stringify( + [entry, mapbox, implementation, camera, mapView].map(packageRelativePath), + ), + ); +} diff --git a/__tests__/packageResolution.test.js b/__tests__/packageResolution.test.js new file mode 100644 index 000000000..454f3bb8f --- /dev/null +++ b/__tests__/packageResolution.test.js @@ -0,0 +1,87 @@ +import fs from 'fs'; +import path from 'path'; +import { execFileSync } from 'child_process'; + +import { packPackage } from './fixtures/packPackage'; + +describe('Packed package resolution', () => { + let consumerDir; + let projectRoot; + + beforeAll(() => { + ({ consumerDir, projectRoot } = packPackage(['module'])); + // Only supply the declared Expo peer. Runtime React Native must never be + // imported while loading the config plugin in Node. + fs.symlinkSync( + path.join(projectRoot, 'node_modules/expo'), + path.join(consumerDir, 'node_modules/expo'), + 'junction', + ); + }, 60000); + + afterAll(() => { + if (consumerDir) { + fs.rmSync(consumerDir, { recursive: true, force: true }); + } + }); + + function probe(...args) { + // Jest maps @rnmapbox/maps to src and transforms ESM. Use unmodified Node + // and Metro so neither behavior hides a published-artifact regression. + return JSON.parse( + execFileSync( + process.execPath, + [ + path.join(__dirname, 'fixtures/packageResolution.js'), + consumerDir, + projectRoot, + ...args, + ], + { encoding: 'utf8', timeout: 10000 }, + ), + ); + } + + // Expo Tools bundles 4.1.5, whose nearest-package.json lookup differs from + // the SDK resolver. Exercise both real implementations against the archive. + it.each(['expo-config-plugins-legacy', '@expo/config-plugins'])( + 'loads the package-name plugin using %s', + (resolver) => { + expect(probe('plugin', resolver, '@rnmapbox/maps')).toEqual({ + pluginFile: 'app.plugin.js', + isPluginFile: true, + mods: ['android', 'ios'], + }); + }, + ); + + it('keeps the explicit app.plugin.js export working', () => { + expect( + probe('plugin', '@expo/config-plugins', '@rnmapbox/maps/app.plugin.js'), + ).toEqual({ + pluginFile: 'app.plugin.js', + isPluginFile: true, + mods: ['android', 'ios'], + }); + }); + + it.each( + ['ios', 'android', 'web'].flatMap((platform) => + ['compiled', 'source'].map((condition) => [platform, condition]), + ), + )('resolves %s with the %s entry', (platform, condition) => { + const directory = condition === 'source' ? 'src' : 'lib/module'; + const extension = condition === 'source' ? 'ts' : 'js'; + const componentExtension = condition === 'source' ? 'tsx' : 'js'; + const web = platform === 'web'; + expect(probe('metro', platform, condition)).toEqual([ + `${directory}/index.${extension}`, + `${directory}/Mapbox${web ? '' : '.native'}.${extension}`, + web + ? `${directory}/web/index.${componentExtension}` + : `${directory}/RNMBXModule.${extension}`, + `${directory}/${web ? 'web/' : ''}components/Camera.${componentExtension}`, + `${directory}/${web ? 'web/' : ''}components/MapView.${componentExtension}`, + ]); + }); +}); diff --git a/__tests__/typeExports.test.js b/__tests__/typeExports.test.js index f422e2c73..930d9bed6 100644 --- a/__tests__/typeExports.test.js +++ b/__tests__/typeExports.test.js @@ -1,9 +1,10 @@ import fs from 'fs'; -import os from 'os'; import path from 'path'; import { execFileSync } from 'child_process'; import ts from 'typescript'; +import { packPackage } from './fixtures/packPackage'; + const legacyPaths = [ '@rnmapbox/maps/src/utils/MapboxStyles', '@rnmapbox/maps/src/components/SymbolLayer', @@ -70,74 +71,12 @@ describe('Published type exports', () => { let packageDir; beforeAll(() => { - consumerDir = fs.mkdtempSync( - path.join(os.tmpdir(), 'rnmapbox-type-exports-'), - ); - packageDir = path.join(consumerDir, 'node_modules/@rnmapbox/maps'); - fs.mkdirSync(packageDir, { recursive: true }); + const packed = packPackage(); + consumerDir = packed.consumerDir; + packageDir = packed.packageDir; + const { projectRoot } = packed; fs.writeFileSync(path.join(consumerDir, 'index.ts'), consumerSource); - const projectRoot = path.resolve(__dirname, '..'); - const buildDir = path.join(consumerDir, 'build'); - fs.mkdirSync(buildDir); - fs.cpSync(path.join(projectRoot, 'src'), path.join(buildDir, 'src'), { - recursive: true, - }); - for (const file of [ - 'package.json', - 'tsconfig.json', - 'tsconfig.build.json', - 'yarn.lock', - ]) { - fs.copyFileSync(path.join(projectRoot, file), path.join(buildDir, file)); - } - fs.mkdirSync(path.join(buildDir, 'example')); - fs.copyFileSync( - path.join(projectRoot, 'example/package.json'), - path.join(buildDir, 'example/package.json'), - ); - fs.writeFileSync( - path.join(buildDir, '.yarnrc.yml'), - 'nodeLinker: node-modules\n', - ); - fs.symlinkSync( - path.join(projectRoot, 'node_modules'), - path.join(buildDir, 'node_modules'), - 'junction', - ); - - // Use the real build targets and package file selection, including Bob's - // generated package metadata. Build outside the checkout to avoid stale - // output and interference with other checks. - const env = { ...process.env }; - delete env.npm_execpath; - const bob = require.resolve('react-native-builder-bob/bin/bob'); - for (const target of ['module', 'typescript']) { - execFileSync(process.execPath, [bob, 'build', '--target', target], { - cwd: buildDir, - env, - timeout: 60000, - }); - } - const archive = path.join(consumerDir, 'package.tgz'); - execFileSync( - process.execPath, - [ - path.join(projectRoot, '.yarn/releases/yarn-3.6.1.cjs'), - 'pack', - '--out', - archive, - ], - { cwd: buildDir, env, timeout: 30000 }, - ); - execFileSync('tar', [ - '-xzf', - archive, - '--strip-components=1', - '-C', - packageDir, - ]); - // Resolve React and GeoJSON types as a consumer would, rather than letting // missing declaration dependencies degrade to any under skipLibCheck. for (const dependency of [ @@ -177,16 +116,13 @@ describe('Published type exports', () => { }, ); - it('includes the generated package metadata and export targets', () => { + it('keeps the package root as the scope and includes export targets', () => { + // Legacy Expo Tools locates app.plugin.js beside the nearest package.json + // to the runtime entry. Bob's ESM metadata would hide the actual root. for (const output of ['module', 'typescript']) { expect( - JSON.parse( - fs.readFileSync( - path.join(packageDir, 'lib', output, 'package.json'), - 'utf8', - ), - ), - ).toEqual({ type: 'module' }); + fs.existsSync(path.join(packageDir, 'lib', output, 'package.json')), + ).toBe(false); } const manifest = JSON.parse( fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8'), diff --git a/package.json b/package.json index f414fd44d..c47146f21 100644 --- a/package.json +++ b/package.json @@ -120,6 +120,7 @@ "eslint-plugin-jest": "^27.9.0", "eslint-plugin-prettier": "^5.5.4", "expo": "^54.0.10", + "expo-config-plugins-legacy": "npm:@expo/config-plugins@4.1.5", "expo-module-scripts": "^5.0.7", "globals": "^16.4.0", "jest": "29.7.0", @@ -179,7 +180,7 @@ [ "module", { - "esm": true + "esm": false } ], [ diff --git a/tsconfig.json b/tsconfig.json index e080b4272..1fd6b734f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,6 +30,7 @@ "verbatimModuleSyntax": true }, "exclude": [ + "lib", "**/*.codepart.ts" ] } diff --git a/yarn.lock b/yarn.lock index 61933d2b9..25726f3e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2168,6 +2168,13 @@ __metadata: languageName: node linkType: hard +"@expo/config-types@npm:^45.0.0": + version: 45.0.0 + resolution: "@expo/config-types@npm:45.0.0" + checksum: 9b4866540654da61af0985ebfc975b3cb690625acf7742443a7e56263bf134b261e22719720982223407f8957d08a3646178c79f482861218882f0956d804021 + languageName: node + linkType: hard + "@expo/config-types@npm:^54.0.8": version: 54.0.8 resolution: "@expo/config-types@npm:54.0.8" @@ -2276,6 +2283,17 @@ __metadata: languageName: node linkType: hard +"@expo/json-file@npm:8.2.36": + version: 8.2.36 + resolution: "@expo/json-file@npm:8.2.36" + dependencies: + "@babel/code-frame": ~7.10.4 + json5: ^1.0.1 + write-file-atomic: ^2.3.0 + checksum: 37ce80b3472fef2a56136ebff5993d98ab4fbd45c4d7791ff47be80438dbeabd84bc699a401da0c314357ef65d8fff87a5a1241b3119db2d575878f9321bd1e7 + languageName: node + linkType: hard + "@expo/json-file@npm:^10.0.7, @expo/json-file@npm:~10.0.7": version: 10.0.7 resolution: "@expo/json-file@npm:10.0.7" @@ -2411,6 +2429,17 @@ __metadata: languageName: node linkType: hard +"@expo/plist@npm:0.0.18": + version: 0.0.18 + resolution: "@expo/plist@npm:0.0.18" + dependencies: + "@xmldom/xmldom": ~0.7.0 + base64-js: ^1.2.3 + xmlbuilder: ^14.0.0 + checksum: 42f5743fcd2a07b55a9f048d27cf0f273510ab35dde1f7030b22dc8c30ab2cfb65c6e68f8aa58fbcfa00177fdc7c9696d0004083c9a47c36fd4ac7fea27d6ccc + languageName: node + linkType: hard + "@expo/plist@npm:^0.4.7": version: 0.4.7 resolution: "@expo/plist@npm:0.4.7" @@ -4700,6 +4729,13 @@ __metadata: languageName: node linkType: hard +"@react-native/normalize-color@npm:^2.0.0": + version: 2.1.0 + resolution: "@react-native/normalize-color@npm:2.1.0" + checksum: 8ccbd40b3c7629f1dc97b3e9aadd95fd3507fcf2e37535a6299a70436ab891c34cbdc4240b07380553d6e85dd909e23d5773b5be1da2906b026312e0b0768838 + languageName: node + linkType: hard + "@react-native/normalize-colors@npm:0.81.1": version: 0.81.1 resolution: "@react-native/normalize-colors@npm:0.81.1" @@ -4981,6 +5017,7 @@ __metadata: eslint-plugin-jest: ^27.9.0 eslint-plugin-prettier: ^5.5.4 expo: ^54.0.10 + expo-config-plugins-legacy: "npm:@expo/config-plugins@4.1.5" expo-module-scripts: ^5.0.7 globals: ^16.4.0 jest: 29.7.0 @@ -6394,6 +6431,13 @@ __metadata: languageName: node linkType: hard +"@xmldom/xmldom@npm:~0.7.0": + version: 0.7.13 + resolution: "@xmldom/xmldom@npm:0.7.13" + checksum: b4054078530e5fa8ede9677425deff0fce6d965f4c477ca73f8490d8a089e60b8498a15560425a1335f5ff99ecb851ed2c734b0a9a879299a5694302f212f37a + languageName: node + linkType: hard + "abab@npm:^2.0.6": version: 2.0.6 resolution: "abab@npm:2.0.6" @@ -10263,6 +10307,29 @@ __metadata: languageName: node linkType: hard +"expo-config-plugins-legacy@npm:@expo/config-plugins@4.1.5": + version: 4.1.5 + resolution: "@expo/config-plugins@npm:4.1.5" + dependencies: + "@expo/config-types": ^45.0.0 + "@expo/json-file": 8.2.36 + "@expo/plist": 0.0.18 + "@expo/sdk-runtime-versions": ^1.0.0 + "@react-native/normalize-color": ^2.0.0 + chalk: ^4.1.2 + debug: ^4.3.1 + find-up: ~5.0.0 + getenv: ^1.0.0 + glob: 7.1.6 + resolve-from: ^5.0.0 + semver: ^7.3.5 + slash: ^3.0.0 + xcode: ^3.0.1 + xml2js: 0.4.23 + checksum: f631217251281b1e25949adbec175ef1362dd08d837ce676ed8350c1401b5764091ba100f76f42adb623e4cdcde5b270be77ad6606f978d419c07fd8c81b701c + languageName: node + linkType: hard + "expo-constants@npm:~18.0.9": version: 18.0.9 resolution: "expo-constants@npm:18.0.9" @@ -10653,7 +10720,7 @@ __metadata: languageName: node linkType: hard -"find-up@npm:^5.0.0": +"find-up@npm:^5.0.0, find-up@npm:~5.0.0": version: 5.0.0 resolution: "find-up@npm:5.0.0" dependencies: @@ -11040,6 +11107,13 @@ __metadata: languageName: node linkType: hard +"getenv@npm:^1.0.0": + version: 1.0.0 + resolution: "getenv@npm:1.0.0" + checksum: 19ae5cad603a1cf1bcb8fa3bed48e00d062eb0572a4404c02334b67f3b3499f238383082b064bb42515e9e25c2b08aef1a3e3d2b6852347721aa8b174825bd56 + languageName: node + linkType: hard + "getenv@npm:^2.0.0": version: 2.0.0 resolution: "getenv@npm:2.0.0" @@ -11147,6 +11221,20 @@ __metadata: languageName: node linkType: hard +"glob@npm:7.1.6": + version: 7.1.6 + resolution: "glob@npm:7.1.6" + dependencies: + fs.realpath: ^1.0.0 + inflight: ^1.0.4 + inherits: 2 + minimatch: ^3.0.4 + once: ^1.3.0 + path-is-absolute: ^1.0.0 + checksum: 351d549dd90553b87c2d3f90ce11aed9e1093c74130440e7ae0592e11bbcd2ce7f0ebb8ba6bfe63aaf9b62166a7f4c80cb84490ae5d78408bb2572bf7d4ee0a6 + languageName: node + linkType: hard + "glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.4.2": version: 10.4.5 resolution: "glob@npm:10.4.5" @@ -13879,7 +13967,7 @@ __metadata: languageName: node linkType: hard -"json5@npm:^1.0.2": +"json5@npm:^1.0.1, json5@npm:^1.0.2": version: 1.0.2 resolution: "json5@npm:1.0.2" dependencies: @@ -22207,6 +22295,17 @@ __metadata: languageName: node linkType: hard +"write-file-atomic@npm:^2.3.0": + version: 2.4.3 + resolution: "write-file-atomic@npm:2.4.3" + dependencies: + graceful-fs: ^4.1.11 + imurmurhash: ^0.1.4 + signal-exit: ^3.0.2 + checksum: 2db81f92ae974fd87ab4a5e7932feacaca626679a7c98fcc73ad8fcea5a1950eab32fa831f79e9391ac99b562ca091ad49be37a79045bd65f595efbb8f4596ae + languageName: node + linkType: hard + "write-file-atomic@npm:^4.0.2": version: 4.0.2 resolution: "write-file-atomic@npm:4.0.2" @@ -22292,6 +22391,16 @@ __metadata: languageName: node linkType: hard +"xml2js@npm:0.4.23": + version: 0.4.23 + resolution: "xml2js@npm:0.4.23" + dependencies: + sax: ">=0.6.0" + xmlbuilder: ~11.0.0 + checksum: ca0cf2dfbf6deeaae878a891c8fbc0db6fd04398087084edf143cdc83d0509ad0fe199b890f62f39c4415cf60268a27a6aed0d343f0658f8779bd7add690fa98 + languageName: node + linkType: hard + "xml2js@npm:0.6.0": version: 0.6.0 resolution: "xml2js@npm:0.6.0" @@ -22302,6 +22411,13 @@ __metadata: languageName: node linkType: hard +"xmlbuilder@npm:^14.0.0": + version: 14.0.0 + resolution: "xmlbuilder@npm:14.0.0" + checksum: 9e93d3c73957dbb21acde63afa5d241b19057bdbdca9d53534d8351e70f1d5c9db154e3ca19bd3e9ea84c082539ab6e7845591c8778a663e8b5d3470d5427a8b + languageName: node + linkType: hard + "xmlbuilder@npm:^15.1.1": version: 15.1.1 resolution: "xmlbuilder@npm:15.1.1"