Repository: graphql/graphql-js
Pull request #3741 (2022-09-19 15:13:23)

build-npm/deno: use TS to find all *.ts files
Motivation: Remove custom code for finding *.ts files and reuse code from typescript library

github-actions[bot]: Hi @IvanGoncharov, I'm @github-actions bot happy to help you with this PR 👋

<details>
<summary> Supported commands </summary>

Please post this commands in separate comments and only one per comment:
* `@github-actions run-benchmark` - Run benchmark comparing base and merge commits for this PR
* `@github-actions publish-pr-on-npm` - Build package from this PR and publish it on NPM
</details>

netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *compassionate-pike-271cb3* processing.


|  Name | Link |
|---------------------------------|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | be345af99b2c8664f63c4ab17573505604797831 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/compassionate-pike-271cb3/deploys/632887150801840008f94f44 |

Patch:
diff --git a/resources/build-deno.ts b/resources/build-deno.ts
index 04424624..c5da8ce7 100644
--- a/resources/build-deno.ts
+++ b/resources/build-deno.ts
@@ -5,36 +5,34 @@ import ts from 'typescript';
 
 import { changeExtensionInImportPaths } from './change-extension-in-import-paths.js';
 import { inlineInvariant } from './inline-invariant.js';
-import { readdirRecursive, showDirStats, writeGeneratedFile } from './utils.js';
+import { readTSConfig, showDirStats, writeGeneratedFile } from './utils.js';
 
 fs.rmSync('./denoDist', { recursive: true, force: true });
 fs.mkdirSync('./denoDist');
 
