Skip to content

Commit

Permalink
feat: support tsconfig paths and package-relative imports [sc-22644] (#…
Browse files Browse the repository at this point in the history
…1006)

* feat: async dependency collector

* refactor: switch to a sync implementation to avoid changes to constructs

* refactor: remove unnecessary try/catch

* feat: look up original TS sources when dealing with folders as modules

* fix: include package.json when resolving module folders

* feat: let parser know how tsconfig paths were resolved for a file

This optionally allows the parser to massage the file structure into an
alternate output format.

* feat: add jsconfig.json support

* fix: supported module check was accidentally negated

* feat: bundle relevant tsconfig/jsconfig.json files

These files are currently not utilized by the backend but they might be
in the near future.

* feat: cache all source files and set up unique IDs for later dedup purposes

* feat: add tests for tsconfig behavior

* feat: more complete support for imports with extensions

* chore: remove unused code

* fix: remove mistakenly implemented useless package-relative path support

* fix: remove unused variable

* fix: remove unused variable

* feat: cache common dependencies within a Session

Shares a common Parser (or Parsers, one per runtime) within a Session and
avoids unnecessary AST walks for filePaths the parser has already seen when
parsing other entrypoints.

Share PackageFilesResolver so that its file caches can be shared within the
same Parser (which is now also shared within the Session), and cache
its result per filePath to avoid duplicate work.

Helps use cases where there are multiple entrypoints that share
common libraries.

* fix: remove allowImportingTsExtensions support

Does not play well during a nested lookup when we're looking up a path that
we've already resolved to a candidate with a .ts extension earlier in the
process. Not a super useful feature anyway.

* chore: empty out package-lock.json in fixtures, keep file

* feat: support `index.json` which apparently works
  • Loading branch information
