@remix-run/cli 0.3.4 → 0.5.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/README.md +105 -1
- package/dist/index.js +1 -1
- package/dist/lib/app-root.d.ts +11 -0
- package/dist/lib/app-root.d.ts.map +1 -0
- package/dist/lib/app-root.js +38 -0
- package/dist/lib/bootstrap-project.js +2 -2
- package/dist/lib/cli-context.d.ts +6 -0
- package/dist/lib/cli-context.d.ts.map +1 -1
- package/dist/lib/cli-context.js +12 -1
- package/dist/lib/cli.d.ts +3 -2
- package/dist/lib/cli.d.ts.map +1 -1
- package/dist/lib/cli.js +42 -14
- package/dist/lib/commands/completion.js +4 -4
- package/dist/lib/commands/db.d.ts +4 -0
- package/dist/lib/commands/db.d.ts.map +1 -0
- package/dist/lib/commands/db.js +283 -0
- package/dist/lib/commands/doctor.d.ts +5 -0
- package/dist/lib/commands/doctor.d.ts.map +1 -1
- package/dist/lib/commands/doctor.js +26 -251
- package/dist/lib/commands/help.d.ts.map +1 -1
- package/dist/lib/commands/help.js +17 -9
- package/dist/lib/commands/new.js +6 -6
- package/dist/lib/commands/routes.js +6 -6
- package/dist/lib/commands/test.d.ts +20 -1
- package/dist/lib/commands/test.d.ts.map +1 -1
- package/dist/lib/commands/test.js +353 -10
- package/dist/lib/commands/version.js +3 -3
- package/dist/lib/completion.d.ts +1 -1
- package/dist/lib/completion.d.ts.map +1 -1
- package/dist/lib/completion.js +229 -6
- package/dist/lib/controller-files.d.ts +0 -8
- package/dist/lib/controller-files.d.ts.map +1 -1
- package/dist/lib/controller-files.js +0 -23
- package/dist/lib/controller-ownership.d.ts +4 -12
- package/dist/lib/controller-ownership.d.ts.map +1 -1
- package/dist/lib/controller-ownership.js +31 -127
- package/dist/lib/database-command.d.ts +20 -0
- package/dist/lib/database-command.d.ts.map +1 -0
- package/dist/lib/database-command.js +7 -0
- package/dist/lib/doctor/controller-findings.d.ts +2 -2
- package/dist/lib/doctor/controller-findings.d.ts.map +1 -1
- package/dist/lib/doctor/controller-findings.js +23 -39
- package/dist/lib/doctor/controllers.d.ts +1 -2
- package/dist/lib/doctor/controllers.d.ts.map +1 -1
- package/dist/lib/doctor/controllers.js +4 -7
- package/dist/lib/doctor/environment.js +2 -2
- package/dist/lib/doctor/fixes.js +1 -1
- package/dist/lib/doctor/project.d.ts.map +1 -1
- package/dist/lib/doctor/project.js +3 -212
- package/dist/lib/doctor/run.d.ts +9 -0
- package/dist/lib/doctor/run.d.ts.map +1 -0
- package/dist/lib/doctor/run.js +237 -0
- package/dist/lib/doctor/types.d.ts +2 -1
- package/dist/lib/doctor/types.d.ts.map +1 -1
- package/dist/lib/doctor/types.js +4 -1
- package/dist/lib/errors.d.ts +30 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +55 -0
- package/dist/lib/help-text.js +1 -1
- package/dist/lib/parse-args.d.ts.map +1 -1
- package/dist/lib/parse-args.js +17 -4
- package/dist/lib/remix-config.d.ts +75 -0
- package/dist/lib/remix-config.d.ts.map +1 -0
- package/dist/lib/remix-config.js +411 -0
- package/dist/lib/reporter.js +1 -1
- package/dist/lib/route-map.js +13 -33
- package/package.json +13 -7
- package/schema/remix.json +242 -0
- package/src/lib/app-root.ts +46 -0
- package/src/lib/cli-context.ts +17 -0
- package/src/lib/cli.ts +41 -6
- package/src/lib/commands/db.ts +377 -0
- package/src/lib/commands/doctor.ts +27 -329
- package/src/lib/commands/help.ts +10 -1
- package/src/lib/commands/test.ts +453 -12
- package/src/lib/completion.ts +321 -6
- package/src/lib/controller-files.ts +0 -38
- package/src/lib/controller-ownership.ts +50 -203
- package/src/lib/database-command.ts +31 -0
- package/src/lib/doctor/controller-findings.ts +30 -47
- package/src/lib/doctor/controllers.ts +2 -6
- package/src/lib/doctor/project.ts +0 -236
- package/src/lib/doctor/run.ts +311 -0
- package/src/lib/doctor/types.ts +6 -5
- package/src/lib/errors.ts +65 -0
- package/src/lib/parse-args.ts +19 -3
- package/src/lib/remix-config.ts +639 -0
- package/src/lib/route-map.ts +11 -48
- package/template/.agents/skills/remix/SKILL.md +38 -27
- package/template/.agents/skills/remix/references/assets-and-browser-modules.md +54 -11
- package/template/.agents/skills/remix/references/auth-and-sessions.md +5 -5
- package/template/.agents/skills/remix/references/data-and-validation.md +15 -15
- package/template/.agents/skills/remix/references/hydration-frames-navigation.md +63 -9
- package/template/.agents/skills/remix/references/middleware-and-server.md +68 -6
- package/template/.agents/skills/remix/references/mixins-styling-events.md +2 -0
- package/template/.agents/skills/remix/references/routing-and-controllers.md +8 -8
- package/template/.agents/skills/remix/references/testing-patterns.md +19 -18
- package/template/AGENTS.md +6 -3
- package/template/README.md +6 -3
- package/template/app/actions/controller.tsx +1 -1
- package/template/app/{ui → actions}/document.tsx +7 -5
- package/template/app/{ui/scaffold-home-page.tsx → actions/home-page.tsx} +1 -2
- package/template/app/actions/public/entry.ts +48 -0
- package/template/app/{assets → actions/public}/prompt-button.tsx +29 -25
- package/template/app/assets.ts +15 -2
- package/template/app/middleware/render.tsx +8 -2
- package/template/hmr.ts +46 -0
- package/template/package.json +1 -0
- package/template/server.ts +8 -1
- package/template/tsconfig.json +1 -1
- package/dist/lib/doctor/controller-fix-plans.d.ts +0 -4
- package/dist/lib/doctor/controller-fix-plans.d.ts.map +0 -1
- package/dist/lib/doctor/controller-fix-plans.js +0 -119
- package/dist/lib/doctor/controller-placeholders.d.ts +0 -3
- package/dist/lib/doctor/controller-placeholders.d.ts.map +0 -1
- package/dist/lib/doctor/controller-placeholders.js +0 -52
- package/src/lib/doctor/controller-fix-plans.ts +0 -168
- package/src/lib/doctor/controller-placeholders.ts +0 -70
- package/template/app/assets/entry.ts +0 -17
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as process from 'node:process';
|
|
4
|
+
import { runRemixDb } from '@remix-run/data-table/cli';
|
|
5
|
+
import { loadMigrations, loadSeed } from '@remix-run/data-table/migrations/node';
|
|
6
|
+
import { findAppRoot } from '../app-root.js';
|
|
7
|
+
import { isDatabaseCommand, } from '../database-command.js';
|
|
8
|
+
import { dbConfigRequired, dbForceRequired, invalidOptionValue, remixConfigNotFound, renderCliError, toCliError, unknownCommand, } from '../errors.js';
|
|
9
|
+
import { formatHelpText } from '../help-text.js';
|
|
10
|
+
import { parseArgs } from '../parse-args.js';
|
|
11
|
+
import { loadRemixConfig, } from '../remix-config.js';
|
|
12
|
+
const connectionOption = { flag: '--connection-env', type: 'string' };
|
|
13
|
+
const journalOption = { flag: '--journal-table', type: 'string' };
|
|
14
|
+
const migrationsOption = { flag: '--migrations', type: 'string' };
|
|
15
|
+
const seedOption = { flag: '--seed', type: 'string' };
|
|
16
|
+
export async function runDbCommand(argv, context) {
|
|
17
|
+
if (argv.length === 0 || argv.includes('-h') || argv.includes('--help')) {
|
|
18
|
+
process.stdout.write(getDbCommandHelpText());
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
let invocation = parseDbCommandArgs(argv);
|
|
23
|
+
let resolved = await resolveDbConfig(context);
|
|
24
|
+
let plan = resolveDatabaseCommandPlan(invocation, resolved.config, context.cwd);
|
|
25
|
+
return await runDatabaseCommand(plan, resolved.configDir);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
process.stderr.write(renderCliError(toCliError(error), { helpText: getDbCommandHelpText(process.stderr) }));
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function getDbCommandHelpText(target = process.stdout) {
|
|
33
|
+
return formatHelpText({
|
|
34
|
+
description: 'Manage the current app database.',
|
|
35
|
+
examples: [
|
|
36
|
+
'remix db wipe --force',
|
|
37
|
+
'remix db migrate',
|
|
38
|
+
'remix db migrate --to 20260715123000_add_users',
|
|
39
|
+
'remix db status',
|
|
40
|
+
'remix db seed',
|
|
41
|
+
'remix db reset --force',
|
|
42
|
+
],
|
|
43
|
+
options: [
|
|
44
|
+
{
|
|
45
|
+
description: 'Read the database connection from an environment variable',
|
|
46
|
+
label: '--connection-env <name>',
|
|
47
|
+
},
|
|
48
|
+
{ description: 'Confirm a destructive command (wipe and reset only)', label: '--force' },
|
|
49
|
+
{
|
|
50
|
+
description: 'Use a migration journal table (migrate, status, and reset only)',
|
|
51
|
+
label: '--journal-table <name>',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
description: 'Load migrations from a directory (migrate, status, and reset only)',
|
|
55
|
+
label: '--migrations <path>',
|
|
56
|
+
},
|
|
57
|
+
{ description: 'Run a SQL seed file (seed and reset only)', label: '--seed <path>' },
|
|
58
|
+
{
|
|
59
|
+
description: 'Stop after applying the specified migration (migrate only)',
|
|
60
|
+
label: '--to <migration>',
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
usage: [
|
|
64
|
+
'remix db wipe --force [options]',
|
|
65
|
+
'remix db migrate [--to <migration>] [options]',
|
|
66
|
+
'remix db status [options]',
|
|
67
|
+
'remix db seed [options]',
|
|
68
|
+
'remix db reset --force [options]',
|
|
69
|
+
],
|
|
70
|
+
}, target);
|
|
71
|
+
}
|
|
72
|
+
function parseDbCommandArgs(argv) {
|
|
73
|
+
let [command, ...commandArgv] = argv;
|
|
74
|
+
if (!isDatabaseCommand(command)) {
|
|
75
|
+
throw unknownCommand(`db ${command}`);
|
|
76
|
+
}
|
|
77
|
+
if (command === 'migrate') {
|
|
78
|
+
let parsed = parseArgs(commandArgv, {
|
|
79
|
+
connectionEnv: connectionOption,
|
|
80
|
+
journalTable: journalOption,
|
|
81
|
+
migrations: migrationsOption,
|
|
82
|
+
to: { flag: '--to', type: 'string' },
|
|
83
|
+
}, { maxPositionals: 0 });
|
|
84
|
+
return { command, ...parsed.options };
|
|
85
|
+
}
|
|
86
|
+
if (command === 'reset') {
|
|
87
|
+
let parsed = parseArgs(commandArgv, {
|
|
88
|
+
connectionEnv: connectionOption,
|
|
89
|
+
force: { flag: '--force', type: 'boolean' },
|
|
90
|
+
journalTable: journalOption,
|
|
91
|
+
migrations: migrationsOption,
|
|
92
|
+
seed: seedOption,
|
|
93
|
+
}, { maxPositionals: 0 });
|
|
94
|
+
if (!parsed.options.force)
|
|
95
|
+
throw dbForceRequired(command);
|
|
96
|
+
let { force: _, ...options } = parsed.options;
|
|
97
|
+
return { command, ...options };
|
|
98
|
+
}
|
|
99
|
+
if (command === 'wipe') {
|
|
100
|
+
let parsed = parseArgs(commandArgv, {
|
|
101
|
+
connectionEnv: connectionOption,
|
|
102
|
+
force: { flag: '--force', type: 'boolean' },
|
|
103
|
+
}, { maxPositionals: 0 });
|
|
104
|
+
if (!parsed.options.force)
|
|
105
|
+
throw dbForceRequired(command);
|
|
106
|
+
return { command, connectionEnv: parsed.options.connectionEnv };
|
|
107
|
+
}
|
|
108
|
+
if (command === 'seed') {
|
|
109
|
+
let parsed = parseArgs(commandArgv, { connectionEnv: connectionOption, seed: seedOption }, { maxPositionals: 0 });
|
|
110
|
+
return { command, ...parsed.options };
|
|
111
|
+
}
|
|
112
|
+
let parsed = parseArgs(commandArgv, {
|
|
113
|
+
connectionEnv: connectionOption,
|
|
114
|
+
journalTable: journalOption,
|
|
115
|
+
migrations: migrationsOption,
|
|
116
|
+
}, { maxPositionals: 0 });
|
|
117
|
+
return { command, ...parsed.options };
|
|
118
|
+
}
|
|
119
|
+
async function resolveDbConfig(context) {
|
|
120
|
+
let configPath;
|
|
121
|
+
let config;
|
|
122
|
+
if (context.configPath !== undefined) {
|
|
123
|
+
configPath = path.resolve(context.cwd, context.configPath);
|
|
124
|
+
config = await context.loadConfig();
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
let configDir = await findAppRoot(context.cwd, 'remix.json');
|
|
128
|
+
if (configDir === null)
|
|
129
|
+
throw remixConfigNotFound(path.join(context.cwd, 'remix.json'));
|
|
130
|
+
configPath = path.join(configDir, 'remix.json');
|
|
131
|
+
config = await loadRemixConfig(configDir, undefined);
|
|
132
|
+
}
|
|
133
|
+
if (config.db === undefined)
|
|
134
|
+
throw dbConfigRequired(configPath);
|
|
135
|
+
return { config: config.db, configDir: path.dirname(configPath) };
|
|
136
|
+
}
|
|
137
|
+
function resolveDatabaseCommandPlan(invocation, config, cwd) {
|
|
138
|
+
let adapter = overrideConnection(config.adapter, invocation.connectionEnv, cwd);
|
|
139
|
+
let migrations = invocation.migrations
|
|
140
|
+
? path.resolve(cwd, invocation.migrations)
|
|
141
|
+
: config.migrations?.directory;
|
|
142
|
+
let journalTable = invocation.journalTable ?? config.migrations?.journalTable;
|
|
143
|
+
let seed = invocation.seed ? path.resolve(cwd, invocation.seed) : config.seed;
|
|
144
|
+
if ((invocation.command === 'migrate' ||
|
|
145
|
+
invocation.command === 'reset' ||
|
|
146
|
+
invocation.command === 'status') &&
|
|
147
|
+
migrations === undefined) {
|
|
148
|
+
throw invalidOptionValue(`Database command "${invocation.command}" requires db.migrations.directory or --migrations`);
|
|
149
|
+
}
|
|
150
|
+
if (invocation.command === 'seed' && seed === undefined) {
|
|
151
|
+
throw invalidOptionValue('Database command "seed" requires db.seed or --seed');
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
adapter,
|
|
155
|
+
command: invocation.command,
|
|
156
|
+
journalTable,
|
|
157
|
+
migrations,
|
|
158
|
+
seed,
|
|
159
|
+
to: invocation.to,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function overrideConnection(adapter, environmentName, cwd) {
|
|
163
|
+
if (environmentName === undefined)
|
|
164
|
+
return adapter;
|
|
165
|
+
if (adapter.type === 'sqlite') {
|
|
166
|
+
// Resolve a relative sqlite filename here, against the directory the
|
|
167
|
+
// command was invoked from (like --migrations and --seed). Filenames from
|
|
168
|
+
// remix.json resolve against the config directory instead, which would
|
|
169
|
+
// silently retarget the path.
|
|
170
|
+
let filename = process.env[environmentName];
|
|
171
|
+
if (filename === undefined || filename === '') {
|
|
172
|
+
throw invalidOptionValue(`Database environment variable ${environmentName} is not set`);
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
...adapter,
|
|
176
|
+
filename: filename === ':memory:' ? filename : path.resolve(cwd, filename),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (adapter.type === 'postgres') {
|
|
180
|
+
return { ...adapter, connectionString: { env: environmentName } };
|
|
181
|
+
}
|
|
182
|
+
return { ...adapter, uri: { env: environmentName } };
|
|
183
|
+
}
|
|
184
|
+
async function runDatabaseCommand(plan, configDir) {
|
|
185
|
+
let db = await createConfiguredDatabase(plan.adapter, configDir);
|
|
186
|
+
let result;
|
|
187
|
+
try {
|
|
188
|
+
result = await executeDatabaseCommand(plan, db);
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
try {
|
|
192
|
+
await db.close();
|
|
193
|
+
}
|
|
194
|
+
catch (closeError) {
|
|
195
|
+
throw new AggregateError([error, closeError], 'Database command and close both failed', {
|
|
196
|
+
cause: error,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
await db.close();
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
async function executeDatabaseCommand(plan, db) {
|
|
205
|
+
if (plan.command === 'wipe') {
|
|
206
|
+
return runRemixDb({ command: plan.command, db });
|
|
207
|
+
}
|
|
208
|
+
if (plan.command === 'seed') {
|
|
209
|
+
if (plan.seed === undefined) {
|
|
210
|
+
throw invalidOptionValue('Database command "seed" requires db.seed or --seed');
|
|
211
|
+
}
|
|
212
|
+
return runRemixDb({ command: plan.command, db, seed: await loadSeed(plan.seed) });
|
|
213
|
+
}
|
|
214
|
+
if (plan.migrations === undefined) {
|
|
215
|
+
throw invalidOptionValue(`Database command "${plan.command}" requires db.migrations.directory or --migrations`);
|
|
216
|
+
}
|
|
217
|
+
let migrations = await loadMigrations(plan.migrations);
|
|
218
|
+
if (plan.command === 'migrate') {
|
|
219
|
+
return runRemixDb({
|
|
220
|
+
command: plan.command,
|
|
221
|
+
db,
|
|
222
|
+
migrations,
|
|
223
|
+
to: plan.to,
|
|
224
|
+
journalTable: plan.journalTable,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (plan.command === 'reset') {
|
|
228
|
+
let seed = plan.seed === undefined ? undefined : await loadSeed(plan.seed);
|
|
229
|
+
return runRemixDb({
|
|
230
|
+
command: plan.command,
|
|
231
|
+
db,
|
|
232
|
+
migrations,
|
|
233
|
+
seed,
|
|
234
|
+
journalTable: plan.journalTable,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return runRemixDb({
|
|
238
|
+
command: plan.command,
|
|
239
|
+
db,
|
|
240
|
+
migrations,
|
|
241
|
+
journalTable: plan.journalTable,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
async function createConfiguredDatabase(adapter, configDir) {
|
|
245
|
+
// Adapter packages are imported lazily because their database drivers are
|
|
246
|
+
// optional peer dependencies; only the configured adapter's driver needs to
|
|
247
|
+
// be installed.
|
|
248
|
+
if (adapter.type === 'sqlite') {
|
|
249
|
+
let { createSqliteDatabase } = await import('@remix-run/data-table-sqlite');
|
|
250
|
+
let filename = resolveDbString(adapter.filename);
|
|
251
|
+
if (filename !== ':memory:') {
|
|
252
|
+
// Filenames from remix.json are config-relative; explicit flag overrides
|
|
253
|
+
// arrive here already resolved against the invocation directory.
|
|
254
|
+
filename = path.resolve(configDir, filename);
|
|
255
|
+
await fs.mkdir(path.dirname(filename), { recursive: true });
|
|
256
|
+
}
|
|
257
|
+
return createSqliteDatabase({
|
|
258
|
+
filename,
|
|
259
|
+
foreignKeys: adapter.foreignKeys,
|
|
260
|
+
busyTimeout: adapter.busyTimeout,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
if (adapter.type === 'postgres') {
|
|
264
|
+
let { createPostgresDatabase } = await import('@remix-run/data-table-postgres');
|
|
265
|
+
return createPostgresDatabase({ connectionString: resolveDbString(adapter.connectionString) }, {
|
|
266
|
+
maintenanceDatabase: adapter.maintenanceDatabase,
|
|
267
|
+
template: adapter.template,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
let { createMysqlDatabase } = await import('@remix-run/data-table-mysql');
|
|
271
|
+
return createMysqlDatabase({ uri: resolveDbString(adapter.uri), multipleStatements: true }, { characterSet: adapter.characterSet, collation: adapter.collation });
|
|
272
|
+
}
|
|
273
|
+
function resolveDbString(value) {
|
|
274
|
+
if (typeof value === 'string')
|
|
275
|
+
return value;
|
|
276
|
+
// An env var that is set but empty is treated as unset so it falls back to
|
|
277
|
+
// the configured default instead of producing an empty connection value.
|
|
278
|
+
let resolved = process.env[value.env] || value.default;
|
|
279
|
+
if (resolved === undefined || resolved === '') {
|
|
280
|
+
throw invalidOptionValue(`Database environment variable ${value.env} is not set`);
|
|
281
|
+
}
|
|
282
|
+
return resolved;
|
|
283
|
+
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { CliContext } from '../cli-context.ts';
|
|
2
|
+
import { type RunRemixDoctorOptions } from '../doctor/run.ts';
|
|
3
|
+
import type { RemixDoctorCommandConfig } from '../remix-config.ts';
|
|
4
|
+
type DoctorCommandOptions = Pick<RunRemixDoctorOptions, 'fix' | 'json' | 'strict'>;
|
|
2
5
|
export declare function runDoctorCommand(argv: string[], context: CliContext): Promise<number>;
|
|
3
6
|
export declare function getDoctorCommandHelpText(target?: NodeJS.WriteStream): string;
|
|
7
|
+
export declare function resolveDoctorCommandOptions(argv: string[], config: RemixDoctorCommandConfig | undefined): DoctorCommandOptions;
|
|
8
|
+
export {};
|
|
4
9
|
//# sourceMappingURL=doctor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../../src/lib/commands/doctor.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../../src/lib/commands/doctor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAkB,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AAI7E,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAA;AAElE,KAAK,oBAAoB,GAAG,IAAI,CAAC,qBAAqB,EAAE,KAAK,GAAG,MAAM,GAAG,QAAQ,CAAC,CAAA;AAElF,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAoB3F;AAED,wBAAgB,wBAAwB,CAAC,MAAM,GAAE,MAAM,CAAC,WAA4B,GAAG,MAAM,CA0B5F;AAED,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,MAAM,EAAE,EACd,MAAM,EAAE,wBAAwB,GAAG,SAAS,GAC3C,oBAAoB,CAuBtB"}
|
|
@@ -1,65 +1,24 @@
|
|
|
1
|
-
import * as path from 'node:path';
|
|
2
1
|
import * as process from 'node:process';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { createDoctorSuite, createSkippedDoctorSuite, } from "../doctor/types.js";
|
|
8
|
-
import { renderCliError, toCliError } from "../errors.js";
|
|
9
|
-
import { formatHelpText } from "../help-text.js";
|
|
10
|
-
import { parseArgs } from "../parse-args.js";
|
|
11
|
-
import { createCommandReporter, createStepProgressReporter, } from "../reporter.js";
|
|
12
|
-
const DOCTOR_SUITE_LABELS = {
|
|
13
|
-
actions: {
|
|
14
|
-
complete: 'actions',
|
|
15
|
-
running: 'Checking actions',
|
|
16
|
-
},
|
|
17
|
-
environment: {
|
|
18
|
-
complete: 'environment',
|
|
19
|
-
running: 'Checking environment',
|
|
20
|
-
},
|
|
21
|
-
project: {
|
|
22
|
-
complete: 'project',
|
|
23
|
-
running: 'Checking project',
|
|
24
|
-
},
|
|
25
|
-
};
|
|
2
|
+
import { runRemixDoctor } from '../doctor/run.js';
|
|
3
|
+
import { renderCliError, toCliError } from '../errors.js';
|
|
4
|
+
import { formatHelpText } from '../help-text.js';
|
|
5
|
+
import { parseArgs } from '../parse-args.js';
|
|
26
6
|
export async function runDoctorCommand(argv, context) {
|
|
27
7
|
if (argv.includes('-h') || argv.includes('--help')) {
|
|
28
8
|
process.stdout.write(getDoctorCommandHelpText());
|
|
29
9
|
return 0;
|
|
30
10
|
}
|
|
31
|
-
let reporter = null;
|
|
32
|
-
let progress = null;
|
|
33
11
|
try {
|
|
34
|
-
let
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
});
|
|
42
|
-
progress = reporter == null ? null : createDoctorProgressReporter(reporter);
|
|
43
|
-
if (reporter != null) {
|
|
44
|
-
await reporter.status.commandHeader('doctor');
|
|
45
|
-
}
|
|
46
|
-
let report = await collectDoctorReport(progress, options, reporter, context);
|
|
47
|
-
if (options.json) {
|
|
48
|
-
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
49
|
-
}
|
|
50
|
-
else if (reporter != null) {
|
|
51
|
-
writeDoctorReport(reporter, report);
|
|
52
|
-
reporter.finish();
|
|
53
|
-
}
|
|
54
|
-
if ((options.fix || options.strict) && hasWarningFindings(report.findings)) {
|
|
55
|
-
return 1;
|
|
56
|
-
}
|
|
57
|
-
return 0;
|
|
12
|
+
let config = await context.loadConfig();
|
|
13
|
+
let options = resolveDoctorCommandOptions(argv, config.doctor);
|
|
14
|
+
return await runRemixDoctor({
|
|
15
|
+
...options,
|
|
16
|
+
cwd: context.cwd,
|
|
17
|
+
remixVersion: context.remixVersion,
|
|
18
|
+
});
|
|
58
19
|
}
|
|
59
20
|
catch (error) {
|
|
60
|
-
progress?.writeSummaryGap();
|
|
61
21
|
process.stderr.write(renderCliError(toCliError(error), { helpText: getDoctorCommandHelpText(process.stderr) }));
|
|
62
|
-
reporter?.finish();
|
|
63
22
|
return 1;
|
|
64
23
|
}
|
|
65
24
|
}
|
|
@@ -78,216 +37,32 @@ export function getDoctorCommandHelpText(target = process.stdout) {
|
|
|
78
37
|
description: 'Exit with status 1 when warning-level findings are present',
|
|
79
38
|
label: '--strict',
|
|
80
39
|
},
|
|
81
|
-
{
|
|
40
|
+
{
|
|
41
|
+
description: 'Do not exit with status 1 when warning-level findings are present',
|
|
42
|
+
label: '--no-strict',
|
|
43
|
+
},
|
|
44
|
+
{ description: 'Apply available low-risk fixes', label: '--fix' },
|
|
82
45
|
],
|
|
83
46
|
usage: ['remix doctor [--json] [--strict] [--fix] [--no-color]'],
|
|
84
47
|
}, target);
|
|
85
48
|
}
|
|
86
|
-
function
|
|
49
|
+
export function resolveDoctorCommandOptions(argv, config) {
|
|
87
50
|
let parsed = parseArgs(argv, {
|
|
88
51
|
fix: { flag: '--fix', type: 'boolean' },
|
|
89
52
|
json: { flag: '--json', type: 'boolean' },
|
|
53
|
+
noStrict: { flag: '--no-strict', type: 'boolean' },
|
|
90
54
|
strict: { flag: '--strict', type: 'boolean' },
|
|
91
55
|
}, { maxPositionals: 0 });
|
|
56
|
+
let strict = config?.strict ?? false;
|
|
57
|
+
for (let arg of argv) {
|
|
58
|
+
if (arg === '--strict')
|
|
59
|
+
strict = true;
|
|
60
|
+
if (arg === '--no-strict')
|
|
61
|
+
strict = false;
|
|
62
|
+
}
|
|
92
63
|
return {
|
|
93
64
|
fix: parsed.options.fix,
|
|
94
65
|
json: parsed.options.json,
|
|
95
|
-
strict
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
async function collectDoctorReport(progress, options, reporter, context) {
|
|
99
|
-
let cwd = context.cwd;
|
|
100
|
-
let appliedFixes = [];
|
|
101
|
-
let environment = await runDoctorSuite(progress, 'environment', async () => {
|
|
102
|
-
let result = await checkEnvironment(cwd);
|
|
103
|
-
let suiteAppliedFixes = [];
|
|
104
|
-
let finalResult = result;
|
|
105
|
-
if (options.fix && result.projectRoot != null) {
|
|
106
|
-
let fixPlans = getEnvironmentFixPlans(result, context.remixVersion);
|
|
107
|
-
if (fixPlans.length > 0) {
|
|
108
|
-
suiteAppliedFixes = await applyDoctorFixPlans(result.projectRoot, fixPlans);
|
|
109
|
-
finalResult = await checkEnvironment(result.projectRoot);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
let suite = finalResult.suite;
|
|
113
|
-
if (suiteAppliedFixes.length > 0) {
|
|
114
|
-
suite = {
|
|
115
|
-
...suite,
|
|
116
|
-
appliedFixes: suiteAppliedFixes,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
return {
|
|
120
|
-
appliedFixes: suiteAppliedFixes,
|
|
121
|
-
projectRoot: finalResult.projectRoot ?? result.projectRoot,
|
|
122
|
-
suite,
|
|
123
|
-
};
|
|
124
|
-
});
|
|
125
|
-
let findings = [...environment.suite.findings];
|
|
126
|
-
appliedFixes.push(...(environment.appliedFixes ?? []));
|
|
127
|
-
let suites = [environment.suite];
|
|
128
|
-
let routesFile = environment.projectRoot == null
|
|
129
|
-
? undefined
|
|
130
|
-
: path.join(environment.projectRoot, 'app', 'routes.ts');
|
|
131
|
-
writeDoctorSuiteDetails(reporter, environment.suite);
|
|
132
|
-
progress?.writeSummaryGap();
|
|
133
|
-
if (hasWarningFindings(environment.suite.findings)) {
|
|
134
|
-
let projectSuite = createSkippedDoctorSuite('project', 'Blocked by environment warnings.');
|
|
135
|
-
let actionsSuite = createSkippedDoctorSuite('actions', 'Blocked by environment warnings.');
|
|
136
|
-
suites.push(projectSuite, actionsSuite);
|
|
137
|
-
progress?.skip(projectSuite.name, projectSuite.reason);
|
|
138
|
-
writeDoctorSuiteDetails(reporter, projectSuite);
|
|
139
|
-
progress?.writeSummaryGap();
|
|
140
|
-
progress?.skip(actionsSuite.name, actionsSuite.reason);
|
|
141
|
-
writeDoctorSuiteDetails(reporter, actionsSuite);
|
|
142
|
-
progress?.writeSummaryGap();
|
|
143
|
-
return {
|
|
144
|
-
appRoot: environment.projectRoot,
|
|
145
|
-
appliedFixes,
|
|
146
|
-
findings,
|
|
147
|
-
remainingFindings: findings,
|
|
148
|
-
routesFile,
|
|
149
|
-
suites,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
let project = await runDoctorSuite(progress, 'project', async () => {
|
|
153
|
-
let result = await checkProject(environment.projectRoot);
|
|
154
|
-
let suiteAppliedFixes = [];
|
|
155
|
-
let finalResult = result;
|
|
156
|
-
if (options.fix) {
|
|
157
|
-
let fixPlans = await getProjectFixPlans(environment.projectRoot);
|
|
158
|
-
if (fixPlans.length > 0) {
|
|
159
|
-
suiteAppliedFixes = await applyDoctorFixPlans(environment.projectRoot, fixPlans);
|
|
160
|
-
finalResult = await checkProject(environment.projectRoot);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
let suite = finalResult.suite;
|
|
164
|
-
if (suiteAppliedFixes.length > 0) {
|
|
165
|
-
suite = {
|
|
166
|
-
...suite,
|
|
167
|
-
appliedFixes: suiteAppliedFixes,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
return {
|
|
171
|
-
appliedFixes: suiteAppliedFixes,
|
|
172
|
-
routeManifest: finalResult.routeManifest,
|
|
173
|
-
routesFile: finalResult.routesFile,
|
|
174
|
-
suite,
|
|
175
|
-
};
|
|
176
|
-
});
|
|
177
|
-
findings.push(...project.suite.findings);
|
|
178
|
-
appliedFixes.push(...(project.appliedFixes ?? []));
|
|
179
|
-
routesFile = project.routesFile;
|
|
180
|
-
suites.push(project.suite);
|
|
181
|
-
writeDoctorSuiteDetails(reporter, project.suite);
|
|
182
|
-
progress?.writeSummaryGap();
|
|
183
|
-
if (hasWarningFindings(project.suite.findings)) {
|
|
184
|
-
let actionsSuite = createSkippedDoctorSuite('actions', 'Blocked by project warnings.');
|
|
185
|
-
suites.push(actionsSuite);
|
|
186
|
-
progress?.skip(actionsSuite.name, actionsSuite.reason);
|
|
187
|
-
writeDoctorSuiteDetails(reporter, actionsSuite);
|
|
188
|
-
progress?.writeSummaryGap();
|
|
189
|
-
return {
|
|
190
|
-
appRoot: environment.projectRoot,
|
|
191
|
-
appliedFixes,
|
|
192
|
-
findings,
|
|
193
|
-
remainingFindings: findings,
|
|
194
|
-
routesFile,
|
|
195
|
-
suites,
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
let actions = await runDoctorSuite(progress, 'actions', async () => {
|
|
199
|
-
let controllerResult = await checkControllerConventions(project.routeManifest.appRoot, project.routeManifest.tree);
|
|
200
|
-
let remainingFindings = controllerResult.suite.findings;
|
|
201
|
-
let suiteAppliedFixes = [];
|
|
202
|
-
if (options.fix && controllerResult.fixPlans.length > 0) {
|
|
203
|
-
suiteAppliedFixes = await applyDoctorFixPlans(project.routeManifest.appRoot, controllerResult.fixPlans);
|
|
204
|
-
let finalControllerResult = await checkControllerConventions(project.routeManifest.appRoot, project.routeManifest.tree);
|
|
205
|
-
remainingFindings = finalControllerResult.suite.findings;
|
|
206
|
-
}
|
|
207
|
-
let suite = createDoctorSuite('actions', remainingFindings);
|
|
208
|
-
if (suiteAppliedFixes.length > 0) {
|
|
209
|
-
suite.appliedFixes = suiteAppliedFixes;
|
|
210
|
-
}
|
|
211
|
-
return { appliedFixes: suiteAppliedFixes, suite };
|
|
212
|
-
});
|
|
213
|
-
findings.push(...actions.suite.findings);
|
|
214
|
-
appliedFixes.push(...(actions.appliedFixes ?? []));
|
|
215
|
-
suites.push(actions.suite);
|
|
216
|
-
writeDoctorSuiteDetails(reporter, actions.suite);
|
|
217
|
-
progress?.writeSummaryGap();
|
|
218
|
-
let report = {
|
|
219
|
-
appRoot: environment.projectRoot,
|
|
220
|
-
findings,
|
|
221
|
-
routesFile,
|
|
222
|
-
suites,
|
|
66
|
+
strict,
|
|
223
67
|
};
|
|
224
|
-
if (options.fix) {
|
|
225
|
-
report.appliedFixes = appliedFixes;
|
|
226
|
-
report.remainingFindings = findings;
|
|
227
|
-
}
|
|
228
|
-
return report;
|
|
229
|
-
}
|
|
230
|
-
function hasWarningFindings(findings) {
|
|
231
|
-
return findings.some((finding) => finding.severity === 'warn');
|
|
232
|
-
}
|
|
233
|
-
function formatAppliedFix(appliedFix) {
|
|
234
|
-
if (appliedFix.kind === 'update-file') {
|
|
235
|
-
return `Updated ${appliedFix.path}`;
|
|
236
|
-
}
|
|
237
|
-
return `Created ${appliedFix.path}`;
|
|
238
|
-
}
|
|
239
|
-
async function runDoctorSuite(progress, label, callback) {
|
|
240
|
-
progress?.start(label);
|
|
241
|
-
try {
|
|
242
|
-
let result = await callback();
|
|
243
|
-
if (result.suite.status === 'ok') {
|
|
244
|
-
progress?.succeed(label);
|
|
245
|
-
}
|
|
246
|
-
else if (result.suite.status === 'issues') {
|
|
247
|
-
progress?.fail(label);
|
|
248
|
-
}
|
|
249
|
-
else {
|
|
250
|
-
progress?.skip(label, result.suite.reason);
|
|
251
|
-
}
|
|
252
|
-
return result;
|
|
253
|
-
}
|
|
254
|
-
catch (error) {
|
|
255
|
-
progress?.fail(label);
|
|
256
|
-
throw error;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
function createDoctorProgressReporter(reporter) {
|
|
260
|
-
return createStepProgressReporter(reporter.status, DOCTOR_SUITE_LABELS);
|
|
261
|
-
}
|
|
262
|
-
function writeDoctorReport(reporter, report) {
|
|
263
|
-
let warningCount = report.findings.filter((finding) => finding.severity === 'warn').length;
|
|
264
|
-
let adviceCount = report.findings.length - warningCount;
|
|
265
|
-
if ((report.appliedFixes?.length ?? 0) > 0) {
|
|
266
|
-
let fixCount = report.appliedFixes.length;
|
|
267
|
-
reporter.out.line(`Applied ${fixCount} ${fixCount === 1 ? 'fix' : 'fixes'}.`);
|
|
268
|
-
}
|
|
269
|
-
if (report.findings.length === 0) {
|
|
270
|
-
reporter.out.line('Doctor found no issues.');
|
|
271
|
-
}
|
|
272
|
-
reporter.out.line(`Summary: ${warningCount} warnings, ${adviceCount} advice.`);
|
|
273
|
-
}
|
|
274
|
-
function writeDoctorSuiteDetails(reporter, suite) {
|
|
275
|
-
if (reporter == null) {
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
if (suite.findings.length === 0 && (suite.appliedFixes?.length ?? 0) === 0) {
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
reporter.out.withIndent(() => {
|
|
282
|
-
for (let finding of suite.findings) {
|
|
283
|
-
reporter.out.bullet(reporter.out.label(finding.severity.toUpperCase(), finding.message, {
|
|
284
|
-
tone: finding.severity === 'warn' ? 'warn' : undefined,
|
|
285
|
-
}));
|
|
286
|
-
}
|
|
287
|
-
if ((suite.appliedFixes?.length ?? 0) > 0) {
|
|
288
|
-
reporter.out.section('Applied fixes:', () => {
|
|
289
|
-
reporter.out.bullets((suite.appliedFixes ?? []).map(formatAppliedFix));
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
68
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../../src/lib/commands/help.ts"],"names":[],"mappings":"AAKA,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAepE;AAED,wBAAgB,cAAc,CAAC,MAAM,GAAE,MAAM,CAAC,WAA4B,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../../src/lib/commands/help.ts"],"names":[],"mappings":"AAKA,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAepE;AAED,wBAAgB,cAAc,CAAC,MAAM,GAAE,MAAM,CAAC,WAA4B,GAAG,MAAM,CAoClF;AAED,wBAAgB,sBAAsB,CAAC,MAAM,GAAE,MAAM,CAAC,WAA4B,GAAG,MAAM,CAkB1F"}
|