Skip to content

lint(types): integrate arethetypeswrong for local and CI verification#9098

Open
mikehardy wants to merge 2 commits into
mainfrom
arethetypeswrong
Open

lint(types): integrate arethetypeswrong for local and CI verification#9098
mikehardy wants to merge 2 commits into
mainfrom
arethetypeswrong

Conversation

@mikehardy

Copy link
Copy Markdown
Collaborator

Description

Adds scoped published-types verification with @arethetypeswrong/cli for local use (yarn attw:check) and CI (new attw job in linting.yml).

Scope follows our consumer matrix (Types-AD-1..4 in okf-bundle/testing/architecture-decisions.md):

  • Primary gate remains yarn tsc:compile:consumer (moduleResolution: bundler).
  • attw uses .attw.json (esm-only profile; ignore internal-resolution-error and cjs-resolves-to-esm) — Node10/Node16-CJS modes and extensionless .d.ts relative imports are out of scope for RN/Metro consumers.
  • Expo config plugins get a packed-consumer smoke test (npm pack → temp install → require('<pkg>/app.plugin.js')), not attw exclusion.

Companion commit refactors Expo plugins: plugin/src/app.plugin.ts compiles to plugin/build/app.plugin.{js,d.ts}; package root keeps a one-line app.plugin.js shim; exports wire ./app.plugin and ./app.plugin.js types to the built entry.

Related issues

N/A — proactive types hygiene / CI gate.

Release Summary

No user-facing runtime changes. Published Expo plugin type exports and CI type-checking coverage improved.

Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
    • Yes
  • My change supports the following platforms;
    • Android
    • iOS
    • Other (macOS, web) — tooling / published package types / Expo config plugins
  • My change includes tests;
    • e2e tests added or updated in packages/**/e2e
    • jest tests added or updated in packages/**/__tests__
  • I have updated TypeScript types that are affected by my change.
  • This is a breaking change;
    • Yes
    • No

Test Plan

  • yarn attw:check — all 19 published packages pass scoped attw; Expo plugin consumer smoke passes
  • yarn lerna run build:plugin --scope '@react-native-firebase/{app,analytics,auth,messaging,crashlytics,perf,app-check,app-distribution}' before attw (plugin/build artifacts)

Think react-native-firebase is great? Please consider supporting the project with any of the below:

Add plugin/src/app.plugin.ts (export =) compiled to plugin/build/ for Expo
entry types and runtime. Keep a one-line package-root app.plugin.js shim for
Expo filesystem resolution; wire exports for ./app.plugin and ./app.plugin.js.
@cursor

cursor Bot commented Jul 10, 2026

Copy link
Copy Markdown

Bugbot is not enabled for this team, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a robust type-checking gate using attw to ensure published package types align with the project's consumer matrix. It also standardizes Expo config plugin structures, ensuring they are correctly exported and smoke-tested during the CI process to prevent regressions in library consumption.

Highlights

  • Tooling Integration: Integrated @arethetypeswrong/cli (attw) to provide automated verification of published package types for both local development and CI pipelines.
  • Expo Plugin Refactor: Refactored Expo config plugins to improve type exports and added a smoke test suite to ensure runtime compatibility during CI.
  • Documentation: Added a new Architecture Decision Record (ADR) to document the type-checking strategy, consumer matrix, and CI gates.
Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/linting.yml
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a validation script to check published package types using @arethetypeswrong/cli (attw) and smoke-tests Expo config plugins, alongside updating package exports and documentation. The reviewer feedback is highly actionable and identifies several key improvements for the validation script, including refactoring shell executions from execSync to spawnSync to prevent OS-specific parsing issues, updating yarn pack commands to use Yarn v4 compatible flags (--out instead of --filename), removing a non-existent @expo/config-plugins version specifier that would cause CI failures, and filtering out private packages from the check.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +126 to +157
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'attw-'));
const tgz = path.join(tmpDir, `${dirName}.tgz`);
const jsonFile = path.join(tmpDir, `${dirName}.json`);