sorccu authored Jan 20, 2025
1 parent 5dbc83f commit 19656b8
Show file tree
Hide file tree
Showing 48 changed files with 1,400 additions and 111 deletions.
6 changes: 1 addition & 5 deletions packages/cli/src/constructs/api-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { Check, CheckProps } from './check'
import { HttpHeader } from './http-header'
import { Session } from './project'
import { QueryParam } from './query-param'
import { Parser } from '../services/check-parser/parser'
import { pathToPosix } from '../services/util'
import { printDeprecationWarning } from '../reporters/util'
import { Content, Entrypoint } from './construct'
Expand Down Expand Up @@ -352,10 +351,7 @@ export class ApiCheck extends Check {
if (!runtime) {
throw new Error(`${runtimeId} is not supported`)
}
const parser = new Parser({
supportedNpmModules: Object.keys(runtime.dependencies),
checkUnsupportedModules: Session.verifyRuntimeDependencies,
})
const parser = Session.getParser(runtime)
const parsed = parser.parse(absoluteEntrypoint)
// Maybe we can get the parsed deps with the content immediately

Expand Down
6 changes: 1 addition & 5 deletions packages/cli/src/constructs/browser-check.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as path from 'path'
import { Check, CheckProps } from './check'
import { Session } from './project'
import { Parser } from '../services/check-parser/parser'
import { CheckConfigDefaults } from '../services/checkly-config-loader'
import { pathToPosix } from '../services/util'
import { Content, Entrypoint } from './construct'
Expand Down Expand Up @@ -119,10 +118,7 @@ export class BrowserCheck extends Check {
if (!runtime) {
throw new Error(`${runtimeId} is not supported`)
}
const parser = new Parser({
supportedNpmModules: Object.keys(runtime.dependencies),
checkUnsupportedModules: Session.verifyRuntimeDependencies,
})
const parser = Session.getParser(runtime)
const parsed = parser.parse(entry)
// Maybe we can get the parsed deps with the content immediately

Expand Down
6 changes: 1 addition & 5 deletions packages/cli/src/constructs/multi-step-check.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as path from 'path'
import { Check, CheckProps } from './check'
import { Session } from './project'
import { Parser } from '../services/check-parser/parser'
import { CheckConfigDefaults } from '../services/checkly-config-loader'
import { pathToPosix } from '../services/util'
import { Content, Entrypoint } from './construct'
Expand Down Expand Up @@ -104,10 +103,7 @@ export class MultiStepCheck extends Check {
if (!runtime) {
throw new Error(`${runtimeId} is not supported`)
}
const parser = new Parser({
supportedNpmModules: Object.keys(runtime.dependencies),
checkUnsupportedModules: Session.verifyRuntimeDependencies,
})
const parser = Session.getParser(runtime)
const parsed = parser.parse(entry)
// Maybe we can get the parsed deps with the content immediately

Expand Down
18 changes: 18 additions & 0 deletions packages/cli/src/constructs/project.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as api from '../rest/api'
import { CheckConfigDefaults } from '../services/checkly-config-loader'
import { Parser } from '../services/check-parser/parser'
import { Construct } from './construct'
import { ValidationError } from './validator-error'

Expand Down Expand Up @@ -147,6 +148,7 @@ export class Session {
static loadingChecklyConfigFile: boolean
static checklyConfigFileConstructs?: Construct[]
static privateLocations: PrivateLocationApi[]
static parsers = new Map<string, Parser>()

static registerConstruct (construct: Construct) {
if (Session.project) {
Expand Down Expand Up @@ -191,4 +193,20 @@ export class Session {
}
return Session.availableRuntimes[effectiveRuntimeId]
}

static getParser (runtime: Runtime): Parser {
const cachedParser = Session.parsers.get(runtime.name)
if (cachedParser !== undefined) {
return cachedParser
}

const parser = new Parser({
supportedNpmModules: Object.keys(runtime.dependencies),
checkUnsupportedModules: Session.verifyRuntimeDependencies,
})

Session.parsers.set(runtime.name, parser)

return parser
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const value = 'hello'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const value = 'world'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const value = 3
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { } from './dep1'
import { } from './dep2.js'
import { } from './dep3.js'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const value = 'hello'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const value = 'world'
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { } from './dep1'
import { } from './dep1.ts'
import { } from './dep1.js'
import { } from './dep2'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const value1 = 'value1'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const value2 = 'value2'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const value3 = 'value3'
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { value1 } from './dep1.ts'
export { value2 } from './dep2.js'
export { value3 } from './dep3'
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "nodenext",
"esModuleInterop": true,
"baseUrl": ".",
"noEmit": true,
"allowImportingTsExtensions": true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v22.12.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { defineConfig } from 'checkly'

const config = defineConfig({
projectName: 'TSConfig Paths Sample Project',
logicalId: 'tsconfig-paths-sample-project',
checks: {
frequency: 10,
locations: ['us-east-1'],
tags: ['mac'],
runtimeId: '2024.09',
checkMatch: '**/__checks__/**/*.check.ts',
browserChecks: {
testMatch: '**/__checks__/**/*.spec.ts',
},
},
cli: {
runLocation: 'us-east-1',
reporters: ['list'],
},
})

export default config
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const value = 'file1'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const value = 'file2'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { value } from './file2'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const value = 'file2'
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { value as value1 } from './file1'
export { value as value2 } from './file2'
export { value as value3 } from './folder/file1'

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "@internal/lib",
"main": "dist/index.js",
"scripts": {
"clean": "rimraf ./dist",
"prepare": "npm run clean && tsc --build"
},
"devDependencies": {
"rimraf": "^6.0.1",
"typescript": "^5.7.2"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"exclude": [
"dist",
"node_modules"
],
"include": [
"./**/*.ts"
],
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "nodenext",
"outDir": "dist",
"declaration": true,
"sourceMap": true,
"declarationMap": true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const name = 'lib2'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const value = 11517
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "nodenext",
"esModuleInterop": true,
"baseUrl": "."
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "tsconfig-paths-sample-project",
"dependencies": {
"checkly": "^4.15.0"
},
"devDependencies": {
"@playwright/test": "^1.49.1",
"typescript": "^5.7.2"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { value1 } from '@internal/lib1'
import { value } from '@/foo/bar'
import { name } from 'lib2'
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "nodenext",
"esModuleInterop": true,
"baseUrl": ".",
"paths": {
"@internal/lib1": [
"./lib1"
],
"@/*": [
"./lib3/*"
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const value = 'nothing here'
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "nodenext",
"esModuleInterop": true,
"baseUrl": ".",
"paths": {
"@internal/lib1": [
"./lib1"
],
"@/*": [
"./lib3/*"
]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,81 @@ describe('dependency-parser - parser()', () => {
])
})

it('should parse typescript dependencies using tsconfig', () => {
const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'tsconfig-paths-sample-project', ...filepath)
const parser = new Parser({
supportedNpmModules: defaultNpmModules,
})
const { dependencies } = parser.parse(toAbsolutePath('src', 'entrypoint.ts'))
expect(dependencies.map(d => d.filePath).sort()).toEqual([
toAbsolutePath('lib1', 'file1.ts'),
toAbsolutePath('lib1', 'file2.ts'),
toAbsolutePath('lib1', 'folder', 'file1.ts'),
toAbsolutePath('lib1', 'folder', 'file2.ts'),
toAbsolutePath('lib1', 'index.ts'),
toAbsolutePath('lib1', 'package.json'),
toAbsolutePath('lib1', 'tsconfig.json'),
toAbsolutePath('lib2', 'index.ts'),
toAbsolutePath('lib3', 'foo', 'bar.ts'),
toAbsolutePath('tsconfig.json'),
])
})

it('should not include tsconfig if not needed', () => {
const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'tsconfig-paths-unused', ...filepath)
const parser = new Parser({
supportedNpmModules: defaultNpmModules,
})
const { dependencies } = parser.parse(toAbsolutePath('src', 'entrypoint.ts'))
expect(dependencies.map(d => d.filePath).sort()).toEqual([])
})

it('should support importing ts extensions if allowed', () => {
const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'tsconfig-allow-importing-ts-extensions', ...filepath)
const parser = new Parser({
supportedNpmModules: defaultNpmModules,
})
const { dependencies } = parser.parse(toAbsolutePath('src', 'entrypoint.ts'))
expect(dependencies.map(d => d.filePath).sort()).toEqual([
toAbsolutePath('src', 'dep1.ts'),
toAbsolutePath('src', 'dep2.ts'),
toAbsolutePath('src', 'dep3.ts'),
])
})

it('should not import TS files from a JS file', () => {
const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'no-import-ts-from-js', ...filepath)
const parser = new Parser({
supportedNpmModules: defaultNpmModules,
})
expect.assertions(1)
try {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { dependencies } = parser.parse(toAbsolutePath('entrypoint.js'))
} catch (err) {
expect(err).toMatchObject({
missingFiles: [
toAbsolutePath('dep1'),
toAbsolutePath('dep1.ts'),
toAbsolutePath('dep1.js'),
],
})
}
})

it('should import JS files from a TS file', () => {
const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'import-js-from-ts', ...filepath)
const parser = new Parser({
supportedNpmModules: defaultNpmModules,
})
const { dependencies } = parser.parse(toAbsolutePath('entrypoint.ts'))
expect(dependencies.map(d => d.filePath).sort()).toEqual([
toAbsolutePath('dep1.js'),
toAbsolutePath('dep2.js'),
toAbsolutePath('dep3.ts'),
])
})

it('should handle ES Modules', () => {
const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'esmodules-example', ...filepath)
const parser = new Parser({
Expand Down
Loading

0 comments on commit 19656b8

Please sign in to comment.