@remix-run/cli 0.0.0 → 0.1.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 (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -3
  3. package/bootstrap/AGENTS.md +35 -0
  4. package/bootstrap/README.md +27 -0
  5. package/bootstrap/app/controllers/auth.tsx +21 -0
  6. package/bootstrap/app/controllers/home.tsx +26 -0
  7. package/bootstrap/app/router.ts +10 -0
  8. package/bootstrap/app/routes.ts +6 -0
  9. package/bootstrap/app/ui/document.tsx +21 -0
  10. package/bootstrap/app/ui/layout.tsx +22 -0
  11. package/bootstrap/app/utils/render.tsx +7 -0
  12. package/bootstrap/package.json +22 -0
  13. package/bootstrap/server.ts +37 -0
  14. package/bootstrap/tsconfig.json +19 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/lib/bootstrap-project.d.ts +17 -0
  19. package/dist/lib/bootstrap-project.d.ts.map +1 -0
  20. package/dist/lib/bootstrap-project.js +137 -0
  21. package/dist/lib/cli-context.d.ts +11 -0
  22. package/dist/lib/cli-context.d.ts.map +1 -0
  23. package/dist/lib/cli-context.js +19 -0
  24. package/dist/lib/cli.d.ts +6 -0
  25. package/dist/lib/cli.d.ts.map +1 -0
  26. package/dist/lib/cli.js +87 -0
  27. package/dist/lib/commands/completion.d.ts +3 -0
  28. package/dist/lib/commands/completion.d.ts.map +1 -0
  29. package/dist/lib/commands/completion.js +51 -0
  30. package/dist/lib/commands/doctor.d.ts +4 -0
  31. package/dist/lib/commands/doctor.d.ts.map +1 -0
  32. package/dist/lib/commands/doctor.js +293 -0
  33. package/dist/lib/commands/help.d.ts +4 -0
  34. package/dist/lib/commands/help.d.ts.map +1 -0
  35. package/dist/lib/commands/help.js +124 -0
  36. package/dist/lib/commands/new.d.ts +4 -0
  37. package/dist/lib/commands/new.d.ts.map +1 -0
  38. package/dist/lib/commands/new.js +70 -0
  39. package/dist/lib/commands/routes.d.ts +4 -0
  40. package/dist/lib/commands/routes.d.ts.map +1 -0
  41. package/dist/lib/commands/routes.js +161 -0
  42. package/dist/lib/commands/skills.d.ts +6 -0
  43. package/dist/lib/commands/skills.d.ts.map +1 -0
  44. package/dist/lib/commands/skills.js +222 -0
  45. package/dist/lib/commands/test.d.ts +4 -0
  46. package/dist/lib/commands/test.d.ts.map +1 -0
  47. package/dist/lib/commands/test.js +19 -0
  48. package/dist/lib/commands/version.d.ts +4 -0
  49. package/dist/lib/commands/version.d.ts.map +1 -0
  50. package/dist/lib/commands/version.js +29 -0
  51. package/dist/lib/completion.d.ts +12 -0
  52. package/dist/lib/completion.d.ts.map +1 -0
  53. package/dist/lib/completion.js +408 -0
  54. package/dist/lib/contained-path.d.ts +2 -0
  55. package/dist/lib/contained-path.d.ts.map +1 -0
  56. package/dist/lib/contained-path.js +10 -0
  57. package/dist/lib/controller-files.d.ts +14 -0
  58. package/dist/lib/controller-files.d.ts.map +1 -0
  59. package/dist/lib/controller-files.js +53 -0
  60. package/dist/lib/controller-ownership.d.ts +41 -0
  61. package/dist/lib/controller-ownership.d.ts.map +1 -0
  62. package/dist/lib/controller-ownership.js +224 -0
  63. package/dist/lib/display-path.d.ts +2 -0
  64. package/dist/lib/display-path.d.ts.map +1 -0
  65. package/dist/lib/display-path.js +12 -0
  66. package/dist/lib/doctor/controller-findings.d.ts +4 -0
  67. package/dist/lib/doctor/controller-findings.d.ts.map +1 -0
  68. package/dist/lib/doctor/controller-findings.js +132 -0
  69. package/dist/lib/doctor/controller-fix-plans.d.ts +4 -0
  70. package/dist/lib/doctor/controller-fix-plans.d.ts.map +1 -0
  71. package/dist/lib/doctor/controller-fix-plans.js +130 -0
  72. package/dist/lib/doctor/controller-placeholders.d.ts +4 -0
  73. package/dist/lib/doctor/controller-placeholders.d.ts.map +1 -0
  74. package/dist/lib/doctor/controller-placeholders.js +183 -0
  75. package/dist/lib/doctor/controllers.d.ts +8 -0
  76. package/dist/lib/doctor/controllers.d.ts.map +1 -0
  77. package/dist/lib/doctor/controllers.js +13 -0
  78. package/dist/lib/doctor/environment.d.ts +17 -0
  79. package/dist/lib/doctor/environment.d.ts.map +1 -0
  80. package/dist/lib/doctor/environment.js +226 -0
  81. package/dist/lib/doctor/fixes.d.ts +3 -0
  82. package/dist/lib/doctor/fixes.d.ts.map +1 -0
  83. package/dist/lib/doctor/fixes.js +65 -0
  84. package/dist/lib/doctor/project.d.ts +10 -0
  85. package/dist/lib/doctor/project.d.ts.map +1 -0
  86. package/dist/lib/doctor/project.js +371 -0
  87. package/dist/lib/doctor/types.d.ts +48 -0
  88. package/dist/lib/doctor/types.d.ts.map +1 -0
  89. package/dist/lib/doctor/types.js +15 -0
  90. package/dist/lib/errors.d.ts +173 -0
  91. package/dist/lib/errors.d.ts.map +1 -0
  92. package/dist/lib/errors.js +318 -0
  93. package/dist/lib/help-text.d.ts +13 -0
  94. package/dist/lib/help-text.d.ts.map +1 -0
  95. package/dist/lib/help-text.js +43 -0
  96. package/dist/lib/load-route-map-worker.d.ts +2 -0
  97. package/dist/lib/load-route-map-worker.d.ts.map +1 -0
  98. package/dist/lib/load-route-map-worker.js +92 -0
  99. package/dist/lib/parse-args.d.ts +22 -0
  100. package/dist/lib/parse-args.d.ts.map +1 -0
  101. package/dist/lib/parse-args.js +35 -0
  102. package/dist/lib/remix-version.d.ts +2 -0
  103. package/dist/lib/remix-version.d.ts.map +1 -0
  104. package/dist/lib/remix-version.js +58 -0
  105. package/dist/lib/reporter.d.ts +56 -0
  106. package/dist/lib/reporter.d.ts.map +1 -0
  107. package/dist/lib/reporter.js +357 -0
  108. package/dist/lib/route-map.d.ts +38 -0
  109. package/dist/lib/route-map.d.ts.map +1 -0
  110. package/dist/lib/route-map.js +195 -0
  111. package/dist/lib/skills-cache.d.ts +19 -0
  112. package/dist/lib/skills-cache.d.ts.map +1 -0
  113. package/dist/lib/skills-cache.js +89 -0
  114. package/dist/lib/skills.d.ts +30 -0
  115. package/dist/lib/skills.d.ts.map +1 -0
  116. package/dist/lib/skills.js +441 -0
  117. package/dist/lib/terminal.d.ts +17 -0
  118. package/dist/lib/terminal.d.ts.map +1 -0
  119. package/dist/lib/terminal.js +66 -0
  120. package/package.json +48 -5
  121. package/src/index.ts +1 -0
  122. package/src/lib/bootstrap-project.ts +222 -0
  123. package/src/lib/cli-context.ts +38 -0
  124. package/src/lib/cli.ts +120 -0
  125. package/src/lib/commands/completion.ts +74 -0
  126. package/src/lib/commands/doctor.ts +393 -0
  127. package/src/lib/commands/help.ts +157 -0
  128. package/src/lib/commands/new.ts +104 -0
  129. package/src/lib/commands/routes.ts +251 -0
  130. package/src/lib/commands/skills.ts +306 -0
  131. package/src/lib/commands/test.ts +25 -0
  132. package/src/lib/commands/version.ts +39 -0
  133. package/src/lib/completion.ts +561 -0
  134. package/src/lib/contained-path.ts +13 -0
  135. package/src/lib/controller-files.ts +79 -0
  136. package/src/lib/controller-ownership.ts +397 -0
  137. package/src/lib/display-path.ts +16 -0
  138. package/src/lib/doctor/controller-findings.ts +157 -0
  139. package/src/lib/doctor/controller-fix-plans.ts +184 -0
  140. package/src/lib/doctor/controller-placeholders.ts +242 -0
  141. package/src/lib/doctor/controllers.ts +23 -0
  142. package/src/lib/doctor/environment.ts +289 -0
  143. package/src/lib/doctor/fixes.ts +84 -0
  144. package/src/lib/doctor/project.ts +435 -0
  145. package/src/lib/doctor/types.ts +95 -0
  146. package/src/lib/errors.ts +379 -0
  147. package/src/lib/help-text.ts +88 -0
  148. package/src/lib/load-route-map-worker.ts +110 -0
  149. package/src/lib/parse-args.ts +82 -0
  150. package/src/lib/remix-version.ts +80 -0
  151. package/src/lib/reporter.ts +535 -0
  152. package/src/lib/route-map.ts +303 -0
  153. package/src/lib/skills-cache.ts +140 -0
  154. package/src/lib/skills.ts +706 -0
  155. package/src/lib/terminal.ts +86 -0
@@ -0,0 +1,183 @@
1
+ import * as path from 'node:path';
2
+ import { getOwnerFileExtension } from "../controller-files.js";
3
+ const RESERVED_BINDING_IDENTIFIERS = new Set([
4
+ 'await',
5
+ 'break',
6
+ 'case',
7
+ 'catch',
8
+ 'class',
9
+ 'const',
10
+ 'continue',
11
+ 'debugger',
12
+ 'default',
13
+ 'delete',
14
+ 'do',
15
+ 'else',
16
+ 'enum',
17
+ 'export',
18
+ 'extends',
19
+ 'false',
20
+ 'finally',
21
+ 'for',
22
+ 'function',
23
+ 'if',
24
+ 'implements',
25
+ 'import',
26
+ 'in',
27
+ 'instanceof',
28
+ 'interface',
29
+ 'let',
30
+ 'new',
31
+ 'null',
32
+ 'package',
33
+ 'private',
34
+ 'protected',
35
+ 'public',
36
+ 'return',
37
+ 'static',
38
+ 'super',
39
+ 'switch',
40
+ 'this',
41
+ 'throw',
42
+ 'true',
43
+ 'try',
44
+ 'typeof',
45
+ 'var',
46
+ 'void',
47
+ 'while',
48
+ 'with',
49
+ 'yield',
50
+ ]);
51
+ export function renderActionPlaceholder(routeNode, entryPath) {
52
+ let routeMessage = `TODO: implement routes.${routeNode.name}`;
53
+ let extension = getOwnerFileExtension(entryPath);
54
+ let exportName = getActionExportName(routeNode.key);
55
+ if (extension === '.js' || extension === '.jsx') {
56
+ return [
57
+ `export const ${exportName} = {`,
58
+ ` handler() {`,
59
+ ` return new Response(${JSON.stringify(routeMessage)})`,
60
+ ` },`,
61
+ `}`,
62
+ '',
63
+ ].join('\n');
64
+ }
65
+ let routeExpression = getRouteAccessExpression(routeNode.name);
66
+ let routesImportPath = getRelativeImportPath(entryPath, 'app/routes.ts');
67
+ let method = routeNode.method ?? 'ANY';
68
+ return [
69
+ `import type { BuildAction } from 'remix/fetch-router'`,
70
+ '',
71
+ `import type { routes } from '${routesImportPath}'`,
72
+ '',
73
+ `export const ${exportName}: BuildAction<'${method}', typeof ${routeExpression}> = {`,
74
+ ` handler() {`,
75
+ ` return new Response(${JSON.stringify(routeMessage)})`,
76
+ ` },`,
77
+ `}`,
78
+ '',
79
+ ].join('\n');
80
+ }
81
+ export function renderControllerPlaceholder(routeNode, entryPath, resolvedEntryPathByRouteName) {
82
+ let extension = getOwnerFileExtension(entryPath);
83
+ let lines = [];
84
+ let childImports = getControllerChildImports(routeNode, entryPath, resolvedEntryPathByRouteName);
85
+ let actionEntries = routeNode.children.map((childNode) => renderControllerActionEntry(childNode, entryPath, resolvedEntryPathByRouteName));
86
+ if (extension === '.js' || extension === '.jsx') {
87
+ if (childImports.length > 0) {
88
+ lines.push(...childImports, '');
89
+ }
90
+ lines.push('export default {', ' actions: {');
91
+ lines.push(...actionEntries);
92
+ lines.push(' },', '}', '');
93
+ return lines.join('\n');
94
+ }
95
+ let routeExpression = getRouteAccessExpression(routeNode.name);
96
+ let routesImportPath = getRelativeImportPath(entryPath, 'app/routes.ts');
97
+ lines.push(`import type { Controller } from 'remix/fetch-router'`, '');
98
+ lines.push(`import type { routes } from '${routesImportPath}'`);
99
+ if (childImports.length > 0) {
100
+ lines.push('', ...childImports);
101
+ }
102
+ lines.push('', 'export default {', ' actions: {');
103
+ lines.push(...actionEntries);
104
+ lines.push(' },', `} satisfies Controller<typeof ${routeExpression}>`, '');
105
+ return lines.join('\n');
106
+ }
107
+ function getControllerChildImports(routeNode, entryPath, resolvedEntryPathByRouteName) {
108
+ let imports = [];
109
+ for (let childNode of routeNode.children) {
110
+ if (childNode.kind !== 'group') {
111
+ continue;
112
+ }
113
+ let childEntryPath = resolvedEntryPathByRouteName.get(childNode.name);
114
+ if (childEntryPath == null) {
115
+ continue;
116
+ }
117
+ imports.push(`import ${getControllerImportName(childNode.key)} from '${getRelativeImportPath(entryPath, childEntryPath)}'`);
118
+ }
119
+ return imports;
120
+ }
121
+ function renderControllerActionEntry(childNode, entryPath, resolvedEntryPathByRouteName) {
122
+ if (childNode.kind === 'route') {
123
+ return [
124
+ ` ${formatObjectKey(childNode.key)}() {`,
125
+ ` return new Response(${JSON.stringify(`TODO: implement routes.${childNode.name}`)})`,
126
+ ` },`,
127
+ ].join('\n');
128
+ }
129
+ let childEntryPath = resolvedEntryPathByRouteName.get(childNode.name);
130
+ if (childEntryPath != null) {
131
+ return ` ${formatObjectKey(childNode.key)}: ${getControllerImportName(childNode.key)},`;
132
+ }
133
+ return ` ${formatObjectKey(childNode.key)}: ${renderInlineControllerPlaceholder(childNode)},`;
134
+ }
135
+ function renderInlineControllerPlaceholder(routeNode) {
136
+ let childEntries = routeNode.children.map((childNode) => childNode.kind === 'route'
137
+ ? `${formatObjectKey(childNode.key)}() { return new Response(${JSON.stringify(`TODO: implement routes.${childNode.name}`)}) }`
138
+ : `${formatObjectKey(childNode.key)}: ${renderInlineControllerPlaceholder(childNode)}`);
139
+ return `{ actions: { ${childEntries.join(', ')} } }`;
140
+ }
141
+ function getControllerImportName(key) {
142
+ return `${toIdentifier(key)}Controller`;
143
+ }
144
+ function getActionExportName(key) {
145
+ return toIdentifier(key);
146
+ }
147
+ function getRouteAccessExpression(routeName) {
148
+ let segments = routeName.split('.');
149
+ let expression = 'routes';
150
+ for (let segment of segments) {
151
+ expression += isIdentifier(segment) ? `.${segment}` : `[${JSON.stringify(segment)}]`;
152
+ }
153
+ return expression;
154
+ }
155
+ function getRelativeImportPath(fromFilePath, toFilePath) {
156
+ let relativePath = path.posix.relative(path.posix.dirname(fromFilePath), toFilePath);
157
+ return relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
158
+ }
159
+ function formatObjectKey(key) {
160
+ return isIdentifier(key) ? key : JSON.stringify(key);
161
+ }
162
+ function toIdentifier(value) {
163
+ let parts = value.split(/[^A-Za-z0-9_$]+/).filter(Boolean);
164
+ if (parts.length === 0) {
165
+ return 'route';
166
+ }
167
+ let identifier = parts
168
+ .map((part, index) => index === 0 ? part.replace(/^./, (char) => char.toLowerCase()) : capitalize(part))
169
+ .join('');
170
+ if (!/^[A-Za-z_$]/.test(identifier)) {
171
+ return `route${capitalize(identifier)}`;
172
+ }
173
+ if (RESERVED_BINDING_IDENTIFIERS.has(identifier)) {
174
+ return `route${capitalize(identifier)}`;
175
+ }
176
+ return identifier;
177
+ }
178
+ function capitalize(value) {
179
+ return value.length === 0 ? value : `${value[0].toUpperCase()}${value.slice(1)}`;
180
+ }
181
+ function isIdentifier(value) {
182
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value);
183
+ }
@@ -0,0 +1,8 @@
1
+ import { type OwnershipRouteNode } from '../controller-ownership.ts';
2
+ import { type DoctorFixPlan, type DoctorSuiteResult } from './types.ts';
3
+ export interface ControllerDoctorResult {
4
+ fixPlans: DoctorFixPlan[];
5
+ suite: DoctorSuiteResult;
6
+ }
7
+ export declare function checkControllerConventions(appRoot: string, tree: OwnershipRouteNode[]): Promise<ControllerDoctorResult>;
8
+ //# sourceMappingURL=controllers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"controllers.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/controllers.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8B,KAAK,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAGhG,OAAO,EAAqB,KAAK,aAAa,EAAE,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAE1F,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,aAAa,EAAE,CAAA;IACzB,KAAK,EAAE,iBAAiB,CAAA;CACzB;AAED,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,kBAAkB,EAAE,GACzB,OAAO,CAAC,sBAAsB,CAAC,CASjC"}
@@ -0,0 +1,13 @@
1
+ import { inspectControllerOwnership } from "../controller-ownership.js";
2
+ import { getControllerFindings } from "./controller-findings.js";
3
+ import { getControllerFixPlans } from "./controller-fix-plans.js";
4
+ import { createDoctorSuite } from "./types.js";
5
+ export async function checkControllerConventions(appRoot, tree) {
6
+ let ownership = await inspectControllerOwnership(appRoot, tree);
7
+ let fixPlans = await getControllerFixPlans(appRoot, tree, ownership);
8
+ let findings = getControllerFindings(ownership, fixPlans);
9
+ return {
10
+ fixPlans,
11
+ suite: createDoctorSuite('controllers', findings),
12
+ };
13
+ }
@@ -0,0 +1,17 @@
1
+ import { type DoctorFixPlan, type DoctorSuiteResult } from './types.ts';
2
+ interface DoctorPackageJson {
3
+ dependencies?: Record<string, string>;
4
+ devDependencies?: Record<string, string>;
5
+ engines?: Record<string, string>;
6
+ name?: string;
7
+ }
8
+ export interface EnvironmentDoctorResult {
9
+ packageJson?: DoctorPackageJson;
10
+ packageJsonPath?: string;
11
+ projectRoot?: string;
12
+ suite: DoctorSuiteResult;
13
+ }
14
+ export declare function checkEnvironment(cwd?: string): Promise<EnvironmentDoctorResult>;
15
+ export declare function getEnvironmentFixPlans(result: EnvironmentDoctorResult, remixVersion: string | undefined): DoctorFixPlan[];
16
+ export {};
17
+ //# sourceMappingURL=environment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"environment.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/environment.ts"],"names":[],"mappings":"AAOA,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACvB,MAAM,YAAY,CAAA;AAEnB,UAAU,iBAAiB;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACrC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,uBAAuB;IACtC,WAAW,CAAC,EAAE,iBAAiB,CAAA;IAC/B,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE,iBAAiB,CAAA;CACzB;AAED,wBAAsB,gBAAgB,CACpC,GAAG,GAAE,MAAsB,GAC1B,OAAO,CAAC,uBAAuB,CAAC,CAwFlC;AAED,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,uBAAuB,EAC/B,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,aAAa,EAAE,CA6CjB"}
@@ -0,0 +1,226 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import * as process from 'node:process';
4
+ import { createRequire } from 'node:module';
5
+ import * as semver from 'semver';
6
+ import { MINIMUM_SUPPORTED_NODE_VERSION } from "../bootstrap-project.js";
7
+ import { createDoctorSuite, } from "./types.js";
8
+ export async function checkEnvironment(cwd = process.cwd()) {
9
+ let projectRoot = await findNearestPackageRoot(cwd);
10
+ if (projectRoot == null) {
11
+ return {
12
+ suite: createDoctorSuite('environment', [
13
+ {
14
+ code: 'project-root-not-found',
15
+ message: 'Could not find package.json. Run this command inside a Remix project.',
16
+ severity: 'warn',
17
+ suite: 'environment',
18
+ },
19
+ ]),
20
+ };
21
+ }
22
+ let packageJsonPath = path.join(projectRoot, 'package.json');
23
+ let packageJson = await readPackageJson(packageJsonPath);
24
+ if ('suite' in packageJson) {
25
+ return {
26
+ projectRoot,
27
+ suite: packageJson.suite,
28
+ };
29
+ }
30
+ let findings = [];
31
+ let nodeRequirement = packageJson.engines?.node;
32
+ if (nodeRequirement == null) {
33
+ findings.push({
34
+ actualPath: 'package.json',
35
+ code: 'node-engine-missing',
36
+ fixable: true,
37
+ message: 'package.json does not declare engines.node. Add one to document the supported Node.js version.',
38
+ severity: 'advice',
39
+ suite: 'environment',
40
+ });
41
+ }
42
+ else {
43
+ let nodeSupport = satisfiesNodeRange(process.version, nodeRequirement);
44
+ if (nodeSupport == null) {
45
+ findings.push({
46
+ actualPath: 'package.json',
47
+ code: 'node-engine-unparseable',
48
+ fixable: true,
49
+ message: `Could not evaluate engines.node "${nodeRequirement}" automatically.`,
50
+ severity: 'advice',
51
+ suite: 'environment',
52
+ });
53
+ }
54
+ else if (!nodeSupport) {
55
+ findings.push({
56
+ actualPath: 'package.json',
57
+ code: 'node-version-unsupported',
58
+ fixable: true,
59
+ message: `Project requires Node.js ${nodeRequirement}, but the current runtime is ${process.version}.`,
60
+ severity: 'warn',
61
+ suite: 'environment',
62
+ });
63
+ }
64
+ }
65
+ if (!hasRemixDependency(packageJson)) {
66
+ findings.push({
67
+ actualPath: 'package.json',
68
+ code: 'remix-dependency-missing',
69
+ fixable: true,
70
+ message: 'package.json does not declare a remix dependency.',
71
+ severity: 'warn',
72
+ suite: 'environment',
73
+ });
74
+ }
75
+ if (!(await canResolveRemix(projectRoot))) {
76
+ findings.push({
77
+ actualPath: 'package.json',
78
+ code: 'remix-install-missing',
79
+ message: 'Could not resolve remix from this project. Install dependencies and try again.',
80
+ severity: 'warn',
81
+ suite: 'environment',
82
+ });
83
+ }
84
+ return {
85
+ packageJson,
86
+ packageJsonPath,
87
+ projectRoot,
88
+ suite: createDoctorSuite('environment', findings),
89
+ };
90
+ }
91
+ export function getEnvironmentFixPlans(result, remixVersion) {
92
+ if (result.projectRoot == null || result.packageJson == null || result.packageJsonPath == null) {
93
+ return [];
94
+ }
95
+ let packageJson = structuredClone(result.packageJson);
96
+ let code = getEnvironmentFixCode(result.suite.findings);
97
+ let changed = false;
98
+ if (result.suite.findings.some((finding) => finding.code === 'node-engine-missing' ||
99
+ finding.code === 'node-engine-unparseable' ||
100
+ finding.code === 'node-version-unsupported')) {
101
+ packageJson.engines = {
102
+ ...packageJson.engines,
103
+ node: `>=${MINIMUM_SUPPORTED_NODE_VERSION}`,
104
+ };
105
+ changed = true;
106
+ }
107
+ if (result.suite.findings.some((finding) => finding.code === 'remix-dependency-missing')) {
108
+ packageJson.dependencies = {
109
+ ...packageJson.dependencies,
110
+ remix: remixVersion ?? 'latest',
111
+ };
112
+ changed = true;
113
+ }
114
+ if (!changed || code == null) {
115
+ return [];
116
+ }
117
+ return [
118
+ {
119
+ code,
120
+ contents: `${JSON.stringify(packageJson, null, 2)}\n`,
121
+ kind: 'update-file',
122
+ path: 'package.json',
123
+ suite: 'environment',
124
+ },
125
+ ];
126
+ }
127
+ async function findNearestPackageRoot(startDir) {
128
+ let currentDir = path.resolve(startDir);
129
+ while (true) {
130
+ if (await pathExists(path.join(currentDir, 'package.json'))) {
131
+ return currentDir;
132
+ }
133
+ let parentDir = path.dirname(currentDir);
134
+ if (parentDir === currentDir) {
135
+ return null;
136
+ }
137
+ currentDir = parentDir;
138
+ }
139
+ }
140
+ async function readPackageJson(packageJsonPath) {
141
+ let source;
142
+ try {
143
+ source = await fs.readFile(packageJsonPath, 'utf8');
144
+ }
145
+ catch (error) {
146
+ let nodeError = error;
147
+ let detail = nodeError.code == null
148
+ ? 'Could not read package.json.'
149
+ : `Could not read package.json (${nodeError.code}).`;
150
+ return {
151
+ suite: createDoctorSuite('environment', [
152
+ {
153
+ actualPath: 'package.json',
154
+ code: 'package-json-read-failed',
155
+ message: detail,
156
+ severity: 'warn',
157
+ suite: 'environment',
158
+ },
159
+ ]),
160
+ };
161
+ }
162
+ try {
163
+ return JSON.parse(source);
164
+ }
165
+ catch {
166
+ return {
167
+ suite: createDoctorSuite('environment', [
168
+ {
169
+ actualPath: 'package.json',
170
+ code: 'package-json-invalid',
171
+ message: 'package.json is not valid JSON.',
172
+ severity: 'warn',
173
+ suite: 'environment',
174
+ },
175
+ ]),
176
+ };
177
+ }
178
+ }
179
+ function hasRemixDependency(packageJson) {
180
+ return packageJson.dependencies?.remix != null || packageJson.devDependencies?.remix != null;
181
+ }
182
+ function getEnvironmentFixCode(findings) {
183
+ let fixableFinding = findings.find((finding) => finding.code === 'node-engine-missing' ||
184
+ finding.code === 'node-engine-unparseable' ||
185
+ finding.code === 'node-version-unsupported' ||
186
+ finding.code === 'remix-dependency-missing');
187
+ return fixableFinding?.code ?? null;
188
+ }
189
+ async function canResolveRemix(projectRoot) {
190
+ if (!(await pathExists(path.join(projectRoot, 'node_modules', 'remix', 'package.json')))) {
191
+ return false;
192
+ }
193
+ try {
194
+ let require = createRequire(path.join(projectRoot, 'package.json'));
195
+ require.resolve('remix/package.json');
196
+ return true;
197
+ }
198
+ catch {
199
+ return false;
200
+ }
201
+ }
202
+ function satisfiesNodeRange(version, range) {
203
+ let parsedVersion = semver.valid(version);
204
+ let trimmedRange = range.trim();
205
+ if (parsedVersion == null || trimmedRange === '') {
206
+ return null;
207
+ }
208
+ let parsedRange = semver.validRange(trimmedRange);
209
+ if (parsedRange == null) {
210
+ return null;
211
+ }
212
+ return semver.satisfies(parsedVersion, parsedRange);
213
+ }
214
+ async function pathExists(filePath) {
215
+ try {
216
+ await fs.access(filePath);
217
+ return true;
218
+ }
219
+ catch (error) {
220
+ let nodeError = error;
221
+ if (nodeError.code === 'ENOENT') {
222
+ return false;
223
+ }
224
+ throw error;
225
+ }
226
+ }
@@ -0,0 +1,3 @@
1
+ import type { DoctorAppliedFix, DoctorFixPlan } from './types.ts';
2
+ export declare function applyDoctorFixPlans(appRoot: string, fixPlans: DoctorFixPlan[]): Promise<DoctorAppliedFix[]>;
3
+ //# sourceMappingURL=fixes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fixes.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/fixes.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAEjE,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,aAAa,EAAE,GACxB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAmC7B"}
@@ -0,0 +1,65 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { resolveContainedPath } from "../contained-path.js";
4
+ export async function applyDoctorFixPlans(appRoot, fixPlans) {
5
+ let appliedFixes = [];
6
+ for (let fixPlan of dedupeDoctorFixPlans(fixPlans)) {
7
+ let absolutePath = resolveDoctorFixPath(appRoot, fixPlan.path);
8
+ if (fixPlan.kind === 'create-directory') {
9
+ await fs.mkdir(absolutePath, { recursive: true });
10
+ appliedFixes.push(toAppliedDoctorFix(fixPlan));
11
+ continue;
12
+ }
13
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
14
+ if (fixPlan.kind === 'update-file') {
15
+ await fs.writeFile(absolutePath, fixPlan.contents ?? '', { encoding: 'utf8' });
16
+ appliedFixes.push(toAppliedDoctorFix(fixPlan));
17
+ continue;
18
+ }
19
+ try {
20
+ await fs.writeFile(absolutePath, fixPlan.contents ?? '', { encoding: 'utf8', flag: 'wx' });
21
+ }
22
+ catch (error) {
23
+ let nodeError = error;
24
+ if (nodeError.code !== 'EEXIST') {
25
+ throw error;
26
+ }
27
+ continue;
28
+ }
29
+ appliedFixes.push(toAppliedDoctorFix(fixPlan));
30
+ }
31
+ return appliedFixes;
32
+ }
33
+ function dedupeDoctorFixPlans(fixPlans) {
34
+ let seen = new Set();
35
+ let uniqueFixPlans = [];
36
+ for (let fixPlan of fixPlans) {
37
+ let key = `${fixPlan.kind}:${fixPlan.path}`;
38
+ if (seen.has(key)) {
39
+ continue;
40
+ }
41
+ seen.add(key);
42
+ uniqueFixPlans.push(fixPlan);
43
+ }
44
+ return uniqueFixPlans;
45
+ }
46
+ function toAppliedDoctorFix(fixPlan) {
47
+ return {
48
+ code: fixPlan.code,
49
+ kind: fixPlan.kind,
50
+ path: fixPlan.path,
51
+ routeName: fixPlan.routeName,
52
+ suite: fixPlan.suite,
53
+ };
54
+ }
55
+ function resolveDoctorFixPath(appRoot, fixPath) {
56
+ try {
57
+ return resolveContainedPath(appRoot, fixPath);
58
+ }
59
+ catch (error) {
60
+ if (error instanceof Error && error.message.includes('escapes the allowed root')) {
61
+ throw new Error(`Doctor fix path resolves outside the app root: ${fixPath}`);
62
+ }
63
+ throw error;
64
+ }
65
+ }
@@ -0,0 +1,10 @@
1
+ import { type LoadedRouteManifest } from '../route-map.ts';
2
+ import { type DoctorFixPlan, type DoctorSuiteResult } from './types.ts';
3
+ export interface ProjectDoctorResult {
4
+ routesFile: string;
5
+ routeManifest?: LoadedRouteManifest;
6
+ suite: DoctorSuiteResult;
7
+ }
8
+ export declare function checkProject(projectRoot: string): Promise<ProjectDoctorResult>;
9
+ export declare function getProjectFixPlans(projectRoot: string): Promise<DoctorFixPlan[]>;
10
+ //# sourceMappingURL=project.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/project.ts"],"names":[],"mappings":"AAUA,OAAO,EAAgC,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AACxF,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACvB,MAAM,YAAY,CAAA;AAEnB,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,mBAAmB,CAAA;IACnC,KAAK,EAAE,iBAAiB,CAAA;CACzB;AAED,wBAAsB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAmCpF;AAED,wBAAsB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAoDtF"}