-const srcFiles = readdirRecursive('./src', { ignoreDir: /^__.*__$/ });
-for (const filepath of srcFiles) {
-  if (filepath.endsWith('.ts')) {
-    const srcPath = path.join('./src', filepath);
-
-    const sourceFile = ts.createSourceFile(
-      srcPath,
-      fs.readFileSync(srcPath, 'utf-8'),
-      ts.ScriptTarget.Latest,
-    );
-
-    const transformed = ts.transform(sourceFile, [
-      changeExtensionInImportPaths({ extension: '.ts' }),
-      inlineInvariant,
-    ]);
-    const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
-    const newContent = printer.printBundle(
-      ts.factory.createBundle(transformed.transformed),
-    );
-
-    transformed.dispose();
-
-    const destPath = path.join('./denoDist', filepath);
-    writeGeneratedFile(destPath, newContent);
+const tsProgram = ts.createProgram(['src/index.ts'], readTSConfig());
+for (const sourceFile of tsProgram.getSourceFiles()) {
+  if (
+    tsProgram.isSourceFileFromExternalLibrary(sourceFile) ||
+    tsProgram.isSourceFileDefaultLibrary(sourceFile)
+  ) {
+    continue;
   }
+
+  const transformed = ts.transform(sourceFile, [
+    changeExtensionInImportPaths({ extension: '.ts' }),
+    inlineInvariant,
+  ]);
+  const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
+  const newContent = printer.printBundle(
+    ts.factory.createBundle(transformed.transformed),
+  );
+
+  transformed.dispose();
+
+  const filepath = path.relative('./src', sourceFile.fileName);
+  const destPath = path.join('./denoDist', filepath);
+  writeGeneratedFile(destPath, newContent);
 }
 
 fs.copyFileSync('./LICENSE', './denoDist/LICENSE');
diff --git a/resources/build-npm.ts b/resources/build-npm.ts
index 3fbbbd65..aec7d99f 100644
--- a/resources/build-npm.ts
+++ b/resources/build-npm.ts
@@ -6,70 +6,24 @@ import ts from 'typescript';
 
 import { inlineInvariant } from './inline-invariant.js';
 import {
-  localRepoPath,
-  readdirRecursive,
   readPackageJSON,
+  readTSConfig,
   showDirStats,
   writeGeneratedFile,
 } from './utils.js';
 
-fs.rmSync('./npmDist', { recursive: true, force: true });
-fs.mkdirSync('./npmDist');
-
-// Based on https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API#getting-the-dts-from-a-javascript-file
-const tsConfigPath = localRepoPath('tsconfig.json');
-const { config: tsConfig, error: tsConfigError } = ts.parseConfigFileTextToJson(
-  tsConfigPath,
-  fs.readFileSync(tsConfigPath, 'utf-8'),
-);
-assert(tsConfigError === undefined, 'Fail to parse config: ' + tsConfigError);
-assert(
-  tsConfig.compilerOptions,
-  '"tsconfig.json" should have `compilerOptions`',
-);
-
-const { options: tsOptions, errors: tsOptionsErrors } =
-  ts.convertCompilerOptionsFromJson(
-    {
-      ...tsConfig.compilerOptions,
-      module: 'es2020',
-      noEmit: false,
-      declaration: true,
-      declarationDir: './npmDist',
-      outDir: './npmDist',
-    },
-    process.cwd(),
-  );
-
-assert(
-  tsOptionsErrors.length === 0,
-  'Fail to parse options: ' + tsOptionsErrors,
-);
-
-const tsHost = ts.createCompilerHost(tsOptions);
-tsHost.writeFile = writeGeneratedFile;
-
-const tsProgram = ts.createProgram(['src/index.ts'], tsOptions, tsHost);
-const tsResult = tsProgram.emit(undefined, undefined, undefined, undefined, {
-  after: [inlineInvariant],
-});
-assert(
-  !tsResult.emitSkipped,
-  'Fail to generate `*.d.ts` files, please run `npm run check`',
-);
+buildPackage();
+showDirStats('./npmDist');
 
-fs.copyFileSync('./LICENSE', './npmDist/LICENSE');
-fs.copyFileSync('./README.md', './npmDist/README.md');
+function buildPackage() {
+  fs.rmSync('./npmDist', { recursive: true, force: true });
+  fs.mkdirSync('./npmDist');
 
-// Should be done as the last step so only valid packages can be published
-writeGeneratedFile(
-  './npmDist/package.json',
-  JSON.stringify(buildPackageJSON()),
-);
+  fs.copyFileSync('./LICENSE', './npmDist/LICENSE');
+  fs.copyFileSync('./README.md', './npmDist/README.md');
 
-showDirStats('./npmDist');
+  const { emittedTSFiles } = emitTSFiles('./npmDist');
 
-function buildPackageJSON() {
   const packageJSON = readPackageJSON();
 
   delete packageJSON.private;
@@ -97,10 +51,10 @@ function buildPackageJSON() {
 
   packageJSON.exports = {};
 
-  for (const filepath of readdirRecursive('./src', { ignoreDir: /^__.*__$/ })) {
-    if (path.basename(filepath) === 'index.ts') {
-      const key = path.dirname(filepath);
-      packageJSON.exports[key] = filepath.replace(/\.ts$/, '.js');
+  for (const filepath of emittedTSFiles) {
+    if (path.basename(filepath) === 'index.js') {
+      const relativePath = './' + path.relative('./npmDist', filepath);
+      packageJSON.exports[path.dirname(relativePath)] = relativePath;
     }
   }
 
@@ -136,5 +90,37 @@ function buildPackageJSON() {
     );
   }
 
-  return packageJSON;
+  // Should be done as the last step so only valid packages can be published
+  writeGeneratedFile('./npmDist/package.json', JSON.stringify(packageJSON));
+}
+
+// Based on https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API#getting-the-dts-from-a-javascript-file
+function emitTSFiles(outDir: string): {
+  emittedTSFiles: ReadonlyArray<string>;
+} {
+  const tsOptions = readTSConfig({
+    module: 'es2020',
+    noEmit: false,
+    declaration: true,
+    declarationDir: outDir,
+    outDir,
+    listEmittedFiles: true,
+  });
+
+  const tsHost = ts.createCompilerHost(tsOptions);
+  tsHost.writeFile = writeGeneratedFile;
+
+  const tsProgram = ts.createProgram(['src/index.ts'], tsOptions, tsHost);
+  const tsResult = tsProgram.emit(undefined, undefined, undefined, undefined, {
+    after: [inlineInvariant],
+  });
+  assert(
+    !tsResult.emitSkipped,
+    'Fail to generate `*.d.ts` files, please run `npm run check`',
+  );
+
+  assert(tsResult.emittedFiles != null);
+  return {
+    emittedTSFiles: tsResult.emittedFiles.sort((a, b) => a.localeCompare(b)),
+  };
 }
diff --git a/resources/utils.ts b/resources/utils.ts
index d1ae8c0b..66b90e6e 100644
--- a/resources/utils.ts
+++ b/resources/utils.ts
@@ -5,6 +5,7 @@ import os from 'node:os';
 import path from 'node:path';
 
 import prettier from 'prettier';
+import ts from 'typescript';
 
 export function localRepoPath(...paths: ReadonlyArray<string>): string {
   const repoDir = new URL('..', import.meta.url).pathname;
@@ -109,30 +110,21 @@ function spawn(
   childProcess.spawnSync(command, args, { stdio: 'inherit', ...options });
 }
 
-export function readdirRecursive(
-  dirPath: string,
-  opts: { ignoreDir?: RegExp } = {},
-): Array<string> {
-  const { ignoreDir } = opts;
-  const result = [];
-  for (const dirent of fs.readdirSync(dirPath, { withFileTypes: true })) {
-    const name = dirent.name;
-    if (!dirent.isDirectory()) {
-      result.push(dirent.name);
-      continue;
-    }
-
-    if (ignoreDir?.test(name)) {
-      continue;
+function* readdirRecursive(dirPath: string): Generator<{
+  name: string;
+  filepath: string;
+  stats: fs.Stats;
+}> {
+  for (const name of fs.readdirSync(dirPath)) {
+    const filepath = path.join(dirPath, name);
+    const stats = fs.lstatSync(filepath);
+
+    if (stats.isDirectory()) {
+      yield* readdirRecursive(filepath);
+    } else {
+      yield { name, filepath, stats };
     }
-    const list = readdirRecursive(path.join(dirPath, name), opts).map((f) =>
-      path.join(name, f),
-    );
-    result.push(...list);
   }
-
-  result.sort((a, b) => a.localeCompare(b));
-  return result.map((filepath) => './' + filepath);
 }
 
 export function showDirStats(dirPath: string): void {
@@ -141,18 +133,14 @@ export function showDirStats(dirPath: string): void {
   } = {};
   let totalSize = 0;
 
-  for (const filepath of readdirRecursive(dirPath)) {
-    const name = filepath.split(path.sep).at(-1);
-    assert(name != null);
-    const [base, ...splitExt] = name.split('.');
-    const ext = splitExt.join('.');
+  for (const { name, filepath, stats } of readdirRecursive(dirPath)) {
+    const ext = name.split('.').slice(1).join('.');
+    const filetype = ext ? '*.' + ext : name;
 
-    const filetype = ext ? '*.' + ext : base;
-    fileTypes[filetype] = fileTypes[filetype] || { filepaths: [], size: 0 };
+    fileTypes[filetype] = fileTypes[filetype] ?? { filepaths: [], size: 0 };
 
-    const { size } = fs.lstatSync(path.join(dirPath, filepath));
-    totalSize += size;
-    fileTypes[filetype].size += size;
+    totalSize += stats.size;
+    fileTypes[filetype].size += stats.size;
     fileTypes[filetype].filepaths.push(filepath);
   }
 
@@ -163,7 +151,8 @@ export function showDirStats(dirPath: string): void {
     if (numFiles > 1) {
       stats.push([filetype + ' x' + numFiles, typeStats.size]);
     } else {
-      stats.push([typeStats.filepaths[0], typeStats.size]);
+      const relativePath = path.relative(dirPath, typeStats.filepaths[0]);
+      stats.push([relativePath, typeStats.size]);
     }
   }
   stats.sort((a, b) => b[1] - a[1]);
@@ -218,3 +207,31 @@ export function readPackageJSON(
   const filepath = path.join(dirPath, 'package.json');
   return JSON.parse(fs.readFileSync(filepath, 'utf-8'));
 }
+
+export function readTSConfig(overrides?: any): ts.CompilerOptions {
+  const tsConfigPath = localRepoPath('tsconfig.json');
+
+  const { config: tsConfig, error: tsConfigError } =
+    ts.parseConfigFileTextToJson(
+      tsConfigPath,
+      fs.readFileSync(tsConfigPath, 'utf-8'),
+    );
+  assert(tsConfigError === undefined, 'Fail to parse config: ' + tsConfigError);
+  assert(
+    tsConfig.compilerOptions,
+    '"tsconfig.json" should have `compilerOptions`',
+  );
+
+  const { options: tsOptions, errors: tsOptionsErrors } =
+    ts.convertCompilerOptionsFromJson(
+      { ...tsConfig.compilerOptions, ...overrides },
+      process.cwd(),
+    );
+
+  assert(
+    tsOptionsErrors.length === 0,
+    'Fail to parse options: ' + tsOptionsErrors,
+  );
+
+  return tsOptions;
+}
diff --git a/src/execution/execute.ts b/src/execution/execute.ts
index c1f025eb..37eed9e1 100644
--- a/src/execution/execute.ts
+++ b/src/execution/execute.ts
@@ -1838,7 +1838,6 @@ function executeStreamField(
     exeContext,
   });
   let completedItem: PromiseOrValue<unknown>;
-  let completedItems: PromiseOrValue<Array<unknown> | null>;
   try {
     try {
       if (isPromise(item)) {
@@ -1885,7 +1884,7 @@ function executeStreamField(
       }
     } catch (rawError) {
       const error = locatedError(rawError, fieldNodes, pathToArray(itemPath));
-      completedItems = handleFieldError(
+      completedItem = handleFieldError(
         error,
         itemType,
         asyncPayloadRecord.errors,
@@ -1899,6 +1898,7 @@ function executeStreamField(
     return asyncPayloadRecord;
   }
 
+  let completedItems: PromiseOrValue<Array<unknown> | null>;
   if (isPromise(completedItem)) {
     completedItems = completedItem.then(
       (value) => [value],


Test patch:
diff --git a/src/execution/__tests__/stream-test.ts b/src/execution/__tests__/stream-test.ts
index df25d6e8..8378d320 100644
--- a/src/execution/__tests__/stream-test.ts
+++ b/src/execution/__tests__/stream-test.ts
@@ -823,10 +823,7 @@ describe('Execute: stream directive', () => {
       }
     `);
     const result = await complete(document, {
-      async *scalarList() {
-        yield await Promise.resolve(friends[0].name);
-        yield await Promise.resolve({});
-      },
+      scalarList: () => [friends[0].name, {}],
     });
     expectJSON(result).toDeepEqual([
       {
@@ -849,9 +846,6 @@ describe('Execute: stream directive', () => {
             ],
           },
         ],
-        hasNext: true,
-      },
-      {
         hasNext: false,
       },
     ]);