@next/codemod 16.2.0-canary.9 → 16.2.0-canary.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/__tests__/agents-md-e2e.test.js +326 -0
- package/lib/agents-md.js +30 -38
- package/package.json +1 -1
- package/lib/__tests__/agents-md.test.js +0 -142
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/* global jest */
|
|
2
|
+
jest.autoMockOff()
|
|
3
|
+
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const path = require('path')
|
|
6
|
+
const os = require('os')
|
|
7
|
+
const { runAgentsMd } = require('../../bin/agents-md')
|
|
8
|
+
const { getNextjsVersion } = require('../../lib/agents-md')
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* TRUE E2E TESTS
|
|
12
|
+
* These tests invoke the actual CLI entry point (runAgentsMd),
|
|
13
|
+
* simulating what happens when a user runs:
|
|
14
|
+
* npx @next/codemod agents-md --version 15.0.0 --output CLAUDE.md
|
|
15
|
+
*/
|
|
16
|
+
describe('agents-md e2e (CLI invocation)', () => {
|
|
17
|
+
let testProjectDir
|
|
18
|
+
let originalConsoleLog
|
|
19
|
+
let consoleOutput
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
// Create isolated test project directory
|
|
23
|
+
const tmpBase = process.env.NEXT_TEST_DIR || os.tmpdir()
|
|
24
|
+
testProjectDir = path.join(
|
|
25
|
+
tmpBase,
|
|
26
|
+
`agents-md-e2e-${Date.now()}-${(Math.random() * 1000) | 0}`
|
|
27
|
+
)
|
|
28
|
+
fs.mkdirSync(testProjectDir, { recursive: true })
|
|
29
|
+
|
|
30
|
+
// Mock console.log to capture CLI output
|
|
31
|
+
originalConsoleLog = console.log
|
|
32
|
+
consoleOutput = []
|
|
33
|
+
console.log = (...args) => {
|
|
34
|
+
consoleOutput.push(args.join(' '))
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
// Restore console.log
|
|
40
|
+
console.log = originalConsoleLog
|
|
41
|
+
|
|
42
|
+
// Clean up test directory
|
|
43
|
+
if (testProjectDir && fs.existsSync(testProjectDir)) {
|
|
44
|
+
fs.rmSync(testProjectDir, { recursive: true, force: true })
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('creates CLAUDE.md and .next-docs directory when run with --version and --output', async () => {
|
|
49
|
+
// Create a minimal package.json (not required, but realistic)
|
|
50
|
+
const packageJson = {
|
|
51
|
+
name: 'test-project',
|
|
52
|
+
version: '1.0.0',
|
|
53
|
+
dependencies: {
|
|
54
|
+
next: '15.0.0',
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
fs.writeFileSync(
|
|
58
|
+
path.join(testProjectDir, 'package.json'),
|
|
59
|
+
JSON.stringify(packageJson, null, 2)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
// Change to test directory
|
|
63
|
+
const originalCwd = process.cwd()
|
|
64
|
+
process.chdir(testProjectDir)
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// Run the actual CLI command
|
|
68
|
+
await runAgentsMd({
|
|
69
|
+
version: '15.0.0',
|
|
70
|
+
output: 'CLAUDE.md',
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// Verify .next-docs directory was created and populated
|
|
74
|
+
const docsDir = path.join(testProjectDir, '.next-docs')
|
|
75
|
+
expect(fs.existsSync(docsDir)).toBe(true)
|
|
76
|
+
|
|
77
|
+
const docFiles = fs.readdirSync(docsDir, { recursive: true })
|
|
78
|
+
expect(docFiles.length).toBeGreaterThan(0)
|
|
79
|
+
|
|
80
|
+
// Should contain mdx/md files
|
|
81
|
+
const mdxFiles = docFiles.filter(
|
|
82
|
+
(f) => f.endsWith('.mdx') || f.endsWith('.md')
|
|
83
|
+
)
|
|
84
|
+
expect(mdxFiles.length).toBeGreaterThan(0)
|
|
85
|
+
|
|
86
|
+
// Verify CLAUDE.md was created
|
|
87
|
+
const claudeMdPath = path.join(testProjectDir, 'CLAUDE.md')
|
|
88
|
+
expect(fs.existsSync(claudeMdPath)).toBe(true)
|
|
89
|
+
|
|
90
|
+
const claudeMdContent = fs.readFileSync(claudeMdPath, 'utf-8')
|
|
91
|
+
|
|
92
|
+
// Verify content structure
|
|
93
|
+
expect(claudeMdContent).toContain('<!-- NEXT-AGENTS-MD-START -->')
|
|
94
|
+
expect(claudeMdContent).toContain('<!-- NEXT-AGENTS-MD-END -->')
|
|
95
|
+
expect(claudeMdContent).toContain('[Next.js Docs Index]')
|
|
96
|
+
expect(claudeMdContent).toContain('root: ./.next-docs')
|
|
97
|
+
|
|
98
|
+
// Verify paths are normalized to forward slashes (cross-platform)
|
|
99
|
+
const lines = claudeMdContent.split('|')
|
|
100
|
+
const pathLines = lines.filter((line) => line.includes(':'))
|
|
101
|
+
pathLines.forEach((line) => {
|
|
102
|
+
// Should not contain Windows backslashes in the output
|
|
103
|
+
const pathPart = line.split(':')[0]
|
|
104
|
+
if (pathPart && pathPart.includes('/')) {
|
|
105
|
+
expect(line).not.toMatch(/[^:]\\/)
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// Verify .gitignore was updated
|
|
110
|
+
const gitignorePath = path.join(testProjectDir, '.gitignore')
|
|
111
|
+
if (fs.existsSync(gitignorePath)) {
|
|
112
|
+
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8')
|
|
113
|
+
expect(gitignoreContent).toContain('.next-docs')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Verify console output
|
|
117
|
+
const output = consoleOutput.join('\n')
|
|
118
|
+
expect(output).toContain('Downloading Next.js')
|
|
119
|
+
expect(output).toContain('15.0.0')
|
|
120
|
+
expect(output).toContain('CLAUDE.md')
|
|
121
|
+
} finally {
|
|
122
|
+
// Restore original directory
|
|
123
|
+
process.chdir(originalCwd)
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('updates existing CLAUDE.md without losing content', async () => {
|
|
128
|
+
const originalCwd = process.cwd()
|
|
129
|
+
process.chdir(testProjectDir)
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
// Create existing CLAUDE.md with custom content
|
|
133
|
+
const existingContent = `# My Project
|
|
134
|
+
|
|
135
|
+
This is my project documentation.
|
|
136
|
+
|
|
137
|
+
## Features
|
|
138
|
+
- Feature 1
|
|
139
|
+
- Feature 2
|
|
140
|
+
`
|
|
141
|
+
fs.writeFileSync(
|
|
142
|
+
path.join(testProjectDir, 'CLAUDE.md'),
|
|
143
|
+
existingContent
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
// Run CLI
|
|
147
|
+
await runAgentsMd({
|
|
148
|
+
version: '15.0.0',
|
|
149
|
+
output: 'CLAUDE.md',
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// Verify file was updated, not replaced
|
|
153
|
+
const claudeMdContent = fs.readFileSync(
|
|
154
|
+
path.join(testProjectDir, 'CLAUDE.md'),
|
|
155
|
+
'utf-8'
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
// Original content should still be there
|
|
159
|
+
expect(claudeMdContent).toContain('# My Project')
|
|
160
|
+
expect(claudeMdContent).toContain('This is my project documentation.')
|
|
161
|
+
expect(claudeMdContent).toContain('## Features')
|
|
162
|
+
|
|
163
|
+
// New index should be injected
|
|
164
|
+
expect(claudeMdContent).toContain('<!-- NEXT-AGENTS-MD-START -->')
|
|
165
|
+
expect(claudeMdContent).toContain('[Next.js Docs Index]')
|
|
166
|
+
} finally {
|
|
167
|
+
process.chdir(originalCwd)
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('handles custom output filename', async () => {
|
|
172
|
+
const originalCwd = process.cwd()
|
|
173
|
+
process.chdir(testProjectDir)
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
// Run with custom output file
|
|
177
|
+
await runAgentsMd({
|
|
178
|
+
version: '15.0.0',
|
|
179
|
+
output: 'AGENTS.md',
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// Verify AGENTS.md was created (not CLAUDE.md)
|
|
183
|
+
expect(fs.existsSync(path.join(testProjectDir, 'AGENTS.md'))).toBe(true)
|
|
184
|
+
expect(fs.existsSync(path.join(testProjectDir, 'CLAUDE.md'))).toBe(false)
|
|
185
|
+
|
|
186
|
+
const agentsMdContent = fs.readFileSync(
|
|
187
|
+
path.join(testProjectDir, 'AGENTS.md'),
|
|
188
|
+
'utf-8'
|
|
189
|
+
)
|
|
190
|
+
expect(agentsMdContent).toContain('[Next.js Docs Index]')
|
|
191
|
+
} finally {
|
|
192
|
+
process.chdir(originalCwd)
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('works when run from a subdirectory', async () => {
|
|
197
|
+
const originalCwd = process.cwd()
|
|
198
|
+
|
|
199
|
+
// Create a subdirectory
|
|
200
|
+
const subDir = path.join(testProjectDir, 'packages', 'app')
|
|
201
|
+
fs.mkdirSync(subDir, { recursive: true })
|
|
202
|
+
|
|
203
|
+
// Create package.json in root
|
|
204
|
+
const packageJson = {
|
|
205
|
+
dependencies: { next: '15.0.0' },
|
|
206
|
+
}
|
|
207
|
+
fs.writeFileSync(
|
|
208
|
+
path.join(testProjectDir, 'package.json'),
|
|
209
|
+
JSON.stringify(packageJson)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
// Change to subdirectory
|
|
213
|
+
process.chdir(subDir)
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
// Run from subdirectory - should create files in CWD (subdirectory)
|
|
217
|
+
await runAgentsMd({
|
|
218
|
+
version: '15.0.0',
|
|
219
|
+
output: 'CLAUDE.md',
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
// Verify files created in subdirectory
|
|
223
|
+
expect(fs.existsSync(path.join(subDir, 'CLAUDE.md'))).toBe(true)
|
|
224
|
+
expect(fs.existsSync(path.join(subDir, '.next-docs'))).toBe(true)
|
|
225
|
+
} finally {
|
|
226
|
+
process.chdir(originalCwd)
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('normalizes paths on Windows (cross-platform test)', async () => {
|
|
231
|
+
const originalCwd = process.cwd()
|
|
232
|
+
process.chdir(testProjectDir)
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
await runAgentsMd({
|
|
236
|
+
version: '15.0.0',
|
|
237
|
+
output: 'CLAUDE.md',
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
const claudeMdContent = fs.readFileSync(
|
|
241
|
+
path.join(testProjectDir, 'CLAUDE.md'),
|
|
242
|
+
'utf-8'
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
// Extract the index content between markers
|
|
246
|
+
const startMarker = '<!-- NEXT-AGENTS-MD-START -->'
|
|
247
|
+
const endMarker = '<!-- NEXT-AGENTS-MD-END -->'
|
|
248
|
+
const startIdx = claudeMdContent.indexOf(startMarker) + startMarker.length
|
|
249
|
+
const endIdx = claudeMdContent.indexOf(endMarker)
|
|
250
|
+
const indexContent = claudeMdContent.slice(startIdx, endIdx)
|
|
251
|
+
|
|
252
|
+
// Parse the index (format: "dir:{file1,file2}|dir2:{file3}")
|
|
253
|
+
const sections = indexContent.split('|').filter((s) => s.includes(':'))
|
|
254
|
+
|
|
255
|
+
sections.forEach((section) => {
|
|
256
|
+
const [dirPath, filesStr] = section.split(':')
|
|
257
|
+
if (dirPath && dirPath.trim() && !dirPath.includes('root')) {
|
|
258
|
+
// Verify no Windows backslashes in directory paths
|
|
259
|
+
expect(dirPath).not.toContain('\\')
|
|
260
|
+
// Verify uses forward slashes
|
|
261
|
+
if (dirPath.includes('/')) {
|
|
262
|
+
expect(dirPath).toMatch(/^[^\\]+$/)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
} finally {
|
|
267
|
+
process.chdir(originalCwd)
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('handles version that requires git clone from GitHub', async () => {
|
|
272
|
+
const originalCwd = process.cwd()
|
|
273
|
+
process.chdir(testProjectDir)
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
// Use a known stable version
|
|
277
|
+
await runAgentsMd({
|
|
278
|
+
version: '14.2.0',
|
|
279
|
+
output: 'CLAUDE.md',
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
// Verify docs were downloaded
|
|
283
|
+
const docsDir = path.join(testProjectDir, '.next-docs')
|
|
284
|
+
expect(fs.existsSync(docsDir)).toBe(true)
|
|
285
|
+
|
|
286
|
+
const docFiles = fs.readdirSync(docsDir, { recursive: true })
|
|
287
|
+
const mdxFiles = docFiles.filter(
|
|
288
|
+
(f) => f.endsWith('.mdx') || f.endsWith('.md')
|
|
289
|
+
)
|
|
290
|
+
expect(mdxFiles.length).toBeGreaterThan(50) // Should have many doc files
|
|
291
|
+
} finally {
|
|
292
|
+
process.chdir(originalCwd)
|
|
293
|
+
}
|
|
294
|
+
}, 30000) // Increase timeout for git clone
|
|
295
|
+
|
|
296
|
+
describe('getNextjsVersion', () => {
|
|
297
|
+
const fixturesDir = path.join(__dirname, 'fixtures/agents-md')
|
|
298
|
+
|
|
299
|
+
it('returns the installed Next.js version from node_modules', () => {
|
|
300
|
+
const fixture = path.join(fixturesDir, 'next-specific-version')
|
|
301
|
+
const result = getNextjsVersion(fixture)
|
|
302
|
+
|
|
303
|
+
expect(result.version).toBe('15.4.0')
|
|
304
|
+
expect(result.error).toBeUndefined()
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('returns actual installed version, not the tag from package.json', () => {
|
|
308
|
+
// package.json has "next": "latest", but node_modules has version "16.0.0"
|
|
309
|
+
const fixture = path.join(fixturesDir, 'next-tag')
|
|
310
|
+
const result = getNextjsVersion(fixture)
|
|
311
|
+
|
|
312
|
+
// Should return the actual installed version, not "latest"
|
|
313
|
+
expect(result.version).toBe('16.0.0')
|
|
314
|
+
expect(result.error).toBeUndefined()
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
it('returns error when Next.js is not installed', () => {
|
|
318
|
+
// Use a directory where next is not installed
|
|
319
|
+
const nonNextDir = '/tmp'
|
|
320
|
+
const result = getNextjsVersion(nonNextDir)
|
|
321
|
+
|
|
322
|
+
expect(result.version).toBeNull()
|
|
323
|
+
expect(result.error).toBe('Next.js is not installed in this project.')
|
|
324
|
+
})
|
|
325
|
+
})
|
|
326
|
+
})
|
package/lib/agents-md.js
CHANGED
|
@@ -16,27 +16,17 @@ exports.buildDocTree = buildDocTree;
|
|
|
16
16
|
exports.generateClaudeMdIndex = generateClaudeMdIndex;
|
|
17
17
|
exports.injectIntoClaudeMd = injectIntoClaudeMd;
|
|
18
18
|
exports.ensureGitignoreEntry = ensureGitignoreEntry;
|
|
19
|
-
const
|
|
19
|
+
const execa_1 = __importDefault(require("execa"));
|
|
20
20
|
const fs_1 = __importDefault(require("fs"));
|
|
21
21
|
const path_1 = __importDefault(require("path"));
|
|
22
22
|
const os_1 = __importDefault(require("os"));
|
|
23
23
|
function getNextjsVersion(cwd) {
|
|
24
|
-
const packageJsonPath = path_1.default.join(cwd, 'package.json');
|
|
25
|
-
if (!fs_1.default.existsSync(packageJsonPath)) {
|
|
26
|
-
return {
|
|
27
|
-
version: null,
|
|
28
|
-
error: 'No package.json found in the current directory',
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
24
|
try {
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const cleanVersion = nextVersion.replace(/^[\^~>=<]+/, '');
|
|
38
|
-
return { version: cleanVersion };
|
|
39
|
-
}
|
|
25
|
+
const nextPkgPath = require.resolve('next/package.json', { paths: [cwd] });
|
|
26
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(nextPkgPath, 'utf-8'));
|
|
27
|
+
return { version: pkg.version };
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
40
30
|
// Not found at root - check for monorepo workspace
|
|
41
31
|
const workspace = detectWorkspace(cwd);
|
|
42
32
|
if (workspace.isMonorepo && workspace.packages.length > 0) {
|
|
@@ -54,12 +44,6 @@ function getNextjsVersion(cwd) {
|
|
|
54
44
|
error: 'Next.js is not installed in this project.',
|
|
55
45
|
};
|
|
56
46
|
}
|
|
57
|
-
catch (err) {
|
|
58
|
-
return {
|
|
59
|
-
version: null,
|
|
60
|
-
error: `Failed to parse package.json: ${err instanceof Error ? err.message : String(err)}`,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
47
|
}
|
|
64
48
|
function versionToGitHubTag(version) {
|
|
65
49
|
return version.startsWith('v') ? version : `v${version}`;
|
|
@@ -108,7 +92,17 @@ async function cloneDocsFolder(tag, destDir) {
|
|
|
108
92
|
const tempDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'next-agents-md-'));
|
|
109
93
|
try {
|
|
110
94
|
try {
|
|
111
|
-
(0,
|
|
95
|
+
await (0, execa_1.default)('git', [
|
|
96
|
+
'clone',
|
|
97
|
+
'--depth',
|
|
98
|
+
'1',
|
|
99
|
+
'--filter=blob:none',
|
|
100
|
+
'--sparse',
|
|
101
|
+
'--branch',
|
|
102
|
+
tag,
|
|
103
|
+
'https://github.com/vercel/next.js.git',
|
|
104
|
+
'.',
|
|
105
|
+
], { cwd: tempDir });
|
|
112
106
|
}
|
|
113
107
|
catch (error) {
|
|
114
108
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -117,7 +111,7 @@ async function cloneDocsFolder(tag, destDir) {
|
|
|
117
111
|
}
|
|
118
112
|
throw error;
|
|
119
113
|
}
|
|
120
|
-
(0,
|
|
114
|
+
await (0, execa_1.default)('git', ['sparse-checkout', 'set', 'docs'], { cwd: tempDir });
|
|
121
115
|
const sourceDocsDir = path_1.default.join(tempDir, 'docs');
|
|
122
116
|
if (!fs_1.default.existsSync(sourceDocsDir)) {
|
|
123
117
|
throw new Error('docs folder not found in cloned repository');
|
|
@@ -137,16 +131,16 @@ async function cloneDocsFolder(tag, destDir) {
|
|
|
137
131
|
function collectDocFiles(dir) {
|
|
138
132
|
return fs_1.default.readdirSync(dir, { recursive: true })
|
|
139
133
|
.filter((f) => (f.endsWith('.mdx') || f.endsWith('.md')) &&
|
|
140
|
-
|
|
141
|
-
|
|
134
|
+
!/[/\\]index\.mdx$/.test(f) &&
|
|
135
|
+
!/[/\\]index\.md$/.test(f) &&
|
|
142
136
|
!f.startsWith('index.'))
|
|
143
137
|
.sort()
|
|
144
|
-
.map((f) => ({ relativePath: f }));
|
|
138
|
+
.map((f) => ({ relativePath: f.replace(/\\/g, '/') }));
|
|
145
139
|
}
|
|
146
140
|
function buildDocTree(files) {
|
|
147
141
|
const sections = new Map();
|
|
148
142
|
for (const file of files) {
|
|
149
|
-
const parts = file.relativePath.split(
|
|
143
|
+
const parts = file.relativePath.split(/[/\\]/);
|
|
150
144
|
if (parts.length < 2)
|
|
151
145
|
continue;
|
|
152
146
|
const topLevelDir = parts[0];
|
|
@@ -221,7 +215,7 @@ function collectAllFilesFromSections(sections) {
|
|
|
221
215
|
function groupByDirectory(files) {
|
|
222
216
|
const grouped = new Map();
|
|
223
217
|
for (const filePath of files) {
|
|
224
|
-
const lastSlash = filePath.lastIndexOf('/');
|
|
218
|
+
const lastSlash = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'));
|
|
225
219
|
const dir = lastSlash === -1 ? '.' : filePath.slice(0, lastSlash);
|
|
226
220
|
const fileName = lastSlash === -1 ? filePath : filePath.slice(lastSlash + 1);
|
|
227
221
|
const existing = grouped.get(dir);
|
|
@@ -387,19 +381,17 @@ function findNextjsInWorkspace(cwd, patterns) {
|
|
|
387
381
|
const packagePaths = expandWorkspacePatterns(cwd, patterns);
|
|
388
382
|
const versions = [];
|
|
389
383
|
for (const pkgPath of packagePaths) {
|
|
390
|
-
const packageJsonPath = path_1.default.join(pkgPath, 'package.json');
|
|
391
|
-
if (!fs_1.default.existsSync(packageJsonPath))
|
|
392
|
-
continue;
|
|
393
384
|
try {
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
385
|
+
const nextPkgPath = require.resolve('next/package.json', {
|
|
386
|
+
paths: [pkgPath],
|
|
387
|
+
});
|
|
388
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(nextPkgPath, 'utf-8'));
|
|
389
|
+
if (pkg.version) {
|
|
390
|
+
versions.push(pkg.version);
|
|
399
391
|
}
|
|
400
392
|
}
|
|
401
393
|
catch {
|
|
402
|
-
//
|
|
394
|
+
// Next.js not installed in this package
|
|
403
395
|
}
|
|
404
396
|
}
|
|
405
397
|
return findHighestVersion(versions);
|
package/package.json
CHANGED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
/* global jest */
|
|
2
|
-
jest.autoMockOff()
|
|
3
|
-
|
|
4
|
-
const { injectIntoClaudeMd, buildDocTree } = require('../agents-md')
|
|
5
|
-
|
|
6
|
-
describe('agents-md', () => {
|
|
7
|
-
describe('injectIntoClaudeMd', () => {
|
|
8
|
-
const START_MARKER = '<!-- NEXT-AGENTS-MD-START -->'
|
|
9
|
-
const END_MARKER = '<!-- NEXT-AGENTS-MD-END -->'
|
|
10
|
-
|
|
11
|
-
it('appends to empty file', () => {
|
|
12
|
-
const result = injectIntoClaudeMd('', 'index content')
|
|
13
|
-
// Empty string doesn't end with \n, so separator is \n\n
|
|
14
|
-
expect(result).toBe(`\n\n${START_MARKER}index content${END_MARKER}\n`)
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
it('appends to file without markers', () => {
|
|
18
|
-
const existing = '# My Project\n\nSome existing content.'
|
|
19
|
-
const result = injectIntoClaudeMd(existing, 'index content')
|
|
20
|
-
expect(result).toBe(
|
|
21
|
-
`${existing}\n\n${START_MARKER}index content${END_MARKER}\n`
|
|
22
|
-
)
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
it('replaces content between existing markers', () => {
|
|
26
|
-
const existing = `# My Project
|
|
27
|
-
|
|
28
|
-
Some content before.
|
|
29
|
-
|
|
30
|
-
${START_MARKER}old index${END_MARKER}
|
|
31
|
-
|
|
32
|
-
Some content after.`
|
|
33
|
-
const result = injectIntoClaudeMd(existing, 'new index')
|
|
34
|
-
expect(result).toBe(`# My Project
|
|
35
|
-
|
|
36
|
-
Some content before.
|
|
37
|
-
|
|
38
|
-
${START_MARKER}new index${END_MARKER}
|
|
39
|
-
|
|
40
|
-
Some content after.`)
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('is idempotent - running twice produces same result', () => {
|
|
44
|
-
const initial = '# Project\n'
|
|
45
|
-
const first = injectIntoClaudeMd(initial, 'index v1')
|
|
46
|
-
const second = injectIntoClaudeMd(first, 'index v1')
|
|
47
|
-
expect(second).toBe(first)
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('preserves content before and after markers on update', () => {
|
|
51
|
-
const before = '# Header\n\nIntro paragraph.'
|
|
52
|
-
const after = '\n\n## Footer\n\nMore content.'
|
|
53
|
-
const existing = `${before}\n\n${START_MARKER}old${END_MARKER}${after}`
|
|
54
|
-
const result = injectIntoClaudeMd(existing, 'new')
|
|
55
|
-
expect(result).toContain(before)
|
|
56
|
-
expect(result).toContain(after)
|
|
57
|
-
expect(result).toContain(`${START_MARKER}new${END_MARKER}`)
|
|
58
|
-
expect(result).not.toContain('old')
|
|
59
|
-
})
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
describe('buildDocTree', () => {
|
|
63
|
-
it('groups files by top-level directory', () => {
|
|
64
|
-
const files = [
|
|
65
|
-
{ relativePath: '01-getting-started/installation.mdx' },
|
|
66
|
-
{ relativePath: '01-getting-started/project-structure.mdx' },
|
|
67
|
-
{ relativePath: '02-app/routing.mdx' },
|
|
68
|
-
]
|
|
69
|
-
const tree = buildDocTree(files)
|
|
70
|
-
|
|
71
|
-
expect(tree).toHaveLength(2)
|
|
72
|
-
expect(tree[0].name).toBe('01-getting-started')
|
|
73
|
-
expect(tree[0].files).toHaveLength(2)
|
|
74
|
-
expect(tree[1].name).toBe('02-app')
|
|
75
|
-
expect(tree[1].files).toHaveLength(1)
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('creates nested subsections for deeper paths', () => {
|
|
79
|
-
const files = [
|
|
80
|
-
{ relativePath: '02-app/01-building/layouts.mdx' },
|
|
81
|
-
{ relativePath: '02-app/01-building/pages.mdx' },
|
|
82
|
-
{ relativePath: '02-app/02-api/route-handlers.mdx' },
|
|
83
|
-
]
|
|
84
|
-
const tree = buildDocTree(files)
|
|
85
|
-
|
|
86
|
-
expect(tree).toHaveLength(1)
|
|
87
|
-
const appSection = tree[0]
|
|
88
|
-
expect(appSection.name).toBe('02-app')
|
|
89
|
-
expect(appSection.files).toHaveLength(0) // no direct files
|
|
90
|
-
expect(appSection.subsections).toHaveLength(2)
|
|
91
|
-
|
|
92
|
-
const building = appSection.subsections.find(
|
|
93
|
-
(s) => s.name === '01-building'
|
|
94
|
-
)
|
|
95
|
-
expect(building).toBeDefined()
|
|
96
|
-
expect(building.files).toHaveLength(2)
|
|
97
|
-
|
|
98
|
-
const api = appSection.subsections.find((s) => s.name === '02-api')
|
|
99
|
-
expect(api).toBeDefined()
|
|
100
|
-
expect(api.files).toHaveLength(1)
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it('handles 4-level deep paths with sub-subsections', () => {
|
|
104
|
-
const files = [
|
|
105
|
-
{ relativePath: '02-app/01-building/01-routing/dynamic-routes.mdx' },
|
|
106
|
-
{ relativePath: '02-app/01-building/01-routing/parallel-routes.mdx' },
|
|
107
|
-
]
|
|
108
|
-
const tree = buildDocTree(files)
|
|
109
|
-
|
|
110
|
-
const routing = tree[0].subsections[0].subsections[0]
|
|
111
|
-
expect(routing.name).toBe('01-routing')
|
|
112
|
-
expect(routing.files).toHaveLength(2)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('skips single-segment paths (root-level files)', () => {
|
|
116
|
-
const files = [
|
|
117
|
-
{ relativePath: 'index.mdx' },
|
|
118
|
-
{ relativePath: '01-getting-started/intro.mdx' },
|
|
119
|
-
]
|
|
120
|
-
const tree = buildDocTree(files)
|
|
121
|
-
|
|
122
|
-
// Root-level index.mdx should be skipped (parts.length < 2)
|
|
123
|
-
expect(tree).toHaveLength(1)
|
|
124
|
-
expect(tree[0].name).toBe('01-getting-started')
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('sorts sections and files alphabetically', () => {
|
|
128
|
-
const files = [
|
|
129
|
-
{ relativePath: 'z-section/b-file.mdx' },
|
|
130
|
-
{ relativePath: 'a-section/z-file.mdx' },
|
|
131
|
-
{ relativePath: 'a-section/a-file.mdx' },
|
|
132
|
-
{ relativePath: 'z-section/a-file.mdx' },
|
|
133
|
-
]
|
|
134
|
-
const tree = buildDocTree(files)
|
|
135
|
-
|
|
136
|
-
expect(tree[0].name).toBe('a-section')
|
|
137
|
-
expect(tree[1].name).toBe('z-section')
|
|
138
|
-
expect(tree[0].files[0].relativePath).toBe('a-section/a-file.mdx')
|
|
139
|
-
expect(tree[0].files[1].relativePath).toBe('a-section/z-file.mdx')
|
|
140
|
-
})
|
|
141
|
-
})
|
|
142
|
-
})
|