@remix-run/cli 0.3.4 → 0.4.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.
Files changed (118) hide show
  1. package/README.md +105 -1
  2. package/dist/index.js +1 -1
  3. package/dist/lib/app-root.d.ts +11 -0
  4. package/dist/lib/app-root.d.ts.map +1 -0
  5. package/dist/lib/app-root.js +38 -0
  6. package/dist/lib/bootstrap-project.js +2 -2
  7. package/dist/lib/cli-context.d.ts +6 -0
  8. package/dist/lib/cli-context.d.ts.map +1 -1
  9. package/dist/lib/cli-context.js +12 -1
  10. package/dist/lib/cli.d.ts +3 -2
  11. package/dist/lib/cli.d.ts.map +1 -1
  12. package/dist/lib/cli.js +42 -14
  13. package/dist/lib/commands/completion.js +4 -4
  14. package/dist/lib/commands/db.d.ts +4 -0
  15. package/dist/lib/commands/db.d.ts.map +1 -0
  16. package/dist/lib/commands/db.js +283 -0
  17. package/dist/lib/commands/doctor.d.ts +5 -0
  18. package/dist/lib/commands/doctor.d.ts.map +1 -1
  19. package/dist/lib/commands/doctor.js +26 -251
  20. package/dist/lib/commands/help.d.ts.map +1 -1
  21. package/dist/lib/commands/help.js +17 -9
  22. package/dist/lib/commands/new.js +6 -6
  23. package/dist/lib/commands/routes.js +6 -6
  24. package/dist/lib/commands/test.d.ts +20 -1
  25. package/dist/lib/commands/test.d.ts.map +1 -1
  26. package/dist/lib/commands/test.js +353 -10
  27. package/dist/lib/commands/version.js +3 -3
  28. package/dist/lib/completion.d.ts +1 -1
  29. package/dist/lib/completion.d.ts.map +1 -1
  30. package/dist/lib/completion.js +229 -6
  31. package/dist/lib/controller-files.d.ts +0 -8
  32. package/dist/lib/controller-files.d.ts.map +1 -1
  33. package/dist/lib/controller-files.js +0 -23
  34. package/dist/lib/controller-ownership.d.ts +4 -12
  35. package/dist/lib/controller-ownership.d.ts.map +1 -1
  36. package/dist/lib/controller-ownership.js +31 -127
  37. package/dist/lib/database-command.d.ts +20 -0
  38. package/dist/lib/database-command.d.ts.map +1 -0
  39. package/dist/lib/database-command.js +7 -0
  40. package/dist/lib/doctor/controller-findings.d.ts +2 -2
  41. package/dist/lib/doctor/controller-findings.d.ts.map +1 -1
  42. package/dist/lib/doctor/controller-findings.js +23 -39
  43. package/dist/lib/doctor/controllers.d.ts +1 -2
  44. package/dist/lib/doctor/controllers.d.ts.map +1 -1
  45. package/dist/lib/doctor/controllers.js +4 -7
  46. package/dist/lib/doctor/environment.js +2 -2
  47. package/dist/lib/doctor/fixes.js +1 -1
  48. package/dist/lib/doctor/project.d.ts.map +1 -1
  49. package/dist/lib/doctor/project.js +3 -212
  50. package/dist/lib/doctor/run.d.ts +9 -0
  51. package/dist/lib/doctor/run.d.ts.map +1 -0
  52. package/dist/lib/doctor/run.js +237 -0
  53. package/dist/lib/doctor/types.d.ts +2 -1
  54. package/dist/lib/doctor/types.d.ts.map +1 -1
  55. package/dist/lib/doctor/types.js +4 -1
  56. package/dist/lib/errors.d.ts +30 -0
  57. package/dist/lib/errors.d.ts.map +1 -1
  58. package/dist/lib/errors.js +55 -0
  59. package/dist/lib/help-text.js +1 -1
  60. package/dist/lib/parse-args.d.ts.map +1 -1
  61. package/dist/lib/parse-args.js +17 -4
  62. package/dist/lib/remix-config.d.ts +75 -0
  63. package/dist/lib/remix-config.d.ts.map +1 -0
  64. package/dist/lib/remix-config.js +411 -0
  65. package/dist/lib/reporter.js +1 -1
  66. package/dist/lib/route-map.js +13 -33
  67. package/package.json +13 -7
  68. package/schema/remix.json +242 -0
  69. package/src/lib/app-root.ts +46 -0
  70. package/src/lib/cli-context.ts +17 -0
  71. package/src/lib/cli.ts +41 -6
  72. package/src/lib/commands/db.ts +377 -0
  73. package/src/lib/commands/doctor.ts +27 -329
  74. package/src/lib/commands/help.ts +10 -1
  75. package/src/lib/commands/test.ts +453 -12
  76. package/src/lib/completion.ts +321 -6
  77. package/src/lib/controller-files.ts +0 -38
  78. package/src/lib/controller-ownership.ts +50 -203
  79. package/src/lib/database-command.ts +31 -0
  80. package/src/lib/doctor/controller-findings.ts +30 -47
  81. package/src/lib/doctor/controllers.ts +2 -6
  82. package/src/lib/doctor/project.ts +0 -236
  83. package/src/lib/doctor/run.ts +311 -0
  84. package/src/lib/doctor/types.ts +6 -5
  85. package/src/lib/errors.ts +65 -0
  86. package/src/lib/parse-args.ts +19 -3
  87. package/src/lib/remix-config.ts +639 -0
  88. package/src/lib/route-map.ts +11 -48
  89. package/template/.agents/skills/remix/SKILL.md +38 -27
  90. package/template/.agents/skills/remix/references/assets-and-browser-modules.md +53 -10
  91. package/template/.agents/skills/remix/references/auth-and-sessions.md +5 -5
  92. package/template/.agents/skills/remix/references/data-and-validation.md +15 -15
  93. package/template/.agents/skills/remix/references/hydration-frames-navigation.md +54 -8
  94. package/template/.agents/skills/remix/references/middleware-and-server.md +68 -6
  95. package/template/.agents/skills/remix/references/mixins-styling-events.md +2 -0
  96. package/template/.agents/skills/remix/references/routing-and-controllers.md +8 -8
  97. package/template/.agents/skills/remix/references/testing-patterns.md +19 -18
  98. package/template/AGENTS.md +6 -3
  99. package/template/README.md +6 -3
  100. package/template/app/actions/controller.tsx +1 -1
  101. package/template/app/{ui → actions}/document.tsx +7 -5
  102. package/template/app/{ui/scaffold-home-page.tsx → actions/home-page.tsx} +1 -2
  103. package/template/app/actions/public/entry.ts +48 -0
  104. package/template/app/{assets → actions/public}/prompt-button.tsx +29 -25
  105. package/template/app/assets.ts +15 -2
  106. package/template/hmr.ts +46 -0
  107. package/template/package.json +1 -0
  108. package/template/server.ts +8 -1
  109. package/template/tsconfig.json +1 -1
  110. package/dist/lib/doctor/controller-fix-plans.d.ts +0 -4
  111. package/dist/lib/doctor/controller-fix-plans.d.ts.map +0 -1
  112. package/dist/lib/doctor/controller-fix-plans.js +0 -119
  113. package/dist/lib/doctor/controller-placeholders.d.ts +0 -3
  114. package/dist/lib/doctor/controller-placeholders.d.ts.map +0 -1
  115. package/dist/lib/doctor/controller-placeholders.js +0 -52
  116. package/src/lib/doctor/controller-fix-plans.ts +0 -168
  117. package/src/lib/doctor/controller-placeholders.ts +0 -70
  118. package/template/app/assets/entry.ts +0 -17
