Skip to content

Commit 6261e78

Browse files
cover Deno workspaces in setup-github-actions (#106)
1 parent 6bd4357 commit 6261e78

5 files changed

Lines changed: 289 additions & 6 deletions

File tree

.changeset/spicy-laws-burn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/intent': patch
3+
---
4+
5+
- Added Deno monorepo setup coverage so setup-github-actions writes workflows at the workspace root and preserves monorepo-aware path substitutions.

packages/intent/src/workspace-patterns.ts

Lines changed: 126 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,101 @@ function hasPackageJson(dir: string): boolean {
2727
return existsSync(join(dir, 'package.json'))
2828
}
2929

30+
function stripJsonCommentsAndTrailingCommas(source: string): string {
31+
let result = ''
32+
let inString = false
33+
let escaped = false
34+
35+
for (let index = 0; index < source.length; index += 1) {
36+
const char = source[index]!
37+
const next = source[index + 1]
38+
39+
if (inString) {
40+
result += char
41+
if (escaped) {
42+
escaped = false
43+
} else if (char === '\\') {
44+
escaped = true
45+
} else if (char === '"') {
46+
inString = false
47+
}
48+
continue
49+
}
50+
51+
if (char === '"') {
52+
inString = true
53+
result += char
54+
continue
55+
}
56+
57+
if (char === '/' && next === '/') {
58+
while (index < source.length && source[index] !== '\n') {
59+
index += 1
60+
}
61+
if (index < source.length) {
62+
result += source[index]!
63+
}
64+
continue
65+
}
66+
67+
if (char === '/' && next === '*') {
68+
const commentStart = index
69+
index += 2
70+
while (
71+
index < source.length &&
72+
!(source[index] === '*' && source[index + 1] === '/')
73+
) {
74+
index += 1
75+
}
76+
if (index >= source.length) {
77+
throw new SyntaxError(
78+
`Unterminated block comment starting at position ${commentStart}`,
79+
)
80+
}
81+
index += 1
82+
continue
83+
}
84+
85+
if (char === ',') {
86+
let lookahead = index + 1
87+
while (lookahead < source.length) {
88+
const la = source[lookahead]!
89+
if (/\s/.test(la)) {
90+
lookahead += 1
91+
} else if (la === '/' && source[lookahead + 1] === '/') {
92+
lookahead += 2
93+
while (lookahead < source.length && source[lookahead] !== '\n') {
94+
lookahead += 1
95+
}
96+
} else if (la === '/' && source[lookahead + 1] === '*') {
97+
lookahead += 2
98+
while (
99+
lookahead < source.length &&
100+
!(source[lookahead] === '*' && source[lookahead + 1] === '/')
101+
) {
102+
lookahead += 1
103+
}
104+
lookahead += 2
105+
} else {
106+
break
107+
}
108+
}
109+
if (source[lookahead] === '}' || source[lookahead] === ']') {
110+
continue
111+
}
112+
}
113+
114+
result += char
115+
}
116+
117+
return result
118+
}
119+
120+
function readJsonFile(path: string, jsonc = false): unknown {
121+
const source = readFileSync(path, 'utf8')
122+
return JSON.parse(jsonc ? stripJsonCommentsAndTrailingCommas(source) : source)
123+
}
124+
30125
export function readWorkspacePatterns(root: string): Array<string> | null {
31126
const pnpmWs = join(root, 'pnpm-workspace.yaml')
32127
if (existsSync(pnpmWs)) {
@@ -40,25 +135,51 @@ export function readWorkspacePatterns(root: string): Array<string> | null {
40135
return patterns
41136
}
42137
} catch (err: unknown) {
138+
const verb = err instanceof SyntaxError ? 'parse' : 'read'
43139
console.error(
44-
`Warning: failed to parse ${pnpmWs}: ${err instanceof Error ? err.message : err}`,
140+
`Warning: failed to ${verb} ${pnpmWs}: ${err instanceof Error ? err.message : err}`,
45141
)
46142
}
47143
}
48144

49145
const pkgPath = join(root, 'package.json')
50146
if (existsSync(pkgPath)) {
51147
try {
52-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
148+
const pkg = readJsonFile(pkgPath) as Record<string, unknown>
149+
const workspaces = pkg.workspaces as Record<string, unknown> | undefined
53150
const patterns =
54-
parseWorkspacePatterns(pkg.workspaces) ??
55-
parseWorkspacePatterns(pkg.workspaces?.packages)
151+
parseWorkspacePatterns(workspaces) ??
152+
parseWorkspacePatterns(workspaces?.packages)
153+
if (patterns) {
154+
return patterns
155+
}
156+
} catch (err: unknown) {
157+
const verb = err instanceof SyntaxError ? 'parse' : 'read'
158+
console.error(
159+
`Warning: failed to ${verb} ${pkgPath}: ${err instanceof Error ? err.message : err}`,
160+
)
161+
}
162+
}
163+
164+
for (const denoConfigName of ['deno.json', 'deno.jsonc']) {
165+
const denoConfigPath = join(root, denoConfigName)
166+
if (!existsSync(denoConfigPath)) {
167+
continue
168+
}
169+
170+
try {
171+
const denoConfig = readJsonFile(denoConfigPath, true) as Record<
172+
string,
173+
unknown
174+
>
175+
const patterns = parseWorkspacePatterns(denoConfig.workspace)
56176
if (patterns) {
57177
return patterns
58178
}
59179
} catch (err: unknown) {
180+
const verb = err instanceof SyntaxError ? 'parse' : 'read'
60181
console.error(
61-
`Warning: failed to parse ${pkgPath}: ${err instanceof Error ? err.message : err}`,
182+
`Warning: failed to ${verb} ${denoConfigPath}: ${err instanceof Error ? err.message : err}`,
62183
)
63184
}
64185
}

packages/intent/tests/project-context.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,4 +136,31 @@ describe('resolveProjectContext', () => {
136136
expect(context.isMonorepo).toBe(true)
137137
expect(context.packageRoot).toBe(packageRoot)
138138
})
139+
140+
it('detects Deno workspaces from a workspace package cwd', () => {
141+
const root = createRoot()
142+
writeJson(join(root, 'package.json'), { name: 'repo-root', private: true })
143+
writeFileSync(
144+
join(root, 'deno.jsonc'),
145+
`{
146+
"workspace": [
147+
"packages/*",
148+
],
149+
}
150+
`,
151+
)
152+
const packageRoot = createWorkspacePackage(root, 'router')
153+
154+
const context = resolveProjectContext({ cwd: packageRoot })
155+
156+
expect(context).toEqual({
157+
cwd: packageRoot,
158+
workspaceRoot: root,
159+
packageRoot,
160+
isMonorepo: true,
161+
workspacePatterns: ['packages/*'],
162+
targetPackageJsonPath: join(packageRoot, 'package.json'),
163+
targetSkillsDir: join(packageRoot, 'skills'),
164+
})
165+
})
139166
})