const pack = spawnSync('yarn', ['pack', '--filename', tgz], {
cwd: packageDir,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
if (pack.status !== 0) {
throw new Error(`yarn pack failed for ${dirName}: ${pack.stderr || pack.stdout}`);
}

let exitCode = 0;
try {
execSync(
`"${attwBin}" --config-path "${attwConfigPath}" --format json --no-color --no-emoji "${tgz}" > "${jsonFile}"`,
{ cwd: tmpDir, stdio: ['ignore', 'pipe', 'pipe'] },
);
} catch (error) {
const status =
typeof error === 'object' && error !== null && 'status' in error
? Number((error as { status?: number }).status)
: 1;
exitCode = Number.isFinite(status) ? status : 1;
}

if (!fs.existsSync(jsonFile) || !fs.readFileSync(jsonFile, 'utf8').trim()) {
throw new Error(`attw produced no output for ${dirName}`);
}

const data = JSON.parse(fs.readFileSync(jsonFile, 'utf8')) as AttwResult;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Refactor runAttw to use spawnSync with arguments instead of execSync with manual string interpolation and shell redirection. This avoids shell execution overhead, prevents potential command parsing issues across different OS platforms (like Windows), and eliminates the need to write and read a temporary JSON file. Additionally, this fixes the yarn pack option from --filename (Yarn v1) to --out (Yarn v4).

  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'attw-'));
  const tgz = path.join(tmpDir, `${dirName}.tgz`);

  const pack = spawnSync('yarn', ['pack', '--out', tgz], {
    cwd: packageDir,
    encoding: 'utf8',
    stdio: ['ignore', 'pipe', 'pipe'],
  });
  if (pack.status !== 0) {
    throw new Error(`yarn pack failed for ${dirName}: ${pack.stderr || pack.stdout}`);
  }

  const result = spawnSync(
    attwBin,
    ['--config-path', attwConfigPath, '--format', 'json', '--no-color', '--no-emoji', tgz],
    { cwd: tmpDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], shell: true }
  );

  const exitCode = result.status ?? 1;
  const stdout = result.stdout?.trim();

  try {
    fs.unlinkSync(tgz);
    fs.rmSync(tmpDir, { recursive: true, force: true });
  } catch {
    // ignore cleanup errors
  }

  if (!stdout) {
    throw new Error(`attw produced no output for ${dirName}: ${result.stderr || ''}`);
  }

  const data = JSON.parse(stdout) as AttwResult;

@mikehardy mikehardy Jul 10, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in b60d54172 (amended into lint(types)).

Accepted: runAttw now uses spawnSync with argument arrays for both yarn pack and attw — no shell, no string interpolation.

Partially declined (temp JSON file): We kept file-based capture rather than parsing stdout directly. spawnSync now writes attw output straight to a file descriptor (stdio: ['ignore', outFd, 'pipe']), which avoids shell redirection and guarantees the full JSON report is captured regardless of size (no maxBuffer truncation). Truncation safety is more important than eliminating the temp file here.

Also applied: yarn pack --out (Yarn v4 flag).

Comment thread .github/scripts/attw/src/index.ts Outdated
}