@@ -0,0 +1,411 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { findNodeAtLocation, getNodeValue, parseTree, printParseErrorCode, } from 'jsonc-parser';
4
+ import { invalidRemixConfig, remixConfigNotFound } from './errors.js';
5
+ const reporters = ['spec', 'files', 'tap', 'dot'];
6
+ const testPools = ['forks', 'threads'];
7
+ const testTypes = ['server', 'browser', 'e2e'];
8
+ export async function loadRemixConfig(cwd, configPath) {
9
+ let filePath = path.resolve(cwd, configPath ?? 'remix.json');
10
+ let text;
11
+ try {
12
+ text = await fs.readFile(filePath, 'utf8');
13
+ }
14
+ catch (error) {
15
+ if (isNodeError(error) && error.code === 'ENOENT') {
16
+ if (configPath === undefined)
17
+ return {};
18
+ throw remixConfigNotFound(filePath);
19
+ }
20
+ throw error;
21
+ }
22
+ // Editors saving "UTF-8 with BOM" prefix the file with U+FEFF, which
23
+ // jsonc-parser reports as an InvalidSymbol error.
24
+ if (text.charCodeAt(0) === 0xfeff)
25
+ text = text.slice(1);
26
+ let parseErrors = [];
27
+ let root = parseTree(text, parseErrors, {
28
+ allowTrailingComma: true,
29
+ disallowComments: false,
30
+ });
31
+ let value = root === undefined ? undefined : getNodeValue(root);
32
+ let source = { filePath, root, text };
33
+ // An empty or comments-only file has no JSON value at all; treat it like a
34
+ // missing file rather than a malformed one.
35
+ if (root === undefined &&
36
+ parseErrors.every((error) => printParseErrorCode(error.error) === 'ValueExpected')) {
37
+ return {};
38
+ }
39
+ if (parseErrors.length > 0) {
40
+ let error = parseErrors[0];
41
+ throwConfigError(source, [], printParseErrorCode(error.error), error.offset);
42
+ }
43
+ return parseConfig(value, source, path.dirname(filePath), cwd);
44
+ }
45
+ function parseConfig(value, source, configDir, cwd) {
46
+ let object = requireObject(value, source, []);
47
+ requireKnownProperties(object, ['$schema', 'db', 'doctor', 'test'], source, []);
48
+ if (object.$schema !== undefined) {
49
+ requireString(object.$schema, source, ['$schema']);
50
+ }
51
+ let config = {};
52
+ if (object.db !== undefined) {
53
+ config.db = parseDbConfig(object.db, source, configDir);
54
+ }
55
+ if (object.doctor !== undefined) {
56
+ config.doctor = parseDoctorConfig(object.doctor, source);
57
+ }
58
+ if (object.test !== undefined) {
59
+ config.test = parseTestConfig(object.test, source, configDir, cwd);
60
+ }
61
+ return config;
62
+ }
63
+ function parseDbConfig(value, source, configDir) {
64
+ let objectPath = ['db'];
65
+ let object = requireObject(value, source, objectPath);
66
+ requireKnownProperties(object, ['adapter', 'migrations', 'seed'], source, objectPath);
67
+ if (object.adapter === undefined) {
68
+ throwConfigError(source, [...objectPath, 'adapter'], 'Expected an adapter configuration');
69
+ }
70
+ let config = {
71
+ adapter: parseDbAdapterConfig(object.adapter, source),
72
+ };
73
+ if (object.migrations !== undefined) {
74
+ let migrationsPath = [...objectPath, 'migrations'];
75
+ let migrations = requireObject(object.migrations, source, migrationsPath);
76
+ requireKnownProperties(migrations, ['directory', 'journalTable'], source, migrationsPath);
77
+ let directory = requireString(migrations.directory, source, [...migrationsPath, 'directory']);
78
+ let journalTable = optionalString(migrations.journalTable, source, [
79
+ ...migrationsPath,
80
+ 'journalTable',
81
+ ]);
82
+ config.migrations = {
83
+ directory: path.resolve(configDir, directory),
84
+ journalTable,
85
+ };
86
+ }
87
+ if (object.seed !== undefined) {
88
+ let seed = requireString(object.seed, source, [...objectPath, 'seed']);
89
+ config.seed = path.resolve(configDir, seed);
90
+ }
91
+ return config;
92
+ }
93
+ function parseDbAdapterConfig(value, source) {
94
+ let objectPath = ['db', 'adapter'];
95
+ let object = requireObject(value, source, objectPath);
96
+ let type = requireEnum(object.type, ['sqlite', 'postgres', 'mysql'], source, [
97
+ ...objectPath,
98
+ 'type',
99
+ ]);
100
+ if (type === 'sqlite') {
101
+ requireKnownProperties(object, ['busyTimeout', 'filename', 'foreignKeys', 'type'], source, objectPath);
102
+ let busyTimeout = optionalNumber(object.busyTimeout, source, [...objectPath, 'busyTimeout']);
103
+ if (busyTimeout !== undefined && busyTimeout < 0) {
104
+ throwConfigError(source, [...objectPath, 'busyTimeout'], 'Expected a non-negative number');
105
+ }
106
+ return {
107
+ type,
108
+ filename: parseDbString(object.filename, source, [...objectPath, 'filename']),
109
+ foreignKeys: optionalBoolean(object.foreignKeys, source, [...objectPath, 'foreignKeys']),
110
+ busyTimeout,
111
+ };
112
+ }
113
+ if (type === 'postgres') {
114
+ requireKnownProperties(object, ['connectionString', 'maintenanceDatabase', 'template', 'type'], source, objectPath);
115
+ return {
116
+ type,
117
+ connectionString: parseDbString(object.connectionString, source, [
118
+ ...objectPath,
119
+ 'connectionString',
120
+ ]),
121
+ maintenanceDatabase: optionalString(object.maintenanceDatabase, source, [
122
+ ...objectPath,
123
+ 'maintenanceDatabase',
124
+ ]),
125
+ template: optionalString(object.template, source, [...objectPath, 'template']),
126
+ };
127
+ }
128
+ requireKnownProperties(object, ['characterSet', 'collation', 'type', 'uri'], source, objectPath);
129
+ return {
130
+ type,
131
+ uri: parseDbString(object.uri, source, [...objectPath, 'uri']),
132
+ characterSet: optionalString(object.characterSet, source, [...objectPath, 'characterSet']),
133
+ collation: optionalString(object.collation, source, [...objectPath, 'collation']),
134
+ };
135
+ }
136
+ function parseDbString(value, source, propertyPath) {
137
+ if (typeof value === 'string')
138
+ return value;
139
+ let object = requireObject(value, source, propertyPath);
140
+ requireKnownProperties(object, ['default', 'env'], source, propertyPath);
141
+ let env = requireString(object.env, source, [...propertyPath, 'env']);
142
+ let defaultValue = optionalString(object.default, source, [...propertyPath, 'default']);
143
+ return defaultValue === undefined ? { env } : { env, default: defaultValue };
144
+ }
145
+ function parseDoctorConfig(value, source) {
146
+ let objectPath = ['doctor'];
147
+ let object = requireObject(value, source, objectPath);
148
+ requireKnownProperties(object, ['strict'], source, objectPath);
149
+ let config = {};
150
+ let strict = optionalBoolean(object.strict, source, [...objectPath, 'strict']);
151
+ if (strict !== undefined)
152
+ config.strict = strict;
153
+ return config;
154
+ }
155
+ function parseTestConfig(value, source, configDir, cwd) {
156
+ let objectPath = ['test'];
157
+ let object = requireObject(value, source, objectPath);
158
+ requireKnownProperties(object, [
159
+ 'browserFiles',
160
+ 'concurrency',
161
+ 'coverage',
162
+ 'e2eFiles',
163
+ 'exclude',
164
+ 'files',
165
+ 'only',
166
+ 'playwright',
167
+ 'pool',
168
+ 'quiet',
169
+ 'reporter',
170
+ 'setup',
171
+ 'type',
172
+ 'watch',
173
+ ], source, objectPath);
174
+ let config = {};
175
+ let browserFiles = optionalStringArray(object.browserFiles, source, [
176
+ ...objectPath,
177
+ 'browserFiles',
178
+ ]);
179
+ let e2eFiles = optionalStringArray(object.e2eFiles, source, [...objectPath, 'e2eFiles']);
180
+ let exclude = optionalStringArray(object.exclude, source, [...objectPath, 'exclude']);
181
+ let files = optionalStringArray(object.files, source, [...objectPath, 'files']);
182
+ let only = optionalStringArray(object.only, source, [...objectPath, 'only']);
183
+ let concurrency = optionalNumber(object.concurrency, source, [...objectPath, 'concurrency']);
184
+ let setup = optionalString(object.setup, source, [...objectPath, 'setup']);
185
+ if (browserFiles !== undefined)
186
+ config.browserFiles = resolveGlobs(browserFiles, configDir, cwd);
187
+ if (e2eFiles !== undefined)
188
+ config.e2eFiles = resolveGlobs(e2eFiles, configDir, cwd);
189
+ if (exclude !== undefined)
190
+ config.exclude = resolveGlobs(exclude, configDir, cwd);
191
+ if (files !== undefined)
192
+ config.files = resolveGlobs(files, configDir, cwd);
193
+ if (only !== undefined) {
194
+ for (let [index, pattern] of only.entries()) {
195
+ validateOnlyPattern(pattern, source, [...objectPath, 'only', index]);
196
+ }
197
+ config.only = only;
198
+ }
199
+ if (concurrency !== undefined) {
200
+ if (!Number.isInteger(concurrency) || concurrency < 1) {
201
+ throwConfigError(source, [...objectPath, 'concurrency'], 'Expected a positive integer');
202
+ }
203
+ config.concurrency = concurrency;
204
+ }
205
+ let pool = optionalEnum(object.pool, testPools, source, [...objectPath, 'pool']);
206
+ if (pool !== undefined)
207
+ config.pool = pool;
208
+ let quiet = optionalBoolean(object.quiet, source, [...objectPath, 'quiet']);
209
+ if (quiet !== undefined)
210
+ config.quiet = quiet;
211
+ let reporter = optionalEnum(object.reporter, reporters, source, [...objectPath, 'reporter']);
212
+ if (reporter !== undefined)
213
+ config.reporter = reporter;
214
+ if (setup !== undefined)
215
+ config.setup = path.resolve(configDir, setup);
216
+ let type = optionalEnumArray(object.type, testTypes, source, [...objectPath, 'type']);
217
+ if (type !== undefined)
218
+ config.type = type;
219
+ let watch = optionalBoolean(object.watch, source, [...objectPath, 'watch']);
220
+ if (watch !== undefined)
221
+ config.watch = watch;
222
+ if (object.coverage !== undefined) {
223
+ config.coverage = parseCoverageConfig(object.coverage, source, configDir, cwd);
224
+ }
225
+ if (object.playwright !== undefined) {
226
+ config.playwright = parsePlaywrightConfig(object.playwright, source, configDir);
227
+ }
228
+ return config;
229
+ }
230
+ function parseCoverageConfig(value, source, configDir, cwd) {
231
+ let objectPath = ['test', 'coverage'];
232
+ let object = requireObject(value, source, objectPath);
233
+ requireKnownProperties(object, ['branches', 'dir', 'enabled', 'exclude', 'functions', 'include', 'lines', 'statements'], source, objectPath);
234
+ let config = {};
235
+ let branches = optionalNumber(object.branches, source, [...objectPath, 'branches']);
236
+ let dir = optionalString(object.dir, source, [...objectPath, 'dir']);
237
+ let enabled = optionalBoolean(object.enabled, source, [...objectPath, 'enabled']);
238
+ let exclude = optionalStringArray(object.exclude, source, [...objectPath, 'exclude']);
239
+ let functions = optionalNumber(object.functions, source, [...objectPath, 'functions']);
240
+ let include = optionalStringArray(object.include, source, [...objectPath, 'include']);
241
+ let lines = optionalNumber(object.lines, source, [...objectPath, 'lines']);
242
+ let statements = optionalNumber(object.statements, source, [...objectPath, 'statements']);
243
+ if (branches !== undefined)
244
+ config.branches = branches;
245
+ if (dir !== undefined)
246
+ config.dir = path.resolve(configDir, dir);
247
+ if (enabled !== undefined)
248
+ config.enabled = enabled;
249
+ if (exclude !== undefined)
250
+ config.exclude = resolveGlobs(exclude, configDir, cwd);
251
+ if (functions !== undefined)
252
+ config.functions = functions;
253
+ if (include !== undefined)
254
+ config.include = resolveGlobs(include, configDir, cwd);
255
+ if (lines !== undefined)
256
+ config.lines = lines;
257
+ if (statements !== undefined)
258
+ config.statements = statements;
259
+ return config;
260
+ }
261
+ function parsePlaywrightConfig(value, source, configDir) {
262
+ let objectPath = ['test', 'playwright'];
263
+ let object = requireObject(value, source, objectPath);
264
+ requireKnownProperties(object, ['configFile', 'echo', 'open', 'projects'], source, objectPath);
265
+ let config = {};
266
+ let configFile = optionalString(object.configFile, source, [...objectPath, 'configFile']);
267
+ let echo = optionalBoolean(object.echo, source, [...objectPath, 'echo']);
268
+ let open = optionalBoolean(object.open, source, [...objectPath, 'open']);
269
+ let projects = optionalStringArray(object.projects, source, [...objectPath, 'projects']);
270
+ if (configFile !== undefined)
271
+ config.configFile = path.resolve(configDir, configFile);
272
+ if (echo !== undefined)
273
+ config.echo = echo;
274
+ if (open !== undefined)
275
+ config.open = open;
276
+ if (projects !== undefined)
277
+ config.projects = projects;
278
+ return config;
279
+ }
280
+ function requireObject(value, source, propertyPath) {
281
+ if (!isRecord(value)) {
282
+ throwConfigError(source, propertyPath, 'Expected an object');
283
+ }
284
+ return value;
285
+ }
286
+ function requireKnownProperties(object, knownProperties, source, propertyPath) {
287
+ for (let property of Object.keys(object)) {
288
+ if (!knownProperties.includes(property)) {
289
+ throwConfigError(source, [...propertyPath, property], `Unknown property "${property}"`);
290
+ }
291
+ }
292
+ }
293
+ function optionalBoolean(value, source, propertyPath) {
294
+ if (value === undefined)
295
+ return undefined;
296
+ if (typeof value !== 'boolean')
297
+ throwConfigError(source, propertyPath, 'Expected a boolean');
298
+ return value;
299
+ }
300
+ function optionalNumber(value, source, propertyPath) {
301
+ if (value === undefined)
302
+ return undefined;
303
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
304
+ throwConfigError(source, propertyPath, 'Expected a number');
305
+ }
306
+ return value;
307
+ }
308
+ function optionalString(value, source, propertyPath) {
309
+ if (value === undefined)
310
+ return undefined;
311
+ return requireString(value, source, propertyPath);
312
+ }
313
+ function requireString(value, source, propertyPath) {
314
+ if (typeof value !== 'string')
315
+ throwConfigError(source, propertyPath, 'Expected a string');
316
+ return value;
317
+ }
318
+ function optionalStringArray(value, source, propertyPath) {
319
+ if (value === undefined)
320
+ return undefined;
321
+ if (!Array.isArray(value))
322
+ throwConfigError(source, propertyPath, 'Expected an array of strings');
323
+ return value.map((item, index) => requireString(item, source, [...propertyPath, index]));
324
+ }
325
+ function optionalEnum(input, values, source, propertyPath) {
326
+ if (input === undefined)
327
+ return undefined;
328
+ return requireEnum(input, values, source, propertyPath);
329
+ }
330
+ function optionalEnumArray(input, values, source, propertyPath) {
331
+ if (input === undefined)
332
+ return undefined;
333
+ if (!Array.isArray(input))
334
+ throwConfigError(source, propertyPath, 'Expected an array');
335
+ return input.map((item, index) => requireEnum(item, values, source, [...propertyPath, index]));
336
+ }
337
+ function requireEnum(input, values, source, propertyPath) {
338
+ let string = requireString(input, source, propertyPath);
339
+ let matched = values.find((value) => value === string);
340
+ if (matched === undefined) {
341
+ throwConfigError(source, propertyPath, `Expected one of: ${values.join(', ')}`);
342
+ }
343
+ return matched;
344
+ }
345
+ function resolveGlobs(values, configDir, cwd) {
346
+ return values.map((value) => toPosix(path.relative(cwd, path.resolve(configDir, value))));
347
+ }
348
+ function toPosix(value) {
349
+ return path.sep === '/' ? value : value.replaceAll(path.sep, '/');
350
+ }
351
+ function validateOnlyPattern(pattern, source, propertyPath) {
352
+ let regex = parseRegexLiteral(pattern) ?? { flags: 'i', source: pattern };
353
+ try {
354
+ new RegExp(regex.source, regex.flags);
355
+ }
356
+ catch (error) {
357
+ let reason = error instanceof Error ? error.message : String(error);
358
+ throwConfigError(source, propertyPath, `Expected a valid JavaScript regular expression: ${reason}`);
359
+ }
360
+ }
361
+ function parseRegexLiteral(pattern) {
362
+ if (!pattern.startsWith('/') || pattern.length < 2)
363
+ return undefined;
364
+ // The closing delimiter is the last unescaped slash; escape state must be
365
+ // tracked left-to-right since it depends on the preceding backslashes.
366
+ let closingIndex = -1;
367
+ let escaped = false;
368
+ for (let index = 1; index < pattern.length; index++) {
369
+ if (escaped) {
370
+ escaped = false;
371
+ }
372
+ else if (pattern[index] === '\\') {
373
+ escaped = true;
374
+ }
375
+ else if (pattern[index] === '/') {
376
+ closingIndex = index;
377
+ }
378
+ }
379
+ if (closingIndex === -1)
380
+ return undefined;
381
+ return {
382
+ flags: pattern.slice(closingIndex + 1),
383
+ source: pattern.slice(1, closingIndex),
384
+ };
385
+ }
386
+ function throwConfigError(source, propertyPath, details, offset) {
387
+ let node = source.root == null ? undefined : findNodeAtLocation(source.root, propertyPath);
388
+ let location = getLineAndColumn(source.text, offset ?? node?.offset ?? 0);
389
+ let label = propertyPath.length === 0 ? '' : ` at ${propertyPath.join('.')}`;
390
+ throw invalidRemixConfig(source.filePath, `${details}${label}`, location.line, location.column);
391
+ }
392
+ function getLineAndColumn(text, offset) {
393
+ let line = 1;
394
+ let column = 1;
395
+ for (let index = 0; index < offset; index++) {
396
+ if (text[index] === '\n') {
397
+ line++;
398
+ column = 1;
399
+ }
400
+ else {
401
+ column++;
402
+ }
403
+ }
404
+ return { column, line };
405
+ }
406
+ function isNodeError(error) {
407
+ return error instanceof Error && 'code' in error;
408
+ }
409
+ function isRecord(value) {
410
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
411
+ }
@@ -1,5 +1,5 @@
1
1
  import * as process from 'node:process';
