@remix-run/cli 0.3.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -1
- package/dist/index.js +1 -1
- package/dist/lib/app-root.d.ts +11 -0
- package/dist/lib/app-root.d.ts.map +1 -0
- package/dist/lib/app-root.js +38 -0
- package/dist/lib/bootstrap-project.js +2 -2
- package/dist/lib/cli-context.d.ts +6 -0
- package/dist/lib/cli-context.d.ts.map +1 -1
- package/dist/lib/cli-context.js +12 -1
- package/dist/lib/cli.d.ts +3 -2
- package/dist/lib/cli.d.ts.map +1 -1
- package/dist/lib/cli.js +42 -14
- package/dist/lib/commands/completion.js +4 -4
- package/dist/lib/commands/db.d.ts +4 -0
- package/dist/lib/commands/db.d.ts.map +1 -0
- package/dist/lib/commands/db.js +283 -0
- package/dist/lib/commands/doctor.d.ts +5 -0
- package/dist/lib/commands/doctor.d.ts.map +1 -1
- package/dist/lib/commands/doctor.js +26 -251
- package/dist/lib/commands/help.d.ts.map +1 -1
- package/dist/lib/commands/help.js +17 -9
- package/dist/lib/commands/new.js +6 -6
- package/dist/lib/commands/routes.js +6 -6
- package/dist/lib/commands/test.d.ts +20 -1
- package/dist/lib/commands/test.d.ts.map +1 -1
- package/dist/lib/commands/test.js +353 -10
- package/dist/lib/commands/version.js +3 -3
- package/dist/lib/completion.d.ts +1 -1
- package/dist/lib/completion.d.ts.map +1 -1
- package/dist/lib/completion.js +229 -6
- package/dist/lib/controller-files.d.ts +0 -8
- package/dist/lib/controller-files.d.ts.map +1 -1
- package/dist/lib/controller-files.js +0 -23
- package/dist/lib/controller-ownership.d.ts +4 -12
- package/dist/lib/controller-ownership.d.ts.map +1 -1
- package/dist/lib/controller-ownership.js +31 -127
- package/dist/lib/database-command.d.ts +20 -0
- package/dist/lib/database-command.d.ts.map +1 -0
- package/dist/lib/database-command.js +7 -0
- package/dist/lib/doctor/controller-findings.d.ts +2 -2
- package/dist/lib/doctor/controller-findings.d.ts.map +1 -1
- package/dist/lib/doctor/controller-findings.js +23 -39
- package/dist/lib/doctor/controllers.d.ts +1 -2
- package/dist/lib/doctor/controllers.d.ts.map +1 -1
- package/dist/lib/doctor/controllers.js +4 -7
- package/dist/lib/doctor/environment.js +2 -2
- package/dist/lib/doctor/fixes.js +1 -1
- package/dist/lib/doctor/project.d.ts.map +1 -1
- package/dist/lib/doctor/project.js +3 -212
- package/dist/lib/doctor/run.d.ts +9 -0
- package/dist/lib/doctor/run.d.ts.map +1 -0
- package/dist/lib/doctor/run.js +237 -0
- package/dist/lib/doctor/types.d.ts +2 -1
- package/dist/lib/doctor/types.d.ts.map +1 -1
- package/dist/lib/doctor/types.js +4 -1
- package/dist/lib/errors.d.ts +30 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +55 -0
- package/dist/lib/help-text.js +1 -1
- package/dist/lib/parse-args.d.ts.map +1 -1
- package/dist/lib/parse-args.js +17 -4
- package/dist/lib/remix-config.d.ts +75 -0
- package/dist/lib/remix-config.d.ts.map +1 -0
- package/dist/lib/remix-config.js +411 -0
- package/dist/lib/reporter.js +1 -1
- package/dist/lib/route-map.js +13 -33
- package/package.json +13 -7
- package/schema/remix.json +242 -0
- package/src/lib/app-root.ts +46 -0
- package/src/lib/cli-context.ts +17 -0
- package/src/lib/cli.ts +41 -6
- package/src/lib/commands/db.ts +377 -0
- package/src/lib/commands/doctor.ts +27 -329
- package/src/lib/commands/help.ts +10 -1
- package/src/lib/commands/test.ts +453 -12
- package/src/lib/completion.ts +321 -6
- package/src/lib/controller-files.ts +0 -38
- package/src/lib/controller-ownership.ts +50 -203
- package/src/lib/database-command.ts +31 -0
- package/src/lib/doctor/controller-findings.ts +30 -47
- package/src/lib/doctor/controllers.ts +2 -6
- package/src/lib/doctor/project.ts +0 -236
- package/src/lib/doctor/run.ts +311 -0
- package/src/lib/doctor/types.ts +6 -5
- package/src/lib/errors.ts +65 -0
- package/src/lib/parse-args.ts +19 -3
- package/src/lib/remix-config.ts +639 -0
- package/src/lib/route-map.ts +11 -48
- package/template/.agents/skills/remix/SKILL.md +38 -27
- package/template/.agents/skills/remix/references/assets-and-browser-modules.md +54 -11
- package/template/.agents/skills/remix/references/auth-and-sessions.md +5 -5
- package/template/.agents/skills/remix/references/data-and-validation.md +15 -15
- package/template/.agents/skills/remix/references/hydration-frames-navigation.md +63 -9
- package/template/.agents/skills/remix/references/middleware-and-server.md +68 -6
- package/template/.agents/skills/remix/references/mixins-styling-events.md +2 -0
- package/template/.agents/skills/remix/references/routing-and-controllers.md +8 -8
- package/template/.agents/skills/remix/references/testing-patterns.md +19 -18
- package/template/AGENTS.md +6 -3
- package/template/README.md +6 -3
- package/template/app/actions/controller.tsx +1 -1
- package/template/app/{ui → actions}/document.tsx +7 -5
- package/template/app/{ui/scaffold-home-page.tsx → actions/home-page.tsx} +1 -2
- package/template/app/actions/public/entry.ts +48 -0
- package/template/app/{assets → actions/public}/prompt-button.tsx +29 -25
- package/template/app/assets.ts +15 -2
- package/template/app/middleware/render.tsx +8 -2
- package/template/hmr.ts +46 -0
- package/template/package.json +1 -0
- package/template/server.ts +8 -1
- package/template/tsconfig.json +1 -1
- package/dist/lib/doctor/controller-fix-plans.d.ts +0 -4
- package/dist/lib/doctor/controller-fix-plans.d.ts.map +0 -1
- package/dist/lib/doctor/controller-fix-plans.js +0 -119
- package/dist/lib/doctor/controller-placeholders.d.ts +0 -3
- package/dist/lib/doctor/controller-placeholders.d.ts.map +0 -1
- package/dist/lib/doctor/controller-placeholders.js +0 -52
- package/src/lib/doctor/controller-fix-plans.ts +0 -168
- package/src/lib/doctor/controller-placeholders.ts +0 -70
- package/template/app/assets/entry.ts +0 -17
|
@@ -16,7 +16,7 @@ export function render() {
|
|
|
16
16
|
frameSrc: request.url,
|
|
17
17
|
signal: request.signal,
|
|
18
18
|
resolveFrame: (src) => resolveFrame(router, request, src),
|
|
19
|
-
// Server rendering turns client entries into browser module URLs.
|
|
19
|
+
// Server rendering turns client entries into browser module URLs and preloads.
|
|
20
20
|
async resolveClientEntry(entryId, component) {
|
|
21
21
|
if (!entryId.startsWith('file://')) {
|
|
22
22
|
throw new Error(
|
|
@@ -24,9 +24,15 @@ export function render() {
|
|
|
24
24
|
)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
let [href, preloads] = await Promise.all([
|
|
28
|
+
assetServer.getHref(entryId),
|
|
29
|
+
assetServer.getPreloads(entryId),
|
|
30
|
+
])
|
|
31
|
+
|
|
27
32
|
return {
|
|
28
|
-
href
|
|
33
|
+
href,
|
|
29
34
|
exportName: entryId.split('#')[1] || component.name || titleCaseFileName(entryId),
|
|
35
|
+
preloads,
|
|
30
36
|
}
|
|
31
37
|
},
|
|
32
38
|
})
|
package/template/hmr.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as http from 'node:http'
|
|
2
|
+
|
|
3
|
+
import { createFetchProxy } from 'remix/fetch-proxy'
|
|
4
|
+
import { createHmrReadyFetch, run } from 'remix/node-hmr'
|
|
5
|
+
import { createRequestListener } from 'remix/node-fetch-server'
|
|
6
|
+
|
|
7
|
+
const hmrProxyPort = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 44100
|
|
8
|
+
const hmrEventPort = process.env.HMR_PORT
|
|
9
|
+
? Number.parseInt(process.env.HMR_PORT, 10)
|
|
10
|
+
: hmrProxyPort + 1
|
|
11
|
+
const appPort = process.env.APP_PORT ? Number.parseInt(process.env.APP_PORT, 10) : hmrEventPort + 1
|
|
12
|
+
|
|
13
|
+
const hmrRunner = run('server.ts', {
|
|
14
|
+
env: {
|
|
15
|
+
...process.env,
|
|
16
|
+
PORT: String(appPort),
|
|
17
|
+
HMR_PROXY_PORT: String(hmrProxyPort),
|
|
18
|
+
},
|
|
19
|
+
nodeArgs: ['--import', 'remix/node-tsx', '--import', 'remix/ui-hmr/node'],
|
|
20
|
+
browserHmrChannel: { port: hmrEventPort },
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const server = http.createServer(
|
|
24
|
+
createRequestListener(
|
|
25
|
+
createHmrReadyFetch(
|
|
26
|
+
hmrRunner,
|
|
27
|
+
createFetchProxy(`http://127.0.0.1:${appPort}`, {
|
|
28
|
+
xForwardedHeaders: true,
|
|
29
|
+
}),
|
|
30
|
+
),
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
server.listen(hmrProxyPort, '127.0.0.1')
|
|
35
|
+
|
|
36
|
+
let shuttingDown = false
|
|
37
|
+
|
|
38
|
+
function shutdown() {
|
|
39
|
+
if (shuttingDown) return
|
|
40
|
+
shuttingDown = true
|
|
41
|
+
server.close(() => hmrRunner.close().finally(() => process.exit(0)))
|
|
42
|
+
server.closeAllConnections()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
process.on('SIGINT', shutdown)
|
|
46
|
+
process.on('SIGTERM', shutdown)
|
package/template/package.json
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "NODE_ENV=development node --watch --import remix/node-tsx server.ts",
|
|
7
|
+
"hmr": "NODE_ENV=development node hmr.ts",
|
|
7
8
|
"start": "NODE_ENV=production node --import remix/node-tsx server.ts",
|
|
8
9
|
"test": "NODE_ENV=test node --import remix/node-tsx --test",
|
|
9
10
|
"typecheck": "tsc --noEmit"
|
package/template/server.ts
CHANGED
|
@@ -4,6 +4,9 @@ import { createRequestListener } from 'remix/node-fetch-server'
|
|
|
4
4
|
import { router } from './app/router.ts'
|
|
5
5
|
|
|
6
6
|
const port = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 44100
|
|
7
|
+
const hmrProxyPort = process.env.HMR_PROXY_PORT
|
|
8
|
+
? Number.parseInt(process.env.HMR_PROXY_PORT, 10)
|
|
9
|
+
: null
|
|
7
10
|
|
|
8
11
|
const server = http.createServer(
|
|
9
12
|
createRequestListener(async (request) => {
|
|
@@ -19,7 +22,11 @@ const server = http.createServer(
|
|
|
19
22
|
)
|
|
20
23
|
|
|
21
24
|
server.listen(port, () => {
|
|
22
|
-
|
|
25
|
+
if (process.env.REMIX_NODE_HMR) {
|
|
26
|
+
import('remix/node-hmr/runtime').then((nodeHmr) => nodeHmr.emitServerReady())
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log(`Server listening on http://localhost:${hmrProxyPort ?? port}`)
|
|
23
30
|
})
|
|
24
31
|
|
|
25
32
|
let shuttingDown = false
|
package/template/tsconfig.json
CHANGED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import type { ControllerOwnership, OwnershipRouteNode } from '../controller-ownership.ts';
|
|
2
|
-
import type { DoctorFixPlan } from './types.ts';
|
|
3
|
-
export declare function getControllerFixPlans(appRoot: string, tree: OwnershipRouteNode[], ownership: ControllerOwnership): Promise<DoctorFixPlan[]>;
|
|
4
|
-
//# sourceMappingURL=controller-fix-plans.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"controller-fix-plans.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/controller-fix-plans.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,mBAAmB,EAEnB,kBAAkB,EACnB,MAAM,4BAA4B,CAAA;AAGnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAI/C,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,kBAAkB,EAAE,EAC1B,SAAS,EAAE,mBAAmB,GAC7B,OAAO,CAAC,aAAa,EAAE,CAAC,CAkD1B"}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs/promises';
|
|
2
|
-
import * as path from 'node:path';
|
|
3
|
-
import { getOwnerCandidateForExtension, getOwnerFileExtension, } from "../controller-files.js";
|
|
4
|
-
import { ROOT_ROUTE_NAME } from "../controller-ownership.js";
|
|
5
|
-
import { renderControllerPlaceholder } from "./controller-placeholders.js";
|
|
6
|
-
const OWNER_EXTENSION_PRIORITY = ['.tsx', '.ts', '.jsx', '.js'];
|
|
7
|
-
export async function getControllerFixPlans(appRoot, tree, ownership) {
|
|
8
|
-
let routeNodesByName = getRouteNodesByName(tree);
|
|
9
|
-
let hasTsconfig = await pathExists(path.join(appRoot, 'tsconfig.json'));
|
|
10
|
-
let resolvedEntryPathByRouteName = new Map();
|
|
11
|
-
for (let subtree of ownership.subtrees) {
|
|
12
|
-
if (subtree.actualEntryPath != null) {
|
|
13
|
-
resolvedEntryPathByRouteName.set(subtree.routeName, subtree.actualEntryPath);
|
|
14
|
-
continue;
|
|
15
|
-
}
|
|
16
|
-
let fixCode = getFixCodeForSubtree(subtree);
|
|
17
|
-
if (fixCode == null) {
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
let extension = inferOwnerExtension(subtree, ownership, hasTsconfig);
|
|
21
|
-
let targetPath = getOwnerCandidateForExtension(subtree.entryCandidates, extension) ?? subtree.entryDisplayPath;
|
|
22
|
-
resolvedEntryPathByRouteName.set(subtree.routeName, targetPath);
|
|
23
|
-
}
|
|
24
|
-
let fixPlans = [];
|
|
25
|
-
for (let subtree of ownership.subtrees) {
|
|
26
|
-
let code = getFixCodeForSubtree(subtree);
|
|
27
|
-
if (code == null) {
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
let routeNode = routeNodesByName.get(subtree.routeName);
|
|
31
|
-
let entryPath = resolvedEntryPathByRouteName.get(subtree.routeName);
|
|
32
|
-
if (routeNode == null || entryPath == null) {
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
let contents = renderControllerPlaceholder(routeNode, entryPath);
|
|
36
|
-
fixPlans.push({
|
|
37
|
-
code,
|
|
38
|
-
contents,
|
|
39
|
-
kind: 'create-file',
|
|
40
|
-
path: entryPath,
|
|
41
|
-
routeName: subtree.routeName,
|
|
42
|
-
suite: 'actions',
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
return fixPlans;
|
|
46
|
-
}
|
|
47
|
-
function getFixCodeForSubtree(subtree) {
|
|
48
|
-
if (subtree.actualEntryPaths.length > 1) {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
if (subtree.actualEntryPath != null) {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
return subtree.claimedFilePaths.length > 0 ? 'incomplete-controller' : 'missing-owner';
|
|
55
|
-
}
|
|
56
|
-
function inferOwnerExtension(subtree, ownership, hasTsconfig) {
|
|
57
|
-
let subtreeExtension = getMostCommonOwnerExtension(subtree.claimedRouteLocalFilePaths);
|
|
58
|
-
if (subtreeExtension != null) {
|
|
59
|
-
return subtreeExtension;
|
|
60
|
-
}
|
|
61
|
-
let projectExtension = getMostCommonOwnerExtension([...ownership.scan.controllerEntryPaths]);
|
|
62
|
-
if (projectExtension != null) {
|
|
63
|
-
return projectExtension;
|
|
64
|
-
}
|
|
65
|
-
return hasTsconfig ? '.tsx' : '.js';
|
|
66
|
-
}
|
|
67
|
-
function getMostCommonOwnerExtension(filePaths) {
|
|
68
|
-
let counts = new Map();
|
|
69
|
-
for (let filePath of filePaths) {
|
|
70
|
-
let extension = getOwnerFileExtension(filePath);
|
|
71
|
-
if (extension == null) {
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
counts.set(extension, (counts.get(extension) ?? 0) + 1);
|
|
75
|
-
}
|
|
76
|
-
if (counts.size === 0) {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
let bestExtension = null;
|
|
80
|
-
let bestCount = -1;
|
|
81
|
-
for (let extension of OWNER_EXTENSION_PRIORITY) {
|
|
82
|
-
let count = counts.get(extension) ?? 0;
|
|
83
|
-
if (count > bestCount) {
|
|
84
|
-
bestCount = count;
|
|
85
|
-
bestExtension = extension;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return bestExtension;
|
|
89
|
-
}
|
|
90
|
-
function getRouteNodesByName(tree) {
|
|
91
|
-
let routeNodesByName = new Map();
|
|
92
|
-
routeNodesByName.set(ROOT_ROUTE_NAME, {
|
|
93
|
-
children: tree,
|
|
94
|
-
key: ROOT_ROUTE_NAME,
|
|
95
|
-
kind: 'group',
|
|
96
|
-
name: ROOT_ROUTE_NAME,
|
|
97
|
-
});
|
|
98
|
-
function visit(nodes) {
|
|
99
|
-
for (let node of nodes) {
|
|
100
|
-
routeNodesByName.set(node.name, node);
|
|
101
|
-
visit(node.children);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
visit(tree);
|
|
105
|
-
return routeNodesByName;
|
|
106
|
-
}
|
|
107
|
-
async function pathExists(filePath) {
|
|
108
|
-
try {
|
|
109
|
-
await fs.access(filePath);
|
|
110
|
-
return true;
|
|
111
|
-
}
|
|
112
|
-
catch (error) {
|
|
113
|
-
let nodeError = error;
|
|
114
|
-
if (nodeError.code === 'ENOENT') {
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
throw error;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"controller-placeholders.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/controller-placeholders.ts"],"names":[],"mappings":"AAGA,OAAO,EAAmB,KAAK,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAErF,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,kBAAkB,EAC7B,SAAS,EAAE,MAAM,GAChB,MAAM,CAyBR"}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import * as path from 'node:path';
|
|
2
|
-
import { getOwnerFileExtension } from "../controller-files.js";
|
|
3
|
-
import { ROOT_ROUTE_NAME } from "../controller-ownership.js";
|
|
4
|
-
export function renderControllerPlaceholder(routeNode, entryPath) {
|
|
5
|
-
let extension = getOwnerFileExtension(entryPath);
|
|
6
|
-
let lines = [];
|
|
7
|
-
let actionEntries = routeNode.children
|
|
8
|
-
.filter((childNode) => childNode.kind === 'route')
|
|
9
|
-
.map((childNode) => renderControllerActionEntry(childNode));
|
|
10
|
-
if (extension === '.js' || extension === '.jsx') {
|
|
11
|
-
lines.push('export default {', ' actions: {');
|
|
12
|
-
lines.push(...actionEntries);
|
|
13
|
-
lines.push(' },', '}', '');
|
|
14
|
-
return lines.join('\n');
|
|
15
|
-
}
|
|
16
|
-
let routeExpression = getRouteAccessExpression(routeNode.name);
|
|
17
|
-
let routesImportPath = getRelativeImportPath(entryPath, 'app/routes.ts');
|
|
18
|
-
lines.push(`import { createController } from 'remix/router'`, '');
|
|
19
|
-
lines.push(`import { routes } from '${routesImportPath}'`);
|
|
20
|
-
lines.push('', `export default createController(${routeExpression}, {`, ' actions: {');
|
|
21
|
-
lines.push(...actionEntries);
|
|
22
|
-
lines.push(' },', '})', '');
|
|
23
|
-
return lines.join('\n');
|
|
24
|
-
}
|
|
25
|
-
function renderControllerActionEntry(childNode) {
|
|
26
|
-
return [
|
|
27
|
-
` ${formatObjectKey(childNode.key)}() {`,
|
|
28
|
-
` return new Response(${JSON.stringify(`TODO: implement routes.${childNode.name}`)})`,
|
|
29
|
-
` },`,
|
|
30
|
-
].join('\n');
|
|
31
|
-
}
|
|
32
|
-
function getRouteAccessExpression(routeName) {
|
|
33
|
-
if (routeName === ROOT_ROUTE_NAME) {
|
|
34
|
-
return 'routes';
|
|
35
|
-
}
|
|
36
|
-
let segments = routeName.split('.');
|
|
37
|
-
let expression = 'routes';
|
|
38
|
-
for (let segment of segments) {
|
|
39
|
-
expression += isIdentifier(segment) ? `.${segment}` : `[${JSON.stringify(segment)}]`;
|
|
40
|
-
}
|
|
41
|
-
return expression;
|
|
42
|
-
}
|
|
43
|
-
function getRelativeImportPath(fromFilePath, toFilePath) {
|
|
44
|
-
let relativePath = path.posix.relative(path.posix.dirname(fromFilePath), toFilePath);
|
|
45
|
-
return relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
|
|
46
|
-
}
|
|
47
|
-
function formatObjectKey(key) {
|
|
48
|
-
return isIdentifier(key) ? key : JSON.stringify(key);
|
|
49
|
-
}
|
|
50
|
-
function isIdentifier(value) {
|
|
51
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value);
|
|
52
|
-
}
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs/promises'
|
|
2
|
-
import * as path from 'node:path'
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
getOwnerCandidateForExtension,
|
|
6
|
-
getOwnerFileExtension,
|
|
7
|
-
type OwnerFileExtension,
|
|
8
|
-
} from '../controller-files.ts'
|
|
9
|
-
import type {
|
|
10
|
-
ControllerOwnership,
|
|
11
|
-
OwnedSubtree,
|
|
12
|
-
OwnershipRouteNode,
|
|
13
|
-
} from '../controller-ownership.ts'
|
|
14
|
-
import { ROOT_ROUTE_NAME } from '../controller-ownership.ts'
|
|
15
|
-
import { renderControllerPlaceholder } from './controller-placeholders.ts'
|
|
16
|
-
import type { DoctorFixPlan } from './types.ts'
|
|
17
|
-
|
|
18
|
-
const OWNER_EXTENSION_PRIORITY: OwnerFileExtension[] = ['.tsx', '.ts', '.jsx', '.js']
|
|
19
|
-
|
|
20
|
-
export async function getControllerFixPlans(
|
|
21
|
-
appRoot: string,
|
|
22
|
-
tree: OwnershipRouteNode[],
|
|
23
|
-
ownership: ControllerOwnership,
|
|
24
|
-
): Promise<DoctorFixPlan[]> {
|
|
25
|
-
let routeNodesByName = getRouteNodesByName(tree)
|
|
26
|
-
let hasTsconfig = await pathExists(path.join(appRoot, 'tsconfig.json'))
|
|
27
|
-
let resolvedEntryPathByRouteName = new Map<string, string>()
|
|
28
|
-
|
|
29
|
-
for (let subtree of ownership.subtrees) {
|
|
30
|
-
if (subtree.actualEntryPath != null) {
|
|
31
|
-
resolvedEntryPathByRouteName.set(subtree.routeName, subtree.actualEntryPath)
|
|
32
|
-
continue
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
let fixCode = getFixCodeForSubtree(subtree)
|
|
36
|
-
if (fixCode == null) {
|
|
37
|
-
continue
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
let extension = inferOwnerExtension(subtree, ownership, hasTsconfig)
|
|
41
|
-
let targetPath =
|
|
42
|
-
getOwnerCandidateForExtension(subtree.entryCandidates, extension) ?? subtree.entryDisplayPath
|
|
43
|
-
|
|
44
|
-
resolvedEntryPathByRouteName.set(subtree.routeName, targetPath)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
let fixPlans: DoctorFixPlan[] = []
|
|
48
|
-
|
|
49
|
-
for (let subtree of ownership.subtrees) {
|
|
50
|
-
let code = getFixCodeForSubtree(subtree)
|
|
51
|
-
if (code == null) {
|
|
52
|
-
continue
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
let routeNode = routeNodesByName.get(subtree.routeName)
|
|
56
|
-
let entryPath = resolvedEntryPathByRouteName.get(subtree.routeName)
|
|
57
|
-
if (routeNode == null || entryPath == null) {
|
|
58
|
-
continue
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
let contents = renderControllerPlaceholder(routeNode, entryPath)
|
|
62
|
-
|
|
63
|
-
fixPlans.push({
|
|
64
|
-
code,
|
|
65
|
-
contents,
|
|
66
|
-
kind: 'create-file',
|
|
67
|
-
path: entryPath,
|
|
68
|
-
routeName: subtree.routeName,
|
|
69
|
-
suite: 'actions',
|
|
70
|
-
})
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return fixPlans
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function getFixCodeForSubtree(subtree: OwnedSubtree): DoctorFixPlan['code'] | null {
|
|
77
|
-
if (subtree.actualEntryPaths.length > 1) {
|
|
78
|
-
return null
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (subtree.actualEntryPath != null) {
|
|
82
|
-
return null
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return subtree.claimedFilePaths.length > 0 ? 'incomplete-controller' : 'missing-owner'
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function inferOwnerExtension(
|
|
89
|
-
subtree: OwnedSubtree,
|
|
90
|
-
ownership: ControllerOwnership,
|
|
91
|
-
hasTsconfig: boolean,
|
|
92
|
-
): OwnerFileExtension {
|
|
93
|
-
let subtreeExtension = getMostCommonOwnerExtension(subtree.claimedRouteLocalFilePaths)
|
|
94
|
-
if (subtreeExtension != null) {
|
|
95
|
-
return subtreeExtension
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
let projectExtension = getMostCommonOwnerExtension([...ownership.scan.controllerEntryPaths])
|
|
99
|
-
if (projectExtension != null) {
|
|
100
|
-
return projectExtension
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return hasTsconfig ? '.tsx' : '.js'
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function getMostCommonOwnerExtension(filePaths: string[]): OwnerFileExtension | null {
|
|
107
|
-
let counts = new Map<OwnerFileExtension, number>()
|
|
108
|
-
|
|
109
|
-
for (let filePath of filePaths) {
|
|
110
|
-
let extension = getOwnerFileExtension(filePath)
|
|
111
|
-
if (extension == null) {
|
|
112
|
-
continue
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
counts.set(extension, (counts.get(extension) ?? 0) + 1)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (counts.size === 0) {
|
|
119
|
-
return null
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
let bestExtension: OwnerFileExtension | null = null
|
|
123
|
-
let bestCount = -1
|
|
124
|
-
|
|
125
|
-
for (let extension of OWNER_EXTENSION_PRIORITY) {
|
|
126
|
-
let count = counts.get(extension) ?? 0
|
|
127
|
-
if (count > bestCount) {
|
|
128
|
-
bestCount = count
|
|
129
|
-
bestExtension = extension
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return bestExtension
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function getRouteNodesByName(tree: OwnershipRouteNode[]): Map<string, OwnershipRouteNode> {
|
|
137
|
-
let routeNodesByName = new Map<string, OwnershipRouteNode>()
|
|
138
|
-
routeNodesByName.set(ROOT_ROUTE_NAME, {
|
|
139
|
-
children: tree,
|
|
140
|
-
key: ROOT_ROUTE_NAME,
|
|
141
|
-
kind: 'group',
|
|
142
|
-
name: ROOT_ROUTE_NAME,
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
function visit(nodes: OwnershipRouteNode[]): void {
|
|
146
|
-
for (let node of nodes) {
|
|
147
|
-
routeNodesByName.set(node.name, node)
|
|
148
|
-
visit(node.children)
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
visit(tree)
|
|
153
|
-
return routeNodesByName
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
async function pathExists(filePath: string): Promise<boolean> {
|
|
157
|
-
try {
|
|
158
|
-
await fs.access(filePath)
|
|
159
|
-
return true
|
|
160
|
-
} catch (error) {
|
|
161
|
-
let nodeError = error as NodeJS.ErrnoException
|
|
162
|
-
if (nodeError.code === 'ENOENT') {
|
|
163
|
-
return false
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
throw error
|
|
167
|
-
}
|
|
168
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import * as path from 'node:path'
|
|
2
|
-
|
|
3
|
-
import { getOwnerFileExtension } from '../controller-files.ts'
|
|
4
|
-
import { ROOT_ROUTE_NAME, type OwnershipRouteNode } from '../controller-ownership.ts'
|
|
5
|
-
|
|
6
|
-
export function renderControllerPlaceholder(
|
|
7
|
-
routeNode: OwnershipRouteNode,
|
|
8
|
-
entryPath: string,
|
|
9
|
-
): string {
|
|
10
|
-
let extension = getOwnerFileExtension(entryPath)
|
|
11
|
-
let lines: string[] = []
|
|
12
|
-
let actionEntries = routeNode.children
|
|
13
|
-
.filter((childNode) => childNode.kind === 'route')
|
|
14
|
-
.map((childNode) => renderControllerActionEntry(childNode))
|
|
15
|
-
|
|
16
|
-
if (extension === '.js' || extension === '.jsx') {
|
|
17
|
-
lines.push('export default {', ' actions: {')
|
|
18
|
-
lines.push(...actionEntries)
|
|
19
|
-
lines.push(' },', '}', '')
|
|
20
|
-
return lines.join('\n')
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
let routeExpression = getRouteAccessExpression(routeNode.name)
|
|
24
|
-
let routesImportPath = getRelativeImportPath(entryPath, 'app/routes.ts')
|
|
25
|
-
|
|
26
|
-
lines.push(`import { createController } from 'remix/router'`, '')
|
|
27
|
-
lines.push(`import { routes } from '${routesImportPath}'`)
|
|
28
|
-
|
|
29
|
-
lines.push('', `export default createController(${routeExpression}, {`, ' actions: {')
|
|
30
|
-
lines.push(...actionEntries)
|
|
31
|
-
lines.push(' },', '})', '')
|
|
32
|
-
|
|
33
|
-
return lines.join('\n')
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function renderControllerActionEntry(childNode: OwnershipRouteNode): string {
|
|
37
|
-
return [
|
|
38
|
-
` ${formatObjectKey(childNode.key)}() {`,
|
|
39
|
-
` return new Response(${JSON.stringify(`TODO: implement routes.${childNode.name}`)})`,
|
|
40
|
-
` },`,
|
|
41
|
-
].join('\n')
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function getRouteAccessExpression(routeName: string): string {
|
|
45
|
-
if (routeName === ROOT_ROUTE_NAME) {
|
|
46
|
-
return 'routes'
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
let segments = routeName.split('.')
|
|
50
|
-
let expression = 'routes'
|
|
51
|
-
|
|
52
|
-
for (let segment of segments) {
|
|
53
|
-
expression += isIdentifier(segment) ? `.${segment}` : `[${JSON.stringify(segment)}]`
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return expression
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function getRelativeImportPath(fromFilePath: string, toFilePath: string): string {
|
|
60
|
-
let relativePath = path.posix.relative(path.posix.dirname(fromFilePath), toFilePath)
|
|
61
|
-
return relativePath.startsWith('.') ? relativePath : `./${relativePath}`
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function formatObjectKey(key: string): string {
|
|
65
|
-
return isIdentifier(key) ? key : JSON.stringify(key)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function isIdentifier(value: string): boolean {
|
|
69
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value)
|
|
70
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { run } from 'remix/ui'
|
|
2
|
-
|
|
3
|
-
run({
|
|
4
|
-
async loadModule(moduleUrl, exportName) {
|
|
5
|
-
let mod = await import(moduleUrl)
|
|
6
|
-
return mod[exportName]
|
|
7
|
-
},
|
|
8
|
-
async resolveFrame(src, signal) {
|
|
9
|
-
let response = await fetch(src, { headers: { Accept: 'text/html' }, signal })
|
|
10
|
-
if (!response.ok) {
|
|
11
|
-
return `<pre>Frame error: ${response.status} ${response.statusText}</pre>`
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
if (response.body) return response.body
|
|
15
|
-
return await response.text()
|
|
16
|
-
},
|
|
17
|
-
})
|