@valbuild/init 0.51.0
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/CHANGELOG.md +0 -0
- package/babel.config.js +6 -0
- package/dist/declarations/src/index.d.ts +1 -0
- package/dist/valbuild-init.cjs.d.ts +2 -0
- package/dist/valbuild-init.cjs.d.ts.map +1 -0
- package/dist/valbuild-init.cjs.dev.js +1373 -0
- package/dist/valbuild-init.cjs.js +7 -0
- package/dist/valbuild-init.cjs.prod.js +1373 -0
- package/dist/valbuild-init.esm.js +1361 -0
- package/out/index.js +9 -0
- package/package.json +55 -0
- package/src/codemods/index.ts +1 -0
- package/src/codemods/transformNextAppRouterValProvider.ts +76 -0
- package/src/codemods.test.ts +75 -0
- package/src/index.ts +49 -0
- package/src/init.ts +611 -0
- package/src/logger.ts +32 -0
- package/src/templates.ts +75 -0
- package/tsconfig.json +11 -0
package/src/init.ts
ADDED
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import simpleGit from "simple-git";
|
|
5
|
+
import { confirm } from "@inquirer/prompts";
|
|
6
|
+
import { transformNextAppRouterValProvider } from "./codemods/transformNextAppRouterValProvider";
|
|
7
|
+
import { diffLines } from "diff";
|
|
8
|
+
import jcs from "jscodeshift";
|
|
9
|
+
import semver from "semver";
|
|
10
|
+
import packageJson from "../package.json";
|
|
11
|
+
import {
|
|
12
|
+
VAL_API_ROUTER,
|
|
13
|
+
VAL_APP_PAGE,
|
|
14
|
+
VAL_CLIENT,
|
|
15
|
+
VAL_CONFIG,
|
|
16
|
+
VAL_SERVER,
|
|
17
|
+
} from "./templates";
|
|
18
|
+
import * as logger from "./logger";
|
|
19
|
+
|
|
20
|
+
const MIN_VAL_VERSION = packageJson.version;
|
|
21
|
+
const MIN_NEXT_VERSION = "13.4.0";
|
|
22
|
+
|
|
23
|
+
let maxResetLength = 0;
|
|
24
|
+
export async function init(
|
|
25
|
+
root: string = process.cwd(),
|
|
26
|
+
{ yes: defaultAnswers }: { yes?: boolean } = {}
|
|
27
|
+
) {
|
|
28
|
+
logger.info('Initializing Val in "' + root + '"...\n');
|
|
29
|
+
process.stdout.write("Analyzing project...");
|
|
30
|
+
const analysis = await analyze(path.resolve(root), walk(path.resolve(root)));
|
|
31
|
+
// reset cursor:
|
|
32
|
+
process.stdout.write("\x1b[0G");
|
|
33
|
+
logger.info("Analysis:" + " ".repeat(maxResetLength));
|
|
34
|
+
|
|
35
|
+
const currentPlan = await plan(analysis, defaultAnswers);
|
|
36
|
+
if (currentPlan.abort) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await execute(currentPlan);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const sep = "/";
|
|
44
|
+
function walk(dir: string, skip: RegExp = /node_modules|.git/): string[] {
|
|
45
|
+
if (!fs.existsSync(dir)) return [];
|
|
46
|
+
process.stdout.write("\x1b[0G");
|
|
47
|
+
const m =
|
|
48
|
+
"Analyzing project... " +
|
|
49
|
+
(dir.length > 30 ? "..." : "") +
|
|
50
|
+
dir.slice(Math.max(dir.length - 30, 0));
|
|
51
|
+
maxResetLength = Math.max(maxResetLength, m.length);
|
|
52
|
+
process.stdout.write(m + " ".repeat(maxResetLength - m.length));
|
|
53
|
+
return fs.readdirSync(dir).reduce((files, fileOrDirName) => {
|
|
54
|
+
const fileOrDirPath = [dir, fileOrDirName].join("/"); // always use / as path separator - should work on windows as well?
|
|
55
|
+
if (fs.statSync(fileOrDirPath).isDirectory() && !skip.test(fileOrDirName)) {
|
|
56
|
+
return files.concat(walk(fileOrDirPath));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return files.concat(fileOrDirPath);
|
|
60
|
+
}, [] as string[]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type Analysis = Partial<{
|
|
64
|
+
root: string;
|
|
65
|
+
srcDir: string;
|
|
66
|
+
packageJsonDir: string;
|
|
67
|
+
valConfigPath: string;
|
|
68
|
+
isTypescript: boolean;
|
|
69
|
+
isJavascript: boolean;
|
|
70
|
+
|
|
71
|
+
// Val package:
|
|
72
|
+
isValInstalled: boolean;
|
|
73
|
+
valVersion: string;
|
|
74
|
+
valVersionIsSatisfied: boolean;
|
|
75
|
+
|
|
76
|
+
// eslint:
|
|
77
|
+
eslintRcJsonPath: string;
|
|
78
|
+
eslintRcJsonText: string;
|
|
79
|
+
eslintRcJsPath: string;
|
|
80
|
+
eslintRcJsText: string;
|
|
81
|
+
valEslintVersion: string;
|
|
82
|
+
isValEslintRulesConfigured: boolean;
|
|
83
|
+
|
|
84
|
+
// next:
|
|
85
|
+
nextVersion: string;
|
|
86
|
+
nextVersionIsSatisfied: boolean;
|
|
87
|
+
isNextInstalled: boolean;
|
|
88
|
+
pagesRouter: boolean;
|
|
89
|
+
appRouter: boolean;
|
|
90
|
+
appRouterLayoutFile: string;
|
|
91
|
+
appRouterPath: string;
|
|
92
|
+
appRouterLayoutPath: string;
|
|
93
|
+
|
|
94
|
+
// git:
|
|
95
|
+
hasGit: boolean;
|
|
96
|
+
isGitHub: boolean;
|
|
97
|
+
isGitClean: boolean;
|
|
98
|
+
|
|
99
|
+
// TODO:
|
|
100
|
+
// check if modules are used
|
|
101
|
+
}>;
|
|
102
|
+
|
|
103
|
+
const analyze = async (root: string, files: string[]): Promise<Analysis> => {
|
|
104
|
+
if (!fs.existsSync(root)) {
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
const analysis: Analysis = { root };
|
|
108
|
+
const packageJsonPath = files.find(
|
|
109
|
+
(file) => file === [root, "package.json"].join(sep)
|
|
110
|
+
);
|
|
111
|
+
analysis.packageJsonDir = packageJsonPath && path.dirname(packageJsonPath);
|
|
112
|
+
|
|
113
|
+
if (packageJsonPath) {
|
|
114
|
+
const packageJsonText = fs.readFileSync(packageJsonPath, "utf8");
|
|
115
|
+
if (packageJsonText) {
|
|
116
|
+
try {
|
|
117
|
+
const packageJson = JSON.parse(packageJsonText);
|
|
118
|
+
analysis.isValInstalled = !!packageJson.dependencies["@valbuild/next"];
|
|
119
|
+
analysis.isNextInstalled = !!packageJson.dependencies["next"];
|
|
120
|
+
analysis.valEslintVersion =
|
|
121
|
+
packageJson.devDependencies["@valbuild/eslint-plugin"] ||
|
|
122
|
+
packageJson.dependencies["@valbuild/eslint-plugin"];
|
|
123
|
+
analysis.nextVersion = packageJson.dependencies["next"];
|
|
124
|
+
analysis.valVersion = packageJson.dependencies["@valbuild/next"];
|
|
125
|
+
} catch (err) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Failed to parse package.json in file: ${packageJsonPath}`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (analysis.nextVersion) {
|
|
133
|
+
const minNextVersion = semver.minVersion(analysis.nextVersion)?.version;
|
|
134
|
+
if (minNextVersion) {
|
|
135
|
+
analysis.nextVersionIsSatisfied = semver.satisfies(
|
|
136
|
+
minNextVersion,
|
|
137
|
+
">=" + MIN_NEXT_VERSION
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (analysis.valVersion) {
|
|
142
|
+
const minValVersion = semver.minVersion(analysis.valVersion)?.version;
|
|
143
|
+
if (minValVersion) {
|
|
144
|
+
analysis.valVersionIsSatisfied = semver.satisfies(
|
|
145
|
+
minValVersion,
|
|
146
|
+
">=" + MIN_VAL_VERSION
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
analysis.eslintRcJsPath = files.find((file) => file.endsWith(".eslintrc.js"));
|
|
152
|
+
if (analysis.eslintRcJsPath) {
|
|
153
|
+
analysis.eslintRcJsText = fs.readFileSync(analysis.eslintRcJsPath, "utf8");
|
|
154
|
+
if (analysis.eslintRcJsText) {
|
|
155
|
+
// TODO: Evaluate and extract config?
|
|
156
|
+
analysis.isValEslintRulesConfigured = analysis.eslintRcJsText.includes(
|
|
157
|
+
"plugin:@valbuild/recommended"
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
analysis.eslintRcJsonPath =
|
|
162
|
+
files.find((file) => file.endsWith(".eslintrc.json")) ||
|
|
163
|
+
files.find((file) => file.endsWith(".eslintrc"));
|
|
164
|
+
if (analysis.eslintRcJsonPath) {
|
|
165
|
+
analysis.eslintRcJsonText = fs.readFileSync(
|
|
166
|
+
analysis.eslintRcJsonPath,
|
|
167
|
+
"utf8"
|
|
168
|
+
);
|
|
169
|
+
if (analysis.eslintRcJsonText) {
|
|
170
|
+
// TODO: Parse properly
|
|
171
|
+
analysis.isValEslintRulesConfigured = analysis.eslintRcJsonText.includes(
|
|
172
|
+
"plugin:@valbuild/recommended"
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const pagesRouterAppPath =
|
|
178
|
+
files.find((file) => file.endsWith("/pages/_app.tsx")) ||
|
|
179
|
+
files.find((file) => file.endsWith("/pages/_app.jsx"));
|
|
180
|
+
analysis.pagesRouter = !!pagesRouterAppPath;
|
|
181
|
+
if (pagesRouterAppPath) {
|
|
182
|
+
analysis.isTypescript = !!pagesRouterAppPath.endsWith(".tsx");
|
|
183
|
+
analysis.isJavascript = !!pagesRouterAppPath.endsWith(".jsx");
|
|
184
|
+
analysis.srcDir = path.dirname(path.dirname(pagesRouterAppPath));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const appRouterLayoutPath =
|
|
188
|
+
files.find((file) => file.endsWith("/app/layout.tsx")) ||
|
|
189
|
+
files.find((file) => file.endsWith("/app/layout.jsx"));
|
|
190
|
+
|
|
191
|
+
if (appRouterLayoutPath) {
|
|
192
|
+
analysis.appRouter = true;
|
|
193
|
+
analysis.appRouterLayoutPath = appRouterLayoutPath;
|
|
194
|
+
analysis.appRouterLayoutFile = fs.readFileSync(appRouterLayoutPath, "utf8");
|
|
195
|
+
analysis.isTypescript = !!appRouterLayoutPath.endsWith(".tsx");
|
|
196
|
+
analysis.isJavascript = !!appRouterLayoutPath.endsWith(".jsx");
|
|
197
|
+
analysis.appRouterPath = path.dirname(appRouterLayoutPath);
|
|
198
|
+
analysis.srcDir = path.dirname(analysis.appRouterPath);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const git = simpleGit(root);
|
|
203
|
+
const gitStatus = await git.status();
|
|
204
|
+
const gitRemoteOrigin = await git.remote(["-v"]);
|
|
205
|
+
analysis.hasGit = true;
|
|
206
|
+
analysis.isGitHub = gitRemoteOrigin
|
|
207
|
+
? !!gitRemoteOrigin.includes("github.com")
|
|
208
|
+
: false;
|
|
209
|
+
analysis.isGitClean = gitStatus.isClean();
|
|
210
|
+
} catch (err) {
|
|
211
|
+
// console.error(err);
|
|
212
|
+
}
|
|
213
|
+
return analysis;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
type FileOp = {
|
|
217
|
+
path: string;
|
|
218
|
+
source: string;
|
|
219
|
+
};
|
|
220
|
+
type Plan = Partial<{
|
|
221
|
+
root: string;
|
|
222
|
+
createValServer: FileOp;
|
|
223
|
+
createValRouter: FileOp;
|
|
224
|
+
createValAppPage: FileOp;
|
|
225
|
+
createConfigFile: FileOp;
|
|
226
|
+
createValRsc: false | FileOp;
|
|
227
|
+
createValClient: false | FileOp;
|
|
228
|
+
updateAppLayout: false | FileOp;
|
|
229
|
+
updateEslint: false | FileOp; // TODO: do this
|
|
230
|
+
useTypescript: boolean;
|
|
231
|
+
useJavascript: boolean;
|
|
232
|
+
abort: boolean;
|
|
233
|
+
ignoreGitDirty: boolean;
|
|
234
|
+
}>;
|
|
235
|
+
|
|
236
|
+
async function plan(
|
|
237
|
+
analysis: Readonly<Analysis>,
|
|
238
|
+
defaultAnswers: boolean = false
|
|
239
|
+
): Promise<Plan> {
|
|
240
|
+
const plan: Plan = { root: analysis.root };
|
|
241
|
+
|
|
242
|
+
if (analysis.root) {
|
|
243
|
+
logger.info(" Root: " + analysis.root, { isGood: true });
|
|
244
|
+
} else {
|
|
245
|
+
logger.error("Failed to find root directory");
|
|
246
|
+
return { abort: true };
|
|
247
|
+
}
|
|
248
|
+
if (
|
|
249
|
+
!analysis.srcDir ||
|
|
250
|
+
!fs.statSync(analysis.srcDir).isDirectory() ||
|
|
251
|
+
!analysis.isNextInstalled
|
|
252
|
+
) {
|
|
253
|
+
logger.error("Val requires a Next.js project");
|
|
254
|
+
return { abort: true };
|
|
255
|
+
}
|
|
256
|
+
if (analysis.srcDir) {
|
|
257
|
+
logger.info(" Source dir: " + analysis.root, { isGood: true });
|
|
258
|
+
} else {
|
|
259
|
+
logger.error("Failed to determine source directory");
|
|
260
|
+
return { abort: true };
|
|
261
|
+
}
|
|
262
|
+
if (!analysis.isNextInstalled) {
|
|
263
|
+
logger.error("Val requires a Next.js project");
|
|
264
|
+
return { abort: true };
|
|
265
|
+
}
|
|
266
|
+
if (!analysis.isValInstalled) {
|
|
267
|
+
logger.error("Install @valbuild/next first");
|
|
268
|
+
return { abort: true };
|
|
269
|
+
} else {
|
|
270
|
+
logger.info(
|
|
271
|
+
` Val version: found ${analysis.valVersion} >= ${MIN_VAL_VERSION}`,
|
|
272
|
+
{ isGood: true }
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
if (!analysis.nextVersionIsSatisfied) {
|
|
276
|
+
logger.error(
|
|
277
|
+
`Val requires Next.js >= ${MIN_NEXT_VERSION}. Found: ${analysis.nextVersion}`
|
|
278
|
+
);
|
|
279
|
+
return { abort: true };
|
|
280
|
+
} else {
|
|
281
|
+
logger.info(
|
|
282
|
+
` Next.js version: found ${analysis.nextVersion} >= ${MIN_NEXT_VERSION}`,
|
|
283
|
+
{ isGood: true }
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
if (analysis.isTypescript) {
|
|
287
|
+
logger.info(" Use: TypeScript", { isGood: true });
|
|
288
|
+
plan.useTypescript = true;
|
|
289
|
+
}
|
|
290
|
+
if (analysis.isJavascript) {
|
|
291
|
+
logger.info(" Use: JavaScript", { isGood: true });
|
|
292
|
+
if (!plan.useTypescript) {
|
|
293
|
+
plan.useJavascript = true;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (analysis.isTypescript) {
|
|
297
|
+
const tsconfigJsonPath = path.join(analysis.root, "tsconfig.json");
|
|
298
|
+
if (fs.statSync(tsconfigJsonPath).isFile()) {
|
|
299
|
+
logger.info(" tsconfig.json: found", { isGood: true });
|
|
300
|
+
} else {
|
|
301
|
+
logger.error("tsconfig.json: Failed to find tsconfig.json");
|
|
302
|
+
return { abort: true };
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
const jsconfigJsonPath = path.join(analysis.root, "jsconfig.json");
|
|
306
|
+
if (fs.statSync(jsconfigJsonPath).isFile()) {
|
|
307
|
+
logger.info(" jsconfig.json: found", { isGood: true });
|
|
308
|
+
} else {
|
|
309
|
+
logger.error(" jsconfig.json: failed to find jsconfig.json");
|
|
310
|
+
return { abort: true };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (analysis.valEslintVersion === undefined) {
|
|
315
|
+
const answer = !defaultAnswers
|
|
316
|
+
? await confirm({
|
|
317
|
+
message:
|
|
318
|
+
"The recommended Val eslint plugin (@valbuild/eslint-plugin) is not installed. Continue?",
|
|
319
|
+
default: false,
|
|
320
|
+
})
|
|
321
|
+
: false;
|
|
322
|
+
if (!answer) {
|
|
323
|
+
logger.error(
|
|
324
|
+
"Aborted: the Val eslint plugin is not installed.\n\nInstall the @valbuild/eslint-plugin package with your favorite package manager.\n\nExample:\n\n npm install -D @valbuild/eslint-plugin\n"
|
|
325
|
+
);
|
|
326
|
+
return { abort: true };
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
logger.info(" @valbuild/eslint-plugin: installed", { isGood: true });
|
|
330
|
+
}
|
|
331
|
+
if (analysis.appRouter) {
|
|
332
|
+
logger.info(" Use: App Router", { isGood: true });
|
|
333
|
+
}
|
|
334
|
+
if (analysis.pagesRouter) {
|
|
335
|
+
logger.info(" Use: Pages Router", { isGood: true });
|
|
336
|
+
}
|
|
337
|
+
if (analysis.isGitClean) {
|
|
338
|
+
logger.info(" Git state: clean", { isGood: true });
|
|
339
|
+
}
|
|
340
|
+
if (!analysis.isGitClean) {
|
|
341
|
+
logger.warn(" Git state: dirty");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (analysis.valEslintVersion) {
|
|
345
|
+
if (analysis.isValEslintRulesConfigured) {
|
|
346
|
+
logger.info(" @valbuild/eslint-plugin rules configured", {
|
|
347
|
+
isGood: true,
|
|
348
|
+
});
|
|
349
|
+
} else {
|
|
350
|
+
if (analysis.eslintRcJsPath) {
|
|
351
|
+
logger.warn(
|
|
352
|
+
'Cannot patch eslint: found .eslintrc.js but can only patch JSON files (at the moment).\nAdd the following to your eslint config:\n\n "extends": ["plugin:@valbuild/recommended"]\n'
|
|
353
|
+
);
|
|
354
|
+
} else if (analysis.eslintRcJsonPath) {
|
|
355
|
+
const answer = !defaultAnswers
|
|
356
|
+
? await confirm({
|
|
357
|
+
message:
|
|
358
|
+
"Patch eslintrc.json to use the recommended Val eslint rules?",
|
|
359
|
+
default: true,
|
|
360
|
+
})
|
|
361
|
+
: true;
|
|
362
|
+
if (answer) {
|
|
363
|
+
const currentEslintRc = fs.readFileSync(
|
|
364
|
+
analysis.eslintRcJsonPath,
|
|
365
|
+
"utf-8"
|
|
366
|
+
);
|
|
367
|
+
const parsedEslint = JSON.parse(currentEslintRc);
|
|
368
|
+
if (typeof parsedEslint !== "object") {
|
|
369
|
+
logger.error(
|
|
370
|
+
`Could not patch eslint: ${analysis.eslintRcJsonPath} was not an object`
|
|
371
|
+
);
|
|
372
|
+
return { abort: true };
|
|
373
|
+
}
|
|
374
|
+
if (typeof parsedEslint.extends === "string") {
|
|
375
|
+
parsedEslint.extends = [parsedEslint.extends];
|
|
376
|
+
}
|
|
377
|
+
parsedEslint.extends = parsedEslint.extends || [];
|
|
378
|
+
parsedEslint.extends.push("plugin:@valbuild/recommended");
|
|
379
|
+
plan.updateEslint = {
|
|
380
|
+
path: analysis.eslintRcJsonPath,
|
|
381
|
+
source: JSON.stringify(parsedEslint, null, 2) + "\n",
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
} else {
|
|
385
|
+
logger.warn("Cannot patch eslint: failed to find eslint config file");
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (!analysis.isGitClean) {
|
|
390
|
+
while (plan.ignoreGitDirty === undefined) {
|
|
391
|
+
const answer = !defaultAnswers
|
|
392
|
+
? await confirm({
|
|
393
|
+
message: "You have uncommitted changes. Continue?",
|
|
394
|
+
default: false,
|
|
395
|
+
})
|
|
396
|
+
: false;
|
|
397
|
+
plan.ignoreGitDirty = answer;
|
|
398
|
+
if (!answer) {
|
|
399
|
+
logger.error("Aborted: git state dirty");
|
|
400
|
+
return { abort: true, ignoreGitDirty: true };
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// New required files:
|
|
406
|
+
const valConfigPath = path.join(analysis.root, "val.config.ts");
|
|
407
|
+
|
|
408
|
+
plan.createConfigFile = {
|
|
409
|
+
path: valConfigPath,
|
|
410
|
+
source: VAL_CONFIG({}),
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const valUtilsDir = path.join(analysis.srcDir, "val");
|
|
414
|
+
const valUtilsImportPath = path
|
|
415
|
+
.relative(valUtilsDir, valConfigPath)
|
|
416
|
+
.replace(".js", "")
|
|
417
|
+
.replace(".ts", "");
|
|
418
|
+
|
|
419
|
+
const valServerPath = path.join(
|
|
420
|
+
valUtilsDir,
|
|
421
|
+
analysis.isTypescript ? "val.server.ts" : "val.server.js"
|
|
422
|
+
);
|
|
423
|
+
plan.createValServer = {
|
|
424
|
+
path: valServerPath,
|
|
425
|
+
source: VAL_SERVER(valUtilsImportPath),
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
if (!analysis.appRouterPath) {
|
|
429
|
+
logger.warn('Creating a new "app" router');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const valAppPagePath = path.join(
|
|
433
|
+
analysis.appRouterPath || path.join(analysis.srcDir, "app"),
|
|
434
|
+
"(val)",
|
|
435
|
+
"val",
|
|
436
|
+
analysis.isTypescript ? "page.tsx" : "page.jsx"
|
|
437
|
+
);
|
|
438
|
+
const valPageImportPath = path
|
|
439
|
+
.relative(path.dirname(valAppPagePath), valConfigPath)
|
|
440
|
+
.replace(".js", "")
|
|
441
|
+
.replace(".ts", "");
|
|
442
|
+
plan.createValAppPage = {
|
|
443
|
+
path: valAppPagePath,
|
|
444
|
+
source: VAL_APP_PAGE(valPageImportPath),
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const valRouterPath = path.join(
|
|
448
|
+
analysis.appRouterPath || path.join(analysis.srcDir, "app"),
|
|
449
|
+
"(val)",
|
|
450
|
+
"api",
|
|
451
|
+
"val",
|
|
452
|
+
analysis.isTypescript ? "router.tsx" : "router.jsx"
|
|
453
|
+
);
|
|
454
|
+
const valRouterImportPath = path
|
|
455
|
+
.relative(path.dirname(valRouterPath), valServerPath)
|
|
456
|
+
.replace(".js", "")
|
|
457
|
+
.replace(".ts", "");
|
|
458
|
+
plan.createValRouter = {
|
|
459
|
+
path: valRouterPath,
|
|
460
|
+
source: VAL_API_ROUTER(valRouterImportPath),
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// Util files:
|
|
464
|
+
|
|
465
|
+
while (plan.createValClient === undefined) {
|
|
466
|
+
const answer = !defaultAnswers
|
|
467
|
+
? await confirm({
|
|
468
|
+
message: "Setup useVal for Client Components",
|
|
469
|
+
default: true,
|
|
470
|
+
})
|
|
471
|
+
: true;
|
|
472
|
+
if (answer) {
|
|
473
|
+
plan.createValClient = {
|
|
474
|
+
path: path.join(
|
|
475
|
+
valUtilsDir,
|
|
476
|
+
analysis.isTypescript ? "val.client.ts" : "val.client.js"
|
|
477
|
+
),
|
|
478
|
+
source: VAL_CLIENT(valUtilsImportPath),
|
|
479
|
+
};
|
|
480
|
+
} else {
|
|
481
|
+
plan.createValClient = false;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
while (plan.createValRsc === undefined) {
|
|
485
|
+
const answer = !defaultAnswers
|
|
486
|
+
? await confirm({
|
|
487
|
+
message: "Setup fetchVal for React Server Components",
|
|
488
|
+
default: true,
|
|
489
|
+
})
|
|
490
|
+
: true;
|
|
491
|
+
if (answer) {
|
|
492
|
+
plan.createValRsc = {
|
|
493
|
+
path: path.join(
|
|
494
|
+
valUtilsDir,
|
|
495
|
+
analysis.isTypescript ? "val.rsc.ts" : "val.rsc.js"
|
|
496
|
+
),
|
|
497
|
+
source: VAL_SERVER(valUtilsImportPath),
|
|
498
|
+
};
|
|
499
|
+
} else {
|
|
500
|
+
plan.createValRsc = false;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (analysis.eslintRcJsPath) {
|
|
505
|
+
logger.warn("ESLint config found: " + analysis.eslintRcJsPath);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Patches:
|
|
509
|
+
|
|
510
|
+
const NO_PATCH_WARNING =
|
|
511
|
+
"Remember to manually patch your pages/_app.tsx file to use Val Provider.\n";
|
|
512
|
+
if (analysis.appRouterLayoutPath) {
|
|
513
|
+
if (!analysis.appRouterLayoutFile) {
|
|
514
|
+
logger.error("Failed to read app router layout file");
|
|
515
|
+
return { abort: true };
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const res = transformNextAppRouterValProvider(
|
|
519
|
+
{
|
|
520
|
+
path: analysis.appRouterLayoutPath,
|
|
521
|
+
source: analysis.appRouterLayoutFile,
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
j: jcs,
|
|
525
|
+
jscodeshift: jcs.withParser("tsx"),
|
|
526
|
+
stats: () => {},
|
|
527
|
+
report: () => {},
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
configImportPath: path
|
|
531
|
+
.relative(path.dirname(analysis.appRouterLayoutPath), valConfigPath)
|
|
532
|
+
.replace(".js", "")
|
|
533
|
+
.replace(".ts", ""),
|
|
534
|
+
}
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
const diff = diffLines(analysis.appRouterLayoutFile, res, {});
|
|
538
|
+
|
|
539
|
+
let s = "";
|
|
540
|
+
diff.forEach((part) => {
|
|
541
|
+
if (part.added) {
|
|
542
|
+
s += chalk.green(part.value);
|
|
543
|
+
} else if (part.removed) {
|
|
544
|
+
s += chalk.red(part.value);
|
|
545
|
+
} else {
|
|
546
|
+
s += part.value;
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
const answer = !defaultAnswers
|
|
550
|
+
? await confirm({
|
|
551
|
+
message: `Automatically patch ${analysis.appRouterLayoutPath} file?`,
|
|
552
|
+
default: true,
|
|
553
|
+
})
|
|
554
|
+
: true;
|
|
555
|
+
if (answer) {
|
|
556
|
+
const answer = !defaultAnswers
|
|
557
|
+
? await confirm({
|
|
558
|
+
message: `Do you accept the following patch:\n${s}\n`,
|
|
559
|
+
default: true,
|
|
560
|
+
})
|
|
561
|
+
: true;
|
|
562
|
+
if (!answer) {
|
|
563
|
+
logger.warn(NO_PATCH_WARNING);
|
|
564
|
+
plan.updateAppLayout = false;
|
|
565
|
+
} else {
|
|
566
|
+
plan.updateAppLayout = {
|
|
567
|
+
path: analysis.appRouterLayoutPath,
|
|
568
|
+
source: res,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
} else {
|
|
572
|
+
logger.warn(NO_PATCH_WARNING);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (analysis.pagesRouter) {
|
|
576
|
+
logger.warn(NO_PATCH_WARNING);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return plan;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async function execute(plan: Plan) {
|
|
583
|
+
if (plan.abort) {
|
|
584
|
+
return logger.warn("Aborted");
|
|
585
|
+
}
|
|
586
|
+
if (!plan.root) {
|
|
587
|
+
return logger.error("Failed to find root directory");
|
|
588
|
+
}
|
|
589
|
+
logger.info("Executing...");
|
|
590
|
+
for (const [key, fileOp] of Object.entries(plan)) {
|
|
591
|
+
writeFile(fileOp, plan.root, key.startsWith("update"));
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function writeFile(
|
|
596
|
+
fileOp: string | FileOp | undefined | boolean,
|
|
597
|
+
rootDir: string,
|
|
598
|
+
isUpdate: boolean
|
|
599
|
+
) {
|
|
600
|
+
if (fileOp && typeof fileOp !== "boolean" && typeof fileOp !== "string") {
|
|
601
|
+
fs.mkdirSync(path.dirname(fileOp.path), { recursive: true });
|
|
602
|
+
fs.writeFileSync(fileOp.path, fileOp.source);
|
|
603
|
+
logger.info(
|
|
604
|
+
` ${isUpdate ? "Patched" : "Created"} file: ${fileOp.path.replace(
|
|
605
|
+
rootDir,
|
|
606
|
+
""
|
|
607
|
+
)}`,
|
|
608
|
+
{ isGood: true }
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
}
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
3
|
+
export function error(message: string) {
|
|
4
|
+
console.error(chalk.red("❌ ERROR: ") + message);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function warn(message: string) {
|
|
8
|
+
console.error(chalk.yellow("⚠️ WARN:") + message);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function info(
|
|
12
|
+
message: string,
|
|
13
|
+
opts: { isCodeSnippet?: true; isGood?: true } = {}
|
|
14
|
+
) {
|
|
15
|
+
if (opts.isCodeSnippet) {
|
|
16
|
+
console.log(chalk.cyanBright("$ > ") + chalk.cyan(message));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (opts.isGood) {
|
|
20
|
+
console.log(chalk.green("✅ ") + message);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
console.log(message);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function debugPrint(str: string) {
|
|
27
|
+
/*eslint-disable no-constant-condition */
|
|
28
|
+
if (process.env["DEBUG"] || true) {
|
|
29
|
+
// TODO: remove true
|
|
30
|
+
console.log(`DEBUG: ${str}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/templates.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export const VAL_CLIENT = (configImportPath: string) => `import "client-only";
|
|
2
|
+
import { initValClient } from "@valbuild/next/client";
|
|
3
|
+
import { config } from "${configImportPath}";
|
|
4
|
+
|
|
5
|
+
const { useValStega: useVal } = initValClient(config);
|
|
6
|
+
|
|
7
|
+
export { useVal };
|
|
8
|
+
`;
|
|
9
|
+
|
|
10
|
+
export const VAL_RSC = (configImportPath: string) => `import "server-only";
|
|
11
|
+
import { initValRsc } from "@valbuild/next/rsc";
|
|
12
|
+
import { config } from "${configImportPath}";
|
|
13
|
+
import { cookies, draftMode, headers } from "next/headers";
|
|
14
|
+
|
|
15
|
+
const { fetchValStega: fetchVal } = initValRsc(config, {
|
|
16
|
+
draftMode,
|
|
17
|
+
headers,
|
|
18
|
+
cookies,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export { fetchVal };
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
export const VAL_SERVER = (configImportPath: string) => `import "server-only";
|
|
25
|
+
import { initValServer } from "@valbuild/next/server";
|
|
26
|
+
import { config } from "${configImportPath}";
|
|
27
|
+
import { draftMode } from "next/headers";
|
|
28
|
+
|
|
29
|
+
const { valNextAppRouter } = initValServer(
|
|
30
|
+
{ ...config },
|
|
31
|
+
{
|
|
32
|
+
draftMode,
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
export { valNextAppRouter };
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
// TODO: use Val config
|
|
40
|
+
type ValConfig = {
|
|
41
|
+
valCloud?: string;
|
|
42
|
+
gitCommit?: string;
|
|
43
|
+
gitBranch?: string;
|
|
44
|
+
valConfigPath?: string;
|
|
45
|
+
};
|
|
46
|
+
export const VAL_CONFIG = (
|
|
47
|
+
options: ValConfig
|
|
48
|
+
) => `import { initVal } from "@valbuild/next";
|
|
49
|
+
|
|
50
|
+
const { s, val, config } = initVal(${JSON.stringify(options, null, 2)});
|
|
51
|
+
|
|
52
|
+
export { s, val, config };
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
export const VAL_API_ROUTER = (
|
|
56
|
+
valServerPath: string
|
|
57
|
+
) => `import { valNextAppRouter } from "${valServerPath}";
|
|
58
|
+
|
|
59
|
+
export const GET = valNextAppRouter;
|
|
60
|
+
export const POST = valNextAppRouter;
|
|
61
|
+
export const PATCH = valNextAppRouter;
|
|
62
|
+
export const DELETE = valNextAppRouter;
|
|
63
|
+
export const PUT = valNextAppRouter;
|
|
64
|
+
export const HEAD = valNextAppRouter;
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
export const VAL_APP_PAGE = (
|
|
68
|
+
configImportPath: string
|
|
69
|
+
) => `import { ValApp } from "@valbuild/next";
|
|
70
|
+
import { config } from "${configImportPath}";
|
|
71
|
+
|
|
72
|
+
export default function Val() {
|
|
73
|
+
return <ValApp config={config} />;
|
|
74
|
+
}
|
|
75
|
+
`;
|