2
- import { bold, canUseAnsi, clearCurrentLine, lightGray, lightGreen, lightRed, lightYellow, remixWordmark, } from "./terminal.js";
2
+ import { bold, canUseAnsi, clearCurrentLine, lightGray, lightGreen, lightRed, lightYellow, remixWordmark, } from './terminal.js';
3
3
  const BULLET = '•';
4
4
  const DOT_FRAMES = ['.', '..', '...'];
5
5
  const DEFAULT_INDENT = ' ';
@@ -1,10 +1,10 @@
1
- import * as fs from 'node:fs/promises';
2
1
  import * as path from 'node:path';
3
2
  import * as process from 'node:process';
4
3
  import { spawn } from 'node:child_process';
5
4
  import { fileURLToPath } from 'node:url';
6
- import { ROOT_ROUTE_NAME, inspectControllerOwnership, } from "./controller-ownership.js";
7
- import { routeMapLoaderFailed, routeMapLoaderInvalidJson, routeMapLoaderSignal, routeOwnerPlanUnresolved, routesFileNotFound, } from "./errors.js";
5
+ import { findAppRoot } from './app-root.js';
6
+ import { ROOT_ROUTE_NAME, inspectControllerOwnership, } from './controller-ownership.js';
7
+ import { routeMapLoaderFailed, routeMapLoaderInvalidJson, routeMapLoaderSignal, routeOwnerPlanUnresolved, routesFileNotFound, } from './errors.js';
8
8
  export async function loadRouteMap(cwd = process.cwd()) {
9
9
  let manifest = await loadRouteManifest(cwd);
10
10
  let ownership = await inspectControllerOwnership(manifest.appRoot, manifest.tree);
@@ -73,15 +73,15 @@ async function loadRawRouteMap(appRoot, routesFile) {
73
73
  function decorateRouteTree(rawTree, ownership) {
74
74
  let subtreesByRouteName = new Map(ownership.subtrees.map((subtree) => [subtree.routeName, subtree]));
75
75
  let directoriesByRouteName = new Map(ownership.routeDirectories.map((directory) => [directory.routeName, directory]));
76
- return decorateRouteTreeWithLookup(rawTree, subtreesByRouteName, directoriesByRouteName, ownership.scan.routeDirectoryPaths);
76
+ return decorateRouteTreeWithLookup(rawTree, subtreesByRouteName, directoriesByRouteName);
77
77
  }
78
- function decorateRouteTreeWithLookup(rawTree, subtreesByRouteName, directoriesByRouteName, actualRouteDirectories, parentSegments = []) {
78
+ function decorateRouteTreeWithLookup(rawTree, subtreesByRouteName, directoriesByRouteName, parentSegments = []) {
79
79
  return rawTree.map((rawNode) => {
80
- let owner = getRouteOwner(rawNode, parentSegments, subtreesByRouteName, directoriesByRouteName, actualRouteDirectories);
80
+ let owner = getRouteOwner(rawNode, parentSegments, subtreesByRouteName, directoriesByRouteName);
81
81
  let nextParentSegments = rawNode.kind === 'group' ? [...parentSegments, rawNode.key] : parentSegments;
82
82
  return {
83
83
  children: rawNode.kind === 'group'
84
- ? decorateRouteTreeWithLookup(rawNode.children, subtreesByRouteName, directoriesByRouteName, actualRouteDirectories, nextParentSegments)
84
+ ? decorateRouteTreeWithLookup(rawNode.children, subtreesByRouteName, directoriesByRouteName, nextParentSegments)
85
85
  : [],
86
86
  key: rawNode.key,
87
87
  kind: rawNode.kind,
@@ -92,7 +92,7 @@ function decorateRouteTreeWithLookup(rawTree, subtreesByRouteName, directoriesBy
92
92
  };
93
93
  });
94
94
  }
95
- function getRouteOwner(rawNode, parentSegments, subtreesByRouteName, directoriesByRouteName, actualRouteDirectories) {
95
+ function getRouteOwner(rawNode, parentSegments, subtreesByRouteName, directoriesByRouteName) {
96
96
  let ownerRouteName = rawNode.kind === 'group'
97
97
  ? rawNode.name
98
98
  : parentSegments.length === 0
@@ -112,7 +112,7 @@ function getRouteOwner(rawNode, parentSegments, subtreesByRouteName, directories
112
112
  throw routeOwnerPlanUnresolved(rawNode.name);
113
113
  }
114
114
  return {
115
- exists: actualRouteDirectories.has(directory.directoryPath),
115
+ exists: directory.exists,
116
116
  kind: 'directory',
117
117
  path: directory.directoryPath,
118
118
  };
@@ -165,31 +165,11 @@ function assertRawRouteTreeNode(value) {
165
165
  };
166
166
  }
167
167
  async function findRemixAppRoot(startDir) {
168
- let currentDir = path.resolve(startDir);
169
- while (true) {
170
- if (await pathExists(path.join(currentDir, 'app', 'routes.ts'))) {
171
- return currentDir;
172
- }
173
- let parentDir = path.dirname(currentDir);
174
- if (parentDir === currentDir) {
175
- break;
176
- }
177
- currentDir = parentDir;
178
- }
179
- throw routesFileNotFound(startDir);
180
- }
181
- async function pathExists(filePath) {
182
- try {
183
- await fs.access(filePath);
184
- return true;
185
- }
186
- catch (error) {
187
- let nodeError = error;
188
- if (nodeError.code === 'ENOENT') {
189
- return false;
190
- }
191
- throw error;
168
+ let appRoot = await findAppRoot(startDir, 'app/routes.ts');
169
+ if (appRoot == null) {
170
+ throw routesFileNotFound(startDir);
192
171
  }
172
+ return appRoot;
193
173
  }
194
174
  function getRouteMapWorkerPath() {
195
175
  let currentFilePath = fileURLToPath(import.meta.url);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remix-run/cli",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "Command-line interface for Remix",
5
5
  "author": "Michael Jackson <mjijackson@gmail.com>",
6
6
  "license": "MIT",
@@ -14,6 +14,7 @@
14
14
  "LICENSE",
15
15
  "README.md",
16
16
  "dist",
17
+ "schema",
17
18
  "src",
18
19
  "template",
19
20
  "!src/**/*.test.ts"
@@ -30,14 +31,19 @@
30
31
  "./package.json": "./package.json"
31
32
  },
32
33
  "dependencies": {
34
+ "jsonc-parser": "^3.3.1",
33
35
  "semver": "^7.7.4",
34
- "@remix-run/test": "^0.5.0",
35
- "@remix-run/terminal": "^0.1.1"
36
+ "@remix-run/data-table": "^0.4.0",
37
+ "@remix-run/data-table-mysql": "^0.5.0",
38
+ "@remix-run/data-table-sqlite": "^0.6.0",
39
+ "@remix-run/terminal": "^0.1.1",
40
+ "@remix-run/data-table-postgres": "^0.5.0",
41
+ "@remix-run/test": "^0.6.0"
36
42
  },
37
43
  "devDependencies": {
38
44
  "@types/node": "^24.6.0",
39
45
  "@types/semver": "^7.5.8",
40
- "@typescript/native-preview": "7.0.0-dev.20251125.1",
46
+ "typescript": "^7.0.2",
41
47
  "@remix-run/assert": "^0.3.0"
42
48
  },
43
49
  "keywords": [
@@ -48,9 +54,9 @@
48
54
  "command-line"
49
55
  ],
50
56
  "scripts": {
51
- "build": "tsgo -p tsconfig.build.json",
57
+ "build": "tsc -p tsconfig.build.json",
52
58
  "clean": "git clean -fdX",
53
- "test": "remix-test --concurrency 1",
54
- "typecheck": "tsgo --noEmit"
59
+ "test": "remix test --concurrency 1",
60
+ "typecheck": "tsc --noEmit"
55
61
  }
56
62
  }