diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ac228e..0b81c8b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,14 +3,12 @@ on: push: branches: - main - tags: - - 'v*' pull_request: paths-ignore: - '*.md' concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + cancel-in-progress: true jobs: nodejs: name: Node.js @@ -18,17 +16,18 @@ jobs: strategy: fail-fast: false matrix: - node-version: [^20.8, ^22, ^24] + node-version: [^22.20, ^24.12, ^25] os: [ubuntu-latest, windows-latest] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} - run: npm install --no-audit - run: npm test - - uses: codecov/codecov-action@v4 + - uses: codecov/codecov-action@v5 with: + disable_search: true files: coverage/lcov.info name: ${{ matrix.os }}/${{ matrix.node-version }} token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 897605b..cd668b3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,88 +1,197 @@ name: Release on: - push: - tags: - - 'v*' workflow_dispatch: inputs: - tag: - description: 'Release tag (e.g., v1.2.3)' + ref: + description: 'Commit ref or existing version tag (e.g., abc1234 or v1.2.3)' required: true type: string + new_version: + description: 'New version for npm version (e.g., patch, minor, major, 1.2.3). Must be empty when providing an existing tag.' + required: false + type: string skip_ci_check: description: 'Skip CI status check' required: false type: boolean default: false -permissions: - contents: write - id-token: write - jobs: - release: - name: Release - runs-on: ubuntu-latest - environment: npm + preflight: + name: Validate + runs-on: ubuntu-slim + permissions: + actions: read + contents: read + outputs: + ref_is_tag: ${{steps.validation.outputs.ref_is_tag}} steps: + - name: Validate inputs + id: validation + env: + REF: ${{inputs.ref}} + NEW_VERSION: ${{inputs.new_version}} + run: | + if [[ "$REF" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then + echo "ref_is_tag=true" >> "$GITHUB_OUTPUT" + if [[ -n "$NEW_VERSION" ]]; then + echo "::error::new_version must be empty when an existing tag is provided" + exit 1 + fi + else + echo "ref_is_tag=false" >> "$GITHUB_OUTPUT" + if [[ -z "$NEW_VERSION" ]]; then + echo "::error::new_version is required when a commit ref is provided" + exit 1 + fi + fi + - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: - ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }} + ref: ${{inputs.ref}} fetch-depth: 0 + - name: Verify ref is HEAD of main + if: steps.validation.outputs.ref_is_tag == 'false' + env: + REF: ${{inputs.ref}} + run: | + MAIN_SHA=$(git rev-parse origin/main) + CURRENT_SHA=$(git rev-parse HEAD) + if [[ "$CURRENT_SHA" != "$MAIN_SHA" ]]; then + echo "::error::ref ${REF} (${CURRENT_SHA}) is not the HEAD of main (${MAIN_SHA})" + exit 1 + fi + - name: Verify tag matches package.json version + if: steps.validation.outputs.ref_is_tag == 'true' + env: + REF: ${{inputs.ref}} run: | - jq --raw-output --exit-status --arg tag "$RELEASE_TAG" ' + jq --raw-output --exit-status --arg tag "$REF" ' if (.version == ($tag | ltrimstr("v"))) then - "Package version (\(.version)) matches tag version (\($tag | ltrimstr("v")))" + "Package version (\(.version)) matches tag version (\($tag | ltrimstr(\"v\")))" else - "Package version (\(.version)) does not match tag version (\($tag | ltrimstr("v")))" | halt_error(1) + "Package version (\(.version)) does not match tag version (\($tag | ltrimstr(\"v\")))" | halt_error(1) end' package.json + + - name: Check CI status + if: '!inputs.skip_ci_check' + run: | + gh run list --commit "$(git rev-parse HEAD)" --workflow ci.yml --status success --json databaseId \ + | jq --raw-output --exit-status ' + if (length > 0) then + "CI checks have passed" + else + "CI has not completed successfully for this commit" | halt_error(1) + end' env: - RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }} + GH_TOKEN: ${{secrets.GITHUB_TOKEN}} - - name: Verify commit is in main branch + release: + name: Release + runs-on: ubuntu-latest + needs: preflight + environment: npm + permissions: + contents: read + id-token: write + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{inputs.ref}} + fetch-depth: 0 + + - name: Verify ref is still HEAD of main + if: needs.preflight.outputs.ref_is_tag == 'false' + env: + REF: ${{inputs.ref}} run: | - # Check if the tagged commit is included in the main branch - if git merge-base --is-ancestor ${{ github.sha }} origin/main; then - echo "Tagged commit is properly included in main branch" - else - echo "Tagged commit is not included in the main branch" - echo "Please push the commit to main before releasing" + MAIN_SHA=$(git rev-parse origin/main) + CURRENT_SHA=$(git rev-parse HEAD) + if [[ "$CURRENT_SHA" != "$MAIN_SHA" ]]; then + echo "::error::ref ${REF} (${CURRENT_SHA}) is no longer the HEAD of main (${MAIN_SHA})" exit 1 fi - - name: Check CI status - if: ${{ !inputs.skip_ci_check }} - run: | - # Check if CI has completed successfully for this commit - gh run list --commit ${{ github.sha }} --status success --json workflowName | jq --raw-output --exit-status ' - if any(.[]; .workflowName == "Install and test @ava/typescript") then - "All CI checks have passed!" - else - "CI has not completed successfully for this commit" | halt_error(1) - end' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Generate app token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{vars.LAUNCHBOT_ID}} + private-key: ${{secrets.LAUNCHBOT_PRIVATE_KEY}} - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: package.json + cache: npm registry-url: https://registry.npmjs.org + - name: Bump version + if: needs.preflight.outputs.ref_is_tag == 'false' + env: + NEW_VERSION: ${{inputs.new_version}} + run: npm version "$NEW_VERSION" --no-git-tag-version + + - name: Push version commit + if: needs.preflight.outputs.ref_is_tag == 'false' + id: push-commit + env: + GH_TOKEN: ${{steps.app-token.outputs.token}} + run: | + RELEASE_TAG="v$(jq --raw-output .version package.json)" + PARENT_SHA=$(git rev-parse HEAD) + + jq --null-input \ + --arg repo "$GITHUB_REPOSITORY" \ + --arg parentSha "$PARENT_SHA" \ + --arg headline "$RELEASE_TAG" \ + --rawfile pkgContent package.json \ + --rawfile lockContent package-lock.json \ + '{ + query: "mutation($input: CreateCommitOnBranchInput!) { createCommitOnBranch(input: $input) { commit { oid } } }", + variables: { + input: { + branch: { repositoryNameWithOwner: $repo, branchName: "main" }, + message: { headline: $headline }, + expectedHeadOid: $parentSha, + fileChanges: { + additions: [ + { path: "package.json", contents: ($pkgContent | @base64) }, + { path: "package-lock.json", contents: ($lockContent | @base64) } + ] + } + } + } + }' > /tmp/request.json + + COMMIT_SHA=$(gh api graphql --input /tmp/request.json \ + --jq '.data.createCommitOnBranch.commit.oid') + + echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT" + echo "version_commit_sha=$COMMIT_SHA" >> "$GITHUB_OUTPUT" + + - name: Create version tag + if: needs.preflight.outputs.ref_is_tag == 'false' + run: | + gh api "repos/$GITHUB_REPOSITORY/git/refs" \ + -f ref="refs/tags/${{steps.push-commit.outputs.release_tag}}" \ + -f sha="${{steps.push-commit.outputs.version_commit_sha}}" + env: + GH_TOKEN: ${{steps.app-token.outputs.token}} + - name: Publish to npm with provenance run: npm publish --provenance - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Create GitHub Release run: | + RELEASE_TAG="v$(jq --raw-output .version package.json)" gh release create "$RELEASE_TAG" \ --title "$RELEASE_TAG" \ --draft \ --generate-notes env: - RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{steps.app-token.outputs.token}} diff --git a/README.md b/README.md index f5c4225..1ad18b3 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ This is designed to work for projects that precompile TypeScript. It allows AVA In other words, say you have a test file at `src/test.ts`. You've configured TypeScript to output to `build/`. Using `@ava/typescript` you can run the test using `npx ava src/test.ts`. +Compatible with AVA 6, 7 and 8. Requires Node.js 22.20, 24.12 or 25 (or newer). + ## Enabling TypeScript support Add this package to your project: @@ -37,11 +39,15 @@ You can enable compilation via the `compile` property. If `false`, AVA will assu Output files are expected to have the `.js` extension. -AVA searches your entire project for `*.js`, `*.cjs`, `*.mjs`, `*.ts`, `*.cts` and `*.mts` files (or other extensions you've configured). It will ignore such files found in the `rewritePaths` targets (e.g. `build/`). If you use more specific paths, for instance `build/main/`, you may need to change AVA's `files` configuration to ignore other directories. +AVA searches your entire project for `*.js`, `*.mjs`, `*.ts` and `*.mts` files﹡ (or other extensions you've configured). It will ignore such files found in the `rewritePaths` targets (e.g. `build/`). If you use more specific paths, for instance `build/main/`, you may need to change AVA's `files` configuration to ignore other directories. + +﹡ AVA 6 and 7 will also search for `*.cjs` and `*.cts` files. ## ES Modules -If your `package.json` has configured `"type": "module"`, or you've configured AVA to treat the `js` extension as `module`, then `@ava/typescript` will import the output file as an ES module. Note that this is based on the *output file*, not the `ts` extension. +With AVA 6 and 7, if your `package.json` has configured `"type": "module"`, or you've configured AVA to treat the `js` extension as `module`, then `@ava/typescript` will import the output file as an ES module. Note that this is based on the _output file_, not the `ts` extension. + +AVA 8 will _always_ import all files as ES modules. ## Add additional extensions @@ -53,10 +59,7 @@ You can configure AVA to recognize additional file extensions. To add (partial { "ava": { "typescript": { - "extensions": [ - "ts", - "tsx" - ], + "extensions": ["ts", "tsx"], "rewritePaths": { "src/": "build/" } @@ -69,4 +72,4 @@ If you use the [`allowJs` TypeScript option](https://www.typescriptlang.org/tsco See also AVA's [`extensions` option](https://github.com/avajs/ava/blob/master/docs/06-configuration.md#options). -† Note that the [*preserve* mode for JSX](https://www.typescriptlang.org/docs/handbook/jsx.html) is not (yet) supported. +† Note that the [_preserve_ mode for JSX](https://www.typescriptlang.org/docs/handbook/jsx.html) is not (yet) supported. diff --git a/index.js b/index.js index dd1fc94..2960bbc 100644 --- a/index.js +++ b/index.js @@ -3,9 +3,9 @@ import path from 'node:path'; import {pathToFileURL} from 'node:url'; import escapeStringRegexp from 'escape-string-regexp'; import {execa} from 'execa'; +import pkg from './package.json' with {type: 'json'}; -const package_ = JSON.parse(fs.readFileSync(new URL('package.json', import.meta.url))); -const help = `See https://github.com/avajs/typescript/blob/v${package_.version}/README.md`; +const help = `See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md`; function isPlainObject(x) { return x !== null && typeof x === 'object' && Reflect.getPrototypeOf(x) === Object.prototype; @@ -74,12 +74,17 @@ const changeInterpretations = Object.freeze(Object.assign(Object.create(null), { waitForOutOfBandCompilation: 2, })); +const defaultExtensions = ['ts', 'mts']; +const defaultAva6Extensions = ['ts', 'cts', 'mts']; + export default function typescriptProvider({negotiateProtocol}) { - const protocol = negotiateProtocol(['ava-6'], {version: package_.version}); + const protocol = negotiateProtocol(['ava-6', 'ava-8'], {version: pkg.version}); if (protocol === null) { return; } + const isAva8 = protocol.identifier === 'ava-8'; + return { main({config}) { if (!isPlainObject(config)) { @@ -89,7 +94,7 @@ export default function typescriptProvider({negotiateProtocol}) { validate(config, configProperties); const { - extensions = ['ts', 'cts', 'mts'], + extensions = isAva8 ? defaultExtensions : defaultAva6Extensions, rewritePaths: relativeRewritePaths, compile, } = config; @@ -98,7 +103,7 @@ export default function typescriptProvider({negotiateProtocol}) { path.join(protocol.projectDir, from), path.join(protocol.projectDir, to), ]); - const testFileExtension = new RegExp(`\\.(${extensions.map(extension => escapeStringRegexp(extension)).join('|')})$`); + const testFileExtension = new RegExp(String.raw`\.(${extensions.map(extension => escapeStringRegexp(extension)).join('|')})$`, 'v'); const watchMode = { changeInterpretations, @@ -128,7 +133,7 @@ export default function typescriptProvider({negotiateProtocol}) { } // Only recognize .cjs, .mjs and .js files. - if (!/\.(c|m)?js$/.test(filePath)) { + if (!/\.(c|m)?js$/v.test(filePath)) { return null; } @@ -142,11 +147,11 @@ export default function typescriptProvider({negotiateProtocol}) { if (filePath.endsWith('.cjs')) { if (extensions.includes('cjs')) { - possibleExtensions.push({replace: /\.cjs$/, extension: 'cjs'}); + possibleExtensions.push({replace: /\.cjs$/v, extension: 'cjs'}); } if (extensions.includes('cts')) { - possibleExtensions.push({replace: /\.cjs$/, extension: 'cts'}); + possibleExtensions.push({replace: /\.cjs$/v, extension: 'cts'}); } if (possibleExtensions.length === 0) { @@ -156,11 +161,11 @@ export default function typescriptProvider({negotiateProtocol}) { if (filePath.endsWith('.mjs')) { if (extensions.includes('mjs')) { - possibleExtensions.push({replace: /\.mjs$/, extension: 'mjs'}); + possibleExtensions.push({replace: /\.mjs$/v, extension: 'mjs'}); } if (extensions.includes('mts')) { - possibleExtensions.push({replace: /\.mjs$/, extension: 'mts'}); + possibleExtensions.push({replace: /\.mjs$/v, extension: 'mts'}); } if (possibleExtensions.length === 0) { @@ -170,15 +175,15 @@ export default function typescriptProvider({negotiateProtocol}) { if (filePath.endsWith('.js')) { if (extensions.includes('js')) { - possibleExtensions.push({replace: /\.js$/, extension: 'js'}); + possibleExtensions.push({replace: /\.js$/v, extension: 'js'}); } if (extensions.includes('ts')) { - possibleExtensions.push({replace: /\.js$/, extension: 'ts'}); + possibleExtensions.push({replace: /\.js$/v, extension: 'ts'}); } if (extensions.includes('tsx')) { - possibleExtensions.push({replace: /\.js$/, extension: 'tsx'}); + possibleExtensions.push({replace: /\.js$/v, extension: 'tsx'}); } if (possibleExtensions.length === 0) { @@ -244,23 +249,23 @@ export default function typescriptProvider({negotiateProtocol}) { }, worker({extensionsToLoadAsModules, state: {extensions, rewritePaths}}) { - const importJs = extensionsToLoadAsModules.includes('js'); - const testFileExtension = new RegExp(`\\.(${extensions.map(extension => escapeStringRegexp(extension)).join('|')})$`); + const importJs = isAva8 || extensionsToLoadAsModules.includes('js'); + const testFileExtension = new RegExp(String.raw`\.(${extensions.map(extension => escapeStringRegexp(extension)).join('|')})$`, 'v'); return { canLoad(reference) { return testFileExtension.test(reference) && rewritePaths.some(([from]) => reference.startsWith(from)); }, - async load(reference, {requireFn}) { + async load(reference, {requireFn} = {}) { const [from, to] = rewritePaths.find(([from]) => reference.startsWith(from)); let rewritten = `${to}${reference.slice(from.length)}`; let useImport = true; if (reference.endsWith('.cts')) { - rewritten = rewritten.replace(/\.cts$/, '.cjs'); - useImport = false; + rewritten = rewritten.replace(/\.cts$/v, '.cjs'); + useImport = isAva8; } else if (reference.endsWith('.mts')) { - rewritten = rewritten.replace(/\.mts$/, '.mjs'); + rewritten = rewritten.replace(/\.mts$/v, '.mjs'); } else { rewritten = rewritten.replace(testFileExtension, '.js'); useImport = importJs; diff --git a/package.json b/package.json index 127b75a..721510e 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "6.0.0", "description": "TypeScript provider for AVA", "engines": { - "node": "^20.8 || ^22 || >=24" + "node": "^22.20 || ^24.12 || >=25" }, "files": [ "index.js" @@ -24,17 +24,17 @@ }, "dependencies": { "escape-string-regexp": "^5.0.0", - "execa": "^9.6.0" + "execa": "^9.6.1" }, "devDependencies": { - "ava": "^6.4.0", - "c8": "^10.1.3", - "del": "^8.0.0", - "typescript": "^5.8.3", - "xo": "^1.1.0" + "ava": "^7.0.0", + "c8": "^11.0.0", + "del": "^8.0.1", + "typescript": "^6.0.2", + "xo": "^2.0.2" }, "volta": { - "node": "22.16.0", - "npm": "11.4.2" + "node": "24.14.1", + "npm": "11.12.1" } } diff --git a/test/_with-provider.js b/test/_with-provider.js index 6a23896..ea1de7c 100644 --- a/test/_with-provider.js +++ b/test/_with-provider.js @@ -1,25 +1,45 @@ -import fs from 'node:fs'; +// eslint-disable-line ava/no-ignored-test-files import path from 'node:path'; -import {fileURLToPath} from 'node:url'; +import test from 'ava'; +import pkg from '../package.json' with {type: 'json'}; import makeProvider from '@ava/typescript'; -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const package_ = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url))); +const {file: testFile} = test.meta; -const createProviderMacro = (identifier, avaVersion, projectDirectory = __dirname) => (t, run) => run(t, makeProvider({ - negotiateProtocol(identifiers, {version}) { - t.true(identifiers.includes(identifier)); - t.is(version, package_.version); - return { - ava: {avaVersion}, - identifier, - normalizeGlobPatterns: patterns => patterns, - async findFiles({patterns}) { - return patterns.map(file => path.join(projectDirectory, file)); - }, - projectDir: projectDirectory, - }; - }, -})); +const createProviderMacro = ( + identifier, + avaVersion, + projectDirectory = import.meta.dirname, +) => + test.macro({ + exec(t, run) { + return run( + t, + makeProvider({ + negotiateProtocol(identifiers, {version}) { + t.true(identifiers.includes(identifier)); + t.is(version, pkg.version); + return { + ava: {avaVersion}, + identifier, + normalizeGlobPatterns: patterns => patterns, + async findFiles({patterns}) { + return patterns.map(file => + path.join(projectDirectory, file)); + }, + projectDir: projectDirectory, + }; + }, + }), + ); + }, + title(title = '') { + if (testFile.includes(identifier)) { + return title; + } + + return `${title} (with ${identifier} provider)`; + }, + }); export default createProviderMacro; diff --git a/test/broken-fixtures/tsconfig.json b/test/broken-fixtures/tsconfig.json index e9b2017..8caba87 100644 --- a/test/broken-fixtures/tsconfig.json +++ b/test/broken-fixtures/tsconfig.json @@ -1,9 +1,8 @@ { "compilerOptions": { - "outDir": "typescript/compiled", - "lib": [ - "es2022" - ] + "outDir": "./typescript/compiled", + "rootDir": "./typescript", + "tsBuildInfoFile": "./typescript/compiled/.tsbuildinfo" }, "include": [ "typescript" diff --git a/test/compilation.js b/test/compilation.js index 34698f2..4bf9668 100644 --- a/test/compilation.js +++ b/test/compilation.js @@ -1,61 +1,99 @@ import path from 'node:path'; -import {fileURLToPath} from 'node:url'; import test from 'ava'; import {deleteAsync} from 'del'; import {execaNode} from 'execa'; import createProviderMacro from './_with-provider.js'; -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const withProvider = createProviderMacro('ava-6', '6.0.0', path.join(__dirname, 'fixtures')); -const withAltProvider = createProviderMacro('ava-6', '6.0.0', path.join(__dirname, 'broken-fixtures')); - -test.before('deleting compiled files', async t => { +test.beforeEach('deleting compiled files', async t => { t.log(await deleteAsync('test/fixtures/typescript/compiled')); t.log(await deleteAsync('test/broken-fixtures/typescript/compiled')); }); const compile = async provider => ({ - state: await provider.main({ - config: { - rewritePaths: { - 'ts/': 'typescript/', - 'compiled/': 'typescript/compiled/', + state: await provider + .main({ + config: { + rewritePaths: { + 'ts/': 'typescript/', + 'compiled/': 'typescript/compiled/', + }, + compile: 'tsc', }, - compile: 'tsc', - }, - }).compile(), + }) + .compile(), }); -test('worker(): load rewritten paths files', withProvider, async (t, provider) => { - const {state} = await compile(provider); - const {stdout, stderr} = await execaNode( - path.join(__dirname, 'fixtures/install-and-load'), - [JSON.stringify({state}), path.join(__dirname, 'fixtures/ts', 'file.ts')], - {cwd: path.join(__dirname, 'fixtures')}, - ); - if (stderr.length > 0) { - t.log(stderr); - } - - t.snapshot(stdout); -}); +for (const [identifier, withProvider, withAltProvider] of [ + [ + 'ava-6', + createProviderMacro( + 'ava-6', + '6.0.0', + path.join(import.meta.dirname, 'fixtures'), + ), + createProviderMacro( + 'ava-6', + '6.0.0', + path.join(import.meta.dirname, 'broken-fixtures'), + ), + ], + [ + 'ava-8', + createProviderMacro( + 'ava-8', + '8.0.0', + path.join(import.meta.dirname, 'fixtures'), + ), + createProviderMacro( + 'ava-8', + '8.0.0', + path.join(import.meta.dirname, 'broken-fixtures'), + ), + ], +]) { + test( + 'worker(): load rewritten paths files', + withProvider, + async (t, provider) => { + const {state} = await compile(provider); + const {stdout, stderr} = await execaNode( + path.join(import.meta.dirname, 'fixtures/install-and-load'), + [ + identifier, + JSON.stringify({state}), + path.join(import.meta.dirname, 'fixtures/ts', 'file.ts'), + ], + {cwd: path.join(import.meta.dirname, 'fixtures')}, + ); + if (stderr.length > 0) { + t.log(stderr); + } -test('worker(): runs compiled files', withProvider, async (t, provider) => { - const {state} = await compile(provider); - const {stdout, stderr} = await execaNode( - path.join(__dirname, 'fixtures/install-and-load'), - [JSON.stringify({state}), path.join(__dirname, 'fixtures/compiled', 'index.ts')], - {cwd: path.join(__dirname, 'fixtures')}, + t.snapshot(stdout); + }, ); - if (stderr.length > 0) { - t.log(stderr); - } - t.snapshot(stdout); -}); + test('worker(): runs compiled files', withProvider, async (t, provider) => { + const {state} = await compile(provider); + const {stdout, stderr} = await execaNode( + path.join(import.meta.dirname, 'fixtures/install-and-load'), + [ + identifier, + JSON.stringify({state}), + path.join(import.meta.dirname, 'fixtures/compiled', 'index.ts'), + ], + {cwd: path.join(import.meta.dirname, 'fixtures')}, + ); + if (stderr.length > 0) { + t.log(stderr); + } -test('compile() error', withAltProvider, async (t, provider) => { - const {message} = await t.throwsAsync(compile(provider)); + t.snapshot(stdout); + }); - t.snapshot(message); -}); + test('compile() error', withAltProvider, async (t, provider) => { + const {message} = await t.throwsAsync(compile(provider)); + + t.snapshot(message); + }); +} diff --git a/test/fixtures/install-and-load.js b/test/fixtures/install-and-load.js index e5df78e..7b87d6b 100644 --- a/test/fixtures/install-and-load.js +++ b/test/fixtures/install-and-load.js @@ -1,25 +1,28 @@ import {createRequire} from 'node:module'; import path from 'node:path'; import process from 'node:process'; -import {fileURLToPath} from 'node:url'; import makeProvider from '@ava/typescript'; -const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const isAva8 = process.argv[2] === 'ava-8'; const provider = makeProvider({ negotiateProtocol() { - return {identifier: 'ava-6', ava: {version: '6.0.0'}, projectDir: __dirname}; + return {identifier: isAva8 ? 'ava-8' : 'ava-6', ava: {version: isAva8 ? '8.0.0' : '6.0.0'}, projectDir: import.meta.dirname}; }, }); const worker = provider.worker({ - extensionsToLoadAsModules: [], + ...(!isAva8 && {extensionsToLoadAsModules: []}), state: {}, - ...JSON.parse(process.argv[2]), + ...JSON.parse(process.argv[3]), }); -const reference = path.resolve(process.argv[3]); +const reference = path.resolve(process.argv[4]); if (worker.canLoad(reference)) { - worker.load(reference, {requireFn: createRequire(import.meta.url)}); + if (isAva8) { + worker.load(reference); + } else { + worker.load(reference, {requireFn: createRequire(import.meta.url)}); + } } diff --git a/test/fixtures/load/compiled/index.js b/test/fixtures/load/compiled/index.js index 3d88d7a..f34e420 100644 --- a/test/fixtures/load/compiled/index.js +++ b/test/fixtures/load/compiled/index.js @@ -1,2 +1,2 @@ +"use strict"; console.log('logged in fixtures/load/index.ts'); -export {}; diff --git a/test/fixtures/load/tsconfig.json b/test/fixtures/load/tsconfig.json index a00ea6f..41da438 100644 --- a/test/fixtures/load/tsconfig.json +++ b/test/fixtures/load/tsconfig.json @@ -1,7 +1,5 @@ { "compilerOptions": { - "strictNullChecks": true, - "module": "node18", "outDir": "compiled" }, "include": [ diff --git a/test/fixtures/tsconfig.json b/test/fixtures/tsconfig.json index 5051a16..8caba87 100644 --- a/test/fixtures/tsconfig.json +++ b/test/fixtures/tsconfig.json @@ -1,11 +1,8 @@ { "compilerOptions": { - "strictNullChecks": true, - "lib": [ - "es2022", - "dom" - ], - "outDir": "typescript/compiled" + "outDir": "./typescript/compiled", + "rootDir": "./typescript", + "tsBuildInfoFile": "./typescript/compiled/.tsbuildinfo" }, "include": [ "typescript" diff --git a/test/load.js b/test/load.js index 292aa20..24c32c4 100644 --- a/test/load.js +++ b/test/load.js @@ -1,61 +1,90 @@ import path from 'node:path'; -import {fileURLToPath} from 'node:url'; import test from 'ava'; import {execaNode} from 'execa'; import createProviderMacro from './_with-provider.js'; -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const withProvider = createProviderMacro('ava-6', '6.0.0', path.join(__dirname, 'fixtures')); - const setup = async provider => ({ - state: await provider.main({ - config: { - rewritePaths: { - 'load/': 'load/compiled/', + state: await provider + .main({ + config: { + rewritePaths: { + 'load/': 'load/compiled/', + }, + compile: false, }, - compile: false, - }, - }).compile(), + }) + .compile(), }); -test('worker(): load .cts', withProvider, async (t, provider) => { - const {state} = await setup(provider); - const {stdout, stderr} = await execaNode( - path.join(__dirname, 'fixtures/install-and-load'), - [JSON.stringify({state}), path.join(__dirname, 'fixtures/load', 'index.cts')], - {cwd: path.join(__dirname, 'fixtures')}, - ); - if (stderr.length > 0) { - t.log(stderr); - } +for (const [identifier, withProvider] of [ + [ + 'ava-6', + createProviderMacro( + 'ava-6', + '6.0.0', + path.join(import.meta.dirname, 'fixtures'), + ), + ], + [ + 'ava-8', + createProviderMacro( + 'ava-8', + '8.0.0', + path.join(import.meta.dirname, 'fixtures'), + ), + ], +]) { + test('worker(): load .cts', withProvider, async (t, provider) => { + const {state} = await setup(provider); + const {stdout, stderr} = await execaNode( + path.join(import.meta.dirname, 'fixtures/install-and-load'), + [ + identifier, + JSON.stringify({state}), + path.join(import.meta.dirname, 'fixtures/load', 'index.cts'), + ], + {cwd: path.join(import.meta.dirname, 'fixtures')}, + ); + if (stderr.length > 0) { + t.log(stderr); + } - t.snapshot(stdout); -}); + t.snapshot(stdout); + }); -test('worker(): load .mts', withProvider, async (t, provider) => { - const {state} = await setup(provider); - const {stdout, stderr} = await execaNode( - path.join(__dirname, 'fixtures/install-and-load'), - [JSON.stringify({state}), path.join(__dirname, 'fixtures/load', 'index.mts')], - {cwd: path.join(__dirname, 'fixtures')}, - ); - if (stderr.length > 0) { - t.log(stderr); - } + test('worker(): load .mts', withProvider, async (t, provider) => { + const {state} = await setup(provider); + const {stdout, stderr} = await execaNode( + path.join(import.meta.dirname, 'fixtures/install-and-load'), + [ + identifier, + JSON.stringify({state}), + path.join(import.meta.dirname, 'fixtures/load', 'index.mts'), + ], + {cwd: path.join(import.meta.dirname, 'fixtures')}, + ); + if (stderr.length > 0) { + t.log(stderr); + } - t.snapshot(stdout); -}); + t.snapshot(stdout); + }); -test('worker(): load .ts', withProvider, async (t, provider) => { - const {state} = await setup(provider); - const {stdout, stderr} = await execaNode( - path.join(__dirname, 'fixtures/install-and-load'), - [JSON.stringify({extensionsToLoadAsModules: ['js'], state}), path.join(__dirname, 'fixtures/load', 'index.ts')], - {cwd: path.join(__dirname, 'fixtures')}, - ); - if (stderr.length > 0) { - t.log(stderr); - } + test('worker(): load .ts', withProvider, async (t, provider) => { + const {state} = await setup(provider); + const {stdout, stderr} = await execaNode( + path.join(import.meta.dirname, 'fixtures/install-and-load'), + [ + identifier, + JSON.stringify({extensionsToLoadAsModules: ['js'], state}), + path.join(import.meta.dirname, 'fixtures/load', 'index.ts'), + ], + {cwd: path.join(import.meta.dirname, 'fixtures')}, + ); + if (stderr.length > 0) { + t.log(stderr); + } - t.snapshot(stdout); -}); + t.snapshot(stdout); + }); +} diff --git a/test/protocol-ava-6.js b/test/protocol-ava-6.js index b0ce7fa..faab3c1 100644 --- a/test/protocol-ava-6.js +++ b/test/protocol-ava-6.js @@ -1,16 +1,14 @@ -import fs from 'node:fs'; import path from 'node:path'; -import {fileURLToPath} from 'node:url'; import test from 'ava'; +import pkg from '../package.json' with {type: 'json'}; import createProviderMacro from './_with-provider.js'; -const projectDirectory = path.dirname(fileURLToPath(import.meta.url)); -const package_ = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url))); +const projectDirectory = import.meta.dirname; const withProvider = createProviderMacro('ava-6', '5.3.0'); const validateConfig = (t, provider, config) => { const error = t.throws(() => provider.main({config})); - error.message = error.message.replace(`v${package_.version}`, 'v${pkg.version}'); // eslint-disable-line no-template-curly-in-string + error.message = error.message.replace(`v${pkg.version}`, 'v${pkg.version}'); // eslint-disable-line no-template-curly-in-string t.snapshot(error); }; diff --git a/test/protocol-ava-8.js b/test/protocol-ava-8.js new file mode 100644 index 0000000..24f2aad --- /dev/null +++ b/test/protocol-ava-8.js @@ -0,0 +1,148 @@ +import path from 'node:path'; +import test from 'ava'; +import pkg from '../package.json' with {type: 'json'}; +import createProviderMacro from './_with-provider.js'; + +const projectDirectory = import.meta.dirname; +const withProvider = createProviderMacro('ava-8', '8.0.0'); + +const validateConfig = (t, provider, config) => { + const error = t.throws(() => provider.main({config})); + error.message = error.message.replace(`v${pkg.version}`, 'v${pkg.version}'); // eslint-disable-line no-template-curly-in-string + t.snapshot(error); +}; + +test('negotiates ava-8 protocol', withProvider, t => t.plan(2)); + +test('main() config validation: throw when config is not a plain object', withProvider, (t, provider) => { + validateConfig(t, provider, false); + validateConfig(t, provider, true); + validateConfig(t, provider, null); + validateConfig(t, provider, []); +}); + +test('main() config validation: throw when config contains keys other than \'extensions\', \'rewritePaths\' or \'compile\'', withProvider, (t, provider) => { + validateConfig(t, provider, {compile: false, foo: 1, rewritePaths: {'src/': 'build/'}}); +}); + +test('main() config validation: throw when config.extensions contains empty strings', withProvider, (t, provider) => { + validateConfig(t, provider, {extensions: ['']}); +}); + +test('main() config validation: throw when config.extensions contains non-strings', withProvider, (t, provider) => { + validateConfig(t, provider, {extensions: [1]}); +}); + +test('main() config validation: throw when config.extensions contains duplicates', withProvider, (t, provider) => { + validateConfig(t, provider, {extensions: ['ts', 'ts']}); +}); + +test('main() config validation: config may not be an empty object', withProvider, (t, provider) => { + validateConfig(t, provider, {}); +}); + +test('main() config validation: throw when config.compile is invalid', withProvider, (t, provider) => { + validateConfig(t, provider, {rewritePaths: {'src/': 'build/'}, compile: 1}); + validateConfig(t, provider, {rewritePaths: {'src/': 'build/'}, compile: undefined}); +}); + +test('main() config validation: rewrite paths must end in a /', withProvider, (t, provider) => { + validateConfig(t, provider, {rewritePaths: {src: 'build/', compile: false}}); + validateConfig(t, provider, {rewritePaths: {'src/': 'build', compile: false}}); +}); + +test('main() extensions: defaults to [\'ts\', \'mts\']', withProvider, (t, provider) => { + t.deepEqual(provider.main({config: {rewritePaths: {'src/': 'build/'}, compile: false}}).extensions, ['ts', 'mts']); +}); + +test('main() extensions: returns configured extensions', withProvider, (t, provider) => { + const extensions = ['tsx']; + t.deepEqual(provider.main({config: {extensions, rewritePaths: {'src/': 'build/'}, compile: false}}).extensions, extensions); +}); + +test('main() extensions: always returns new arrays', withProvider, (t, provider) => { + const main = provider.main({config: {rewritePaths: {'src/': 'build/'}, compile: false}}); + t.not(main.extensions, main.extensions); +}); + +test('main() updateGlobs()', withProvider, (t, provider) => { + const main = provider.main({config: {rewritePaths: {'src/': 'build/'}, compile: false}}); + t.snapshot(main.updateGlobs({ + filePatterns: ['src/test.ts'], + ignoredByWatcherPatterns: ['assets/**'], + })); +}); + +test('main() interpretChange() without compilation', withProvider, (t, provider) => { + const main = provider.main({config: {rewritePaths: {'src/': 'build/'}, compile: false}}); + t.is(main.interpretChange(path.join(projectDirectory, 'src/foo.ts')), main.changeInterpretations.waitForOutOfBandCompilation); + t.is(main.interpretChange(path.join(projectDirectory, 'build/foo.js')), main.changeInterpretations.unspecified); + t.is(main.interpretChange(path.join(projectDirectory, 'src/foo.txt')), main.changeInterpretations.unspecified); +}); + +test('main() interpretChange() with compilation', withProvider, (t, provider) => { + const main = provider.main({config: {rewritePaths: {'src/': 'build/'}, compile: 'tsc'}}); + t.is(main.interpretChange(path.join(projectDirectory, 'src/foo.ts')), main.changeInterpretations.unspecified); + t.is(main.interpretChange(path.join(projectDirectory, 'build/foo.js')), main.changeInterpretations.ignoreCompiled); + t.is(main.interpretChange(path.join(projectDirectory, 'src/foo.txt')), main.changeInterpretations.unspecified); +}); + +test('main() resolvePossibleOutOfBandCompilationSources() with compilation', withProvider, (t, provider) => { + const main = provider.main({config: {rewritePaths: {'src/': 'build/'}, compile: 'tsc'}}); + t.is(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDirectory, 'build/foo.js')), null); +}); + +test('main() resolvePossibleOutOfBandCompilationSources() unknown extension', withProvider, (t, provider) => { + const main = provider.main({config: {rewritePaths: {'src/': 'build/'}, compile: false}}); + t.is(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDirectory, 'build/foo.bar')), null); +}); + +test('main() resolvePossibleOutOfBandCompilationSources() not a build path', withProvider, (t, provider) => { + const main = provider.main({config: {rewritePaths: {'src/': 'build/'}, compile: false}}); + t.is(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDirectory, 'lib/foo.js')), null); +}); + +test('main() resolvePossibleOutOfBandCompilationSources() .cjs but .cts not configured', withProvider, (t, provider) => { + const main = provider.main({config: {extensions: ['ts'], rewritePaths: {'src/': 'build/'}, compile: false}}); + t.is(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDirectory, 'build/foo.cjs')), null); +}); + +test('main() resolvePossibleOutOfBandCompilationSources() .mjs but .mts not configured', withProvider, (t, provider) => { + const main = provider.main({config: {extensions: ['ts'], rewritePaths: {'src/': 'build/'}, compile: false}}); + t.is(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDirectory, 'build/foo.mjs')), null); +}); + +test('main() resolvePossibleOutOfBandCompilationSources() .js but .ts not configured', withProvider, (t, provider) => { + const main = provider.main({config: {extensions: ['cts'], rewritePaths: {'src/': 'build/'}, compile: false}}); + t.is(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDirectory, 'build/foo.js')), null); +}); + +test('main() resolvePossibleOutOfBandCompilationSources() .cjs and .cjs and .cts configured', withProvider, (t, provider) => { + const main = provider.main({config: {extensions: ['cjs', 'cts'], rewritePaths: {'src/': 'build/'}, compile: false}}); + t.deepEqual(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDirectory, 'build/foo.cjs')), [ + path.join(projectDirectory, 'src/foo.cjs'), + path.join(projectDirectory, 'src/foo.cts'), + ]); +}); + +test('main() resolvePossibleOutOfBandCompilationSources() .mjs and .mjs and .mts configured', withProvider, (t, provider) => { + const main = provider.main({config: {extensions: ['mjs', 'mts'], rewritePaths: {'src/': 'build/'}, compile: false}}); + t.deepEqual(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDirectory, 'build/foo.mjs')), [ + path.join(projectDirectory, 'src/foo.mjs'), + path.join(projectDirectory, 'src/foo.mts'), + ]); +}); + +test('main() resolvePossibleOutOfBandCompilationSources() .js and .js, .ts and .tsx configured', withProvider, (t, provider) => { + const main = provider.main({config: {extensions: ['js', 'ts', 'tsx'], rewritePaths: {'src/': 'build/'}, compile: false}}); + t.deepEqual(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDirectory, 'build/foo.js')), [ + path.join(projectDirectory, 'src/foo.js'), + path.join(projectDirectory, 'src/foo.ts'), + path.join(projectDirectory, 'src/foo.tsx'), + ]); +}); + +test('main() resolvePossibleOutOfBandCompilationSources() returns the first possible path that exists', withProvider, (t, provider) => { + const main = provider.main({config: {extensions: ['js', 'ts', 'tsx'], rewritePaths: {'fixtures/load/': 'fixtures/load/compiled/'}, compile: false}}); + t.deepEqual(main.resolvePossibleOutOfBandCompilationSources(path.join(projectDirectory, 'fixtures/load/compiled/index.js')), [path.join(projectDirectory, 'fixtures/load/index.ts')]); +}); diff --git a/test/snapshots/compilation.js.md b/test/snapshots/compilation.js.md index f036173..eca3e13 100644 --- a/test/snapshots/compilation.js.md +++ b/test/snapshots/compilation.js.md @@ -4,19 +4,39 @@ The actual snapshot is saved in `compilation.js.snap`. Generated by [AVA](https://avajs.dev). -## worker(): load rewritten paths files +## worker(): load rewritten paths files (with ava-6 provider) > Snapshot 1 'logged in file.js' -## worker(): runs compiled files +## worker(): runs compiled files (with ava-6 provider) > Snapshot 1 'logged in fixtures/typescript/index.ts' -## compile() error +## compile() error (with ava-6 provider) + +> Snapshot 1 + + `Command failed with exit code 2: tsc --incremental␊ + ␊ + typescript/typescript.ts(1,1): error TS2304: Cannot find name 'a'.` + +## worker(): load rewritten paths files (with ava-8 provider) + +> Snapshot 1 + + 'logged in file.js' + +## worker(): runs compiled files (with ava-8 provider) + +> Snapshot 1 + + 'logged in fixtures/typescript/index.ts' + +## compile() error (with ava-8 provider) > Snapshot 1 diff --git a/test/snapshots/compilation.js.snap b/test/snapshots/compilation.js.snap index ae9adce..7911f71 100644 Binary files a/test/snapshots/compilation.js.snap and b/test/snapshots/compilation.js.snap differ diff --git a/test/snapshots/load.js.md b/test/snapshots/load.js.md index 8e1e28e..c95822d 100644 --- a/test/snapshots/load.js.md +++ b/test/snapshots/load.js.md @@ -4,19 +4,37 @@ The actual snapshot is saved in `load.js.snap`. Generated by [AVA](https://avajs.dev). -## worker(): load .cts +## worker(): load .cts (with ava-6 provider) > Snapshot 1 'logged in fixtures/load/index.cts' -## worker(): load .mts +## worker(): load .mts (with ava-6 provider) > Snapshot 1 'logged in fixtures/load/index.mts' -## worker(): load .ts +## worker(): load .ts (with ava-6 provider) + +> Snapshot 1 + + 'logged in fixtures/load/index.ts' + +## worker(): load .cts (with ava-8 provider) + +> Snapshot 1 + + '' + +## worker(): load .mts (with ava-8 provider) + +> Snapshot 1 + + 'logged in fixtures/load/index.mts' + +## worker(): load .ts (with ava-8 provider) > Snapshot 1 diff --git a/test/snapshots/load.js.snap b/test/snapshots/load.js.snap index 8aff8c9..3922b2b 100644 Binary files a/test/snapshots/load.js.snap and b/test/snapshots/load.js.snap differ diff --git a/test/snapshots/protocol-ava-8.js.md b/test/snapshots/protocol-ava-8.js.md new file mode 100644 index 0000000..5f6212a --- /dev/null +++ b/test/snapshots/protocol-ava-8.js.md @@ -0,0 +1,117 @@ +# Snapshot report for `test/protocol-ava-8.js` + +The actual snapshot is saved in `protocol-ava-8.js.snap`. + +Generated by [AVA](https://avajs.dev). + +## main() config validation: throw when config is not a plain object + +> Snapshot 1 + + Error { + message: 'Unexpected Typescript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md', + } + +> Snapshot 2 + + Error { + message: 'Unexpected Typescript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md', + } + +> Snapshot 3 + + Error { + message: 'Unexpected Typescript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md', + } + +> Snapshot 4 + + Error { + message: 'Unexpected Typescript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md', + } + +## main() config validation: throw when config contains keys other than 'extensions', 'rewritePaths' or 'compile' + +> Snapshot 1 + + Error { + message: 'Unexpected \'foo\' property in TypeScript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md', + } + +## main() config validation: throw when config.extensions contains empty strings + +> Snapshot 1 + + Error { + message: 'Missing \'compile\' property in TypeScript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md', + } + +## main() config validation: throw when config.extensions contains non-strings + +> Snapshot 1 + + Error { + message: 'Missing \'compile\' property in TypeScript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md', + } + +## main() config validation: throw when config.extensions contains duplicates + +> Snapshot 1 + + Error { + message: 'Missing \'compile\' property in TypeScript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md', + } + +## main() config validation: config may not be an empty object + +> Snapshot 1 + + Error { + message: 'Missing \'compile\' property in TypeScript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md', + } + +## main() config validation: throw when config.compile is invalid + +> Snapshot 1 + + Error { + message: 'Invalid \'compile\' property in TypeScript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md', + } + +> Snapshot 2 + + Error { + message: 'Invalid \'compile\' property in TypeScript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md', + } + +## main() config validation: rewrite paths must end in a / + +> Snapshot 1 + + Error { + message: 'Missing \'compile\' property in TypeScript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md', + } + +> Snapshot 2 + + Error { + message: 'Missing \'compile\' property in TypeScript configuration for AVA. See https://github.com/avajs/typescript/blob/v${pkg.version}/README.md', + } + +## main() updateGlobs() + +> Snapshot 1 + + { + filePatterns: [ + 'src/test.ts', + '!**/*.d.ts', + '!build/**', + ], + ignoredByWatcherPatterns: [ + 'assets/**', + 'build/**/*.js.map', + 'build/**/*.cjs.map', + 'build/**/*.mjs.map', + ], + } diff --git a/test/snapshots/protocol-ava-8.js.snap b/test/snapshots/protocol-ava-8.js.snap new file mode 100644 index 0000000..b1c0239 Binary files /dev/null and b/test/snapshots/protocol-ava-8.js.snap differ