@next/codemod 16.3.0-preview.5 → 16.3.0-preview.7

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.
@@ -43,6 +43,7 @@ program
43
43
  .argument('[revision]', 'Specify the upgrade type ("patch", "minor", "major"), an NPM dist tag (e.g. "latest", "canary", "rc"), or an exact version (e.g. "15.0.0"). Defaults to "minor".')
44
44
  .usage('[revision] [options]')
45
45
  .option('--verbose', 'Verbose output', false)
46
+ .option('-y, --yes', 'Skip every interactive prompt and accept its default. Also auto-enabled when stdin is not a TTY (e.g. running under an agent or in CI).', false)
46
47
  .action(async (revision, options) => {
47
48
  try {
48
49
  await (0, upgrade_1.runUpgrade)(revision, options);
package/bin/upgrade.js CHANGED
@@ -47,6 +47,7 @@ const picocolors_1 = __importDefault(require("picocolors"));
47
47
  const handle_package_1 = require("../lib/handle-package");
48
48
  const transform_1 = require("./transform");
49
49
  const utils_1 = require("../lib/utils");
50
+ const agents_md_1 = require("../lib/agents-md");
50
51
  const shared_1 = require("./shared");
51
52
  const optionalNextjsPackages = [
52
53
  'create-next-app',
@@ -114,6 +115,10 @@ function resolveSemanticRevision(revision, installedVersion) {
114
115
  }
115
116
  async function runUpgrade(revision, options) {
116
117
  const { verbose } = options;
118
+ const nonInteractive = options.yes === true || !process.stdin.isTTY;
119
+ if (nonInteractive) {
120
+ console.log(` Running in non-interactive mode. Every prompt will accept its default.`);
121
+ }
117
122
  const appPackageJsonPath = path_1.default.resolve(cwd, 'package.json');
118
123
  let appPackageJson = JSON.parse(fs_1.default.readFileSync(appPackageJsonPath, 'utf8'));
119
124
  const installedNextVersion = getInstalledNextVersion();
@@ -189,18 +194,24 @@ async function runUpgrade(revision, options) {
189
194
  // The mixed case is tricky to handle from a types perspective.
190
195
  // We'll recommend to upgrade in the prompt but users can decide to try 18.
191
196
  !isPureAppRouter) {
192
- const shouldStayOnReact18Res = await (0, prompts_1.default)({
193
- type: 'confirm',
194
- name: 'shouldStayOnReact18',
195
- message: `Do you prefer to stay on React 18?` +
196
- (isMixedApp
197
- ? " Since you're using both pages/ and app/, we recommend upgrading React to use a consistent version throughout your app."
198
- : ''),
199
- initial: false,
200
- active: 'Yes',
201
- inactive: 'No',
202
- }, { onCancel: utils_1.onCancel });
203
- shouldStayOnReact18 = shouldStayOnReact18Res.shouldStayOnReact18;
197
+ if (nonInteractive) {
198
+ // Default: upgrade React past 18.
199
+ shouldStayOnReact18 = false;
200
+ }
201
+ else {
202
+ const shouldStayOnReact18Res = await (0, prompts_1.default)({
203
+ type: 'confirm',
204
+ name: 'shouldStayOnReact18',
205
+ message: `Do you prefer to stay on React 18?` +
206
+ (isMixedApp
207
+ ? " Since you're using both pages/ and app/, we recommend upgrading React to use a consistent version throughout your app."
208
+ : ''),
209
+ initial: false,
210
+ active: 'Yes',
211
+ inactive: 'No',
212
+ }, { onCancel: utils_1.onCancel });
213
+ shouldStayOnReact18 = shouldStayOnReact18Res.shouldStayOnReact18;
214
+ }
204
215
  }
205
216
  // We're resolving a specific version here to avoid including "ugly" version queries
206
217
  // in the manifest.
@@ -212,9 +223,9 @@ async function runUpgrade(revision, options) {
212
223
  : await loadHighestNPMVersionMatching(`react@${targetNextPackageJson.peerDependencies['react']}`);
213
224
  if ((0, semver_1.compare)(targetNextVersion, '15.0.0-canary') >= 0 &&
214
225
  (0, semver_1.compare)(targetNextVersion, '16.0.0-canary') < 0) {
215
- await suggestTurbopack(appPackageJson, targetNextVersion);
226
+ await suggestTurbopack(appPackageJson, targetNextVersion, nonInteractive);
216
227
  }
217
- const codemods = await suggestCodemods(installedNextVersion, targetNextVersion);
228
+ const codemods = await suggestCodemods(installedNextVersion, targetNextVersion, nonInteractive);
218
229
  const packageManager = (0, handle_package_1.getPkgManager)(cwd);
219
230
  let shouldRunReactCodemods = false;
220
231
  let shouldRunReactTypesCodemods = false;
@@ -223,8 +234,9 @@ async function runUpgrade(revision, options) {
223
234
  if (!shouldStayOnReact18 &&
224
235
  (0, semver_1.compare)(targetReactVersion, '19.0.0-0') >= 0 &&
225
236
  (0, semver_1.compare)(installedReactVersion, '19.0.0-0') < 0) {
226
- shouldRunReactCodemods = await suggestReactCodemods();
227
- shouldRunReactTypesCodemods = await suggestReactTypesCodemods();
237
+ shouldRunReactCodemods = await suggestReactCodemods(nonInteractive);
238
+ shouldRunReactTypesCodemods =
239
+ await suggestReactTypesCodemods(nonInteractive);
228
240
  execCommand = getNpxCommand(packageManager);
229
241
  }
230
242
  fs_1.default.writeFileSync(appPackageJsonPath, JSON.stringify(appPackageJson, null, 2));
@@ -284,6 +296,37 @@ async function runUpgrade(revision, options) {
284
296
  };
285
297
  }
286
298
  }
299
+ // Bump `eslint` alongside `eslint-config-next` so the install doesn't fail
300
+ // on a peer-dep mismatch. e.g. `eslint-config-next@16.x` requires
301
+ // `eslint@>=9`, but a project upgrading from Next 15 will still have
302
+ // `eslint@^8` from create-next-app. Skip silently if anything goes wrong;
303
+ // the worst case is the user hits the same peer-dep error they would have
304
+ // without this bump.
305
+ //
306
+ // Only act when the project is actually using `eslint-config-next` — we
307
+ // don't want to silently upgrade eslint majors for projects that use
308
+ // eslint for unrelated reasons.
309
+ if (allDependencies['eslint'] && allDependencies['eslint-config-next']) {
310
+ try {
311
+ const eslintConfigNextPeerDepsJSON = (0, child_process_1.execSync)(`npm --silent view "eslint-config-next@${targetNextVersion}" peerDependencies --json`, { encoding: 'utf-8' });
312
+ const eslintConfigNextPeerDeps = eslintConfigNextPeerDepsJSON.trim() === ''
313
+ ? {}
314
+ : JSON.parse(eslintConfigNextPeerDepsJSON);
315
+ const eslintRange = eslintConfigNextPeerDeps?.eslint;
316
+ if (eslintRange) {
317
+ const targetEslintVersion = await loadHighestNPMVersionMatching(`eslint@${eslintRange}`);
318
+ versionMapping['eslint'] = {
319
+ version: targetEslintVersion,
320
+ required: false,
321
+ };
322
+ }
323
+ }
324
+ catch (e) {
325
+ if (verbose) {
326
+ console.warn(` Could not determine eslint peer range from eslint-config-next@${targetNextVersion}. Leaving eslint version alone.`, e);
327
+ }
328
+ }
329
+ }
287
330
  // Even though we only need those if we alias `@types/react` to types-react,
288
331
  // we still do it out of safety due to https://github.com/microsoft/DefinitelyTyped-tools/issues/433.
289
332
  const overrides = {};
@@ -320,10 +363,11 @@ async function runUpgrade(revision, options) {
320
363
  // understanding of the codemods, we run all of the applicable codemods.
321
364
  if (shouldRunReactCodemods) {
322
365
  // https://react.dev/blog/2024/04/25/react-19-upgrade-guide#run-all-react-19-codemods
323
- (0, child_process_1.execSync)(
324
366
  // `--no-interactive` skips the interactive prompt that asks for confirmation
325
367
  // https://github.com/codemod-com/codemod/blob/c0cf00d13161a0ec0965b6cc6bc5d54076839cc8/apps/cli/src/flags.ts#L160
326
- `${execCommand} codemod@latest react/19/migration-recipe --no-interactive`, { stdio: 'inherit' });
368
+ // `--allow-dirty` is required because the upgrade above modified package.json
369
+ // and the lockfile; the recipe refuses to run on a dirty tree otherwise.
370
+ (0, child_process_1.execSync)(`${execCommand} codemod@latest react/19/migration-recipe --no-interactive --allow-dirty`, { stdio: 'inherit' });
327
371
  }
328
372
  if (shouldRunReactTypesCodemods) {
329
373
  // https://react.dev/blog/2024/04/25/react-19-upgrade-guide#typescript-changes
@@ -337,6 +381,14 @@ async function runUpgrade(revision, options) {
337
381
  if (codemods.length > 0) {
338
382
  console.log(`${picocolors_1.default.green('✔')} Codemods have been applied successfully.`);
339
383
  }
384
+ try {
385
+ if ((0, agents_md_1.refreshAgentRulesBlock)(cwd) === 'refreshed') {
386
+ console.log(`${picocolors_1.default.green('✔')} Refreshed the managed agent-rules block in AGENTS.md / CLAUDE.md to match the upgraded Next.js.`);
387
+ }
388
+ }
389
+ catch {
390
+ // The block refresh is best-effort — never fail the upgrade over it.
391
+ }
340
392
  warnDependenciesOutOfRange(appPackageJson, versionMapping);
341
393
  endMessage(targetNextVersion);
342
394
  }
@@ -383,7 +435,7 @@ function isUsingAppDir(projectPath) {
383
435
  * 3. Otherwise, we ask the user to manually add `--turbopack` to their dev command,
384
436
  * showing the current dev command as the initial value.
385
437
  */
386
- async function suggestTurbopack(packageJson, targetNextVersion) {
438
+ async function suggestTurbopack(packageJson, targetNextVersion, nonInteractive) {
387
439
  const devScript = packageJson.scripts?.['dev'];
388
440
  // Turbopack flag was changed from `--turbo` to `--turbopack` in v15.0.1-canary.3
389
441
  // PR: https://github.com/vercel/next.js/pull/71657
@@ -406,19 +458,28 @@ async function suggestTurbopack(packageJson, targetNextVersion) {
406
458
  }
407
459
  return;
408
460
  }
409
- const responseTurbopack = await (0, prompts_1.default)({
410
- type: 'confirm',
411
- name: 'enable',
412
- message: `Enable Turbopack for ${picocolors_1.default.bold('next dev')}?`,
413
- initial: true,
414
- }, { onCancel: utils_1.onCancel });
415
- if (!responseTurbopack.enable) {
461
+ let enable = true;
462
+ if (!nonInteractive) {
463
+ const responseTurbopack = await (0, prompts_1.default)({
464
+ type: 'confirm',
465
+ name: 'enable',
466
+ message: `Enable Turbopack for ${picocolors_1.default.bold('next dev')}?`,
467
+ initial: true,
468
+ }, { onCancel: utils_1.onCancel });
469
+ enable = responseTurbopack.enable;
470
+ }
471
+ if (!enable) {
416
472
  return;
417
473
  }
418
474
  packageJson.scripts['dev'] = devScript.replace('next dev', `next dev ${turboPackFlag}`);
419
475
  return;
420
476
  }
421
477
  console.log(`${picocolors_1.default.yellow('⚠')} Could not find "${picocolors_1.default.bold('next dev')}" in your dev script.`);
478
+ if (nonInteractive) {
479
+ // Without a TTY we can't ask the user for a replacement script.
480
+ // Keep the existing dev script untouched.
481
+ return;
482
+ }
422
483
  const responseCustomDevScript = await (0, prompts_1.default)({
423
484
  type: 'text',
424
485
  name: 'customDevScript',
@@ -428,7 +489,7 @@ async function suggestTurbopack(packageJson, targetNextVersion) {
428
489
  packageJson.scripts['dev'] =
429
490
  responseCustomDevScript.customDevScript || devScript;
430
491
  }
431
- async function suggestCodemods(initialNextVersion, targetNextVersion) {
492
+ async function suggestCodemods(initialNextVersion, targetNextVersion, nonInteractive) {
432
493
  // example:
433
494
  // codemod version: 15.0.0-canary.45
434
495
  // 14.3 -> 15.0.0-canary.45: apply
@@ -451,6 +512,13 @@ async function suggestCodemods(initialNextVersion, targetNextVersion) {
451
512
  if (relevantCodemods.length === 0) {
452
513
  return [];
453
514
  }
515
+ if (nonInteractive) {
516
+ // Default: apply every recommended codemod, matching `selected: true` below.
517
+ const all = relevantCodemods.map(({ value }) => value);
518
+ console.log(` Applying all ${picocolors_1.default.blue('codemods')} recommended for your upgrade:\n` +
519
+ all.map((value) => ` - ${value}`).join('\n'));
520
+ return all;
521
+ }
454
522
  const { codemods } = await (0, prompts_1.default)({
455
523
  type: 'multiselect',
456
524
  name: 'codemods',
@@ -466,7 +534,10 @@ async function suggestCodemods(initialNextVersion, targetNextVersion) {
466
534
  }, { onCancel: utils_1.onCancel });
467
535
  return codemods;
468
536
  }
469
- async function suggestReactCodemods() {
537
+ async function suggestReactCodemods(nonInteractive) {
538
+ if (nonInteractive) {
539
+ return true;
540
+ }
470
541
  const { runReactCodemod } = await (0, prompts_1.default)({
471
542
  type: 'confirm',
472
543
  name: 'runReactCodemod',
@@ -475,7 +546,10 @@ async function suggestReactCodemods() {
475
546
  }, { onCancel: utils_1.onCancel });
476
547
  return runReactCodemod;
477
548
  }
478
- async function suggestReactTypesCodemods() {
549
+ async function suggestReactTypesCodemods(nonInteractive) {
550
+ if (nonInteractive) {
551
+ return true;
552
+ }
479
553
  const { runReactTypesCodemod } = await (0, prompts_1.default)({
480
554
  type: 'confirm',
481
555
  name: 'runReactTypesCodemod',
package/lib/agents-md.js CHANGED
@@ -9,6 +9,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
9
9
  return (mod && mod.__esModule) ? mod : { "default": mod };
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.refreshAgentRulesBlock = refreshAgentRulesBlock;
12
13
  exports.getNextjsVersion = getNextjsVersion;
13
14
  exports.getBundledDocsInfo = getBundledDocsInfo;
14
15
  exports.getBundledDocsLinkPath = getBundledDocsLinkPath;
@@ -22,6 +23,49 @@ const execa_1 = __importDefault(require("execa"));
22
23
  const fs_1 = __importDefault(require("fs"));
23
24
  const path_1 = __importDefault(require("path"));
24
25
  const os_1 = __importDefault(require("os"));
26
+ const AGENT_RULES_START_MARKER = '<!-- BEGIN:nextjs-agent-rules -->';
27
+ /**
28
+ * After an upgrade, refresh the managed agent-rules block in
29
+ * AGENTS.md / CLAUDE.md so its content matches the Next.js version
30
+ * that is now installed.
31
+ *
32
+ * Delegates to the installed package's own generator
33
+ * (`next/dist/server/lib/generate-agent-files`), so the block text is
34
+ * always the one shipped with that version — this codemod never
35
+ * carries its own copy. Returns `'refreshed'` when a file was
36
+ * rewritten, `'current'` when the block was already up to date, and
37
+ * `'skipped'` when there is nothing to do: the project never adopted
38
+ * the managed block, or the installed Next.js predates the generator
39
+ * (< 16.3).
40
+ */
41
+ function refreshAgentRulesBlock(cwd) {
42
+ const hostsBlock = ['AGENTS.md', 'CLAUDE.md'].some((file) => {
43
+ try {
44
+ return fs_1.default
45
+ .readFileSync(path_1.default.join(cwd, file), 'utf-8')
46
+ .includes(AGENT_RULES_START_MARKER);
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ });
52
+ if (!hostsBlock)
53
+ return 'skipped';
54
+ let writeAgentFiles;
55
+ try {
56
+ const generatorPath = require.resolve('next/dist/server/lib/generate-agent-files', { paths: [cwd] });
57
+ writeAgentFiles = require(generatorPath).writeAgentFiles;
58
+ if (typeof writeAgentFiles !== 'function')
59
+ return 'skipped';
60
+ }
61
+ catch {
62
+ return 'skipped';
63
+ }
64
+ const result = writeAgentFiles(cwd);
65
+ return result.agentsMd === 'updated' || result.claudeMd === 'updated'
66
+ ? 'refreshed'
67
+ : 'current';
68
+ }
25
69
  function getNextjsVersion(cwd) {
26
70
  try {
27
71
  const nextPkgPath = require.resolve('next/package.json', { paths: [cwd] });
package/lib/utils.js CHANGED
@@ -137,5 +137,10 @@ exports.TRANSFORMER_INQUIRER_CHOICES = [
137
137
  value: 'cache-components-instant-false',
138
138
  version: '16.3.0',
139
139
  },
140
+ {
141
+ title: "Remove `export const prefetch = 'partial'` Route Segment Config from App Router pages and layouts after enabling `partialPrefetching` globally",
142
+ value: 'remove-partial-prefetch',
143
+ version: '16.3.0',
144
+ },
140
145
  ];
141
146
  //# sourceMappingURL=utils.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@next/codemod",
3
- "version": "16.3.0-preview.5",
3
+ "version": "16.3.0-preview.7",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,7 +18,7 @@ const utils_1 = require("./lib/utils");
18
18
  // Properties that need to be moved to experimental.turbopack*
19
19
  const RENAMED_EXPERIMENTAL_PROPERTIES = {
20
20
  minify: 'turbopackMinify',
21
- treeShaking: 'turbopackTreeShaking',
21
+ treeShaking: 'turbopackModuleFragments',
22
22
  sourceMaps: 'turbopackSourceMaps',
23
23
  };
24
24
  // `memoryLimit` is no longer supported and is removed entirely. We drop it under
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = transformer;
4
+ const parser_1 = require("../lib/parser");
5
+ // Route Segment Config name and the only value this codemod strips.
6
+ const CONFIG_NAME = 'prefetch';
7
+ const TARGET_VALUE = 'partial';
8
+ // Unwrap `'partial' as const` / `'partial' satisfies T` so the value guard
9
+ // still matches when the export is annotated.
10
+ function unwrap(j, node) {
11
+ if ((j.TSAsExpression && j.TSAsExpression.check(node)) ||
12
+ (j.TSSatisfiesExpression && j.TSSatisfiesExpression.check(node))) {
13
+ return node.expression;
14
+ }
15
+ return node;
16
+ }
17
+ // Only `prefetch = 'partial'` matches. A different value such as
18
+ // `prefetch = 'allow-runtime'` is a legitimate config and is left untouched.
19
+ function isTargetPrefetch(j, decl) {
20
+ if (!j.VariableDeclarator.check(decl) || !j.Identifier.check(decl.id)) {
21
+ return false;
22
+ }
23
+ if (decl.id.name !== CONFIG_NAME || !decl.init) {
24
+ return false;
25
+ }
26
+ const init = unwrap(j, decl.init);
27
+ return ((j.StringLiteral.check(init) && init.value === TARGET_VALUE) ||
28
+ (j.Literal.check(init) && init.value === TARGET_VALUE));
29
+ }
30
+ // Drop only the `prefetch = 'partial'` declarator, leaving any sibling
31
+ // declarators (e.g. `export const runtime = 'edge', prefetch = 'partial'`)
32
+ // intact. Returns the number of declarators left so the caller can remove the
33
+ // whole statement when it becomes empty.
34
+ function stripTargetDeclarators(j, declaration) {
35
+ declaration.declarations = declaration.declarations.filter((decl) => !isTargetPrefetch(j, decl));
36
+ return declaration.declarations.length;
37
+ }
38
+ // Removing a statement also removes the comments attached above it — which
39
+ // may be a user's note or a deliberate `// TODO(...)` marker. Reattach the
40
+ // leading comments to the next statement (or the previous one when the
41
+ // removed statement is last) so they survive the removal.
42
+ function preserveLeadingComments(path) {
43
+ const comments = path.node.comments?.filter((comment) => comment.leading);
44
+ if (!comments?.length) {
45
+ return;
46
+ }
47
+ const body = path.parent.node.body;
48
+ if (!Array.isArray(body)) {
49
+ return;
50
+ }
51
+ const index = body.indexOf(path.node);
52
+ const next = body[index + 1];
53
+ const prev = body[index - 1];
54
+ if (next) {
55
+ next.comments = [...comments, ...(next.comments ?? [])];
56
+ }
57
+ else if (prev) {
58
+ for (const comment of comments) {
59
+ comment.leading = false;
60
+ comment.trailing = true;
61
+ }
62
+ prev.comments = [...(prev.comments ?? []), ...comments];
63
+ }
64
+ }
65
+ function transformer(file, _api) {
66
+ // Run on App Router page/layout files, except for test environment. The
67
+ // `prefetch` Route Segment Config only applies to pages and layouts, so
68
+ // route handlers are intentionally excluded.
69
+ // `(^|[/\\])app` matches both an absolute path and a relative `app/...` path
70
+ // (what `npx @next/codemod ... ./app` passes), so top-level app files aren't
71
+ // silently skipped.
72
+ if (process.env.NODE_ENV !== 'test' &&
73
+ !/(^|[/\\])app[/\\](?:.*[/\\])?(page|layout)(\.[^/\\]*)?$/.test(file.path)) {
74
+ return file.source;
75
+ }
76
+ const j = (0, parser_1.createParserFromPath)(file.path);
77
+ const root = j(file.source);
78
+ let hasChanges = false;
79
+ // `export const prefetch = 'partial'` (possibly alongside other configs in
80
+ // the same statement).
81
+ root
82
+ .find(j.ExportNamedDeclaration, {
83
+ declaration: { type: 'VariableDeclaration' },
84
+ })
85
+ .filter((path) => {
86
+ const declaration = path.node.declaration;
87
+ return (j.VariableDeclaration.check(declaration) &&
88
+ declaration.declarations.some((decl) => isTargetPrefetch(j, decl)));
89
+ })
90
+ .forEach((path) => {
91
+ const declaration = path.node.declaration;
92
+ const remaining = stripTargetDeclarators(j, declaration);
93
+ // Remove the whole export only when nothing else was declared with it.
94
+ if (remaining === 0) {
95
+ preserveLeadingComments(path);
96
+ j(path).remove();
97
+ }
98
+ hasChanges = true;
99
+ });
100
+ // Bare `const prefetch = 'partial'` declarations are only Route Segment
101
+ // Configs when the file also exports them as `prefetch` via a local
102
+ // `export { prefetch }`. Re-exports (`export { prefetch } from '...'`) bind
103
+ // another module's value, and aliased exports export a different name
104
+ // (`export { prefetch as other }`) or a different binding
105
+ // (`export { other as prefetch }`), so neither counts. When an aliased
106
+ // export shares the `prefetch` binding, removing the declaration would
107
+ // break it, so the whole file is left untouched.
108
+ let hasPlainPrefetchExportSpecifier = false;
109
+ let hasAliasedPrefetchBinding = false;
110
+ root
111
+ .find(j.ExportNamedDeclaration)
112
+ .filter((path) => !path.node.source)
113
+ .forEach((path) => {
114
+ for (const spec of path.node.specifiers ?? []) {
115
+ if (!j.ExportSpecifier.check(spec) ||
116
+ !j.Identifier.check(spec.local) ||
117
+ spec.local.name !== CONFIG_NAME) {
118
+ continue;
119
+ }
120
+ if (j.Identifier.check(spec.exported) &&
121
+ spec.exported.name === CONFIG_NAME) {
122
+ hasPlainPrefetchExportSpecifier = true;
123
+ }
124
+ else {
125
+ hasAliasedPrefetchBinding = true;
126
+ }
127
+ }
128
+ });
129
+ const hasLocalPrefetchExportSpecifier = hasPlainPrefetchExportSpecifier && !hasAliasedPrefetchBinding;
130
+ // Track that we removed a bare declaration so we only drop the matching
131
+ // export specifier below.
132
+ let removedBareDeclaration = false;
133
+ if (hasLocalPrefetchExportSpecifier) {
134
+ root
135
+ .find(j.VariableDeclaration)
136
+ .filter((path) => {
137
+ // `export const prefetch` is handled above; skip it here.
138
+ if (j.ExportNamedDeclaration.check(path.parent.node)) {
139
+ return false;
140
+ }
141
+ // Only top-level declarations can be the exported config. A local
142
+ // `const prefetch` inside a function or block is unrelated code.
143
+ if (!j.Program.check(path.parent.node)) {
144
+ return false;
145
+ }
146
+ return path.node.declarations.some((decl) => isTargetPrefetch(j, decl));
147
+ })
148
+ .forEach((path) => {
149
+ const remaining = stripTargetDeclarators(j, path.node);
150
+ if (remaining === 0) {
151
+ preserveLeadingComments(path);
152
+ j(path).remove();
153
+ }
154
+ removedBareDeclaration = true;
155
+ hasChanges = true;
156
+ });
157
+ }
158
+ // Handle `export { prefetch }` and `export { prefetch, other }`, but only
159
+ // when the paired declaration was the `'partial'` one we removed above.
160
+ if (removedBareDeclaration) {
161
+ root
162
+ .find(j.ExportNamedDeclaration)
163
+ // Skip re-exports (`export { prefetch } from '...'`): their specifiers
164
+ // reference another module's binding, not the declaration we removed.
165
+ .filter((path) => !path.node.source && Boolean(path.node.specifiers?.length))
166
+ .forEach((path) => {
167
+ const specifiers = path.node.specifiers;
168
+ if (!specifiers)
169
+ return;
170
+ const filteredSpecifiers = specifiers.filter((spec) => {
171
+ // Remove only the plain `export { prefetch }` specifier. Aliased
172
+ // specifiers export a different name or bind a different value, so
173
+ // they aren't the Route Segment Config.
174
+ if (j.ExportSpecifier.check(spec) &&
175
+ j.Identifier.check(spec.local) &&
176
+ j.Identifier.check(spec.exported)) {
177
+ return !(spec.local.name === CONFIG_NAME &&
178
+ spec.exported.name === CONFIG_NAME);
179
+ }
180
+ return true;
181
+ });
182
+ if (filteredSpecifiers.length !== specifiers.length) {
183
+ hasChanges = true;
184
+ if (filteredSpecifiers.length === 0) {
185
+ preserveLeadingComments(path);
186
+ j(path).remove();
187
+ }
188
+ else {
189
+ path.node.specifiers = filteredSpecifiers;
190
+ }
191
+ }
192
+ });
193
+ }
194
+ if (hasChanges) {
195
+ return root.toSource();
196
+ }
197
+ return file.source;
198
+ }
199
+ //# sourceMappingURL=remove-partial-prefetch.js.map