@next/codemod 16.5.0-canary.3 → 16.5.0-canary.30
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/bin/agents-md.js +18 -10
- package/lib/__tests__/agents-md-e2e.test.js +448 -3
- package/lib/agents-md.js +82 -16
- package/package.json +4 -2
package/bin/agents-md.js
CHANGED
|
@@ -58,18 +58,22 @@ async function runAgentsMd(options) {
|
|
|
58
58
|
nextjsVersion = promptedOptions.nextVersion;
|
|
59
59
|
targetFile = promptedOptions.targetFile;
|
|
60
60
|
}
|
|
61
|
-
const agentsMdPath = path_1.default.
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
const agentsMdPath = path_1.default.resolve(cwd, targetFile);
|
|
62
|
+
// Agents resolve paths in the index from the directory of the output file,
|
|
63
|
+
// so every link written into it is relative to that directory, not the cwd.
|
|
64
|
+
const outputDir = path_1.default.dirname(agentsMdPath);
|
|
65
|
+
// Next.js >= 16.2.0 ships its docs inside the published package. When an
|
|
66
|
+
// installed version (from the cwd or, in a monorepo, a workspace package)
|
|
67
|
+
// matches the requested one, index the bundled docs directly instead of
|
|
68
|
+
// downloading a copy into .next-docs.
|
|
69
|
+
const bundledDocs = (0, agents_md_1.getBundledDocsInfo)(cwd, nextjsVersion);
|
|
70
|
+
const useBundledDocs = bundledDocs !== null;
|
|
67
71
|
const docsPath = useBundledDocs
|
|
68
72
|
? bundledDocs.docsPath
|
|
69
73
|
: path_1.default.join(cwd, DOCS_DIR_NAME);
|
|
70
74
|
const docsLinkPath = useBundledDocs
|
|
71
|
-
? (0, agents_md_1.getBundledDocsLinkPath)(
|
|
72
|
-
:
|
|
75
|
+
? (0, agents_md_1.getBundledDocsLinkPath)(outputDir, bundledDocs.docsPath, bundledDocs.resolvedFrom)
|
|
76
|
+
: (0, agents_md_1.getRelativeLinkPath)(outputDir, docsPath);
|
|
73
77
|
let sizeBefore = 0;
|
|
74
78
|
let isNewFile = true;
|
|
75
79
|
let existingContent = '';
|
|
@@ -79,7 +83,10 @@ async function runAgentsMd(options) {
|
|
|
79
83
|
isNewFile = false;
|
|
80
84
|
}
|
|
81
85
|
if (useBundledDocs) {
|
|
82
|
-
|
|
86
|
+
const fromWorkspacePackage = path_1.default.resolve(bundledDocs.resolvedFrom) !== path_1.default.resolve(cwd)
|
|
87
|
+
? ` from workspace package ${picocolors_1.default.cyan(path_1.default.relative(cwd, bundledDocs.resolvedFrom).replace(/\\/g, '/'))}`
|
|
88
|
+
: '';
|
|
89
|
+
console.log(`\nUsing the docs bundled with Next.js ${picocolors_1.default.cyan(nextjsVersion)}${fromWorkspacePackage} at ${picocolors_1.default.cyan(docsLinkPath)} (no download needed).`);
|
|
83
90
|
}
|
|
84
91
|
else {
|
|
85
92
|
console.log(`\nDownloading Next.js ${picocolors_1.default.cyan(nextjsVersion)} documentation to ${picocolors_1.default.cyan(DOCS_DIR_NAME)}...`);
|
|
@@ -97,7 +104,8 @@ async function runAgentsMd(options) {
|
|
|
97
104
|
const indexContent = (0, agents_md_1.generateAgentsMdIndex)({
|
|
98
105
|
docsPath: docsLinkPath,
|
|
99
106
|
sections,
|
|
100
|
-
|
|
107
|
+
// The regeneration hint is run from the output file's directory.
|
|
108
|
+
outputFile: path_1.default.relative(outputDir, agentsMdPath).replace(/\\/g, '/'),
|
|
101
109
|
});
|
|
102
110
|
const newContent = (0, agents_md_1.injectIntoAgentsMd)(existingContent, indexContent);
|
|
103
111
|
fs_1.default.writeFileSync(agentsMdPath, newContent, 'utf-8');
|
|
@@ -4,9 +4,22 @@ jest.autoMockOff()
|
|
|
4
4
|
const fs = require('fs')
|
|
5
5
|
const path = require('path')
|
|
6
6
|
const os = require('os')
|
|
7
|
+
const { execFileSync } = require('child_process')
|
|
7
8
|
const { runAgentsMd } = require('../../bin/agents-md')
|
|
8
9
|
const { getNextjsVersion } = require('../../lib/agents-md')
|
|
9
10
|
|
|
11
|
+
function execNodeWithoutNodePath(script, options = {}) {
|
|
12
|
+
// pnpm 12's Jest launcher exposes hoisted dependencies through NODE_PATH.
|
|
13
|
+
// A fresh Node process must omit it to resolve only the fixture's dependencies.
|
|
14
|
+
const env = { ...process.env, ...options.env }
|
|
15
|
+
delete env.NODE_PATH
|
|
16
|
+
return execFileSync(process.execPath, ['-e', script], {
|
|
17
|
+
...options,
|
|
18
|
+
env,
|
|
19
|
+
encoding: 'utf8',
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
10
23
|
/**
|
|
11
24
|
* TRUE E2E TESTS
|
|
12
25
|
* These tests invoke the actual CLI entry point (runAgentsMd),
|
|
@@ -406,6 +419,432 @@ This is my project documentation.
|
|
|
406
419
|
}, 30000) // Increase timeout for git clone
|
|
407
420
|
})
|
|
408
421
|
|
|
422
|
+
// The index `root` is resolved by agents from the directory of the output
|
|
423
|
+
// file, so it must be relative to that directory, not to the cwd.
|
|
424
|
+
describe('paths relative to the output file', () => {
|
|
425
|
+
const agentsMdLib = require('../../lib/agents-md')
|
|
426
|
+
let originalCwd
|
|
427
|
+
let pullDocsSpy
|
|
428
|
+
|
|
429
|
+
// Install `next` the way pnpm's isolated linker does: the real package
|
|
430
|
+
// lives in the root `node_modules/.pnpm` store and is only symlinked into
|
|
431
|
+
// the package that depends on it.
|
|
432
|
+
function installNextIsolated(projectDir, pkgDir, version) {
|
|
433
|
+
const storeDir = path.join(
|
|
434
|
+
projectDir,
|
|
435
|
+
'node_modules',
|
|
436
|
+
'.pnpm',
|
|
437
|
+
`next@${version}`,
|
|
438
|
+
'node_modules',
|
|
439
|
+
'next'
|
|
440
|
+
)
|
|
441
|
+
writeBundledNextPackage(storeDir, version)
|
|
442
|
+
const linkDir = path.join(pkgDir, 'node_modules')
|
|
443
|
+
fs.mkdirSync(linkDir, { recursive: true })
|
|
444
|
+
fs.symlinkSync(storeDir, path.join(linkDir, 'next'), 'junction')
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function writeBundledNextPackage(nextDir, version) {
|
|
448
|
+
const docsDir = path.join(nextDir, 'dist', 'docs', '01-app')
|
|
449
|
+
fs.mkdirSync(docsDir, { recursive: true })
|
|
450
|
+
fs.writeFileSync(
|
|
451
|
+
path.join(nextDir, 'package.json'),
|
|
452
|
+
JSON.stringify({ name: 'next', version })
|
|
453
|
+
)
|
|
454
|
+
fs.writeFileSync(path.join(docsDir, `docs-${version}.md`), '# Docs')
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function readIndex(agentsMdPath) {
|
|
458
|
+
const content = fs.readFileSync(agentsMdPath, 'utf-8')
|
|
459
|
+
return {
|
|
460
|
+
content,
|
|
461
|
+
root: content.match(/\|root: ([^|]+)\|/)[1],
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Returns the absolute path `root` points at, resolved the way an agent
|
|
466
|
+
// reading the output file would resolve it.
|
|
467
|
+
function resolveRoot(agentsMdPath) {
|
|
468
|
+
return path.resolve(path.dirname(agentsMdPath), readIndex(agentsMdPath).root)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
beforeEach(() => {
|
|
472
|
+
originalCwd = process.cwd()
|
|
473
|
+
// Stub the download so the `.next-docs` branch is testable offline.
|
|
474
|
+
pullDocsSpy = jest
|
|
475
|
+
.spyOn(agentsMdLib, 'pullDocs')
|
|
476
|
+
.mockImplementation(async ({ docsDir }) => {
|
|
477
|
+
const sectionDir = path.join(docsDir, '01-app')
|
|
478
|
+
fs.mkdirSync(sectionDir, { recursive: true })
|
|
479
|
+
fs.writeFileSync(path.join(sectionDir, 'downloaded.md'), '# Docs')
|
|
480
|
+
return { success: true, docsPath: docsDir }
|
|
481
|
+
})
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
afterEach(() => {
|
|
485
|
+
process.chdir(originalCwd)
|
|
486
|
+
pullDocsSpy.mockRestore()
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
it('links bundled docs relative to an output file outside the cwd', async () => {
|
|
490
|
+
const appDir = path.join(testProjectDir, 'apps', 'web')
|
|
491
|
+
installNextIsolated(testProjectDir, appDir, '16.4.0')
|
|
492
|
+
process.chdir(appDir)
|
|
493
|
+
|
|
494
|
+
await runAgentsMd({ output: '../../AGENTS.md' })
|
|
495
|
+
|
|
496
|
+
const agentsMdPath = path.join(testProjectDir, 'AGENTS.md')
|
|
497
|
+
expect(readIndex(agentsMdPath).root).toBe(
|
|
498
|
+
'./apps/web/node_modules/next/dist/docs'
|
|
499
|
+
)
|
|
500
|
+
expect(
|
|
501
|
+
fs.existsSync(path.join(resolveRoot(agentsMdPath), '01-app'))
|
|
502
|
+
).toBe(true)
|
|
503
|
+
expect(pullDocsSpy).not.toHaveBeenCalled()
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
it('links downloaded docs relative to an output file outside the cwd', async () => {
|
|
507
|
+
const appDir = path.join(testProjectDir, 'apps', 'web')
|
|
508
|
+
fs.mkdirSync(appDir, { recursive: true })
|
|
509
|
+
process.chdir(appDir)
|
|
510
|
+
|
|
511
|
+
await runAgentsMd({ version: '15.0.0', output: '../../AGENTS.md' })
|
|
512
|
+
|
|
513
|
+
const agentsMdPath = path.join(testProjectDir, 'AGENTS.md')
|
|
514
|
+
expect(readIndex(agentsMdPath).root).toBe('./apps/web/.next-docs')
|
|
515
|
+
expect(fs.realpathSync(resolveRoot(agentsMdPath))).toBe(
|
|
516
|
+
fs.realpathSync(path.join(appDir, '.next-docs'))
|
|
517
|
+
)
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
it('keeps the ./ prefix for the .next-docs dot-directory', async () => {
|
|
521
|
+
process.chdir(testProjectDir)
|
|
522
|
+
|
|
523
|
+
await runAgentsMd({ version: '15.0.0', output: 'AGENTS.md' })
|
|
524
|
+
|
|
525
|
+
expect(readIndex(path.join(testProjectDir, 'AGENTS.md')).root).toBe(
|
|
526
|
+
'./.next-docs'
|
|
527
|
+
)
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
it('links the indexed install when the output dir resolves a different next', async () => {
|
|
531
|
+
// Root has a hoisted next@16.3.0; the app has its own next@16.4.0.
|
|
532
|
+
writeBundledNextPackage(
|
|
533
|
+
path.join(testProjectDir, 'node_modules', 'next'),
|
|
534
|
+
'16.3.0'
|
|
535
|
+
)
|
|
536
|
+
const appDir = path.join(testProjectDir, 'apps', 'web')
|
|
537
|
+
installNextIsolated(testProjectDir, appDir, '16.4.0')
|
|
538
|
+
process.chdir(appDir)
|
|
539
|
+
|
|
540
|
+
await runAgentsMd({ output: '../../AGENTS.md' })
|
|
541
|
+
|
|
542
|
+
const agentsMdPath = path.join(testProjectDir, 'AGENTS.md')
|
|
543
|
+
const linkedPkg = JSON.parse(
|
|
544
|
+
fs.readFileSync(
|
|
545
|
+
path.join(resolveRoot(agentsMdPath), '..', '..', 'package.json'),
|
|
546
|
+
'utf-8'
|
|
547
|
+
)
|
|
548
|
+
)
|
|
549
|
+
expect(linkedPkg.version).toBe('16.4.0')
|
|
550
|
+
expect(readIndex(agentsMdPath).content).toContain('docs-16.4.0.md')
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
it('writes a regeneration hint that is relative to the output file', async () => {
|
|
554
|
+
const appDir = path.join(testProjectDir, 'apps', 'web')
|
|
555
|
+
installNextIsolated(testProjectDir, appDir, '16.4.0')
|
|
556
|
+
process.chdir(appDir)
|
|
557
|
+
|
|
558
|
+
await runAgentsMd({ output: '../../AGENTS.md' })
|
|
559
|
+
|
|
560
|
+
const { content } = readIndex(path.join(testProjectDir, 'AGENTS.md'))
|
|
561
|
+
expect(content).toContain(
|
|
562
|
+
"If docs missing, run this command from this file's directory: npx @next/codemod agents-md --output AGENTS.md|"
|
|
563
|
+
)
|
|
564
|
+
expect(content).not.toContain('--output ../../AGENTS.md')
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
it('supports an absolute --output path', async () => {
|
|
568
|
+
const appDir = path.join(testProjectDir, 'apps', 'web')
|
|
569
|
+
installNextIsolated(testProjectDir, appDir, '16.4.0')
|
|
570
|
+
const outputDir = path.join(testProjectDir, 'docs-out')
|
|
571
|
+
fs.mkdirSync(outputDir)
|
|
572
|
+
const agentsMdPath = path.join(outputDir, 'CLAUDE.md')
|
|
573
|
+
process.chdir(appDir)
|
|
574
|
+
|
|
575
|
+
await runAgentsMd({ output: agentsMdPath })
|
|
576
|
+
|
|
577
|
+
expect(fs.existsSync(agentsMdPath)).toBe(true)
|
|
578
|
+
const { root, content } = readIndex(agentsMdPath)
|
|
579
|
+
expect(root).toBe('../apps/web/node_modules/next/dist/docs')
|
|
580
|
+
expect(
|
|
581
|
+
fs.existsSync(path.join(resolveRoot(agentsMdPath), '01-app'))
|
|
582
|
+
).toBe(true)
|
|
583
|
+
expect(content).toContain('--output CLAUDE.md|')
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
// The cwd/output path and the path `next` resolves to can be spelled
|
|
587
|
+
// differently for the same directory: a symlinked alias, or an 8.3 short
|
|
588
|
+
// name like `C:\Users\RUNNER~1\...` on Windows, where `process.cwd()`
|
|
589
|
+
// keeps the short name while module resolution returns the long one.
|
|
590
|
+
// Links must still be computed between consistent spellings.
|
|
591
|
+
describe('when the cwd is spelled differently than the realpath', () => {
|
|
592
|
+
let aliasBase
|
|
593
|
+
let aliasDir
|
|
594
|
+
let cwdSpy
|
|
595
|
+
|
|
596
|
+
// Run from `dir` (inside the real project) while `process.cwd()`
|
|
597
|
+
// reports it through the alias, like a non-canonical cwd on Windows.
|
|
598
|
+
function chdirViaAlias(dir) {
|
|
599
|
+
process.chdir(dir)
|
|
600
|
+
const aliased = path.join(
|
|
601
|
+
aliasDir,
|
|
602
|
+
path.relative(fs.realpathSync(testProjectDir), fs.realpathSync(dir))
|
|
603
|
+
)
|
|
604
|
+
cwdSpy = jest.spyOn(process, 'cwd').mockReturnValue(aliased)
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
beforeEach(() => {
|
|
608
|
+
aliasBase = fs.mkdtempSync(path.join(os.tmpdir(), 'agents-md-alias-'))
|
|
609
|
+
aliasDir = path.join(aliasBase, 'project')
|
|
610
|
+
fs.symlinkSync(testProjectDir, aliasDir, 'junction')
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
afterEach(() => {
|
|
614
|
+
cwdSpy?.mockRestore()
|
|
615
|
+
cwdSpy = undefined
|
|
616
|
+
fs.rmSync(aliasBase, { recursive: true, force: true })
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
it('links the conventional path through a symlinked cwd', async () => {
|
|
620
|
+
writeBundledNextPackage(
|
|
621
|
+
path.join(testProjectDir, 'node_modules', 'next'),
|
|
622
|
+
'16.4.0'
|
|
623
|
+
)
|
|
624
|
+
chdirViaAlias(testProjectDir)
|
|
625
|
+
|
|
626
|
+
await runAgentsMd({ output: 'AGENTS.md' })
|
|
627
|
+
|
|
628
|
+
const agentsMdPath = path.join(testProjectDir, 'AGENTS.md')
|
|
629
|
+
expect(readIndex(agentsMdPath).root).toBe(
|
|
630
|
+
'./node_modules/next/dist/docs'
|
|
631
|
+
)
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
it('links a hoisted install canonically through a symlinked cwd', async () => {
|
|
635
|
+
writeBundledNextPackage(
|
|
636
|
+
path.join(testProjectDir, 'node_modules', 'next'),
|
|
637
|
+
'16.4.0'
|
|
638
|
+
)
|
|
639
|
+
const appDir = path.join(testProjectDir, 'apps', 'web')
|
|
640
|
+
fs.mkdirSync(appDir, { recursive: true })
|
|
641
|
+
chdirViaAlias(appDir)
|
|
642
|
+
|
|
643
|
+
await runAgentsMd({ output: 'AGENTS.md' })
|
|
644
|
+
|
|
645
|
+
const agentsMdPath = path.join(testProjectDir, 'apps', 'web', 'AGENTS.md')
|
|
646
|
+
expect(readIndex(agentsMdPath).root).toBe(
|
|
647
|
+
'../../node_modules/next/dist/docs'
|
|
648
|
+
)
|
|
649
|
+
expect(
|
|
650
|
+
fs.existsSync(path.join(resolveRoot(agentsMdPath), '01-app'))
|
|
651
|
+
).toBe(true)
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
it('detects the same install when only the native realpath canonicalizes', async () => {
|
|
655
|
+
// Simulate Windows 8.3 short names: the JS `fs.realpathSync` leaves
|
|
656
|
+
// the alias spelling as-is, while `fs.realpathSync.native` expands it.
|
|
657
|
+
writeBundledNextPackage(
|
|
658
|
+
path.join(testProjectDir, 'node_modules', 'next'),
|
|
659
|
+
'16.4.0'
|
|
660
|
+
)
|
|
661
|
+
chdirViaAlias(testProjectDir)
|
|
662
|
+
const nativeRealpath = fs.realpathSync.native
|
|
663
|
+
const realpathSpy = jest
|
|
664
|
+
.spyOn(fs, 'realpathSync')
|
|
665
|
+
.mockImplementation((p) => path.resolve(String(p)))
|
|
666
|
+
fs.realpathSync.native = nativeRealpath
|
|
667
|
+
|
|
668
|
+
try {
|
|
669
|
+
await runAgentsMd({ output: 'AGENTS.md' })
|
|
670
|
+
} finally {
|
|
671
|
+
realpathSpy.mockRestore()
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
expect(readIndex(path.join(testProjectDir, 'AGENTS.md')).root).toBe(
|
|
675
|
+
'./node_modules/next/dist/docs'
|
|
676
|
+
)
|
|
677
|
+
})
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
it('always prefixes relative links with ./ or ../', () => {
|
|
681
|
+
const { getRelativeLinkPath } = agentsMdLib
|
|
682
|
+
const base = path.join(testProjectDir, 'a', 'b')
|
|
683
|
+
|
|
684
|
+
expect(getRelativeLinkPath(base, base)).toBe('./')
|
|
685
|
+
expect(getRelativeLinkPath(base, path.join(testProjectDir, 'a'))).toBe(
|
|
686
|
+
'../'
|
|
687
|
+
)
|
|
688
|
+
expect(getRelativeLinkPath(base, path.join(base, '.next-docs'))).toBe(
|
|
689
|
+
'./.next-docs'
|
|
690
|
+
)
|
|
691
|
+
expect(
|
|
692
|
+
getRelativeLinkPath(base, path.join(testProjectDir, 'a', 'docs'))
|
|
693
|
+
).toBe('../docs')
|
|
694
|
+
expect(getRelativeLinkPath(base, path.join(base, '..docs'))).toBe(
|
|
695
|
+
'./..docs'
|
|
696
|
+
)
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
// In a pnpm monorepo `next` is usually not resolvable from the repo root,
|
|
700
|
+
// so the bundled docs have to be found through the workspace packages.
|
|
701
|
+
describe('bundled docs from workspace packages', () => {
|
|
702
|
+
function setupWorkspace(patterns, packages) {
|
|
703
|
+
fs.writeFileSync(
|
|
704
|
+
path.join(testProjectDir, 'package.json'),
|
|
705
|
+
JSON.stringify({ name: 'root', private: true })
|
|
706
|
+
)
|
|
707
|
+
fs.writeFileSync(
|
|
708
|
+
path.join(testProjectDir, 'pnpm-workspace.yaml'),
|
|
709
|
+
`packages:\n${patterns.map((p) => ` - '${p}'`).join('\n')}\n`
|
|
710
|
+
)
|
|
711
|
+
for (const [pkg, version] of Object.entries(packages)) {
|
|
712
|
+
const pkgDir = path.join(testProjectDir, pkg)
|
|
713
|
+
fs.mkdirSync(pkgDir, { recursive: true })
|
|
714
|
+
fs.writeFileSync(
|
|
715
|
+
path.join(pkgDir, 'package.json'),
|
|
716
|
+
JSON.stringify({ name: path.basename(pkg) })
|
|
717
|
+
)
|
|
718
|
+
if (version) installNextIsolated(testProjectDir, pkgDir, version)
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function output() {
|
|
723
|
+
// Package names are colored; strip ANSI so FORCE_COLOR doesn't matter.
|
|
724
|
+
return require('strip-ansi')(consoleOutput.join('\n'))
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function runFromWorkspaceRoot() {
|
|
728
|
+
return require('strip-ansi')(
|
|
729
|
+
execNodeWithoutNodePath(
|
|
730
|
+
`const { runAgentsMd } = require(${JSON.stringify(
|
|
731
|
+
path.join(__dirname, '../../bin/agents-md.js')
|
|
732
|
+
)})
|
|
733
|
+
runAgentsMd({ output: 'AGENTS.md' }).catch((error) => {
|
|
734
|
+
console.error(error)
|
|
735
|
+
process.exitCode = 1
|
|
736
|
+
})`,
|
|
737
|
+
{ cwd: testProjectDir }
|
|
738
|
+
)
|
|
739
|
+
)
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
it('uses the bundled docs of a workspace package when run from the root', () => {
|
|
743
|
+
setupWorkspace(['apps/*'], { 'apps/web': '16.4.0' })
|
|
744
|
+
|
|
745
|
+
const cliOutput = runFromWorkspaceRoot()
|
|
746
|
+
|
|
747
|
+
const agentsMdPath = path.join(testProjectDir, 'AGENTS.md')
|
|
748
|
+
expect(readIndex(agentsMdPath).root).toBe(
|
|
749
|
+
'./apps/web/node_modules/next/dist/docs'
|
|
750
|
+
)
|
|
751
|
+
expect(
|
|
752
|
+
fs.existsSync(path.join(resolveRoot(agentsMdPath), '01-app'))
|
|
753
|
+
).toBe(true)
|
|
754
|
+
expect(fs.existsSync(path.join(testProjectDir, '.next-docs'))).toBe(
|
|
755
|
+
false
|
|
756
|
+
)
|
|
757
|
+
expect(fs.existsSync(path.join(testProjectDir, '.gitignore'))).toBe(
|
|
758
|
+
false
|
|
759
|
+
)
|
|
760
|
+
expect(cliOutput).toContain('workspace package apps/web')
|
|
761
|
+
})
|
|
762
|
+
|
|
763
|
+
it('indexes the package with the highest Next.js version', () => {
|
|
764
|
+
setupWorkspace(['apps/a', 'apps/b'], {
|
|
765
|
+
'apps/a': '16.3.0',
|
|
766
|
+
'apps/b': '16.4.0',
|
|
767
|
+
})
|
|
768
|
+
|
|
769
|
+
const cliOutput = runFromWorkspaceRoot()
|
|
770
|
+
|
|
771
|
+
const { root, content } = readIndex(
|
|
772
|
+
path.join(testProjectDir, 'AGENTS.md')
|
|
773
|
+
)
|
|
774
|
+
expect(root).toBe('./apps/b/node_modules/next/dist/docs')
|
|
775
|
+
expect(content).toContain('docs-16.4.0.md')
|
|
776
|
+
expect(cliOutput).not.toContain('Downloading')
|
|
777
|
+
expect(fs.existsSync(path.join(testProjectDir, '.next-docs'))).toBe(
|
|
778
|
+
false
|
|
779
|
+
)
|
|
780
|
+
})
|
|
781
|
+
|
|
782
|
+
it('picks the first matching package in workspace order and logs it', () => {
|
|
783
|
+
// Listed b before a, so the choice follows the workspace patterns
|
|
784
|
+
// rather than alphabetical order.
|
|
785
|
+
setupWorkspace(['apps/b', 'apps/a'], {
|
|
786
|
+
'apps/a': '16.4.0',
|
|
787
|
+
'apps/b': '16.4.0',
|
|
788
|
+
})
|
|
789
|
+
|
|
790
|
+
const cliOutput = runFromWorkspaceRoot()
|
|
791
|
+
|
|
792
|
+
expect(readIndex(path.join(testProjectDir, 'AGENTS.md')).root).toBe(
|
|
793
|
+
'./apps/b/node_modules/next/dist/docs'
|
|
794
|
+
)
|
|
795
|
+
expect(cliOutput).toContain('workspace package apps/b')
|
|
796
|
+
expect(cliOutput).not.toContain('workspace package apps/a')
|
|
797
|
+
})
|
|
798
|
+
|
|
799
|
+
it('prefers a matching install resolved from the cwd', async () => {
|
|
800
|
+
setupWorkspace(['apps/*'], { 'apps/web': '16.4.0' })
|
|
801
|
+
writeBundledNextPackage(
|
|
802
|
+
path.join(testProjectDir, 'node_modules', 'next'),
|
|
803
|
+
'16.4.0'
|
|
804
|
+
)
|
|
805
|
+
process.chdir(testProjectDir)
|
|
806
|
+
|
|
807
|
+
await runAgentsMd({ output: 'AGENTS.md' })
|
|
808
|
+
|
|
809
|
+
expect(readIndex(path.join(testProjectDir, 'AGENTS.md')).root).toBe(
|
|
810
|
+
'./node_modules/next/dist/docs'
|
|
811
|
+
)
|
|
812
|
+
expect(output()).not.toContain('workspace package')
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
it('uses a workspace package matching --version when the cwd resolves another version', async () => {
|
|
816
|
+
setupWorkspace(['apps/*'], { 'apps/web': '16.4.0' })
|
|
817
|
+
writeBundledNextPackage(
|
|
818
|
+
path.join(testProjectDir, 'node_modules', 'next'),
|
|
819
|
+
'16.3.0'
|
|
820
|
+
)
|
|
821
|
+
process.chdir(testProjectDir)
|
|
822
|
+
|
|
823
|
+
await runAgentsMd({ version: '16.4.0', output: 'AGENTS.md' })
|
|
824
|
+
|
|
825
|
+
const { root, content } = readIndex(
|
|
826
|
+
path.join(testProjectDir, 'AGENTS.md')
|
|
827
|
+
)
|
|
828
|
+
expect(root).toBe('./apps/web/node_modules/next/dist/docs')
|
|
829
|
+
expect(content).toContain('docs-16.4.0.md')
|
|
830
|
+
expect(pullDocsSpy).not.toHaveBeenCalled()
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
it('still downloads when no workspace package has matching bundled docs', async () => {
|
|
834
|
+
setupWorkspace(['apps/*'], { 'apps/web': '16.4.0' })
|
|
835
|
+
process.chdir(testProjectDir)
|
|
836
|
+
|
|
837
|
+
await runAgentsMd({ version: '15.0.0', output: 'AGENTS.md' })
|
|
838
|
+
|
|
839
|
+
expect(pullDocsSpy).toHaveBeenCalled()
|
|
840
|
+
expect(readIndex(path.join(testProjectDir, 'AGENTS.md')).root).toBe(
|
|
841
|
+
'./.next-docs'
|
|
842
|
+
)
|
|
843
|
+
expect(output()).not.toContain('workspace package')
|
|
844
|
+
})
|
|
845
|
+
})
|
|
846
|
+
})
|
|
847
|
+
|
|
409
848
|
describe('getNextjsVersion', () => {
|
|
410
849
|
const fixturesDir = path.join(__dirname, 'fixtures/agents-md')
|
|
411
850
|
|
|
@@ -428,9 +867,15 @@ This is my project documentation.
|
|
|
428
867
|
})
|
|
429
868
|
|
|
430
869
|
it('returns error when Next.js is not installed', () => {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
870
|
+
const result = JSON.parse(
|
|
871
|
+
execNodeWithoutNodePath(
|
|
872
|
+
`const { getNextjsVersion } = require(${JSON.stringify(
|
|
873
|
+
path.join(__dirname, '../agents-md.js')
|
|
874
|
+
)})
|
|
875
|
+
console.log(JSON.stringify(getNextjsVersion(process.cwd())))`,
|
|
876
|
+
{ cwd: testProjectDir }
|
|
877
|
+
)
|
|
878
|
+
)
|
|
434
879
|
|
|
435
880
|
expect(result.version).toBeNull()
|
|
436
881
|
expect(result.error).toBe('Next.js is not installed in this project.')
|
package/lib/agents-md.js
CHANGED
|
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
exports.refreshAgentRulesBlock = refreshAgentRulesBlock;
|
|
13
13
|
exports.getNextjsVersion = getNextjsVersion;
|
|
14
14
|
exports.getBundledDocsInfo = getBundledDocsInfo;
|
|
15
|
+
exports.getRelativeLinkPath = getRelativeLinkPath;
|
|
15
16
|
exports.getBundledDocsLinkPath = getBundledDocsLinkPath;
|
|
16
17
|
exports.pullDocs = pullDocs;
|
|
17
18
|
exports.collectDocFiles = collectDocFiles;
|
|
@@ -87,33 +88,98 @@ function getNextjsVersion(cwd) {
|
|
|
87
88
|
}
|
|
88
89
|
/**
|
|
89
90
|
* Next.js ships its documentation inside the published package (at
|
|
90
|
-
* `dist/docs`) since 16.2.0. When
|
|
91
|
-
* bundled docs, the index can point at them directly instead
|
|
92
|
-
* downloading a copy into `.next-docs`.
|
|
91
|
+
* `dist/docs`) since 16.2.0. When an installed `next` of the requested
|
|
92
|
+
* `version` has bundled docs, the index can point at them directly instead
|
|
93
|
+
* of downloading a copy into `.next-docs`.
|
|
94
|
+
*
|
|
95
|
+
* The install resolved from `cwd` wins; otherwise the workspace packages are
|
|
96
|
+
* searched.
|
|
93
97
|
*/
|
|
94
|
-
function getBundledDocsInfo(cwd) {
|
|
98
|
+
function getBundledDocsInfo(cwd, version) {
|
|
99
|
+
const fromCwd = getBundledDocsInfoFrom(cwd);
|
|
100
|
+
if (fromCwd?.version === version) {
|
|
101
|
+
return fromCwd;
|
|
102
|
+
}
|
|
103
|
+
// Mirror getNextjsVersion: with isolated installs (pnpm), `next` is often
|
|
104
|
+
// only resolvable from the workspace package that depends on it, not from
|
|
105
|
+
// the repo root. Use the first package, in workspace order, that ships
|
|
106
|
+
// bundled docs for the requested version.
|
|
107
|
+
const workspace = detectWorkspace(cwd);
|
|
108
|
+
if (workspace.isMonorepo) {
|
|
109
|
+
for (const pkgPath of expandWorkspacePatterns(cwd, workspace.packages)) {
|
|
110
|
+
const info = getBundledDocsInfoFrom(pkgPath);
|
|
111
|
+
if (info?.version === version) {
|
|
112
|
+
return info;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function getBundledDocsInfoFrom(dir) {
|
|
95
119
|
try {
|
|
96
|
-
const nextPkgPath = require.resolve('next/package.json', { paths: [
|
|
120
|
+
const nextPkgPath = require.resolve('next/package.json', { paths: [dir] });
|
|
97
121
|
const pkg = JSON.parse(fs_1.default.readFileSync(nextPkgPath, 'utf-8'));
|
|
98
122
|
const docsPath = path_1.default.join(path_1.default.dirname(nextPkgPath), 'dist', 'docs');
|
|
99
123
|
if (!pkg.version || collectDocFiles(docsPath).length === 0) {
|
|
100
124
|
return null;
|
|
101
125
|
}
|
|
102
|
-
return { docsPath, version: pkg.version };
|
|
126
|
+
return { docsPath, version: pkg.version, resolvedFrom: dir };
|
|
103
127
|
}
|
|
104
128
|
catch {
|
|
105
129
|
return null;
|
|
106
130
|
}
|
|
107
131
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Returns `target` as a `./`- or `../`-prefixed, forward-slash path relative
|
|
134
|
+
* to `fromDir`. Agents resolve paths in the index from the directory of the
|
|
135
|
+
* output file, so `fromDir` must be that directory, not the cwd.
|
|
136
|
+
*/
|
|
137
|
+
function getRelativeLinkPath(fromDir, target) {
|
|
138
|
+
const relative = path_1.default.relative(fromDir, target).replace(/\\/g, '/');
|
|
139
|
+
if (relative === '')
|
|
140
|
+
return './';
|
|
141
|
+
if (relative === '..')
|
|
142
|
+
return '../';
|
|
143
|
+
// A dot-directory such as `.next-docs` still needs the `./` prefix.
|
|
144
|
+
return relative.startsWith('../') ? relative : `./${relative}`;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Canonical spelling of `p`: symlinks resolved and, on Windows, 8.3 short
|
|
148
|
+
* names (e.g. `C:\Users\RUNNER~1`) expanded. `process.cwd()` can keep a
|
|
149
|
+
* non-canonical spelling while module resolution returns the canonical one,
|
|
150
|
+
* so compare and relate paths only in this form. Falls back to the resolved
|
|
151
|
+
* path when `p` cannot be canonicalized (e.g. it does not exist).
|
|
152
|
+
*/
|
|
153
|
+
function canonicalPath(p) {
|
|
154
|
+
try {
|
|
155
|
+
return fs_1.default.realpathSync.native(p);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return path_1.default.resolve(p);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Link to the bundled docs of the install resolved from `resolvedFrom`,
|
|
163
|
+
* relative to `fromDir` (the directory of the output file).
|
|
164
|
+
*/
|
|
165
|
+
function getBundledDocsLinkPath(fromDir, docsPath, resolvedFrom) {
|
|
166
|
+
// Prefer the conventional path of the resolved install over its realpath:
|
|
167
|
+
// pnpm's `.pnpm/next@x.y.z/...` realpath changes on every upgrade, while
|
|
168
|
+
// the `node_modules/next` symlink stays stable. Check it under the
|
|
169
|
+
// directory `next` was resolved from, not under `fromDir`, which may see a
|
|
170
|
+
// different `next` install.
|
|
171
|
+
//
|
|
172
|
+
// Paths are related in canonical form (see canonicalPath) so a cwd or
|
|
173
|
+
// --output spelled differently than the resolved install still produces a
|
|
174
|
+
// clean link. Only the parent of the `node_modules/next` symlink is
|
|
175
|
+
// canonicalized, never the symlink itself, so the stable path is kept.
|
|
176
|
+
const outputDir = canonicalPath(fromDir);
|
|
177
|
+
const conventional = path_1.default.join(canonicalPath(resolvedFrom), 'node_modules', 'next', 'dist', 'docs');
|
|
178
|
+
if (fs_1.default.existsSync(conventional) &&
|
|
179
|
+
canonicalPath(conventional) === canonicalPath(docsPath)) {
|
|
180
|
+
return getRelativeLinkPath(outputDir, conventional);
|
|
181
|
+
}
|
|
182
|
+
return getRelativeLinkPath(outputDir, canonicalPath(docsPath));
|
|
117
183
|
}
|
|
118
184
|
function versionToGitHubTag(version) {
|
|
119
185
|
return version.startsWith('v') ? version : `v${version}`;
|
|
@@ -264,7 +330,7 @@ function generateAgentsMdIndex(data) {
|
|
|
264
330
|
parts.push(`root: ${docsPath}`);
|
|
265
331
|
parts.push('STOP. What you remember about Next.js is WRONG for this project. Always search docs and read before any task.');
|
|
266
332
|
const targetFile = outputFile || 'AGENTS.md';
|
|
267
|
-
parts.push(`If docs missing, run this command
|
|
333
|
+
parts.push(`If docs missing, run this command from this file's directory: npx @next/codemod agents-md --output ${targetFile}`);
|
|
268
334
|
const allFiles = collectAllFilesFromSections(sections);
|
|
269
335
|
const grouped = groupByDirectory(allFiles);
|
|
270
336
|
for (const [dir, files] of grouped) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@next/codemod",
|
|
3
|
-
"version": "16.5.0-canary.
|
|
3
|
+
"version": "16.5.0-canary.30",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
"lib/**/*.js",
|
|
29
29
|
"lib/cra-to-next/gitignore"
|
|
30
30
|
],
|
|
31
|
-
"bin":
|
|
31
|
+
"bin": {
|
|
32
|
+
"codemod": "./bin/next-codemod.js"
|
|
33
|
+
},
|
|
32
34
|
"devDependencies": {
|
|
33
35
|
"@types/find-up": "4.0.0",
|
|
34
36
|
"@types/js-yaml": "4.0.9",
|