@remix-run/cli 0.0.0 → 0.2.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/LICENSE +21 -0
- package/README.md +77 -3
- package/bootstrap/.agents/skills/remix/SKILL.md +501 -0
- package/bootstrap/.agents/skills/remix/references/animate-elements.md +195 -0
- package/bootstrap/.agents/skills/remix/references/assets-and-browser-modules.md +122 -0
- package/bootstrap/.agents/skills/remix/references/auth-and-sessions.md +420 -0
- package/bootstrap/.agents/skills/remix/references/component-model.md +282 -0
- package/bootstrap/.agents/skills/remix/references/create-mixins.md +158 -0
- package/bootstrap/.agents/skills/remix/references/data-and-validation.md +363 -0
- package/bootstrap/.agents/skills/remix/references/hydration-frames-navigation.md +297 -0
- package/bootstrap/.agents/skills/remix/references/middleware-and-server.md +243 -0
- package/bootstrap/.agents/skills/remix/references/mixins-styling-events.md +213 -0
- package/bootstrap/.agents/skills/remix/references/routing-and-controllers.md +324 -0
- package/bootstrap/.agents/skills/remix/references/testing-patterns.md +156 -0
- package/bootstrap/AGENTS.md +39 -0
- package/bootstrap/README.md +27 -0
- package/bootstrap/app/assets/entry.ts +19 -0
- package/bootstrap/app/assets.ts +18 -0
- package/bootstrap/app/controllers/auth.tsx +21 -0
- package/bootstrap/app/controllers/home.tsx +11 -0
- package/bootstrap/app/router.ts +16 -0
- package/bootstrap/app/routes.ts +7 -0
- package/bootstrap/app/ui/document.tsx +26 -0
- package/bootstrap/app/ui/layout.tsx +22 -0
- package/bootstrap/app/ui/prompt-button.tsx +162 -0
- package/bootstrap/app/ui/scaffold-home-page.tsx +526 -0
- package/bootstrap/app/utils/render.tsx +26 -0
- package/bootstrap/package.json +22 -0
- package/bootstrap/server.ts +37 -0
- package/bootstrap/tsconfig.json +18 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/bootstrap-project.d.ts +17 -0
- package/dist/lib/bootstrap-project.d.ts.map +1 -0
- package/dist/lib/bootstrap-project.js +137 -0
- package/dist/lib/cli-context.d.ts +11 -0
- package/dist/lib/cli-context.d.ts.map +1 -0
- package/dist/lib/cli-context.js +19 -0
- package/dist/lib/cli.d.ts +6 -0
- package/dist/lib/cli.d.ts.map +1 -0
- package/dist/lib/cli.js +84 -0
- package/dist/lib/commands/completion.d.ts +3 -0
- package/dist/lib/commands/completion.d.ts.map +1 -0
- package/dist/lib/commands/completion.js +51 -0
- package/dist/lib/commands/doctor.d.ts +4 -0
- package/dist/lib/commands/doctor.d.ts.map +1 -0
- package/dist/lib/commands/doctor.js +293 -0
- package/dist/lib/commands/help.d.ts +4 -0
- package/dist/lib/commands/help.d.ts.map +1 -0
- package/dist/lib/commands/help.js +100 -0
- package/dist/lib/commands/new.d.ts +4 -0
- package/dist/lib/commands/new.d.ts.map +1 -0
- package/dist/lib/commands/new.js +70 -0
- package/dist/lib/commands/routes.d.ts +4 -0
- package/dist/lib/commands/routes.d.ts.map +1 -0
- package/dist/lib/commands/routes.js +161 -0
- package/dist/lib/commands/test.d.ts +4 -0
- package/dist/lib/commands/test.d.ts.map +1 -0
- package/dist/lib/commands/test.js +23 -0
- package/dist/lib/commands/version.d.ts +4 -0
- package/dist/lib/commands/version.d.ts.map +1 -0
- package/dist/lib/commands/version.js +29 -0
- package/dist/lib/completion.d.ts +12 -0
- package/dist/lib/completion.d.ts.map +1 -0
- package/dist/lib/completion.js +310 -0
- package/dist/lib/contained-path.d.ts +2 -0
- package/dist/lib/contained-path.d.ts.map +1 -0
- package/dist/lib/contained-path.js +10 -0
- package/dist/lib/controller-files.d.ts +14 -0
- package/dist/lib/controller-files.d.ts.map +1 -0
- package/dist/lib/controller-files.js +53 -0
- package/dist/lib/controller-ownership.d.ts +41 -0
- package/dist/lib/controller-ownership.d.ts.map +1 -0
- package/dist/lib/controller-ownership.js +224 -0
- package/dist/lib/display-path.d.ts +2 -0
- package/dist/lib/display-path.d.ts.map +1 -0
- package/dist/lib/display-path.js +12 -0
- package/dist/lib/doctor/controller-findings.d.ts +4 -0
- package/dist/lib/doctor/controller-findings.d.ts.map +1 -0
- package/dist/lib/doctor/controller-findings.js +132 -0
- package/dist/lib/doctor/controller-fix-plans.d.ts +4 -0
- package/dist/lib/doctor/controller-fix-plans.d.ts.map +1 -0
- package/dist/lib/doctor/controller-fix-plans.js +130 -0
- package/dist/lib/doctor/controller-placeholders.d.ts +4 -0
- package/dist/lib/doctor/controller-placeholders.d.ts.map +1 -0
- package/dist/lib/doctor/controller-placeholders.js +183 -0
- package/dist/lib/doctor/controllers.d.ts +8 -0
- package/dist/lib/doctor/controllers.d.ts.map +1 -0
- package/dist/lib/doctor/controllers.js +13 -0
- package/dist/lib/doctor/environment.d.ts +17 -0
- package/dist/lib/doctor/environment.d.ts.map +1 -0
- package/dist/lib/doctor/environment.js +226 -0
- package/dist/lib/doctor/fixes.d.ts +3 -0
- package/dist/lib/doctor/fixes.d.ts.map +1 -0
- package/dist/lib/doctor/fixes.js +65 -0
- package/dist/lib/doctor/project.d.ts +10 -0
- package/dist/lib/doctor/project.d.ts.map +1 -0
- package/dist/lib/doctor/project.js +371 -0
- package/dist/lib/doctor/types.d.ts +48 -0
- package/dist/lib/doctor/types.d.ts.map +1 -0
- package/dist/lib/doctor/types.js +15 -0
- package/dist/lib/errors.d.ts +167 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +307 -0
- package/dist/lib/help-text.d.ts +13 -0
- package/dist/lib/help-text.d.ts.map +1 -0
- package/dist/lib/help-text.js +43 -0
- package/dist/lib/load-route-map-worker.d.ts +2 -0
- package/dist/lib/load-route-map-worker.d.ts.map +1 -0
- package/dist/lib/load-route-map-worker.js +92 -0
- package/dist/lib/parse-args.d.ts +22 -0
- package/dist/lib/parse-args.d.ts.map +1 -0
- package/dist/lib/parse-args.js +35 -0
- package/dist/lib/remix-version.d.ts +2 -0
- package/dist/lib/remix-version.d.ts.map +1 -0
- package/dist/lib/remix-version.js +58 -0
- package/dist/lib/reporter.d.ts +56 -0
- package/dist/lib/reporter.d.ts.map +1 -0
- package/dist/lib/reporter.js +357 -0
- package/dist/lib/route-map.d.ts +38 -0
- package/dist/lib/route-map.d.ts.map +1 -0
- package/dist/lib/route-map.js +195 -0
- package/dist/lib/terminal.d.ts +17 -0
- package/dist/lib/terminal.d.ts.map +1 -0
- package/dist/lib/terminal.js +66 -0
- package/package.json +47 -5
- package/src/index.ts +1 -0
- package/src/lib/bootstrap-project.ts +222 -0
- package/src/lib/cli-context.ts +38 -0
- package/src/lib/cli.ts +116 -0
- package/src/lib/commands/completion.ts +74 -0
- package/src/lib/commands/doctor.ts +393 -0
- package/src/lib/commands/help.ts +123 -0
- package/src/lib/commands/new.ts +104 -0
- package/src/lib/commands/routes.ts +251 -0
- package/src/lib/commands/test.ts +31 -0
- package/src/lib/commands/version.ts +39 -0
- package/src/lib/completion.ts +418 -0
- package/src/lib/contained-path.ts +13 -0
- package/src/lib/controller-files.ts +79 -0
- package/src/lib/controller-ownership.ts +397 -0
- package/src/lib/display-path.ts +16 -0
- package/src/lib/doctor/controller-findings.ts +157 -0
- package/src/lib/doctor/controller-fix-plans.ts +184 -0
- package/src/lib/doctor/controller-placeholders.ts +242 -0
- package/src/lib/doctor/controllers.ts +23 -0
- package/src/lib/doctor/environment.ts +289 -0
- package/src/lib/doctor/fixes.ts +84 -0
- package/src/lib/doctor/project.ts +435 -0
- package/src/lib/doctor/types.ts +95 -0
- package/src/lib/errors.ts +367 -0
- package/src/lib/help-text.ts +88 -0
- package/src/lib/load-route-map-worker.ts +110 -0
- package/src/lib/parse-args.ts +82 -0
- package/src/lib/remix-version.ts +80 -0
- package/src/lib/reporter.ts +535 -0
- package/src/lib/route-map.ts +303 -0
- package/src/lib/terminal.ts +86 -0
|
@@ -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 @@
|
|
|
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"}
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { getOwnerFileExtension, isActionFileName, isControllerEntryFileName, } from "../controller-files.js";
|
|
4
|
+
import { CliError } from "../errors.js";
|
|
5
|
+
import { loadRouteManifestFromAppRoot } from "../route-map.js";
|
|
6
|
+
import { createDoctorSuite, } from "./types.js";
|
|
7
|
+
export async function checkProject(projectRoot) {
|
|
8
|
+
let routesFile = path.join(projectRoot, 'app', 'routes.ts');
|
|
9
|
+
if (!(await pathExists(routesFile))) {
|
|
10
|
+
return {
|
|
11
|
+
routesFile,
|
|
12
|
+
suite: createDoctorSuite('project', [
|
|
13
|
+
{
|
|
14
|
+
code: 'routes-file-missing',
|
|
15
|
+
expectedPath: 'app/routes.ts',
|
|
16
|
+
fixable: true,
|
|
17
|
+
message: 'Project is missing app/routes.ts.',
|
|
18
|
+
severity: 'warn',
|
|
19
|
+
suite: 'project',
|
|
20
|
+
},
|
|
21
|
+
]),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
let routeManifest = await loadRouteManifestFromAppRoot(projectRoot);
|
|
26
|
+
return {
|
|
27
|
+
routeManifest,
|
|
28
|
+
routesFile,
|
|
29
|
+
suite: createDoctorSuite('project', []),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
let finding = toProjectFinding(error);
|
|
34
|
+
return {
|
|
35
|
+
routesFile,
|
|
36
|
+
suite: createDoctorSuite('project', [finding]),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export async function getProjectFixPlans(projectRoot) {
|
|
41
|
+
let routesFile = path.join(projectRoot, 'app', 'routes.ts');
|
|
42
|
+
let homeActionPath = normalizeRelativePath(path.join('app', 'controllers', `home${await inferHomeOwnerExtension(projectRoot)}`));
|
|
43
|
+
if (await pathExists(routesFile)) {
|
|
44
|
+
let routesSource = await fs.readFile(routesFile, 'utf8');
|
|
45
|
+
if (!hasOnlyWhitespaceAndComments(routesSource)) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
return [
|
|
49
|
+
{
|
|
50
|
+
code: 'routes-export-missing',
|
|
51
|
+
contents: renderDefaultRoutesFile(),
|
|
52
|
+
kind: 'update-file',
|
|
53
|
+
path: 'app/routes.ts',
|
|
54
|
+
suite: 'project',
|
|
55
|
+
},
|
|
56
|
+
...((await pathExists(path.join(projectRoot, homeActionPath)))
|
|
57
|
+
? []
|
|
58
|
+
: [
|
|
59
|
+
{
|
|
60
|
+
code: 'missing-owner',
|
|
61
|
+
contents: renderDefaultHomeAction(homeActionPath),
|
|
62
|
+
kind: 'create-file',
|
|
63
|
+
path: homeActionPath,
|
|
64
|
+
routeName: 'home',
|
|
65
|
+
suite: 'project',
|
|
66
|
+
},
|
|
67
|
+
]),
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
return [
|
|
71
|
+
{
|
|
72
|
+
code: 'routes-file-missing',
|
|
73
|
+
contents: renderDefaultRoutesFile(),
|
|
74
|
+
kind: 'create-file',
|
|
75
|
+
path: 'app/routes.ts',
|
|
76
|
+
suite: 'project',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
code: 'missing-owner',
|
|
80
|
+
contents: renderDefaultHomeAction(homeActionPath),
|
|
81
|
+
kind: 'create-file',
|
|
82
|
+
path: homeActionPath,
|
|
83
|
+
routeName: 'home',
|
|
84
|
+
suite: 'project',
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
function toProjectFinding(error) {
|
|
89
|
+
if (error instanceof CliError) {
|
|
90
|
+
if (error.code === 'RMX_ROUTE_MAP_LOADER_INVALID_JSON') {
|
|
91
|
+
return {
|
|
92
|
+
code: 'route-map-invalid-json',
|
|
93
|
+
message: 'Route-map loader returned invalid JSON while loading app/routes.ts.',
|
|
94
|
+
severity: 'warn',
|
|
95
|
+
suite: 'project',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (error.code === 'RMX_ROUTE_MAP_LOADER_SIGNAL') {
|
|
99
|
+
return {
|
|
100
|
+
code: 'route-map-loader-signal',
|
|
101
|
+
message: error.message,
|
|
102
|
+
severity: 'warn',
|
|
103
|
+
suite: 'project',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (error.code === 'RMX_ROUTE_MAP_LOADER_FAILED') {
|
|
107
|
+
return classifyRouteMapLoaderFailure(error.message);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
let message = error instanceof Error ? error.message : String(error);
|
|
111
|
+
return {
|
|
112
|
+
code: 'route-module-import-failed',
|
|
113
|
+
message: `Failed to load app/routes.ts: ${message}`,
|
|
114
|
+
severity: 'warn',
|
|
115
|
+
suite: 'project',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function classifyRouteMapLoaderFailure(message) {
|
|
119
|
+
if (message.includes('must export a named "routes" value')) {
|
|
120
|
+
return {
|
|
121
|
+
code: 'routes-export-missing',
|
|
122
|
+
message: 'app/routes.ts must export a named "routes" value.',
|
|
123
|
+
severity: 'warn',
|
|
124
|
+
suite: 'project',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (message.startsWith('Invalid route map value at "') ||
|
|
128
|
+
message.startsWith('Detected a route map cycle at "')) {
|
|
129
|
+
return {
|
|
130
|
+
code: 'route-map-invalid',
|
|
131
|
+
message,
|
|
132
|
+
severity: 'warn',
|
|
133
|
+
suite: 'project',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
code: 'route-module-import-failed',
|
|
138
|
+
message: `Failed to load app/routes.ts: ${message}`,
|
|
139
|
+
severity: 'warn',
|
|
140
|
+
suite: 'project',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
async function pathExists(filePath) {
|
|
144
|
+
try {
|
|
145
|
+
await fs.access(filePath);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
let nodeError = error;
|
|
150
|
+
if (nodeError.code === 'ENOENT') {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function hasOnlyWhitespaceAndComments(source) {
|
|
157
|
+
for (let index = 0; index < source.length;) {
|
|
158
|
+
let char = source[index];
|
|
159
|
+
let next = source[index + 1];
|
|
160
|
+
if (char == null || /\s/.test(char)) {
|
|
161
|
+
index += 1;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (char === '/' && next === '/') {
|
|
165
|
+
index += 2;
|
|
166
|
+
while (index < source.length && source[index] !== '\n') {
|
|
167
|
+
index += 1;
|
|
168
|
+
}
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (char === '/' && next === '*') {
|
|
172
|
+
index += 2;
|
|
173
|
+
while (index < source.length && !(source[index] === '*' && source[index + 1] === '/')) {
|
|
174
|
+
index += 1;
|
|
175
|
+
}
|
|
176
|
+
index = Math.min(index + 2, source.length);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
async function inferHomeOwnerExtension(projectRoot) {
|
|
184
|
+
let controllersDir = path.join(projectRoot, 'app', 'controllers');
|
|
185
|
+
let extensions = await collectOwnerExtensions(controllersDir);
|
|
186
|
+
if (extensions.length > 0) {
|
|
187
|
+
return getMostCommonExtension(extensions);
|
|
188
|
+
}
|
|
189
|
+
if (await pathExists(path.join(projectRoot, 'tsconfig.json'))) {
|
|
190
|
+
return '.tsx';
|
|
191
|
+
}
|
|
192
|
+
return '.js';
|
|
193
|
+
}
|
|
194
|
+
async function collectOwnerExtensions(directory) {
|
|
195
|
+
try {
|
|
196
|
+
let entries = await fs.readdir(directory, { withFileTypes: true });
|
|
197
|
+
let extensions = [];
|
|
198
|
+
for (let entry of entries) {
|
|
199
|
+
let absolutePath = path.join(directory, entry.name);
|
|
200
|
+
if (entry.isDirectory()) {
|
|
201
|
+
extensions.push(...(await collectOwnerExtensions(absolutePath)));
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
let fileName = path.basename(absolutePath);
|
|
205
|
+
if (!isActionFileName(fileName) && !isControllerEntryFileName(fileName)) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
let extension = getOwnerFileExtension(fileName);
|
|
209
|
+
if (extension != null) {
|
|
210
|
+
extensions.push(extension);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return extensions;
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
let nodeError = error;
|
|
217
|
+
if (nodeError.code === 'ENOENT') {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function getMostCommonExtension(extensions) {
|
|
224
|
+
let priority = ['.tsx', '.ts', '.jsx', '.js'];
|
|
225
|
+
let counts = new Map();
|
|
226
|
+
for (let extension of extensions) {
|
|
227
|
+
counts.set(extension, (counts.get(extension) ?? 0) + 1);
|
|
228
|
+
}
|
|
229
|
+
let bestExtension = priority[0];
|
|
230
|
+
let bestCount = -1;
|
|
231
|
+
for (let extension of priority) {
|
|
232
|
+
let count = counts.get(extension) ?? 0;
|
|
233
|
+
if (count > bestCount) {
|
|
234
|
+
bestCount = count;
|
|
235
|
+
bestExtension = extension;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return bestExtension;
|
|
239
|
+
}
|
|
240
|
+
function renderDefaultRoutesFile() {
|
|
241
|
+
return [
|
|
242
|
+
`import { route } from 'remix/fetch-router/routes'`,
|
|
243
|
+
'',
|
|
244
|
+
'export const routes = route({',
|
|
245
|
+
` home: '/',`,
|
|
246
|
+
'})',
|
|
247
|
+
'',
|
|
248
|
+
].join('\n');
|
|
249
|
+
}
|
|
250
|
+
function normalizeRelativePath(filePath) {
|
|
251
|
+
return filePath.split(path.sep).join('/');
|
|
252
|
+
}
|
|
253
|
+
function renderDefaultHomeAction(entryPath) {
|
|
254
|
+
let extension = getOwnerFileExtension(entryPath);
|
|
255
|
+
if (extension === '.js') {
|
|
256
|
+
return [
|
|
257
|
+
`import { html } from 'remix/html-template'`,
|
|
258
|
+
`import { createHtmlResponse } from 'remix/response/html'`,
|
|
259
|
+
'',
|
|
260
|
+
'export const home = {',
|
|
261
|
+
' handler() {',
|
|
262
|
+
' let page = html`',
|
|
263
|
+
' <html lang="en">',
|
|
264
|
+
' <head>',
|
|
265
|
+
' <meta charset="utf-8" />',
|
|
266
|
+
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
267
|
+
' <title>Home</title>',
|
|
268
|
+
' </head>',
|
|
269
|
+
' <body>',
|
|
270
|
+
' <h1>Home</h1>',
|
|
271
|
+
' <p>Update app/routes.ts and app/controllers/home to keep building your app.</p>',
|
|
272
|
+
' </body>',
|
|
273
|
+
' </html>',
|
|
274
|
+
' `',
|
|
275
|
+
' return createHtmlResponse(page)',
|
|
276
|
+
' },',
|
|
277
|
+
'}',
|
|
278
|
+
'',
|
|
279
|
+
].join('\n');
|
|
280
|
+
}
|
|
281
|
+
if (extension === '.ts') {
|
|
282
|
+
return [
|
|
283
|
+
`import type { BuildAction } from 'remix/fetch-router'`,
|
|
284
|
+
`import { html } from 'remix/html-template'`,
|
|
285
|
+
`import { createHtmlResponse } from 'remix/response/html'`,
|
|
286
|
+
'',
|
|
287
|
+
`import type { routes } from '../routes.ts'`,
|
|
288
|
+
'',
|
|
289
|
+
`export const home: BuildAction<'ANY', typeof routes.home> = {`,
|
|
290
|
+
' handler() {',
|
|
291
|
+
' let page = html`',
|
|
292
|
+
' <html lang="en">',
|
|
293
|
+
' <head>',
|
|
294
|
+
' <meta charset="utf-8" />',
|
|
295
|
+
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
296
|
+
' <title>Home</title>',
|
|
297
|
+
' </head>',
|
|
298
|
+
' <body>',
|
|
299
|
+
' <h1>Home</h1>',
|
|
300
|
+
' <p>Update app/routes.ts and app/controllers/home to keep building your app.</p>',
|
|
301
|
+
' </body>',
|
|
302
|
+
' </html>',
|
|
303
|
+
' `',
|
|
304
|
+
' return createHtmlResponse(page)',
|
|
305
|
+
' },',
|
|
306
|
+
'}',
|
|
307
|
+
'',
|
|
308
|
+
].join('\n');
|
|
309
|
+
}
|
|
310
|
+
if (extension === '.jsx') {
|
|
311
|
+
return [
|
|
312
|
+
`import { renderToStream } from 'remix/ui/server'`,
|
|
313
|
+
`import { createHtmlResponse } from 'remix/response/html'`,
|
|
314
|
+
'',
|
|
315
|
+
'export const home = {',
|
|
316
|
+
' handler() {',
|
|
317
|
+
' let page = <HomePage />',
|
|
318
|
+
' return createHtmlResponse(renderToStream(page))',
|
|
319
|
+
' },',
|
|
320
|
+
'}',
|
|
321
|
+
'',
|
|
322
|
+
'function HomePage() {',
|
|
323
|
+
' return () => (',
|
|
324
|
+
' <html lang="en">',
|
|
325
|
+
' <head>',
|
|
326
|
+
' <meta charSet="utf-8" />',
|
|
327
|
+
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
328
|
+
' <title>Home</title>',
|
|
329
|
+
' </head>',
|
|
330
|
+
' <body>',
|
|
331
|
+
' <h1>Home</h1>',
|
|
332
|
+
' <p>Update app/routes.ts and app/controllers/home to keep building your app.</p>',
|
|
333
|
+
' </body>',
|
|
334
|
+
' </html>',
|
|
335
|
+
' )',
|
|
336
|
+
'}',
|
|
337
|
+
'',
|
|
338
|
+
].join('\n');
|
|
339
|
+
}
|
|
340
|
+
return [
|
|
341
|
+
`import type { BuildAction } from 'remix/fetch-router'`,
|
|
342
|
+
`import { renderToStream } from 'remix/ui/server'`,
|
|
343
|
+
`import { createHtmlResponse } from 'remix/response/html'`,
|
|
344
|
+
'',
|
|
345
|
+
`import type { routes } from '../routes.ts'`,
|
|
346
|
+
'',
|
|
347
|
+
`export const home: BuildAction<'ANY', typeof routes.home> = {`,
|
|
348
|
+
' handler() {',
|
|
349
|
+
' let page = <HomePage />',
|
|
350
|
+
' return createHtmlResponse(renderToStream(page))',
|
|
351
|
+
' },',
|
|
352
|
+
'}',
|
|
353
|
+
'',
|
|
354
|
+
'function HomePage() {',
|
|
355
|
+
' return () => (',
|
|
356
|
+
' <html lang="en">',
|
|
357
|
+
' <head>',
|
|
358
|
+
' <meta charSet="utf-8" />',
|
|
359
|
+
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
360
|
+
' <title>Home</title>',
|
|
361
|
+
' </head>',
|
|
362
|
+
' <body>',
|
|
363
|
+
' <h1>Home</h1>',
|
|
364
|
+
' <p>Update app/routes.ts and app/controllers/home to keep building your app.</p>',
|
|
365
|
+
' </body>',
|
|
366
|
+
' </html>',
|
|
367
|
+
' )',
|
|
368
|
+
'}',
|
|
369
|
+
'',
|
|
370
|
+
].join('\n');
|
|
371
|
+
}
|