packages/intent/tests/setup.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,72 @@ describe('runSetupGithubActions', () => {
344344

345345
rmSync(monoRoot, { recursive: true, force: true })
346346
})
347+
348+
it('writes workflows to the Deno workspace root from a workspace package', () => {
349+
const monoRoot = createMonorepo({
350+
usePackageJsonWorkspaces: true,
351+
packages: [
352+
{ name: 'router', hasSkills: true },
353+
{ name: 'start', hasSkills: true },
354+
],
355+
})
356+
357+
writeFileSync(
358+
join(monoRoot, 'package.json'),
359+
JSON.stringify({ name: 'root', private: true }, null, 2),
360+
)
361+
writeFileSync(
362+
join(monoRoot, 'deno.jsonc'),
363+
`{
364+
// Deno workspace config should be used for monorepo resolution.
365+
"workspace": [
366+
"packages/*",
367+
],
368+
}
369+
`,
370+
)
371+
writeFileSync(
372+
join(monoRoot, 'packages', 'router', 'package.json'),
373+
JSON.stringify(
374+
{
375+
name: '@tanstack/react-router',
376+
intent: { repo: 'TanStack/router', docs: 'docs/' },
377+
},
378+
null,
379+
2,
380+
),
381+
)
382+
mkdirSync(join(monoRoot, 'packages', 'router', 'src'), { recursive: true })
383+
mkdirSync(join(monoRoot, 'packages', 'router', 'docs'), { recursive: true })
384+
mkdirSync(join(monoRoot, 'packages', 'start', 'src'), { recursive: true })
385+
386+
const result = runSetupGithubActions(
387+
join(monoRoot, 'packages', 'router'),
388+
metaDir,
389+
)
390+
391+
expect(result.workflows).toEqual(
392+
expect.arrayContaining([
393+
join(monoRoot, '.github', 'workflows', 'notify-intent.yml'),
394+
join(monoRoot, '.github', 'workflows', 'check-skills.yml'),
395+
]),
396+
)
397+
expect(
398+
existsSync(join(monoRoot, 'packages', 'router', '.github', 'workflows')),
399+
).toBe(false)
400+
401+
const notifyContent = readFileSync(
402+
join(monoRoot, '.github', 'workflows', 'notify-intent.yml'),
403+
'utf8',
404+
)
405+
expect(notifyContent).toContain('package: @tanstack/router')
406+
expect(notifyContent).toContain('repo: TanStack/router')
407+
expect(notifyContent).toContain("- 'packages/router/docs/**'")
408+
expect(notifyContent).toContain("- 'packages/router/src/**'")
409+
expect(notifyContent).toContain("- 'packages/start/src/**'")
410+
411+
rmSync(monoRoot, { recursive: true, force: true })
412+
})
347413
})
348414

