YousefED / typescript-json-schema

Generate json-schema from your Typescript sources
BSD 3-Clause "New" or "Revised" License
3.08k stars 318 forks source link

`basePath` does not work if absolute, or if relative to a current working directory that is a sibling of the target directory. #599

Open broofa opened 2 months ago

broofa commented 2 months ago

Issue

When generating a schema programmatically, specifying an absolute basePath causes a "Error: type [target TS type] not found" error, presumably because TSJ is failing to find the specified source file(s).

Similarly, if the basePath is relative to a current working directory that is off to the side of the target directory (i.e. cwd is not a parent of the target directory so basePath has ".." paths in it), it fails similarly.

Impact: This is a non-trivial issue because, combined, these two problems make it next to impossible to run multiple async schema generation tasks. The inability to provide an absolute path means basePath must be specified relative to process.cwd(). But relative paths only work if they descend directly from process.cwd(), which means you very likely need to change the working directory for things to work. (But that will likely break any other TSJ task that happens to depend on the current cwd).

To Reproduce

  1. Download and unzip tsj-test.zip. This is a minimal, reproducible example of the problem.
  2. cd tsj-test
  3. npm install
  4. npm test (with node@18 or node@20)

You should see output something like this:

▶ basePath tests
  ✔ {"cwd":"appdir","basePath":".","rootNames":["src/app.ts"]} (456.918416ms)
  ✔ {"cwd":".","basePath":".","rootNames":["appdir/src/app.ts"]} (273.435166ms)
  ✖ {"cwd":".","basePath":"/Users/kieffer/tsj-test/appdir","rootNames":["src/app.ts"]} (249.84875ms)
    AssertionError [ERR_ASSERTION]: Got unwanted rejection.
    Actual message: "type TargetType not found"
        at async TestContext.<anonymous> (file:///Users/kieffer/tsj-test/test.js:56:7)
        at async Test.run (node:internal/test_runner/test:632:9)
        at async Suite.processPendingSubtests (node:internal/test_runner/test:374:7) {
      generatedMessage: false,
      code: 'ERR_ASSERTION',
      actual: Error: type TargetType not found
          at JsonSchemaGenerator.getSchemaForSymbol (/Users/kieffer/tsj-test/node_modules/typescript-json-schema/dist/typescript-json-schema.js:1171:19)
          at Module.generateSchema (/Users/kieffer/tsj-test/node_modules/typescript-json-schema/dist/typescript-json-schema.js:1369:26)
          at generateSchemaFromType (file:///Users/kieffer/tsj-test/test.js:30:14)
          at file:///Users/kieffer/tsj-test/test.js:57:30
          at waitForActual (node:assert:777:21)
          at Function.doesNotReject (node:assert:932:39)
          at TestContext.<anonymous> (file:///Users/kieffer/tsj-test/test.js:56:20)
          at Test.runInAsyncScope (node:async_hooks:203:9)
          at Test.run (node:internal/test_runner/test:631:25)
          at Suite.processPendingSubtests (node:internal/test_runner/test:374:18),
      expected: undefined,
      operator: 'doesNotReject'
    }

  ✖ {"cwd":"./stubdir","basePath":"../otherdir","rootNames":["src/app.ts"]} (237.169625ms)
    AssertionError [ERR_ASSERTION]: Got unwanted rejection.
    Actual message: "type TargetType not found"
        at async TestContext.<anonymous> (file:///Users/kieffer/tsj-test/test.js:56:7)
        at async Test.run (node:internal/test_runner/test:632:9)
        at async Suite.processPendingSubtests (node:internal/test_runner/test:374:7) {
      generatedMessage: false,
      code: 'ERR_ASSERTION',
      actual: Error: type TargetType not found
          at JsonSchemaGenerator.getSchemaForSymbol (/Users/kieffer/tsj-test/node_modules/typescript-json-schema/dist/typescript-json-schema.js:1171:19)
          at Module.generateSchema (/Users/kieffer/tsj-test/node_modules/typescript-json-schema/dist/typescript-json-schema.js:1369:26)
          at generateSchemaFromType (file:///Users/kieffer/tsj-test/test.js:30:14)
          at file:///Users/kieffer/tsj-test/test.js:57:30
          at waitForActual (node:assert:777:21)
          at Function.doesNotReject (node:assert:932:39)
          at TestContext.<anonymous> (file:///Users/kieffer/tsj-test/test.js:56:20)
          at Test.runInAsyncScope (node:async_hooks:203:9)
          at Test.run (node:internal/test_runner/test:631:25)
          at Suite.processPendingSubtests (node:internal/test_runner/test:374:18),
      expected: undefined,
      operator: 'doesNotReject'
    }

The test attempts to generate schemas for the exact same code, using four different configurations of current working directory, basePath, and rootNames. The first two - which use relative basePath values with a current working directory in or above the target directory - work fine.

The third test, which uses an absolute basePath that points to the same directory as the second test, fails.

Similarly, the fourth test uses a relative path that points to the same directory, but with a current working directory "off to the side" (in ./stubdir) of the target directory, also fails.

Expected behavior

All four tests should pass.

broofa commented 2 months ago

Quick followup to confirm that this issue does, indeed, cause TJS.generateSchema() operations to fail when run concurrently (async) on multiple projects. 😢

broofa commented 2 months ago

[Breadcrumb for anyone else that might stumble across this issue...]

This appears to be an issue with the TS compiler API. Specifically, in ts.createProgram(). It looks like repeated calls to this method reuse the cached file information for any rootNames paths that match previous rootNames, regardless of basePath.

FWIW, you can diagnose this problem by just doing console.log(program) and looking at the contents of the program.sourceFileToPackageName map. If it's showing unexpected files... this is probably the issue.

The solution ("workaround"? "Hack"?) is to use a basePath in getProgramFiles() that is "high enough" to encompass all of the rootNames paths to be processed, so each rootNames path is unique across all sources being processed.

Maintainers: I'm going to leave this open since it affects the expected use of TJS, and in the hopes someone more knowledgable than I might know a better workaround. But if not, feel free to close.