function packPackage(packageDir: string, outputPath: string): void {
const pack = spawnSync('yarn', ['pack', '--filename', outputPath], {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In Yarn v4 (Berry), the yarn pack command uses --out or -o to specify the output filename instead of --filename (which was used in Yarn v1). Using --filename will result in a CLI usage error in Yarn v4.

Suggested change
const pack = spawnSync('yarn', ['pack', '--filename', outputPath], {
const pack = spawnSync('yarn', ['pack', '--out', outputPath], {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in b60d54172 (amended into lint(types)).

Accepted: packPackage now uses yarn pack --out (Yarn v4) instead of --filename.

Comment thread .github/scripts/attw/src/index.ts Outdated
Comment on lines +284 to +321
execSync('npm init -y', { cwd: installDir, stdio: 'ignore' });

const installArgs = [
'install',
'--silent',
'--no-fund',
'--no-audit',
appTgz,
pkgTgz,
'@expo/config-plugins@54',
];
try {
execSync(`npm ${installArgs.join(' ')}`, {
cwd: installDir,
stdio: ['ignore', 'pipe', 'pipe'],
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
failures.push({
package: JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')).name,
detail: `npm install failed: ${message}`,
});
continue;
}

const pkgName = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')).name as string;
try {
execSync(
`node -e "const plugin = require('${pkgName}/app.plugin.js'); if (typeof plugin !== 'function' && typeof plugin.default !== 'function') { process.exit(2); }"`,
{ cwd: installDir, stdio: ['ignore', 'pipe', 'pipe'] },
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
failures.push({
package: pkgName,
detail: `require('${pkgName}/app.plugin.js') failed: ${message}`,
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Refactor checkExpoPlugins to use spawnSync instead of execSync to avoid shell execution and potential command parsing issues. Also, remove the hardcoded @expo/config-plugins@54 version specifier, as version 54 does not exist on npm and will cause the installation to fail in CI. Installing @expo/config-plugins without a version specifier or with a valid range is safer and more robust.

      spawnSync('npm', ['init', '-y'], { cwd: installDir, stdio: 'ignore', shell: true });

      const installArgs = [
        'install',
        '--silent',
        '--no-fund',
        '--no-audit',
        appTgz,
        pkgTgz,
        '@expo/config-plugins',
      ];
      const installResult = spawnSync('npm', installArgs, {
        cwd: installDir,
        stdio: ['ignore', 'pipe', 'pipe'],
        shell: true,
      });
      if (installResult.status !== 0) {
        failures.push({
          package: JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')).name,
          detail: `npm install failed: ${installResult.stderr?.toString() || 'unknown error'}`,
        });
        continue;
      }

      const pkgName = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')).name as string;
      const runResult = spawnSync(
        'node',
        [
          '-e',
          `const plugin = require('${pkgName}/app.plugin.js'); if (typeof plugin !== 'function' && typeof plugin.default !== 'function') { process.exit(2); }`,
        ],
        { cwd: installDir, stdio: ['ignore', 'pipe', 'pipe'] }
      );
      if (runResult.status !== 0) {
        failures.push({
          package: pkgName,
          detail: `require('${pkgName}/app.plugin.js') failed: ${runResult.stderr?.toString() || 'unknown error'}`,
        });
      }

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in b60d54172 (amended into lint(types)).

Accepted: checkExpoPlugins refactored to spawnSync for npm init, npm install, and the node -e require check — no shell execution.

Accepted (unpinned): @expo/config-plugins@54 removed; smoke test now installs @expo/config-plugins unpinned so we track the latest release. Expo consumers upgrade quickly, and this acts as an early-warning canary for breaking config-plugin API changes. Verified locally: smoke passes against current latest (57.x).

Note: @expo/config-plugins@54 does exist on npm (54.0.0–54.0.4) and CI was already green with the pin — the unpinned change is intentional drift detection, not a fix for a missing version.

Comment on lines +118 to +123
function listPublishedPackages(): string[] {
return fs
.readdirSync(packagesDir)
.filter(dir => fs.existsSync(path.join(packagesDir, dir, 'package.json')))
.sort();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Filter out private packages from the list of published packages. Private packages (marked with "private": true in package.json) are not published to npm, so running attw on them is unnecessary and may fail if they are not configured for publishing.

function listPublishedPackages(): string[] {
  return fs
    .readdirSync(packagesDir)
    .filter(dir => {
      const pkgJsonPath = path.join(packagesDir, dir, 'package.json');
      if (!fs.existsSync(pkgJsonPath)) return false;
      try {
        const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
        return pkg.private !== true;
      } catch {
        return false;
      }
    })
    .sort();
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in b60d54172 (amended into lint(types)).

Accepted: listPublishedPackages() now skips packages with "private": true in package.json. None of the current packages/* are private, but this is cheap future-proofing.

@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 53.13%. Comparing base (1bad118) to head (b60d541).

❗ There is a different number of reports uploaded between BASE (1bad118) and HEAD (b60d541). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (1bad118) HEAD (b60d541)
jest 1 0
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #9098      +/-   ##
============================================
- Coverage     58.66%   53.13%   -5.52%     
+ Complexity     1617     1616       -1     
============================================
  Files           503      428      -75     
  Lines         39180    33646    -5534     
  Branches       5807     4636    -1171     
============================================
- Hits          22981    17875    -5106     
+ Misses        14800    14320     -480     
- Partials       1399     1451      +52     
Flag Coverage Δ
android-native 53.56% <ø> (-<0.01%) ⬇️
e2e-ts-android 49.98% <ø> (-0.02%) ⬇️
e2e-ts-ios 53.88% <ø> (-0.07%) ⬇️
e2e-ts-macos 41.49% <ø> (ø)
ios-native 53.88% <ø> (-0.07%) ⬇️
jest ?

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Add yarn attw:check (scoped via .attw.json esm-only profile, ignoring
node16-esm internal-resolution and cjs-resolves-to-esm per Types-AD-2) plus
Expo plugin consumer smoke. Document consumer-matrix decisions in OKF
architecture-decisions.md (Types-AD-1..4).
@mikehardy

Copy link
Copy Markdown
Collaborator Author

@gemini-code-assist — all review feedback addressed in b60d54172 (amended into lint(types)). Summary:

Feedback Action
spawnSync over execSync (runAttw, checkExpoPlugins) ✅ Accepted — all child processes now use spawnSync with argument arrays, no shell
yarn pack --out (Yarn v4) ✅ Accepted — both pack call sites
Remove @expo/config-plugins@54 pin ✅ Accepted — unpinned intentionally as early-warning canary for Expo API drift; smoke passes against latest (57.x). Note: v54 does exist on npm — this is drift detection, not a missing-version fix
Filter private packages ✅ Accepted — listPublishedPackages() skips "private": true
Drop temp JSON file / parse stdout directly ❌ Declined — kept file-descriptor capture (stdio: ['ignore', outFd, 'pipe']) to guarantee no maxBuffer truncation on large attw reports

Verified: yarn attw:check passes locally with all changes.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Thanks for the update, Mike. The changes look solid. Your reasoning for keeping the file-descriptor capture for the attw output is spot on—it's a much more robust approach for large reports than relying on stdout buffers, which avoids the maxBuffer truncation risk. I've noted the verification of the Expo plugin smoke tests. Everything looks good to go.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant