nx-factory-cli 2.1.24 → 2.1.26
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/dist/commands/add-app.d.ts.map +1 -1
- package/dist/commands/add-app.js +92 -90
- package/dist/commands/add-app.js.map +1 -1
- package/dist/commands/add-auth.d.ts.map +1 -1
- package/dist/commands/add-auth.js +42 -3
- package/dist/commands/add-auth.js.map +1 -1
- package/dist/commands/add-lib.d.ts.map +1 -1
- package/dist/commands/add-lib.js +76 -40
- package/dist/commands/add-lib.js.map +1 -1
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +131 -59
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/migrate.d.ts +12 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +644 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/exec.d.ts +3 -1
- package/dist/exec.d.ts.map +1 -1
- package/dist/exec.js +7 -2
- package/dist/exec.js.map +1 -1
- package/dist/index.js +61 -8
- package/dist/index.js.map +1 -1
- package/dist/setups/auth/base.d.ts.map +1 -1
- package/dist/setups/auth/base.js +5 -21
- package/dist/setups/auth/base.js.map +1 -1
- package/dist/setups/auth/index.d.ts +1 -1
- package/dist/setups/auth/index.d.ts.map +1 -1
- package/dist/setups/auth/systems/better-auth.d.ts.map +1 -1
- package/dist/setups/auth/systems/better-auth.js +88 -266
- package/dist/setups/auth/systems/better-auth.js.map +1 -1
- package/dist/setups/auth/systems/clerk.d.ts.map +1 -1
- package/dist/setups/auth/systems/clerk.js +61 -142
- package/dist/setups/auth/systems/clerk.js.map +1 -1
- package/dist/setups/auth/systems/workos.d.ts.map +1 -1
- package/dist/setups/auth/systems/workos.js +92 -203
- package/dist/setups/auth/systems/workos.js.map +1 -1
- package/dist/setups/auth/types.d.ts +12 -10
- package/dist/setups/auth/types.d.ts.map +1 -1
- package/dist/tsconfigs.d.ts +88 -0
- package/dist/tsconfigs.d.ts.map +1 -0
- package/dist/tsconfigs.js +296 -0
- package/dist/tsconfigs.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
import inquirer from "inquirer";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { loadConfig, resolveScope, saveConfig, } from "../config.js";
|
|
4
|
+
import { detectPackageManager } from "../exec.js";
|
|
5
|
+
import { pathExists, writeJson } from "../files.js";
|
|
6
|
+
import { MonorepoRootNotFoundError, requireMonorepoRoot, } from "../resolve-root.js";
|
|
7
|
+
import { appTsConfig, packageTsConfig, rootTsConfigBase, rootTsConfigSolution, typescriptPackageJson, typescriptPresets, } from "../tsconfigs.js";
|
|
8
|
+
import { c, createStepRunner, printError, printSection, printSuccess, q, } from "../ui.js";
|
|
9
|
+
export async function migrateCommand(options) {
|
|
10
|
+
// ── Resolve monorepo root ──────────────────────────────────────────────────
|
|
11
|
+
let root;
|
|
12
|
+
try {
|
|
13
|
+
root = await requireMonorepoRoot();
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
if (err instanceof MonorepoRootNotFoundError) {
|
|
17
|
+
printError({
|
|
18
|
+
title: "Could not find a workspace to migrate",
|
|
19
|
+
detail: String(err),
|
|
20
|
+
recovery: [
|
|
21
|
+
{
|
|
22
|
+
label: "Run from inside an nx-factory-cli workspace:",
|
|
23
|
+
cmd: "cd <workspace-root>",
|
|
24
|
+
},
|
|
25
|
+
{ label: "Or start fresh:", cmd: "nx-factory-cli init" },
|
|
26
|
+
],
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
printError({
|
|
31
|
+
title: "Unexpected error",
|
|
32
|
+
detail: String(err),
|
|
33
|
+
recovery: [
|
|
34
|
+
{ label: "Run from workspace root:", cmd: "cd <workspace-root>" },
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
process.exit(1);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
printSection("Analysing workspace...");
|
|
42
|
+
// ── Analyse current state ─────────────────────────────────────────────────
|
|
43
|
+
const status = await analyseWorkspace(root);
|
|
44
|
+
printAnalysis(status);
|
|
45
|
+
if (isFullyMigrated(status)) {
|
|
46
|
+
console.log(`\n ${c.green("✓")} ${c.white("Workspace is already up to date — nothing to migrate.")}\n`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// ── Confirm ───────────────────────────────────────────────────────────────
|
|
50
|
+
const cfg = await loadConfig();
|
|
51
|
+
const scope = resolveScope(cfg);
|
|
52
|
+
const detectedPm = await detectPackageManager(root);
|
|
53
|
+
const pm = detectedPm ?? cfg?.pkgManager ?? "pnpm";
|
|
54
|
+
const answers = options.yes
|
|
55
|
+
? {
|
|
56
|
+
proceed: true,
|
|
57
|
+
uiVisibility: "internal",
|
|
58
|
+
removeJsExtensions: false,
|
|
59
|
+
cleanupBackups: false,
|
|
60
|
+
}
|
|
61
|
+
: await inquirer.prompt([
|
|
62
|
+
{
|
|
63
|
+
type: "confirm",
|
|
64
|
+
name: "proceed",
|
|
65
|
+
message: q("Apply all migrations?", "a backup of each changed file will be written as <file>.migration-backup"),
|
|
66
|
+
default: true,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
type: "select",
|
|
70
|
+
name: "uiVisibility",
|
|
71
|
+
message: q("UI package visibility", "internal = workspace only · public = published to npm"),
|
|
72
|
+
choices: [
|
|
73
|
+
{
|
|
74
|
+
name: "internal — private: true, workspace only",
|
|
75
|
+
value: "internal",
|
|
76
|
+
},
|
|
77
|
+
{ name: "public — will be published to npm", value: "public" },
|
|
78
|
+
],
|
|
79
|
+
default: "internal",
|
|
80
|
+
when: !status.hasUiPackageVisibility,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
type: "confirm",
|
|
84
|
+
name: "removeJsExtensions",
|
|
85
|
+
message: q("Remove .js extensions from internal package imports?", `found ${status.internalPackagesWithJsExtensions.length} package(s) with .js extensions — safe to remove for Bundler resolution`),
|
|
86
|
+
default: status.internalPackagesWithJsExtensions.length > 0,
|
|
87
|
+
when: status.internalPackagesWithJsExtensions.length > 0,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
type: "confirm",
|
|
91
|
+
name: "cleanupBackups",
|
|
92
|
+
message: q(`Delete ${status.backupFileCount} existing .migration-backup file(s)?`, "these are from a previous migration run"),
|
|
93
|
+
default: false,
|
|
94
|
+
when: !options.yes && status.backupFileCount > 0,
|
|
95
|
+
},
|
|
96
|
+
]);
|
|
97
|
+
if (!answers.proceed) {
|
|
98
|
+
console.log(c.dim("\n Migration cancelled.\n"));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const uiVisibility = (answers.uiVisibility ??
|
|
102
|
+
"internal");
|
|
103
|
+
const removeJsExtensions = answers.removeJsExtensions ?? false;
|
|
104
|
+
const cleanupBackupsNow = options.yes
|
|
105
|
+
? false
|
|
106
|
+
: (answers.cleanupBackups ?? false);
|
|
107
|
+
// ── Count steps dynamically ───────────────────────────────────────────────
|
|
108
|
+
let totalSteps = 0;
|
|
109
|
+
if (!status.hasTsConfigBase)
|
|
110
|
+
totalSteps++;
|
|
111
|
+
if (!status.hasTypescriptPackage)
|
|
112
|
+
totalSteps++;
|
|
113
|
+
if (!status.hasUiPackageVisibility && status.uiPackageDir)
|
|
114
|
+
totalSteps++;
|
|
115
|
+
if (status.packagesWithBadTsConfig.length > 0)
|
|
116
|
+
totalSteps++;
|
|
117
|
+
if (status.appsWithBadTsConfig.length > 0)
|
|
118
|
+
totalSteps++;
|
|
119
|
+
if (!status.hasConfig || !status.hasUiPackageVisibility)
|
|
120
|
+
totalSteps++;
|
|
121
|
+
totalSteps += 1; // always update solution tsconfig.json
|
|
122
|
+
if (removeJsExtensions && status.internalPackagesWithJsExtensions.length > 0)
|
|
123
|
+
totalSteps++;
|
|
124
|
+
if (cleanupBackupsNow && status.backupFileCount > 0)
|
|
125
|
+
totalSteps++;
|
|
126
|
+
if (totalSteps === 0)
|
|
127
|
+
totalSteps = 1;
|
|
128
|
+
printSection(`${options.dryRun ? "[dry run] " : ""}Migrating workspace at ${root}`);
|
|
129
|
+
const step = createStepRunner(totalSteps, options.dryRun);
|
|
130
|
+
// ── Step 1: Write tsconfig.base.json ──────────────────────────────────────
|
|
131
|
+
if (!status.hasTsConfigBase) {
|
|
132
|
+
await step("Write tsconfig.base.json", async () => {
|
|
133
|
+
const tsBasePath = path.join(root, "tsconfig.base.json");
|
|
134
|
+
await backupIfExists(tsBasePath, options.dryRun);
|
|
135
|
+
await writeJson(tsBasePath, rootTsConfigBase(scope));
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
// ── Step 1b: Scaffold packages/typescript ───────────────────────────────
|
|
139
|
+
if (!status.hasTypescriptPackage) {
|
|
140
|
+
await step("Scaffold packages/typescript workspace package", async () => {
|
|
141
|
+
const { default: fs } = await import("fs-extra");
|
|
142
|
+
const pkgDir = path.join(root, "tooling", "typescript");
|
|
143
|
+
await fs.ensureDir(pkgDir);
|
|
144
|
+
await writeJson(path.join(pkgDir, "package.json"), typescriptPackageJson(scope));
|
|
145
|
+
const presets = typescriptPresets();
|
|
146
|
+
for (const [filename, content] of Object.entries(presets)) {
|
|
147
|
+
await writeJson(path.join(pkgDir, filename), content);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
// ── Step 2: Migrate UI package tsconfig + package.json ──────────────────
|
|
152
|
+
if (!status.hasUiPackageVisibility && status.uiPackageDir) {
|
|
153
|
+
const uiPkgName = path.basename(status.uiPackageDir);
|
|
154
|
+
await step(`Migrate packages/${uiPkgName} (tsconfig + package.json)`, async () => {
|
|
155
|
+
const { default: fs } = await import("fs-extra");
|
|
156
|
+
// tsconfig.json
|
|
157
|
+
const tsCfgPath = path.join(status.uiPackageDir ?? "", "tsconfig.json");
|
|
158
|
+
await backupIfExists(tsCfgPath, options.dryRun);
|
|
159
|
+
await writeJson(tsCfgPath, packageTsConfig({
|
|
160
|
+
scope,
|
|
161
|
+
pkgName: uiPkgName,
|
|
162
|
+
visibility: uiVisibility,
|
|
163
|
+
react: true,
|
|
164
|
+
}));
|
|
165
|
+
// package.json — add exports + publishConfig if public, ensure private:true if internal
|
|
166
|
+
const pkgPath = path.join(status.uiPackageDir ?? "", "package.json");
|
|
167
|
+
if (await pathExists(pkgPath)) {
|
|
168
|
+
await backupIfExists(pkgPath, options.dryRun);
|
|
169
|
+
const pkg = (await fs.readJson(pkgPath));
|
|
170
|
+
if (uiVisibility === "internal") {
|
|
171
|
+
pkg.private = true;
|
|
172
|
+
delete pkg.publishConfig;
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
delete pkg.private;
|
|
176
|
+
pkg.publishConfig = { access: "public" };
|
|
177
|
+
pkg.files = ["dist", "styles"];
|
|
178
|
+
}
|
|
179
|
+
// Ensure exports field is present
|
|
180
|
+
if (!pkg.exports) {
|
|
181
|
+
pkg.exports = {
|
|
182
|
+
".": { import: "./dist/index.js", types: "./dist/index.d.ts" },
|
|
183
|
+
"./styles": "./styles/globals.css",
|
|
184
|
+
"./components/*": {
|
|
185
|
+
import: "./dist/components/*.js",
|
|
186
|
+
types: "./dist/components/*.d.ts",
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
// ── Step 3: Migrate other packages in packages/ ───────────────────────────
|
|
195
|
+
if (status.packagesWithBadTsConfig.length > 0) {
|
|
196
|
+
await step(`Migrate ${status.packagesWithBadTsConfig.length} package tsconfig(s)`, async () => {
|
|
197
|
+
for (const pkgName of status.packagesWithBadTsConfig) {
|
|
198
|
+
const pkgDir = path.join(root, "packages", pkgName);
|
|
199
|
+
const tsCfgPath = path.join(pkgDir, "tsconfig.json");
|
|
200
|
+
await backupIfExists(tsCfgPath, options.dryRun);
|
|
201
|
+
// Detect if this package uses React (has @types/react or jsx in existing config)
|
|
202
|
+
let isReact = false;
|
|
203
|
+
try {
|
|
204
|
+
const { default: fs } = await import("fs-extra");
|
|
205
|
+
const existing = (await fs.readJson(tsCfgPath));
|
|
206
|
+
const co = (existing.compilerOptions ?? {});
|
|
207
|
+
isReact = !!co.jsx;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
/* no tsconfig — use safe default */
|
|
211
|
+
}
|
|
212
|
+
// Detect visibility from package.json
|
|
213
|
+
let visibility = "internal";
|
|
214
|
+
try {
|
|
215
|
+
const { default: fs } = await import("fs-extra");
|
|
216
|
+
const pkg = (await fs.readJson(path.join(pkgDir, "package.json")));
|
|
217
|
+
if (!pkg.private)
|
|
218
|
+
visibility = "public";
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
/* keep internal */
|
|
222
|
+
}
|
|
223
|
+
await writeJson(tsCfgPath, packageTsConfig({ scope, pkgName, visibility, react: isReact }));
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
// ── Step 4: Migrate app tsconfigs ─────────────────────────────────────────
|
|
228
|
+
if (status.appsWithBadTsConfig.length > 0) {
|
|
229
|
+
await step(`Migrate ${status.appsWithBadTsConfig.length} app tsconfig(s)`, async () => {
|
|
230
|
+
const { default: fs } = await import("fs-extra");
|
|
231
|
+
for (const appName of status.appsWithBadTsConfig) {
|
|
232
|
+
const appDir = path.join(root, "apps", appName);
|
|
233
|
+
const tsCfgPath = path.join(appDir, "tsconfig.json");
|
|
234
|
+
if (!(await pathExists(tsCfgPath)))
|
|
235
|
+
continue;
|
|
236
|
+
await backupIfExists(tsCfgPath, options.dryRun);
|
|
237
|
+
const hasSrcDir = await pathExists(path.join(appDir, "src"));
|
|
238
|
+
const fw = await detectFrameworkFromAppDir(appDir);
|
|
239
|
+
const generated = appTsConfig({
|
|
240
|
+
scope,
|
|
241
|
+
framework: fw,
|
|
242
|
+
hasSrcDir,
|
|
243
|
+
typescriptPkgExists: true,
|
|
244
|
+
});
|
|
245
|
+
// Merge — keep framework-generated keys, override extends + paths + include
|
|
246
|
+
const existing = (await fs.readJson(tsCfgPath));
|
|
247
|
+
const merged = {
|
|
248
|
+
...existing,
|
|
249
|
+
...generated,
|
|
250
|
+
compilerOptions: {
|
|
251
|
+
...(existing.compilerOptions ?? {}),
|
|
252
|
+
...generated.compilerOptions,
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
await fs.writeJson(tsCfgPath, merged, { spaces: 2 });
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
// ── Step 4b: Update/create root solution tsconfig.json ────────────────────
|
|
260
|
+
await step("Update root tsconfig.json solution file", async () => {
|
|
261
|
+
const { default: fs } = await import("fs-extra");
|
|
262
|
+
const pkgNames = [];
|
|
263
|
+
const appNames = [];
|
|
264
|
+
try {
|
|
265
|
+
const pkgDir = path.join(root, "packages");
|
|
266
|
+
if (await pathExists(pkgDir))
|
|
267
|
+
pkgNames.push(...(await fs.readdir(pkgDir)));
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
/* ok */
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
const appsDir = path.join(root, "apps");
|
|
274
|
+
if (await pathExists(appsDir))
|
|
275
|
+
appNames.push(...(await fs.readdir(appsDir)));
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
/* ok */
|
|
279
|
+
}
|
|
280
|
+
await writeJson(path.join(root, "tsconfig.json"), rootTsConfigSolution(pkgNames, appNames));
|
|
281
|
+
});
|
|
282
|
+
// ── Step 5: Update nx-factory.config.json ─────────────────────────────────
|
|
283
|
+
if (!status.hasConfig || !status.hasUiPackageVisibility) {
|
|
284
|
+
await step("Update nx-factory.config.json", async () => {
|
|
285
|
+
const uiPkg = cfg?.uiPackage ??
|
|
286
|
+
(status.uiPackageDir ? path.basename(status.uiPackageDir) : "ui");
|
|
287
|
+
const updatedCfg = {
|
|
288
|
+
workspaceName: cfg?.workspaceName ?? path.basename(root),
|
|
289
|
+
scope,
|
|
290
|
+
pkgManager: pm,
|
|
291
|
+
uiPackage: uiPkg,
|
|
292
|
+
uiPackageVisibility: uiVisibility,
|
|
293
|
+
version: "2.1.10",
|
|
294
|
+
};
|
|
295
|
+
await saveConfig(updatedCfg, root);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
// ── Step 6: Strip .js extensions from internal package imports ───────────
|
|
299
|
+
if (removeJsExtensions &&
|
|
300
|
+
status.internalPackagesWithJsExtensions.length > 0) {
|
|
301
|
+
await step(`Remove .js extensions from ${status.internalPackagesWithJsExtensions.length} internal package(s)`, async () => {
|
|
302
|
+
for (const pkgName of status.internalPackagesWithJsExtensions) {
|
|
303
|
+
const pkgDir = path.join(root, "packages", pkgName);
|
|
304
|
+
await stripJsExtensionsFromDir(pkgDir, options.dryRun);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
// ── Step 7: Clean up existing .migration-backup files ────────────────────
|
|
309
|
+
if (cleanupBackupsNow && status.backupFileCount > 0) {
|
|
310
|
+
await step(`Delete ${status.backupFileCount} .migration-backup file(s)`, async () => {
|
|
311
|
+
await cleanupMigrationBackups(root, options.dryRun);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
// ── Done ──────────────────────────────────────────────────────────────────
|
|
315
|
+
const backupsRemaining = cleanupBackupsNow ? 0 : status.backupFileCount;
|
|
316
|
+
printSuccess({
|
|
317
|
+
title: "Migration complete",
|
|
318
|
+
commands: [
|
|
319
|
+
{ cmd: `${pm} install`, comment: "reinstall to pick up any dep changes" },
|
|
320
|
+
{
|
|
321
|
+
cmd: `${pm} nx run-many --target=build`,
|
|
322
|
+
comment: "verify everything builds",
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
tips: [
|
|
326
|
+
...(backupsRemaining > 0
|
|
327
|
+
? [
|
|
328
|
+
{
|
|
329
|
+
label: `${backupsRemaining} backup file(s) remain:`,
|
|
330
|
+
cmd: "nx-factory-cli migrate (run again to clean them up)",
|
|
331
|
+
},
|
|
332
|
+
]
|
|
333
|
+
: []),
|
|
334
|
+
{
|
|
335
|
+
label: "Internal packages now use Bundler resolution:",
|
|
336
|
+
cmd: "No .js extensions needed in source imports — your bundler handles resolution",
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
// ─── Analysis helpers ─────────────────────────────────────────────────────────
|
|
342
|
+
async function analyseWorkspace(root) {
|
|
343
|
+
const { default: fs } = await import("fs-extra");
|
|
344
|
+
const cfg = await loadConfig();
|
|
345
|
+
const hasTsConfigBase = await pathExists(path.join(root, "tsconfig.base.json"));
|
|
346
|
+
const hasTypescriptPackage = await pathExists(path.join(root, "packages", "typescript", "tsconfig.internal.json"));
|
|
347
|
+
// Find UI package
|
|
348
|
+
let uiPackageDir = null;
|
|
349
|
+
const packagesDir = path.join(root, "packages");
|
|
350
|
+
if (await pathExists(packagesDir)) {
|
|
351
|
+
const entries = await fs.readdir(packagesDir);
|
|
352
|
+
for (const e of entries) {
|
|
353
|
+
if (await pathExists(path.join(packagesDir, e, "components.json"))) {
|
|
354
|
+
uiPackageDir = path.join(packagesDir, e);
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// Fallback to first package if no components.json found
|
|
359
|
+
if (!uiPackageDir && entries.length > 0) {
|
|
360
|
+
uiPackageDir = path.join(packagesDir, entries[0]);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// Find packages with outdated tsconfigs (missing composite / still on bundler resolution)
|
|
364
|
+
const packagesWithBadTsConfig = [];
|
|
365
|
+
if (await pathExists(packagesDir)) {
|
|
366
|
+
const entries = await fs.readdir(packagesDir);
|
|
367
|
+
for (const e of entries) {
|
|
368
|
+
const tsCfgPath = path.join(packagesDir, e, "tsconfig.json");
|
|
369
|
+
if (!(await pathExists(tsCfgPath)))
|
|
370
|
+
continue;
|
|
371
|
+
try {
|
|
372
|
+
const tsJson = (await fs.readJson(tsCfgPath));
|
|
373
|
+
const co = (tsJson.compilerOptions ?? {});
|
|
374
|
+
// Outdated if: no composite, or moduleResolution is bundler (should be NodeNext via extends)
|
|
375
|
+
const isOutdated = !co.composite ||
|
|
376
|
+
String(co.moduleResolution ?? "").toLowerCase() === "bundler" ||
|
|
377
|
+
!tsJson.extends;
|
|
378
|
+
if (isOutdated)
|
|
379
|
+
packagesWithBadTsConfig.push(e);
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
/* malformed — flag it */ packagesWithBadTsConfig.push(e);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// Find apps with outdated tsconfigs
|
|
387
|
+
const appsWithBadTsConfig = [];
|
|
388
|
+
const appsDir = path.join(root, "apps");
|
|
389
|
+
if (await pathExists(appsDir)) {
|
|
390
|
+
const entries = await fs.readdir(appsDir);
|
|
391
|
+
for (const e of entries) {
|
|
392
|
+
const tsCfgPath = path.join(appsDir, e, "tsconfig.json");
|
|
393
|
+
if (!(await pathExists(tsCfgPath)))
|
|
394
|
+
continue;
|
|
395
|
+
try {
|
|
396
|
+
const tsJson = (await fs.readJson(tsCfgPath));
|
|
397
|
+
const co = (tsJson.compilerOptions ?? {});
|
|
398
|
+
const paths = (co.paths ?? {});
|
|
399
|
+
// Outdated if: doesn't extend base, or paths are missing the @scope/* mapping,
|
|
400
|
+
// or still has packages/**/* in include
|
|
401
|
+
const hasWrongIncludes = Array.isArray(tsJson.include) &&
|
|
402
|
+
tsJson.include.some((i) => i.includes("packages/**"));
|
|
403
|
+
const missingExtends = !tsJson.extends || !String(tsJson.extends).includes("tsconfig.base");
|
|
404
|
+
const missingPaths = !Object.keys(paths).some((k) => k.startsWith("@") && k.endsWith("/*"));
|
|
405
|
+
if (hasWrongIncludes || missingExtends || missingPaths) {
|
|
406
|
+
appsWithBadTsConfig.push(e);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
/* malformed — flag it */ appsWithBadTsConfig.push(e);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// Find internal packages with .js extensions on relative imports
|
|
415
|
+
const internalPackagesWithJsExtensions = [];
|
|
416
|
+
if (await pathExists(packagesDir)) {
|
|
417
|
+
const entries = await fs.readdir(packagesDir);
|
|
418
|
+
for (const e of entries) {
|
|
419
|
+
if (e === "typescript")
|
|
420
|
+
continue; // skip the typescript config package itself
|
|
421
|
+
const pkgDir = path.join(packagesDir, e);
|
|
422
|
+
// Only check internal packages (private: true)
|
|
423
|
+
try {
|
|
424
|
+
const pkg = (await fs.readJson(path.join(pkgDir, "package.json")));
|
|
425
|
+
if (!pkg.private)
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (await dirHasJsExtensionImports(pkgDir)) {
|
|
432
|
+
internalPackagesWithJsExtensions.push(e);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// Count existing backup files
|
|
437
|
+
const backupFileCount = await countBackupFiles(root);
|
|
438
|
+
return {
|
|
439
|
+
hasConfig: !!cfg,
|
|
440
|
+
configVersion: cfg?.version ?? null,
|
|
441
|
+
hasTsConfigBase,
|
|
442
|
+
hasTypescriptPackage,
|
|
443
|
+
hasUiPackageVisibility: !!cfg?.uiPackageVisibility,
|
|
444
|
+
uiPackageDir,
|
|
445
|
+
appsWithBadTsConfig,
|
|
446
|
+
packagesWithBadTsConfig,
|
|
447
|
+
internalPackagesWithJsExtensions,
|
|
448
|
+
backupFileCount,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function isFullyMigrated(s) {
|
|
452
|
+
return (s.hasConfig &&
|
|
453
|
+
s.hasTsConfigBase &&
|
|
454
|
+
s.hasTypescriptPackage &&
|
|
455
|
+
s.hasUiPackageVisibility &&
|
|
456
|
+
s.appsWithBadTsConfig.length === 0 &&
|
|
457
|
+
s.packagesWithBadTsConfig.length === 0 &&
|
|
458
|
+
s.internalPackagesWithJsExtensions.length === 0 &&
|
|
459
|
+
s.backupFileCount === 0);
|
|
460
|
+
}
|
|
461
|
+
function printAnalysis(s) {
|
|
462
|
+
const tick = c.green("✓");
|
|
463
|
+
const cross = c.yellow("✗");
|
|
464
|
+
console.log();
|
|
465
|
+
console.log(` ${s.hasConfig ? tick : cross} nx-factory.config.json ${s.hasConfig ? c.dim(`(v${s.configVersion ?? "unknown"})`) : c.yellow("missing")}`);
|
|
466
|
+
console.log(` ${s.hasTsConfigBase ? tick : cross} tsconfig.base.json ${s.hasTsConfigBase ? "" : c.yellow("missing — will create")}`);
|
|
467
|
+
console.log(` ${s.hasTypescriptPackage ? tick : cross} packages/typescript presets ${s.hasTypescriptPackage ? "" : c.yellow("missing — will create")}`);
|
|
468
|
+
console.log(` ${s.hasUiPackageVisibility ? tick : cross} UI package visibility ${s.hasUiPackageVisibility ? "" : c.yellow("not set — will prompt")}`);
|
|
469
|
+
if (s.packagesWithBadTsConfig.length > 0) {
|
|
470
|
+
console.log(` ${cross} Packages needing tsconfig migration: ${c.yellow(s.packagesWithBadTsConfig.join(", "))}`);
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
console.log(` ${tick} All package tsconfigs are up to date`);
|
|
474
|
+
}
|
|
475
|
+
if (s.appsWithBadTsConfig.length > 0) {
|
|
476
|
+
console.log(` ${cross} Apps needing tsconfig migration: ${c.yellow(s.appsWithBadTsConfig.join(", "))}`);
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
console.log(` ${tick} All app tsconfigs are up to date`);
|
|
480
|
+
}
|
|
481
|
+
if (s.internalPackagesWithJsExtensions.length > 0) {
|
|
482
|
+
console.log(` ${cross} Internal packages with .js extensions: ${c.yellow(s.internalPackagesWithJsExtensions.join(", "))} ${c.dim("(safe to remove)")}`);
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
console.log(` ${tick} No .js extension issues in internal packages`);
|
|
486
|
+
}
|
|
487
|
+
if (s.backupFileCount > 0) {
|
|
488
|
+
console.log(` ${c.dim("○")} ${s.backupFileCount} .migration-backup file(s) exist from a previous run`);
|
|
489
|
+
}
|
|
490
|
+
console.log();
|
|
491
|
+
}
|
|
492
|
+
async function detectFrameworkFromAppDir(appDir) {
|
|
493
|
+
const checks = [
|
|
494
|
+
["next.config.ts", "nextjs"],
|
|
495
|
+
["next.config.js", "nextjs"],
|
|
496
|
+
["next.config.mjs", "nextjs"],
|
|
497
|
+
["app/root.tsx", "remix"],
|
|
498
|
+
["app/root.jsx", "remix"],
|
|
499
|
+
["app.json", "expo"],
|
|
500
|
+
["app.config.ts", "expo"],
|
|
501
|
+
["vite.config.ts", "vite"],
|
|
502
|
+
["vite.config.js", "vite"],
|
|
503
|
+
];
|
|
504
|
+
for (const [file, fw] of checks) {
|
|
505
|
+
if (await pathExists(path.join(appDir, file)))
|
|
506
|
+
return fw;
|
|
507
|
+
}
|
|
508
|
+
return "nextjs";
|
|
509
|
+
}
|
|
510
|
+
// ─── Backup helper ────────────────────────────────────────────────────────────
|
|
511
|
+
async function backupIfExists(filePath, dryRun) {
|
|
512
|
+
if (dryRun)
|
|
513
|
+
return;
|
|
514
|
+
if (!(await pathExists(filePath)))
|
|
515
|
+
return;
|
|
516
|
+
const { default: fs } = await import("fs-extra");
|
|
517
|
+
await fs.copy(filePath, `${filePath}.migration-backup`, { overwrite: true });
|
|
518
|
+
}
|
|
519
|
+
// ─── .js extension helpers ────────────────────────────────────────────────────
|
|
520
|
+
/**
|
|
521
|
+
* Checks if any .ts file in a directory has relative imports ending with .js
|
|
522
|
+
* (e.g. `from "./utils.js"` instead of `from "./utils.js"`).
|
|
523
|
+
*/
|
|
524
|
+
async function dirHasJsExtensionImports(dir) {
|
|
525
|
+
const { default: fs } = await import("fs-extra");
|
|
526
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
527
|
+
for (const entry of entries) {
|
|
528
|
+
if (entry.name === "node_modules" || entry.name === "dist")
|
|
529
|
+
continue;
|
|
530
|
+
const full = path.join(dir, entry.name);
|
|
531
|
+
if (entry.isDirectory()) {
|
|
532
|
+
if (await dirHasJsExtensionImports(full))
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
else if (entry.isFile() &&
|
|
536
|
+
(entry.name.endsWith(".ts") || entry.name.endsWith(".tsx"))) {
|
|
537
|
+
const content = await fs.readFile(full, "utf-8");
|
|
538
|
+
// Match: from "./something.js" or from "../something.js"
|
|
539
|
+
if (/from\s+["'](\.\.?\/[^"']+)\.js["']/.test(content))
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Recursively removes .js extensions from relative imports in all .ts/.tsx
|
|
547
|
+
* files within a directory. Only touches imports that start with ./ or ../
|
|
548
|
+
*
|
|
549
|
+
* Before: import { fn } from "./utils.js";
|
|
550
|
+
* After: import { fn } from "./utils.js";
|
|
551
|
+
*
|
|
552
|
+
* Does NOT touch:
|
|
553
|
+
* - Package imports (e.g. "react", "@scope/pkg")
|
|
554
|
+
* - Non-.js extensions (.json, .css, .svg, etc.)
|
|
555
|
+
*/
|
|
556
|
+
async function stripJsExtensionsFromDir(dir, dryRun) {
|
|
557
|
+
const { default: fs } = await import("fs-extra");
|
|
558
|
+
const JS_IMPORT_RE = /(from\s+["'])(\.\.?\/[^"']+)\.js(["'])/g;
|
|
559
|
+
const REEXPORT_RE = /(export\s+.*?\s+from\s+["'])(\.\.?\/[^"']+)\.js(["'])/g;
|
|
560
|
+
const DYNAMIC_RE = /(import\s*\(\s*["'])(\.\.?\/[^"']+)\.js(["']\s*\))/g;
|
|
561
|
+
let totalChanged = 0;
|
|
562
|
+
async function processDir(currentDir) {
|
|
563
|
+
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
564
|
+
for (const entry of entries) {
|
|
565
|
+
if (entry.name === "node_modules" || entry.name === "dist")
|
|
566
|
+
continue;
|
|
567
|
+
const full = path.join(currentDir, entry.name);
|
|
568
|
+
if (entry.isDirectory()) {
|
|
569
|
+
await processDir(full);
|
|
570
|
+
}
|
|
571
|
+
else if (entry.isFile() &&
|
|
572
|
+
(entry.name.endsWith(".ts") || entry.name.endsWith(".tsx"))) {
|
|
573
|
+
const original = await fs.readFile(full, "utf-8");
|
|
574
|
+
const modified = original
|
|
575
|
+
.replace(JS_IMPORT_RE, "$1$2$3")
|
|
576
|
+
.replace(REEXPORT_RE, "$1$2$3")
|
|
577
|
+
.replace(DYNAMIC_RE, "$1$2$3");
|
|
578
|
+
if (modified !== original) {
|
|
579
|
+
totalChanged++;
|
|
580
|
+
if (!dryRun) {
|
|
581
|
+
// Back up before modifying
|
|
582
|
+
await fs.copy(full, `${full}.migration-backup`, {
|
|
583
|
+
overwrite: true,
|
|
584
|
+
});
|
|
585
|
+
await fs.writeFile(full, modified, "utf-8");
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
await processDir(dir);
|
|
592
|
+
return totalChanged;
|
|
593
|
+
}
|
|
594
|
+
// ─── Backup cleanup helpers ───────────────────────────────────────────────────
|
|
595
|
+
/**
|
|
596
|
+
* Counts all *.migration-backup files under the workspace root.
|
|
597
|
+
*/
|
|
598
|
+
async function countBackupFiles(root) {
|
|
599
|
+
const { default: fs } = await import("fs-extra");
|
|
600
|
+
let count = 0;
|
|
601
|
+
async function walk(dir) {
|
|
602
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
603
|
+
for (const entry of entries) {
|
|
604
|
+
if (entry.name === "node_modules" || entry.name === ".git")
|
|
605
|
+
continue;
|
|
606
|
+
const full = path.join(dir, entry.name);
|
|
607
|
+
if (entry.isDirectory()) {
|
|
608
|
+
await walk(full);
|
|
609
|
+
}
|
|
610
|
+
else if (entry.isFile() && entry.name.endsWith(".migration-backup")) {
|
|
611
|
+
count++;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
await walk(root);
|
|
616
|
+
return count;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Deletes all *.migration-backup files under the workspace root.
|
|
620
|
+
* Call this once you've verified the migration is correct.
|
|
621
|
+
*/
|
|
622
|
+
export async function cleanupMigrationBackups(root, dryRun) {
|
|
623
|
+
const { default: fs } = await import("fs-extra");
|
|
624
|
+
let deleted = 0;
|
|
625
|
+
async function walk(dir) {
|
|
626
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
627
|
+
for (const entry of entries) {
|
|
628
|
+
if (entry.name === "node_modules" || entry.name === ".git")
|
|
629
|
+
continue;
|
|
630
|
+
const full = path.join(dir, entry.name);
|
|
631
|
+
if (entry.isDirectory()) {
|
|
632
|
+
await walk(full);
|
|
633
|
+
}
|
|
634
|
+
else if (entry.isFile() && entry.name.endsWith(".migration-backup")) {
|
|
635
|
+
deleted++;
|
|
636
|
+
if (!dryRun)
|
|
637
|
+
await fs.remove(full);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
await walk(root);
|
|
642
|
+
return deleted;
|
|
643
|
+
}
|
|
644
|
+
//# sourceMappingURL=migrate.js.map
|