Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/windows-tsgo-close-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/intent': patch
---

Spawn the TypeScript 7 `tsgo` compiler process from the parent directory in `withNativeCompiler` while keeping `--cwd` set to the project root, so Windows callers can remove or rename the checked directory immediately after `intent validate` or `intent repair` without hitting `EPERM`.
12 changes: 11 additions & 1 deletion packages/intent/src/validate/blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,13 +214,23 @@ async function withNativeCompiler<T>(
return await run(async (sources, compilerOptions) => {
// A new configuration per call opens a new project that lists only
// this call's files, whatever the compiler process kept from earlier.
const first = projects === 0
const config = `${virtualDir}/tsconfig-${projects++}.json`
for (const [path, code] of sources) files.set(path, code)
files.set(
config,
JSON.stringify({ compilerOptions, files: [...sources.keys()] }),
)
const snapshot = await api.updateSnapshot({ openProjects: [config] })
// Spawn tsgo from the parent directory so Windows does not lock root
// while the child process closes its stdio pipe after api.close().
const cwd = process.cwd()
if (first) process.chdir(dirname(root))
let snapshot: NativeApi.Snapshot
try {
snapshot = await api.updateSnapshot({ openProjects: [config] })
} finally {
if (first) process.chdir(cwd)
}
const project = snapshot.getProject(config)
if (!project) throw new Error(`could not open ${config}`)
return project
Expand Down
6 changes: 3 additions & 3 deletions packages/intent/tests/integration/load-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ const tempDirs: Array<string> = []
beforeAll(async () => {
registry = await startRegistry()
publishFixtures(registry.url)
}, 30_000)
}, 60_000)

afterAll(() => {
registry?.stop()
afterAll(async () => {
await registry?.stop()
for (const dir of tempDirs) {
rmSync(dir, { recursive: true, force: true })
}
Expand Down
9 changes: 4 additions & 5 deletions packages/intent/tests/integration/packed-release.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { execFileSync, spawnSync } from 'node:child_process'
import { execFileSync, execSync, spawnSync } from 'node:child_process'
import {
existsSync,
mkdirSync,
Expand Down Expand Up @@ -37,9 +37,8 @@ function run(args: Array<string>) {
beforeAll(() => {
root = realpathSync(mkdtempSync(join(tmpdir(), 'intent-packed-release-')))
const packed = JSON.parse(
execFileSync(
'npm',
['pack', '--ignore-scripts', '--json', '--pack-destination', root],
execSync(
`npm pack --ignore-scripts --json --pack-destination=${JSON.stringify(root)}`,
{
cwd: packageRoot,
encoding: 'utf8',
Expand Down Expand Up @@ -348,7 +347,7 @@ describe('packed release', () => {
expect(synced.status, synced.stderr).toBe(0)
expect(run(['maintainer', 'check']).status).toBe(1)
const packed = JSON.parse(
execFileSync('npm', ['pack', '--dry-run', '--ignore-scripts', '--json'], {
execSync('npm pack --dry-run --ignore-scripts --json', {
cwd,
encoding: 'utf8',
timeout,
Expand Down
23 changes: 13 additions & 10 deletions packages/intent/tests/integration/scaffold.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function getFreePort(): Promise<number> {

export interface Registry {
url: string
stop: () => void
stop: () => Promise<void>
}

export async function startRegistry(): Promise<Registry> {
Expand Down Expand Up @@ -70,26 +70,23 @@ export async function startRegistry(): Promise<Registry> {
].join('\n'),
)

const isWindows = process.platform === 'win32'
const verdaccioBin = join(
thisDir,
'..',
'..',
'node_modules',
'.bin',
'verdaccio',
'bin',
'verdaccio',
)

return new Promise((resolve, reject) => {
const child = spawn(
isWindows ? `"${verdaccioBin}"` : verdaccioBin,
isWindows
? ['--config', `"${configPath}"`, '--listen', `127.0.0.1:${port}`]
: ['--config', configPath, '--listen', `127.0.0.1:${port}`],
process.execPath,
[verdaccioBin, '--config', configPath, '--listen', `127.0.0.1:${port}`],
{
stdio: ['ignore', 'pipe', 'pipe'],
detached: false,
shell: isWindows,
},
)

Expand All @@ -109,8 +106,14 @@ export async function startRegistry(): Promise<Registry> {
clearTimeout(timeout)
resolve({
url: `http://127.0.0.1:${port}`,
stop: () => {
child.kill('SIGTERM')
stop: async () => {
if (child.exitCode === null) {
const closed = new Promise<void>((done) =>
child.once('close', () => done()),
)
child.kill('SIGTERM')
await closed
}
rmSync(storageDir, { recursive: true, force: true })
},
})
Expand Down
6 changes: 3 additions & 3 deletions packages/intent/tests/integration/scanner-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ function expectLeafCoreSkill(parsed: CliResult['parsed']): void {
beforeAll(async () => {
registry = await startRegistry()
publishFixtures(registry.url)
}, 30_000)
}, 60_000)

afterAll(() => {
registry?.stop()
afterAll(async () => {
await registry?.stop()
for (const dir of tempDirs) {
rmSync(dir, { recursive: true, force: true })
}
Expand Down
21 changes: 13 additions & 8 deletions packages/intent/tests/reusable-workflows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
writeFileSync,
} from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { delimiter, dirname, join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { afterEach, beforeEach, expect, it } from 'vitest'
import { parse } from 'yaml'

Expand Down Expand Up @@ -37,7 +38,7 @@ function run(script: string, env: Record<string, string> = {}) {
encoding: 'utf8',
env: {
...process.env,
PATH: `${join(root, 'bin')}:${process.env.PATH}`,
PATH: [join(root, 'bin'), process.env.PATH].join(delimiter),
GITHUB_OUTPUT: join(root, 'output'),
GITHUB_PATH: join(root, 'path'),
RUNNER_TEMP: join(root, 'temp'),
Expand Down Expand Up @@ -78,7 +79,7 @@ esac
expect(installed.status, installed.stdout + installed.stderr).toBe(0)
const intentPath = readFileSync(join(root, 'path'), 'utf8').trim()
const invoked = run('intent validate --github-summary', {
PATH: `${intentPath}:${join(root, 'bin')}:${process.env.PATH}`,
PATH: [intentPath, join(root, 'bin'), process.env.PATH].join(delimiter),
})
expect(invoked.status, invoked.stderr).toBe(0)
expect(readFileSync(join(root, 'invoked'), 'utf8')).toBe(
Expand Down Expand Up @@ -173,7 +174,7 @@ it.each(['', 'planning records/$(touch injected)'])(
const trace = join(root, 'arguments.json')
writeFileSync(
join(root, 'intent'),
`#!${process.execPath}\nrequire('node:fs').writeFileSync(${JSON.stringify(trace)}, JSON.stringify(process.argv.slice(2)))\n`,
`#!/usr/bin/env node\nrequire('node:fs').writeFileSync(${JSON.stringify(trace)}, JSON.stringify(process.argv.slice(2)))\n`,
{ mode: 0o755 },
)
const check = workflow(
Expand All @@ -186,7 +187,9 @@ it.each(['', 'planning records/$(touch injected)'])(
encoding: 'utf8',
env: {
...process.env,
PATH: `${root}:${process.env.PATH}`,
PATH: [root, dirname(process.execPath), process.env.PATH].join(
delimiter,
),
INTENT_REVIEW_BASE: 'base-sha',
INTENT_ARTIFACTS: artifacts,
},
Expand Down Expand Up @@ -243,7 +246,7 @@ it.each([
const argumentsPath = join(root, 'arguments.jsonl')
writeFileSync(
join(bin, 'intent'),
`#!${process.execPath}
`#!/usr/bin/env node
const fs = require('node:fs');
const args = process.argv.slice(2);
fs.appendFileSync(${JSON.stringify(argumentsPath)}, JSON.stringify(args) + '\\n');
Expand All @@ -262,7 +265,9 @@ if (args.includes('--patch')) process.stdout.write('suggested patch\\n');
encoding: 'utf8',
env: {
...process.env,
PATH: `${bin}:${process.env.PATH}`,
PATH: [bin, dirname(process.execPath), process.env.PATH].join(
delimiter,
),
RUNNER_TEMP: temporary,
GITHUB_OUTPUT: output,
INTENT_ARTIFACTS: 'planning records/$(touch injected)',
Expand Down Expand Up @@ -357,7 +362,7 @@ function publish(
)
const result = spawnSync(
process.execPath,
['--import', preload, '--input-type=module'],
['--import', pathToFileURL(preload).href, '--input-type=module'],
{
input: script.split("<<'NODE'\n")[1]!.replace(/\nNODE\s*$/, ''),
encoding: 'utf8',
Expand Down
4 changes: 2 additions & 2 deletions packages/intent/tests/review-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
writeFileSync,
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { delimiter, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { parse } from 'yaml'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
Expand Down Expand Up @@ -261,7 +261,7 @@ it('runs the PR gate for maintainer instructions before any review state exists'
cwd: root,
env: {
...process.env,
PATH: `${join(root, 'bin')}:${process.env.PATH}`,
PATH: [join(root, 'bin'), process.env.PATH].join(delimiter),
INTENT_REVIEW_BASE: 'fixture-base',
},
}
Expand Down
14 changes: 14 additions & 0 deletions packages/intent/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import { existsSync, realpathSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { delimiter } from 'node:path'
import { defineConfig } from 'vitest/config'

const gitBin = 'C:\\Program Files\\Git\\bin'
const pathEnv =
process.platform === 'win32' && existsSync(gitBin)
? `${gitBin}${delimiter}${process.env.PATH ?? ''}`
: process.env.PATH

export default defineConfig({
test: {
name: 'intent',
// Source tests deliberately use an explicit immutable fixture reference.
// Packed-release coverage separately checks the artifact's own metadata.
env: {
INTENT_WORKFLOW_REF: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # v9.9.9',
GIT_CEILING_DIRECTORIES: [
...new Set([tmpdir(), realpathSync(tmpdir())]),
].join(delimiter),
TERM: 'xterm-256color',
...(pathEnv ? { PATH: pathEnv } : {}),
},
include: ['tests/**/*.test.ts'],
},
Expand Down
Loading