index.mjs 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. #!/usr/bin/env node
  2. /**
  3. * This script can be used to initiate the transition for a plugin from ESM source to
  4. * TS source. It will rename the files, update the imports, and add a `tsconfig.json`.
  5. */
  6. import { opendir, readFile, open, writeFile, rm } from 'node:fs/promises'
  7. import { createRequire } from 'node:module'
  8. import { argv } from 'node:process'
  9. import { basename, extname, join } from 'node:path'
  10. import { existsSync } from 'node:fs'
  11. const packageRoot = new URL(`../../packages/${argv[2]}/`, import.meta.url)
  12. let dir
  13. try {
  14. dir = await opendir(new URL('./src/', packageRoot), { recursive: true })
  15. } catch (cause) {
  16. throw new Error(`Unable to find package "${argv[2]}"`, { cause })
  17. }
  18. const packageJSON = JSON.parse(
  19. await readFile(new URL('./package.json', packageRoot), 'utf-8'),
  20. )
  21. if (packageJSON.type !== 'module') {
  22. throw new Error('Cannot convert non-ESM package to TS')
  23. }
  24. const uppyDeps = Object.keys(packageJSON.dependencies || {})
  25. .concat(Object.keys(packageJSON.peerDependencies || {}))
  26. .concat(Object.keys(packageJSON.devDependencies || {}))
  27. .filter((pkg) => pkg.startsWith('@uppy/'))
  28. const paths = Object.fromEntries(
  29. (function* generatePaths() {
  30. const require = createRequire(packageRoot)
  31. for (const pkg of uppyDeps) {
  32. const nickname = pkg.slice('@uppy/'.length)
  33. // eslint-disable-next-line import/no-dynamic-require
  34. const pkgJson = require(`../${nickname}/package.json`)
  35. if (pkgJson.exports?.['.']) {
  36. yield [pkg, [`../${nickname}/${pkgJson.exports['.']}`]]
  37. } else if (pkgJson.main) {
  38. yield [pkg, [`../${nickname}/${pkgJson.main}`]]
  39. }
  40. yield [`${pkg}/*`, [`../${nickname}/*`]]
  41. }
  42. })(),
  43. )
  44. const references = uppyDeps.map((pkg) => ({
  45. path: `../${pkg.slice('@uppy/'.length)}/tsconfig.build.json`,
  46. }))
  47. const depsNotYetConvertedToTS = references.filter(
  48. (ref) => !existsSync(new URL(ref.path, packageRoot)),
  49. )
  50. if (depsNotYetConvertedToTS.length) {
  51. // We need to first convert the dependencies, otherwise we won't be working with the correct types.
  52. throw new Error('Some dependencies have not yet been converted to TS', {
  53. cause: depsNotYetConvertedToTS.map((ref) =>
  54. ref.path.replace(/^\.\./, '@uppy'),
  55. ),
  56. })
  57. }
  58. let tsConfig
  59. try {
  60. tsConfig = await open(new URL('./tsconfig.json', packageRoot), 'wx')
  61. } catch (cause) {
  62. throw new Error('It seems this package has already been transitioned to TS', {
  63. cause,
  64. })
  65. }
  66. for await (const dirent of dir) {
  67. if (!dirent.isDirectory()) {
  68. const { name } = dirent
  69. const ext = extname(name)
  70. if (ext !== '.js' && ext !== '.jsx') continue // eslint-disable-line no-continue
  71. const filePath =
  72. basename(dirent.path) === name
  73. ? dirent.path // Some versions of Node.js give the full path as dirent.path.
  74. : join(dirent.path, name) // Others supply only the path to the parent.
  75. await writeFile(
  76. `${filePath.slice(0, -ext.length)}${ext.replace('js', 'ts')}`,
  77. (await readFile(filePath, 'utf-8'))
  78. .replace(
  79. // The following regex aims to capture all imports and reexports of local .js(x) files to replace it to .ts(x)
  80. // It's far from perfect and will have false positives and false negatives.
  81. /((?:^|\n)(?:import(?:\s+\w+\s+from)?|export\s*\*\s*from|(?:import|export)\s*(?:\{[^}]*\}|\*\s*as\s+\w+\s)\s*from)\s*["']\.\.?\/[^'"]+\.)js(x?["'])/g, // eslint-disable-line max-len
  82. '$1ts$2',
  83. )
  84. .replace(
  85. // The following regex aims to capture all local package.json imports.
  86. /\nimport \w+ from ['"]..\/([^'"]+\/)*package.json['"]\n/g,
  87. (originalImport) =>
  88. `// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n` +
  89. `// @ts-ignore We don't want TS to generate types for the package.json${originalImport}`,
  90. ),
  91. )
  92. await rm(filePath)
  93. }
  94. }
  95. await tsConfig.writeFile(
  96. `${JSON.stringify(
  97. {
  98. extends: '../../../tsconfig.shared',
  99. compilerOptions: {
  100. emitDeclarationOnly: false,
  101. noEmit: true,
  102. paths,
  103. },
  104. include: ['./package.json', './src/**/*.*'],
  105. references,
  106. },
  107. undefined,
  108. 2,
  109. )}\n`,
  110. )
  111. await tsConfig.close()
  112. await writeFile(
  113. new URL('./tsconfig.build.json', packageRoot),
  114. `${JSON.stringify(
  115. {
  116. extends: '../../../tsconfig.shared',
  117. compilerOptions: {
  118. noImplicitAny: false,
  119. outDir: './lib',
  120. paths,
  121. resolveJsonModule: false,
  122. rootDir: './src',
  123. skipLibCheck: true,
  124. },
  125. include: ['./src/**/*.*'],
  126. exclude: ['./src/**/*.test.ts'],
  127. references,
  128. },
  129. undefined,
  130. 2,
  131. )}\n`,
  132. )
  133. console.log('Done')