Skip to content
Closed
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/spicy-laws-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/intent': patch
---

- Added Deno monorepo setup coverage so setup-github-actions writes workflows at the workspace root and preserves monorepo-aware path substitutions.
5 changes: 5 additions & 0 deletions .changeset/vast-bags-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/intent': patch
---

Fix intent stale so monorepo package paths resolve to the targeted workspace package instead of scanning the whole workspace.
24 changes: 22 additions & 2 deletions packages/intent/src/cli-support.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { existsSync, readFileSync } from 'node:fs'
import { dirname, join, relative } from 'node:path'
import { dirname, join, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { fail } from './cli-error.js'
import { resolveProjectContext } from './core/project-context.js'
import type { ScanResult, StalenessReport } from './types.js'

export function printWarnings(warnings: Array<string>): void {
Expand Down Expand Up @@ -47,10 +48,29 @@ export async function resolveStaleTargets(
targetDir?: string,
): Promise<{ reports: Array<StalenessReport> }> {
const resolvedRoot = targetDir
? join(process.cwd(), targetDir)
? resolve(process.cwd(), targetDir)
: process.cwd()
const context = resolveProjectContext({
cwd: process.cwd(),
targetPath: targetDir,
})
const { checkStaleness } = await import('./staleness.js')

const targetsResolvedPackage =
context.packageRoot !== null &&
(context.targetSkillsDir !== null || resolvedRoot !== context.workspaceRoot)

if (targetsResolvedPackage && context.packageRoot) {
return {
reports: [
await checkStaleness(
context.packageRoot,
readPackageName(context.packageRoot),
),
],
}
}

if (existsSync(join(resolvedRoot, 'skills'))) {
return {
reports: [
Expand Down
104 changes: 102 additions & 2 deletions packages/intent/src/workspace-patterns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,76 @@ function hasPackageJson(dir: string): boolean {
return existsSync(join(dir, 'package.json'))
}

function stripJsonCommentsAndTrailingCommas(source: string): string {
let result = ''
let inString = false
let escaped = false

for (let index = 0; index < source.length; index += 1) {
const char = source[index]!
const next = source[index + 1]

if (inString) {
result += char
if (escaped) {
escaped = false
} else if (char === '\\') {
escaped = true
} else if (char === '"') {
inString = false
}
continue
}

if (char === '"') {
inString = true
result += char
continue
}

if (char === '/' && next === '/') {
while (index < source.length && source[index] !== '\n') {
index += 1
}
if (index < source.length) {
result += source[index]!
}
continue
}

if (char === '/' && next === '*') {
index += 2
while (
index < source.length &&
!(source[index] === '*' && source[index + 1] === '/')
) {
index += 1
}
index += 1
continue
}

if (char === ',') {
let lookahead = index + 1
while (lookahead < source.length && /\s/.test(source[lookahead]!)) {
lookahead += 1
}
if (source[lookahead] === '}' || source[lookahead] === ']') {
continue
}
}

result += char
}

return result
}

function readJsonFile(path: string, jsonc = false): unknown {
const source = readFileSync(path, 'utf8')
return JSON.parse(jsonc ? stripJsonCommentsAndTrailingCommas(source) : source)
}

export function readWorkspacePatterns(root: string): Array<string> | null {
const pnpmWs = join(root, 'pnpm-workspace.yaml')
if (existsSync(pnpmWs)) {
Expand All @@ -49,10 +119,16 @@ export function readWorkspacePatterns(root: string): Array<string> | null {
const pkgPath = join(root, 'package.json')
if (existsSync(pkgPath)) {
try {
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
const pkg = readJsonFile(pkgPath) as {
workspaces?: unknown | { packages?: unknown }
}
const patterns =
parseWorkspacePatterns(pkg.workspaces) ??
parseWorkspacePatterns(pkg.workspaces?.packages)
parseWorkspacePatterns(
typeof pkg.workspaces === 'object' && pkg.workspaces !== null
? (pkg.workspaces as Record<string, unknown>).packages
: undefined,
)
if (patterns) {
return patterns
}
Expand All @@ -63,6 +139,30 @@ export function readWorkspacePatterns(root: string): Array<string> | null {
}
}

for (const denoConfigName of ['deno.json', 'deno.jsonc']) {
const denoConfigPath = join(root, denoConfigName)
if (!existsSync(denoConfigPath)) {
continue
}

try {
const denoConfig = readJsonFile(
denoConfigPath,
denoConfigName.endsWith('.jsonc'),
) as {
workspace?: unknown
}
const patterns = parseWorkspacePatterns(denoConfig.workspace)
if (patterns) {
return patterns
}
} catch (err: unknown) {
console.error(
`Warning: failed to parse ${denoConfigPath}: ${err instanceof Error ? err.message : err}`,
)
}
}

return null
}

Expand Down
123 changes: 123 additions & 0 deletions packages/intent/tests/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,43 @@ describe('cli commands', () => {
expect(output).toContain('Template variables applied:')
})

it('copies github workflow templates to the workspace root', async () => {
const root = mkdtempSync(join(realTmpdir, 'intent-cli-setup-gha-mono-'))
tempDirs.push(root)

writeJson(join(root, 'package.json'), {
private: true,
workspaces: ['packages/*'],
})
writeJson(join(root, 'packages', 'router', 'package.json'), {
name: '@tanstack/router',
version: '1.0.0',
intent: { version: 1, repo: 'TanStack/router', docs: 'docs/' },
})
writeSkillMd(join(root, 'packages', 'router', 'skills', 'routing'), {
name: 'routing',
description: 'Routing skill',
})

process.chdir(join(root, 'packages', 'router'))

const exitCode = await main(['setup-github-actions'])
const rootWorkflowsDir = join(root, '.github', 'workflows')
const packageWorkflowsDir = join(
root,
'packages',
'router',
'.github',
'workflows',
)
const output = logSpy.mock.calls.flat().join('\n')

expect(exitCode).toBe(0)
expect(existsSync(rootWorkflowsDir)).toBe(true)
expect(existsSync(packageWorkflowsDir)).toBe(false)
expect(output).toContain('Mode: monorepo')
})

it('lists installed intent packages as json', async () => {
const root = mkdtempSync(join(realTmpdir, 'intent-cli-list-'))
tempDirs.push(root)
Expand Down Expand Up @@ -484,6 +521,92 @@ describe('cli commands', () => {

fetchSpy.mockRestore()
})

it('checks only the targeted workspace package for staleness', async () => {
const root = mkdtempSync(join(realTmpdir, 'intent-cli-stale-target-'))
tempDirs.push(root)

writeJson(join(root, 'package.json'), {
private: true,
workspaces: ['packages/*'],
})
writeJson(join(root, 'packages', 'router', 'package.json'), {
name: '@tanstack/router',
})
writeJson(join(root, 'packages', 'query', 'package.json'), {
name: '@tanstack/query',
})
writeSkillMd(join(root, 'packages', 'router', 'skills', 'routing'), {
name: 'routing',
description: 'Routing skill',
library_version: '1.0.0',
})
writeSkillMd(join(root, 'packages', 'query', 'skills', 'cache'), {
name: 'cache',
description: 'Caching skill',
library_version: '1.0.0',
})

const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
ok: true,
json: async () => ({ version: '1.0.0' }),
} as Response)

process.chdir(root)

const exitCode = await main(['stale', 'packages/router/skills', '--json'])
const output = logSpy.mock.calls.at(-1)?.[0]
const reports = JSON.parse(String(output)) as Array<{ library: string }>

expect(exitCode).toBe(0)
expect(reports).toHaveLength(1)
expect(reports[0]!.library).toBe('@tanstack/router')

fetchSpy.mockRestore()
})

it('checks the current workspace package for staleness from package cwd', async () => {
const root = mkdtempSync(join(realTmpdir, 'intent-cli-stale-package-cwd-'))
tempDirs.push(root)

writeJson(join(root, 'package.json'), {
private: true,
workspaces: ['packages/*'],
})
writeJson(join(root, 'packages', 'router', 'package.json'), {
name: '@tanstack/router',
})
writeJson(join(root, 'packages', 'query', 'package.json'), {
name: '@tanstack/query',
})
writeSkillMd(join(root, 'packages', 'router', 'skills', 'routing'), {
name: 'routing',
description: 'Routing skill',
library_version: '1.0.0',
})
writeSkillMd(join(root, 'packages', 'query', 'skills', 'cache'), {
name: 'cache',
description: 'Caching skill',
library_version: '1.0.0',
})

const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
ok: true,
json: async () => ({ version: '1.0.0' }),
} as Response)

process.chdir(join(root, 'packages', 'router'))

const exitCode = await main(['stale', '--json'])
const output = logSpy.mock.calls.at(-1)?.[0]
const reports = JSON.parse(String(output)) as Array<{ library: string }>

expect(exitCode).toBe(0)
expect(reports).toHaveLength(1)
expect(reports[0]!.library).toBe('@tanstack/router')

fetchSpy.mockRestore()
})
})

describe('package metadata', () => {
Expand Down
27 changes: 27 additions & 0 deletions packages/intent/tests/project-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,31 @@ describe('resolveProjectContext', () => {
expect(context.isMonorepo).toBe(true)
expect(context.packageRoot).toBe(packageRoot)
})

it('detects Deno workspaces from a workspace package cwd', () => {
const root = createRoot()
writeJson(join(root, 'package.json'), { name: 'repo-root', private: true })
writeFileSync(
join(root, 'deno.jsonc'),
`{
"workspace": [
"packages/*",
],
}
`,
)
const packageRoot = createWorkspacePackage(root, 'router')

const context = resolveProjectContext({ cwd: packageRoot })

expect(context).toEqual({
cwd: packageRoot,
workspaceRoot: root,
packageRoot,
isMonorepo: true,
workspacePatterns: ['packages/*'],
targetPackageJsonPath: join(packageRoot, 'package.json'),
targetSkillsDir: join(packageRoot, 'skills'),
})
})
})
Loading
Loading