349415
// ---------------------------------------------------------------------------

packages/intent/tests/workspace-patterns.test.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
} from 'node:fs'
88
import { join } from 'node:path'
99
import { tmpdir } from 'node:os'
10-
import { afterEach, describe, expect, it } from 'vitest'
10+
import { afterEach, describe, expect, it, vi } from 'vitest'
1111
import {
1212
findPackagesWithSkills,
1313
findWorkspaceRoot,
@@ -78,6 +78,70 @@ describe('readWorkspacePatterns', () => {
7878
'packages/*',
7979
])
8080
})
81+
82+
it('reads workspace patterns from deno.json', () => {
83+
const root = createRoot()
84+
85+
writeFileSync(
86+
join(root, 'deno.json'),
87+
JSON.stringify({
88+
workspace: ['', './apps/*/', 'packages\\*', 'apps/*'],
89+
}),
90+
)
91+
92+
expect(readWorkspacePatterns(root)).toEqual(['apps/*', 'packages/*'])
93+
})
94+
95+
it('reads workspace patterns from deno.jsonc', () => {
96+
const root = createRoot()
97+
98+
writeFileSync(
99+
join(root, 'deno.jsonc'),
100+
`{
101+
// Deno supports JSONC config files.
102+
"workspace": [
103+
"./packages/*/",
104+
"apps/*",
105+
],
106+
}
107+
`,
108+
)
109+
110+
expect(readWorkspacePatterns(root)).toEqual(['apps/*', 'packages/*'])
111+
})
112+
113+
it('prefers package.json workspaces over Deno workspace config', () => {
114+
const root = createRoot()
115+
116+
writeFileSync(
117+
join(root, 'package.json'),
118+
JSON.stringify({ workspaces: ['packages/*'] }),
119+
)
120+
writeFileSync(
121+
join(root, 'deno.json'),
122+
JSON.stringify({ workspace: ['apps/*'] }),
123+
)
124+
125+
expect(readWorkspacePatterns(root)).toEqual(['packages/*'])
126+
})
127+
128+
it('warns and returns null for invalid Deno config', () => {
129+
const root = createRoot()
130+
const consoleErrorSpy = vi
131+
.spyOn(console, 'error')
132+
.mockImplementation(() => undefined)
133+
134+
writeFileSync(join(root, 'deno.jsonc'), '{ invalid jsonc')
135+
136+
expect(readWorkspacePatterns(root)).toBeNull()
137+
expect(consoleErrorSpy).toHaveBeenCalledWith(
138+
expect.stringContaining(
139+
`Warning: failed to parse ${join(root, 'deno.jsonc')}`,
140+
),
141+
)
142+
143+
consoleErrorSpy.mockRestore()
144+
})
81145
})
82146

83147
describe('resolveWorkspacePackages', () => {

0 commit comments

Comments
 (0)