index.mjs 4.7 KB

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