typecheck.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. #!/usr/bin/env node
  2. // Type-checks the TS files of packages/app (plus each app's own TS) once per platform,
  3. // resolving `.web` / `.native` variants the way webpack and Metro do.
  4. //
  5. // yarn typecheck # web + native
  6. // yarn typecheck web # one platform
  7. //
  8. // A base file shadowed by the platform variant (e.g. video.tsx next to video.web.tsx)
  9. // never ships on that platform, so it is left out of that platform's run. JS files are
  10. // not checked, only read for the types TS files import from them. `.ios` / `.android`
  11. // files are added to the native run as extra roots: their own code is checked, but
  12. // imports still resolve through `.native` / base files.
  13. const fs = require('fs');
  14. const path = require('path');
  15. const { spawnSync } = require('child_process');
  16. const root = path.resolve(__dirname, '..');
  17. const ts = require('typescript/package.json');
  18. const tscBin = require.resolve('typescript/bin/tsc');
  19. const PLATFORM_SUFFIXES = ['.web', '.native', '.ios', '.android'];
  20. const SOURCE_EXTS = ['.ts', '.tsx', '.js', '.jsx'];
  21. const SKIP_DIRS = new Set(['node_modules', '.next', 'out', 'dist', 'build']);
  22. const PLATFORMS = {
  23. web: {
  24. extends: 'apps/next/tsconfig.json',
  25. suffix: '.web',
  26. dirs: ['packages/app', 'apps/next'],
  27. ambient: ['apps/next'],
  28. compilerOptions: { plugins: [], incremental: false },
  29. },
  30. native: {
  31. extends: 'tsconfig.json',
  32. suffix: '.native',
  33. extraSuffixes: ['.ios', '.android'],
  34. dirs: ['packages/app', 'apps/expo/app'],
  35. ambient: ['apps/expo'],
  36. compilerOptions: { strict: true },
  37. },
  38. };
  39. function walk(dir, out = []) {
  40. for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
  41. if (entry.name.startsWith('.') || SKIP_DIRS.has(entry.name)) continue;
  42. const full = path.join(dir, entry.name);
  43. if (entry.isDirectory()) walk(full, out);
  44. else out.push(full);
  45. }
  46. return out;
  47. }
  48. /** `foo.web.tsx` → { base: 'foo', platform: '.web' }; `foo.tsx` → { base: 'foo', platform: '' }. */
  49. function parse(file) {
  50. const ext = path.extname(file);
  51. const stem = file.slice(0, -ext.length);
  52. const platform = PLATFORM_SUFFIXES.find((s) => stem.endsWith(s)) || '';
  53. return { ext, base: stem.slice(0, stem.length - platform.length), platform };
  54. }
  55. function filesFor({ suffix, extraSuffixes = [], dirs, ambient }) {
  56. const files = [];
  57. for (const dir of dirs) {
  58. for (const file of walk(path.join(root, dir))) {
  59. if (file.endsWith('.d.ts')) { files.push(file); continue; }
  60. const { ext, base, platform } = parse(file);
  61. if (ext !== '.ts' && ext !== '.tsx') continue;
  62. if (platform && platform !== suffix && !extraSuffixes.includes(platform)) continue;
  63. const shadowed = !platform && SOURCE_EXTS.some((e) => fs.existsSync(base + suffix + e));
  64. if (!shadowed) files.push(file);
  65. }
  66. }
  67. for (const dir of ambient) {
  68. for (const name of fs.readdirSync(path.join(root, dir))) {
  69. if (name.endsWith('.d.ts')) files.push(path.join(root, dir, name));
  70. }
  71. }
  72. return [...new Set(files)];
  73. }
  74. function run(name) {
  75. const platform = PLATFORMS[name];
  76. const cacheDir = path.join(root, 'node_modules', '.cache', 'neo-typecheck');
  77. fs.mkdirSync(cacheDir, { recursive: true });
  78. const configPath = path.join(cacheDir, `tsconfig.${name}.json`);
  79. const config = {
  80. extends: path.relative(cacheDir, path.join(root, platform.extends)).replace(/\\/g, '/'),
  81. compilerOptions: {
  82. noEmit: true,
  83. moduleSuffixes: [platform.suffix, ''],
  84. // apps/next still sets `baseUrl`, deprecated since TS 6.
  85. ...(parseInt(ts.version, 10) >= 6 ? { ignoreDeprecations: '6.0' } : {}),
  86. ...platform.compilerOptions,
  87. },
  88. include: [],
  89. files: filesFor(platform),
  90. };
  91. fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
  92. console.log(`\n== typecheck ${name} (${config.files.length} files, TypeScript ${ts.version})`);
  93. const result = spawnSync(process.execPath, [tscBin, '-p', configPath, '--pretty'], { stdio: 'inherit' });
  94. return result.status === 0;
  95. }
  96. const requested = process.argv.slice(2);
  97. const names = requested.length ? requested : Object.keys(PLATFORMS);
  98. const unknown = names.filter((n) => !PLATFORMS[n]);
  99. if (unknown.length) {
  100. console.error(`Unknown platform: ${unknown.join(', ')} (expected: ${Object.keys(PLATFORMS).join(', ')})`);
  101. process.exit(2);
  102. }
  103. const failed = names.filter((n) => !run(n));
  104. if (failed.length) {
  105. console.error(`\ntypecheck failed: ${failed.join(', ')}`);
  106. process.exit(1);
  107. }
  108. console.log('\ntypecheck passed');