@next/codemod 16.3.0-canary.60 → 16.3.0-canary.61
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/utils.js
CHANGED
|
@@ -132,5 +132,10 @@ exports.TRANSFORMER_INQUIRER_CHOICES = [
|
|
|
132
132
|
value: 'remove-experimental-ppr',
|
|
133
133
|
version: '16.0.0-canary.11',
|
|
134
134
|
},
|
|
135
|
+
{
|
|
136
|
+
title: 'Add `export const instant = false` to App Router pages and layouts to ease Cache Components adoption',
|
|
137
|
+
value: 'cache-components-instant-false',
|
|
138
|
+
version: '16.3.0',
|
|
139
|
+
},
|
|
135
140
|
];
|
|
136
141
|
//# sourceMappingURL=utils.js.map
|
package/package.json
CHANGED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = transformer;
|
|
4
|
+
const parser_1 = require("../lib/parser");
|
|
5
|
+
/**
|
|
6
|
+
* Blanket-inserts `export const instant = false` into every App Router `page`,
|
|
7
|
+
* `layout`, and `default` file so they're marked as allowed to block when
|
|
8
|
+
* `cacheComponents` is enabled. Each opt-out is meant to be walked back, one
|
|
9
|
+
* route at a time, using the companion adoption skill.
|
|
10
|
+
*
|
|
11
|
+
* - Skips files that already declare or export `instant` in any form (never
|
|
12
|
+
* overrides existing config or appends a duplicate binding).
|
|
13
|
+
* - Skips Client/Server Component modules (`"use client"` / `"use server"`):
|
|
14
|
+
* `instant` is a Server Component route segment config, so exporting it from
|
|
15
|
+
* those modules is a build error.
|
|
16
|
+
* - Targets `page` / `layout` / `default` only (not `route` — `instant` does
|
|
17
|
+
* not apply to route handlers). `default.tsx` is the parallel-route fallback,
|
|
18
|
+
* a server segment that accepts route segment config like the other two.
|
|
19
|
+
*/
|
|
20
|
+
function transformer(file, _api) {
|
|
21
|
+
if (process.env.NODE_ENV !== 'test' &&
|
|
22
|
+
!/(^|[/\\])app[/\\].*?(page|layout|default)\.[^/\\]+$/.test(file.path)) {
|
|
23
|
+
return file.source;
|
|
24
|
+
}
|
|
25
|
+
const j = (0, parser_1.createParserFromPath)(file.path);
|
|
26
|
+
const root = j(file.source);
|
|
27
|
+
// Bail on Client/Server Component modules. `instant` is a Server Component
|
|
28
|
+
// route segment config; exporting it from a `"use client"` (or `"use server"`)
|
|
29
|
+
// module fails the build. Parsers represent the directive either in
|
|
30
|
+
// `program.directives` or as a leading string-literal `ExpressionStatement`.
|
|
31
|
+
const program = root.get().node.program;
|
|
32
|
+
const isClientOrServerDirective = (value) => value === 'use client' || value === 'use server';
|
|
33
|
+
let hasModuleDirective = (program.directives ?? []).some((d) => isClientOrServerDirective(d?.value?.value));
|
|
34
|
+
if (!hasModuleDirective) {
|
|
35
|
+
for (const node of program.body) {
|
|
36
|
+
if (node.type !== 'ExpressionStatement' ||
|
|
37
|
+
(node.expression?.type !== 'StringLiteral' &&
|
|
38
|
+
node.expression?.type !== 'Literal')) {
|
|
39
|
+
// Directives must lead the module; stop at the first non-directive.
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
if (isClientOrServerDirective(node.expression.value)) {
|
|
43
|
+
hasModuleDirective = true;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (hasModuleDirective) {
|
|
49
|
+
return file.source;
|
|
50
|
+
}
|
|
51
|
+
// Bail if `instant` already exists in any form, so we never append a
|
|
52
|
+
// duplicate declaration (which would be a `SyntaxError`). This covers:
|
|
53
|
+
// export const instant = ...
|
|
54
|
+
// export const a = 1, instant = ... (any declarator position)
|
|
55
|
+
// const instant = ... (local binding)
|
|
56
|
+
// const { instant } = ... (destructured binding)
|
|
57
|
+
// export { instant }
|
|
58
|
+
// export { foo as instant }
|
|
59
|
+
// export function instant() {} / export class instant {}
|
|
60
|
+
const bindsInstant = (node) => {
|
|
61
|
+
switch (node?.type) {
|
|
62
|
+
case 'Identifier':
|
|
63
|
+
return node.name === 'instant';
|
|
64
|
+
case 'ObjectPattern':
|
|
65
|
+
return node.properties.some((prop) => prop.type === 'RestElement'
|
|
66
|
+
? bindsInstant(prop.argument)
|
|
67
|
+
: bindsInstant(prop.value ?? prop.argument));
|
|
68
|
+
case 'ArrayPattern':
|
|
69
|
+
return node.elements.some((el) => el != null && bindsInstant(el));
|
|
70
|
+
case 'AssignmentPattern':
|
|
71
|
+
return bindsInstant(node.left);
|
|
72
|
+
case 'RestElement':
|
|
73
|
+
return bindsInstant(node.argument);
|
|
74
|
+
default:
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const hasInstantBinding = root
|
|
79
|
+
.find(j.VariableDeclarator)
|
|
80
|
+
.filter((p) => bindsInstant(p.node.id))
|
|
81
|
+
.size() > 0 ||
|
|
82
|
+
root.find(j.ExportSpecifier, { exported: { name: 'instant' } }).size() >
|
|
83
|
+
0 ||
|
|
84
|
+
root.find(j.FunctionDeclaration, { id: { name: 'instant' } }).size() > 0 ||
|
|
85
|
+
root.find(j.ClassDeclaration, { id: { name: 'instant' } }).size() > 0;
|
|
86
|
+
if (hasInstantBinding) {
|
|
87
|
+
return file.source;
|
|
88
|
+
}
|
|
89
|
+
// Build `export const instant = false`. The two `//` comments above it
|
|
90
|
+
// (TODO + See:) are attached as leading comments on the declaration so
|
|
91
|
+
// recast prints them right above it.
|
|
92
|
+
const todoComment = j.commentLine(' TODO: Cache Components adoption. Refactor this route so this opt-out can be removed.', true, false);
|
|
93
|
+
const seeComment = j.commentLine(' See: https://nextjs.org/docs/app/guides/migrating-to-cache-components', true, false);
|
|
94
|
+
const instantExport = j.exportNamedDeclaration(j.variableDeclaration('const', [
|
|
95
|
+
j.variableDeclarator(j.identifier('instant'), j.booleanLiteral(false)),
|
|
96
|
+
]));
|
|
97
|
+
instantExport.comments = [todoComment, seeComment];
|
|
98
|
+
// Insert after the last top-level import, or at the top of the module
|
|
99
|
+
// if there are no imports.
|
|
100
|
+
const body = program.body;
|
|
101
|
+
let lastImportIndex = -1;
|
|
102
|
+
for (let i = 0; i < body.length; i++) {
|
|
103
|
+
if (body[i].type === 'ImportDeclaration')
|
|
104
|
+
lastImportIndex = i;
|
|
105
|
+
}
|
|
106
|
+
if (lastImportIndex !== -1) {
|
|
107
|
+
body.splice(lastImportIndex + 1, 0, instantExport);
|
|
108
|
+
}
|
|
109
|
+
else if (body.length > 0) {
|
|
110
|
+
// No imports. Inserting at index 0 would steal any file-level leading
|
|
111
|
+
// comments (e.g. `// @ts-nocheck`) from `body[0]` because recast
|
|
112
|
+
// attributes them to whatever is first. Move those leading comments
|
|
113
|
+
// off `body[0]` onto the new export *before* its TODO/See: lines, so
|
|
114
|
+
// they print in their original position.
|
|
115
|
+
const first = body[0];
|
|
116
|
+
const allComments = (first.comments ?? []);
|
|
117
|
+
const firstLeading = allComments.filter((c) => c.leading === true);
|
|
118
|
+
if (firstLeading.length > 0) {
|
|
119
|
+
first.comments = allComments.filter((c) => c.leading !== true);
|
|
120
|
+
instantExport.comments = [...firstLeading, todoComment, seeComment];
|
|
121
|
+
}
|
|
122
|
+
body.unshift(instantExport);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
body.push(instantExport);
|
|
126
|
+
}
|
|
127
|
+
return root.toSource();
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=cache-components-instant-false.js.map
|