@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,195 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as process from 'node:process';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { inspectControllerOwnership } from "./controller-ownership.js";
|
|
7
|
+
import { routeMapLoaderFailed, routeMapLoaderInvalidJson, routeMapLoaderSignal, routeOwnerPlanUnresolved, routesFileNotFound, } from "./errors.js";
|
|
8
|
+
export async function loadRouteMap(cwd = process.cwd()) {
|
|
9
|
+
let manifest = await loadRouteManifest(cwd);
|
|
10
|
+
let ownership = await inspectControllerOwnership(manifest.appRoot, manifest.tree);
|
|
11
|
+
let tree = decorateRouteTree(manifest.tree, ownership.subtrees);
|
|
12
|
+
return {
|
|
13
|
+
appRoot: manifest.appRoot,
|
|
14
|
+
routesFile: manifest.routesFile,
|
|
15
|
+
tree,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export async function loadRouteManifest(cwd = process.cwd()) {
|
|
19
|
+
let appRoot = await findRemixAppRoot(cwd);
|
|
20
|
+
return loadRouteManifestFromAppRoot(appRoot);
|
|
21
|
+
}
|
|
22
|
+
export async function loadRouteManifestFromAppRoot(appRoot) {
|
|
23
|
+
let routesFile = path.join(appRoot, 'app', 'routes.ts');
|
|
24
|
+
let tree = await loadRawRouteMap(appRoot, routesFile);
|
|
25
|
+
return {
|
|
26
|
+
appRoot,
|
|
27
|
+
routesFile,
|
|
28
|
+
tree,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
async function loadRawRouteMap(appRoot, routesFile) {
|
|
32
|
+
let workerPath = getRouteMapWorkerPath();
|
|
33
|
+
let child = spawn(process.execPath, [workerPath, routesFile], {
|
|
34
|
+
cwd: appRoot,
|
|
35
|
+
env: createRouteMapWorkerEnv(),
|
|
36
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
37
|
+
});
|
|
38
|
+
let stdout = '';
|
|
39
|
+
let stderr = '';
|
|
40
|
+
child.stdout.setEncoding('utf8');
|
|
41
|
+
child.stderr.setEncoding('utf8');
|
|
42
|
+
child.stdout.on('data', (chunk) => {
|
|
43
|
+
stdout += chunk;
|
|
44
|
+
});
|
|
45
|
+
child.stderr.on('data', (chunk) => {
|
|
46
|
+
stderr += chunk;
|
|
47
|
+
});
|
|
48
|
+
let exitResult = await new Promise((resolve, reject) => {
|
|
49
|
+
child.once('error', reject);
|
|
50
|
+
child.once('close', (code, signal) => {
|
|
51
|
+
resolve({ code, signal });
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
if (exitResult.signal != null) {
|
|
55
|
+
throw routeMapLoaderSignal(exitResult.signal);
|
|
56
|
+
}
|
|
57
|
+
if (exitResult.code !== 0) {
|
|
58
|
+
let message = stderr.trim();
|
|
59
|
+
if (message.length === 0) {
|
|
60
|
+
message = 'Route-map loader failed.';
|
|
61
|
+
}
|
|
62
|
+
throw routeMapLoaderFailed(message);
|
|
63
|
+
}
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = JSON.parse(stdout);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
throw routeMapLoaderInvalidJson();
|
|
70
|
+
}
|
|
71
|
+
return assertRawRouteTree(parsed);
|
|
72
|
+
}
|
|
73
|
+
function decorateRouteTree(rawTree, subtrees, parentSegments = []) {
|
|
74
|
+
let subtreesByRouteName = new Map(subtrees.map((subtree) => [subtree.routeName, subtree]));
|
|
75
|
+
return decorateRouteTreeWithLookup(rawTree, subtreesByRouteName, parentSegments);
|
|
76
|
+
}
|
|
77
|
+
function decorateRouteTreeWithLookup(rawTree, subtreesByRouteName, parentSegments = []) {
|
|
78
|
+
return rawTree.map((rawNode) => {
|
|
79
|
+
let owner = getRouteOwner(rawNode, parentSegments, subtreesByRouteName);
|
|
80
|
+
let nextParentSegments = rawNode.kind === 'group' ? [...parentSegments, rawNode.key] : parentSegments;
|
|
81
|
+
return {
|
|
82
|
+
children: rawNode.kind === 'group'
|
|
83
|
+
? decorateRouteTreeWithLookup(rawNode.children, subtreesByRouteName, nextParentSegments)
|
|
84
|
+
: [],
|
|
85
|
+
key: rawNode.key,
|
|
86
|
+
kind: rawNode.kind,
|
|
87
|
+
method: rawNode.method,
|
|
88
|
+
name: rawNode.name,
|
|
89
|
+
owner,
|
|
90
|
+
pattern: rawNode.pattern,
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function getRouteOwner(rawNode, parentSegments, subtreesByRouteName) {
|
|
95
|
+
let ownerRouteName = rawNode.kind === 'group'
|
|
96
|
+
? rawNode.name
|
|
97
|
+
: parentSegments.length === 0
|
|
98
|
+
? rawNode.name
|
|
99
|
+
: parentSegments.join('.');
|
|
100
|
+
let subtree = subtreesByRouteName.get(ownerRouteName);
|
|
101
|
+
if (subtree == null) {
|
|
102
|
+
throw routeOwnerPlanUnresolved(rawNode.name);
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
exists: subtree.actualEntryPath != null,
|
|
106
|
+
kind: subtree.kind,
|
|
107
|
+
path: subtree.actualEntryPath ?? subtree.entryDisplayPath,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function assertRawRouteTree(value) {
|
|
111
|
+
if (!Array.isArray(value)) {
|
|
112
|
+
throw new Error('Route-map loader returned an invalid tree.');
|
|
113
|
+
}
|
|
114
|
+
return value.map((entry) => assertRawRouteTreeNode(entry));
|
|
115
|
+
}
|
|
116
|
+
function assertRawRouteTreeNode(value) {
|
|
117
|
+
if (typeof value !== 'object' || value == null) {
|
|
118
|
+
throw new Error('Route-map loader returned an invalid route node.');
|
|
119
|
+
}
|
|
120
|
+
let key = Reflect.get(value, 'key');
|
|
121
|
+
let name = Reflect.get(value, 'name');
|
|
122
|
+
let kind = Reflect.get(value, 'kind');
|
|
123
|
+
let children = Reflect.get(value, 'children');
|
|
124
|
+
if (typeof key !== 'string' || typeof name !== 'string') {
|
|
125
|
+
throw new Error('Route-map loader returned a route node without a valid name.');
|
|
126
|
+
}
|
|
127
|
+
if (kind !== 'group' && kind !== 'route') {
|
|
128
|
+
throw new Error(`Route-map loader returned an unknown node kind for "${name}".`);
|
|
129
|
+
}
|
|
130
|
+
if (!Array.isArray(children)) {
|
|
131
|
+
throw new Error(`Route-map loader returned invalid children for "${name}".`);
|
|
132
|
+
}
|
|
133
|
+
if (kind === 'group') {
|
|
134
|
+
return {
|
|
135
|
+
children: children.map((child) => assertRawRouteTreeNode(child)),
|
|
136
|
+
key,
|
|
137
|
+
kind,
|
|
138
|
+
name,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
let method = Reflect.get(value, 'method');
|
|
142
|
+
let pattern = Reflect.get(value, 'pattern');
|
|
143
|
+
if (typeof method !== 'string' || typeof pattern !== 'string') {
|
|
144
|
+
throw new Error(`Route-map loader returned an invalid route leaf for "${name}".`);
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
children: [],
|
|
148
|
+
key,
|
|
149
|
+
kind,
|
|
150
|
+
method,
|
|
151
|
+
name,
|
|
152
|
+
pattern,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
async function findRemixAppRoot(startDir) {
|
|
156
|
+
let currentDir = path.resolve(startDir);
|
|
157
|
+
while (true) {
|
|
158
|
+
if (await pathExists(path.join(currentDir, 'app', 'routes.ts'))) {
|
|
159
|
+
return currentDir;
|
|
160
|
+
}
|
|
161
|
+
let parentDir = path.dirname(currentDir);
|
|
162
|
+
if (parentDir === currentDir) {
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
currentDir = parentDir;
|
|
166
|
+
}
|
|
167
|
+
throw routesFileNotFound(startDir);
|
|
168
|
+
}
|
|
169
|
+
async function pathExists(filePath) {
|
|
170
|
+
try {
|
|
171
|
+
await fs.access(filePath);
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
let nodeError = error;
|
|
176
|
+
if (nodeError.code === 'ENOENT') {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function getRouteMapWorkerPath() {
|
|
183
|
+
let currentFilePath = fileURLToPath(import.meta.url);
|
|
184
|
+
let extension = currentFilePath.endsWith('.ts') ? '.ts' : '.js';
|
|
185
|
+
return fileURLToPath(new URL(`./load-route-map-worker${extension}`, import.meta.url));
|
|
186
|
+
}
|
|
187
|
+
function createRouteMapWorkerEnv() {
|
|
188
|
+
let env = { ...process.env };
|
|
189
|
+
for (let key of Object.keys(env)) {
|
|
190
|
+
if (key.startsWith('NODE_TEST_')) {
|
|
191
|
+
delete env[key];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return env;
|
|
195
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare function configureColors(options: {
|
|
2
|
+
disabled: boolean;
|
|
3
|
+
}): void;
|
|
4
|
+
export declare function bold(text: string, target?: NodeJS.WriteStream): string;
|
|
5
|
+
export declare function lightGreen(text: string, target?: NodeJS.WriteStream): string;
|
|
6
|
+
export declare function lightBlue(text: string, target?: NodeJS.WriteStream): string;
|
|
7
|
+
export declare function boldLightBlue(text: string, target?: NodeJS.WriteStream): string;
|
|
8
|
+
export declare function lightGray(text: string, target?: NodeJS.WriteStream): string;
|
|
9
|
+
export declare function lightMagenta(text: string, target?: NodeJS.WriteStream): string;
|
|
10
|
+
export declare function lightRed(text: string, target?: NodeJS.WriteStream): string;
|
|
11
|
+
export declare function lightYellow(text: string, target?: NodeJS.WriteStream): string;
|
|
12
|
+
export declare function reset(target: NodeJS.WriteStream): string;
|
|
13
|
+
export declare function remixWordmark(target?: NodeJS.WriteStream): string;
|
|
14
|
+
export declare function clearCurrentLine(): string;
|
|
15
|
+
export declare function restoreTerminalFormatting(): void;
|
|
16
|
+
export declare function canUseAnsi(target: NodeJS.WriteStream): boolean;
|
|
17
|
+
//# sourceMappingURL=terminal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal.d.ts","sourceRoot":"","sources":["../../src/lib/terminal.ts"],"names":[],"mappings":"AAMA,wBAAgB,eAAe,CAAC,OAAO,EAAE;IAAE,QAAQ,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAEpE;AAED,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,CAAC,WAA4B,GAAG,MAAM,CAEtF;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,CAAC,WAA4B,GAAG,MAAM,CAE5F;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,CAAC,WAA4B,GAAG,MAAM,CAE3F;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,CAAC,WAA4B,GAAG,MAAM,CAE/F;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,CAAC,WAA4B,GAAG,MAAM,CAE3F;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,CAAC,WAA4B,GAAG,MAAM,CAE9F;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,CAAC,WAA4B,GAAG,MAAM,CAE1F;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,CAAC,WAA4B,GAAG,MAAM,CAE7F;AAED,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,WAAW,GAAG,MAAM,CAExD;AAED,wBAAgB,aAAa,CAAC,MAAM,GAAE,MAAM,CAAC,WAA4B,GAAG,MAAM,CAYjF;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,wBAAgB,yBAAyB,IAAI,IAAI,CAShD;AAUD,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,WAAW,GAAG,OAAO,CAE9D"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as process from 'node:process';
|
|
2
|
+
import { ansi, createTerminal, shouldUseColors } from '@remix-run/terminal';
|
|
3
|
+
let colorDisabledByFlag = false;
|
|
4
|
+
export function configureColors(options) {
|
|
5
|
+
colorDisabledByFlag = options.disabled;
|
|
6
|
+
}
|
|
7
|
+
export function bold(text, target = process.stdout) {
|
|
8
|
+
return paint(text, ansi.bold, target);
|
|
9
|
+
}
|
|
10
|
+
export function lightGreen(text, target = process.stdout) {
|
|
11
|
+
return paint(text, ansi.greenBright, target);
|
|
12
|
+
}
|
|
13
|
+
export function lightBlue(text, target = process.stdout) {
|
|
14
|
+
return paint(text, ansi.blueBright, target);
|
|
15
|
+
}
|
|
16
|
+
export function boldLightBlue(text, target = process.stdout) {
|
|
17
|
+
return isColorDisabled(target) ? text : `${ansi.bold}${ansi.blueBright}${text}${ansi.reset}`;
|
|
18
|
+
}
|
|
19
|
+
export function lightGray(text, target = process.stdout) {
|
|
20
|
+
return paint(text, ansi.gray, target);
|
|
21
|
+
}
|
|
22
|
+
export function lightMagenta(text, target = process.stdout) {
|
|
23
|
+
return paint(text, ansi.magentaBright, target);
|
|
24
|
+
}
|
|
25
|
+
export function lightRed(text, target = process.stdout) {
|
|
26
|
+
return paint(text, ansi.redBright, target);
|
|
27
|
+
}
|
|
28
|
+
export function lightYellow(text, target = process.stdout) {
|
|
29
|
+
return paint(text, ansi.yellowBright, target);
|
|
30
|
+
}
|
|
31
|
+
export function reset(target) {
|
|
32
|
+
return isColorDisabled(target) ? '' : ansi.reset;
|
|
33
|
+
}
|
|
34
|
+
export function remixWordmark(target = process.stdout) {
|
|
35
|
+
if (isColorDisabled(target)) {
|
|
36
|
+
return 'REMIX';
|
|
37
|
+
}
|
|
38
|
+
return [
|
|
39
|
+
paint('R', ansi.blueBright, target),
|
|
40
|
+
paint('E', ansi.greenBright, target),
|
|
41
|
+
paint('M', ansi.yellowBright, target),
|
|
42
|
+
paint('I', ansi.magentaBright, target),
|
|
43
|
+
paint('X', ansi.redBright, target),
|
|
44
|
+
].join('');
|
|
45
|
+
}
|
|
46
|
+
export function clearCurrentLine() {
|
|
47
|
+
return `\r${ansi.clearLine}`;
|
|
48
|
+
}
|
|
49
|
+
export function restoreTerminalFormatting() {
|
|
50
|
+
if (canUseAnsi(process.stdout)) {
|
|
51
|
+
createTerminal({ stdout: process.stdout }).write(ansi.reset);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (canUseAnsi(process.stderr)) {
|
|
55
|
+
createTerminal({ stderr: process.stderr }).error(ansi.reset);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function paint(text, colorCode, target) {
|
|
59
|
+
return isColorDisabled(target) ? text : `${colorCode}${text}${ansi.reset}`;
|
|
60
|
+
}
|
|
61
|
+
function isColorDisabled(target) {
|
|
62
|
+
return colorDisabledByFlag || !shouldUseColors({ stream: target });
|
|
63
|
+
}
|
|
64
|
+
export function canUseAnsi(target) {
|
|
65
|
+
return process.env.TERM !== 'dumb' && target.isTTY === true;
|
|
66
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remix-run/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Command-line interface for Remix",
|
|
5
|
+
"author": "Michael Jackson <mjijackson@gmail.com>",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
8
9
|
"url": "git+https://github.com/remix-run/remix.git",
|
|
9
10
|
"directory": "packages/cli"
|
|
10
11
|
},
|
|
11
|
-
"
|
|
12
|
-
|
|
12
|
+
"homepage": "https://github.com/remix-run/remix/tree/main/packages/cli#readme",
|
|
13
|
+
"files": [
|
|
14
|
+
"bootstrap",
|
|
15
|
+
"LICENSE",
|
|
16
|
+
"README.md",
|
|
17
|
+
"dist",
|
|
18
|
+
"src",
|
|
19
|
+
"!src/**/*.test.ts"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=24.3.0"
|
|
24
|
+
},
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"semver": "^7.7.4",
|
|
34
|
+
"@remix-run/test": "^0.3.0",
|
|
35
|
+
"@remix-run/terminal": "^0.1.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^24.6.0",
|
|
39
|
+
"@types/semver": "^7.5.8",
|
|
40
|
+
"@typescript/native-preview": "7.0.0-dev.20251125.1",
|
|
41
|
+
"@remix-run/assert": "^0.2.0"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"remix",
|
|
45
|
+
"cli",
|
|
46
|
+
"scaffold",
|
|
47
|
+
"project",
|
|
48
|
+
"command-line"
|
|
49
|
+
],
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsgo -p tsconfig.build.json",
|
|
52
|
+
"clean": "git clean -fdX",
|
|
53
|
+
"test": "remix-test --concurrency 1",
|
|
54
|
+
"typecheck": "tsgo --noEmit"
|
|
13
55
|
}
|
|
14
|
-
}
|
|
56
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { runRemix, type RunRemixOptions } from './lib/cli.ts'
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises'
|
|
2
|
+
import * as path from 'node:path'
|
|
3
|
+
import * as process from 'node:process'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import semver from 'semver'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
appNameUnavailable,
|
|
9
|
+
invalidPackageName,
|
|
10
|
+
targetDirectoryNotEmpty,
|
|
11
|
+
targetPathNotDirectory,
|
|
12
|
+
} from './errors.ts'
|
|
13
|
+
import { runProgressStep, type StepProgressReporter } from './reporter.ts'
|
|
14
|
+
|
|
15
|
+
const BOOTSTRAP_DIRECTORY = path.resolve(
|
|
16
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
17
|
+
'../../bootstrap',
|
|
18
|
+
)
|
|
19
|
+
const BOOTSTRAP_EXCLUDED_NAMES = new Set(['.gitkeep', 'node_modules'])
|
|
20
|
+
export const MINIMUM_SUPPORTED_NODE_VERSION = '24.3.0'
|
|
21
|
+
|
|
22
|
+
export interface BootstrapProjectOptions {
|
|
23
|
+
appName: string | null
|
|
24
|
+
cwd?: string
|
|
25
|
+
force: boolean
|
|
26
|
+
remixVersion?: string
|
|
27
|
+
targetDir: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type BootstrapProjectPhase =
|
|
31
|
+
| 'prepare-target-directory'
|
|
32
|
+
| 'generate-scaffold-files'
|
|
33
|
+
| 'finalize-package-json'
|
|
34
|
+
|
|
35
|
+
export interface BootstrappedProject {
|
|
36
|
+
appDisplayName: string
|
|
37
|
+
targetDir: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type BootstrapProgressReporter = StepProgressReporter<BootstrapProjectPhase>
|
|
41
|
+
|
|
42
|
+
interface BootstrapConfig {
|
|
43
|
+
appDisplayName: string
|
|
44
|
+
packageName: string
|
|
45
|
+
remixVersion: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type TemplateValues = Record<string, string>
|
|
49
|
+
|
|
50
|
+
export async function bootstrapProject(
|
|
51
|
+
options: BootstrapProjectOptions,
|
|
52
|
+
progress?: BootstrapProgressReporter,
|
|
53
|
+
): Promise<BootstrappedProject> {
|
|
54
|
+
let cwd = options.cwd ?? process.cwd()
|
|
55
|
+
let targetDir = path.resolve(cwd, options.targetDir)
|
|
56
|
+
let rawAppName = options.appName ?? path.basename(targetDir)
|
|
57
|
+
if (rawAppName.length === 0) {
|
|
58
|
+
throw appNameUnavailable(targetDir)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let config = {
|
|
62
|
+
appDisplayName: options.appName ?? humanizeName(rawAppName),
|
|
63
|
+
packageName: toPackageName(rawAppName),
|
|
64
|
+
remixVersion: readDefaultRemixVersion(options.remixVersion),
|
|
65
|
+
} satisfies BootstrapConfig
|
|
66
|
+
|
|
67
|
+
await runProgressStep(progress, 'prepare-target-directory', () =>
|
|
68
|
+
ensureTargetDirectory(targetDir, options.force),
|
|
69
|
+
)
|
|
70
|
+
await runProgressStep(progress, 'generate-scaffold-files', () =>
|
|
71
|
+
copyBootstrapDirectory({
|
|
72
|
+
sourceDir: BOOTSTRAP_DIRECTORY,
|
|
73
|
+
targetDir,
|
|
74
|
+
templateValues: createTemplateValues(config),
|
|
75
|
+
}),
|
|
76
|
+
)
|
|
77
|
+
await runProgressStep(progress, 'finalize-package-json', () =>
|
|
78
|
+
writeScaffoldPackageJson(targetDir, config),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
appDisplayName: config.appDisplayName,
|
|
83
|
+
targetDir,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readDefaultRemixVersion(runtimeVersion: string | undefined): string {
|
|
88
|
+
let overriddenVersion = process.env.REMIX_VERSION?.trim()
|
|
89
|
+
if (overriddenVersion) {
|
|
90
|
+
return overriddenVersion
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (runtimeVersion == null) {
|
|
94
|
+
return 'latest'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let validRuntimeVersion = semver.valid(runtimeVersion)
|
|
98
|
+
return validRuntimeVersion == null ? runtimeVersion : `^${validRuntimeVersion}`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function createTemplateValues(config: BootstrapConfig): TemplateValues {
|
|
102
|
+
return {
|
|
103
|
+
'%%RMX_APP_DISPLAY_NAME%%': config.appDisplayName,
|
|
104
|
+
'%%RMX_APP_DISPLAY_NAME_URI_COMPONENT%%': encodeURIComponent(config.appDisplayName),
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function writeScaffoldPackageJson(targetDir: string, config: BootstrapConfig): Promise<void> {
|
|
109
|
+
let packageJsonPath = path.join(targetDir, 'package.json')
|
|
110
|
+
let packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')) as {
|
|
111
|
+
dependencies?: Record<string, string>
|
|
112
|
+
devDependencies?: Record<string, string>
|
|
113
|
+
engines?: Record<string, string>
|
|
114
|
+
name?: string
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
packageJson.name = config.packageName
|
|
118
|
+
packageJson.dependencies = {
|
|
119
|
+
...packageJson.dependencies,
|
|
120
|
+
remix: config.remixVersion,
|
|
121
|
+
tsx: 'latest',
|
|
122
|
+
}
|
|
123
|
+
packageJson.devDependencies = {
|
|
124
|
+
...packageJson.devDependencies,
|
|
125
|
+
'@types/node': 'latest',
|
|
126
|
+
typescript: 'latest',
|
|
127
|
+
}
|
|
128
|
+
packageJson.engines = {
|
|
129
|
+
...packageJson.engines,
|
|
130
|
+
node: `>=${MINIMUM_SUPPORTED_NODE_VERSION}`,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf8')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function ensureTargetDirectory(targetDir: string, force: boolean): Promise<void> {
|
|
137
|
+
try {
|
|
138
|
+
let stats = await fs.stat(targetDir)
|
|
139
|
+
if (!stats.isDirectory()) {
|
|
140
|
+
throw targetPathNotDirectory(targetDir)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let entries = await fs.readdir(targetDir)
|
|
144
|
+
if (entries.length > 0 && !force) {
|
|
145
|
+
throw targetDirectoryNotEmpty(targetDir)
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
let nodeError = error as NodeJS.ErrnoException
|
|
149
|
+
if (nodeError.code !== 'ENOENT') {
|
|
150
|
+
throw error
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await fs.mkdir(targetDir, { recursive: true })
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function copyBootstrapDirectory({
|
|
158
|
+
sourceDir,
|
|
159
|
+
targetDir,
|
|
160
|
+
templateValues,
|
|
161
|
+
}: {
|
|
162
|
+
sourceDir: string
|
|
163
|
+
targetDir: string
|
|
164
|
+
templateValues: TemplateValues
|
|
165
|
+
}): Promise<void> {
|
|
166
|
+
let entries = await fs.readdir(sourceDir, { withFileTypes: true })
|
|
167
|
+
|
|
168
|
+
for (let entry of entries) {
|
|
169
|
+
let sourcePath = path.join(sourceDir, entry.name)
|
|
170
|
+
let targetPath = path.join(targetDir, entry.name)
|
|
171
|
+
|
|
172
|
+
if (BOOTSTRAP_EXCLUDED_NAMES.has(entry.name)) {
|
|
173
|
+
continue
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (entry.isDirectory()) {
|
|
177
|
+
await fs.mkdir(targetPath, { recursive: true })
|
|
178
|
+
await copyBootstrapDirectory({ sourceDir: sourcePath, targetDir: targetPath, templateValues })
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let content = await fs.readFile(sourcePath, 'utf8')
|
|
183
|
+
await fs.writeFile(targetPath, replaceTemplateValues(content, templateValues), 'utf8')
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function replaceTemplateValues(content: string, templateValues: TemplateValues): string {
|
|
188
|
+
for (let [token, value] of Object.entries(templateValues)) {
|
|
189
|
+
content = content.split(token).join(value)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return content
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function humanizeName(value: string): string {
|
|
196
|
+
let parts = value.split(/[-_\s]+/).filter(Boolean)
|
|
197
|
+
if (parts.length === 0) {
|
|
198
|
+
return 'Remix App'
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return parts
|
|
202
|
+
.map((part) => {
|
|
203
|
+
let head = part.slice(0, 1).toUpperCase()
|
|
204
|
+
let tail = part.slice(1)
|
|
205
|
+
return `${head}${tail}`
|
|
206
|
+
})
|
|
207
|
+
.join(' ')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function toPackageName(value: string): string {
|
|
211
|
+
let packageName = value
|
|
212
|
+
.trim()
|
|
213
|
+
.toLowerCase()
|
|
214
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
215
|
+
.replace(/^-+|-+$/g, '')
|
|
216
|
+
|
|
217
|
+
if (packageName.length === 0) {
|
|
218
|
+
throw invalidPackageName(value)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return packageName
|
|
222
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as path from 'node:path'
|
|
2
|
+
import * as process from 'node:process'
|
|
3
|
+
|
|
4
|
+
import { resolveDefaultRemixVersion } from './remix-version.ts'
|
|
5
|
+
|
|
6
|
+
interface ResolveCliContextOptions {
|
|
7
|
+
cwd?: string
|
|
8
|
+
remixVersion?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CliContext {
|
|
12
|
+
cwd: string
|
|
13
|
+
remixVersion?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function resolveCliContext(
|
|
17
|
+
options: ResolveCliContextOptions = {},
|
|
18
|
+
): Promise<CliContext> {
|
|
19
|
+
let cwd = resolveCwd(options.cwd)
|
|
20
|
+
let remixVersion = normalizeRemixVersion(options.remixVersion)
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
cwd,
|
|
24
|
+
remixVersion: remixVersion ?? (await resolveDefaultRemixVersion(cwd)),
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveCwd(cwd: string | undefined): string {
|
|
29
|
+
let normalizedCwd = cwd?.trim()
|
|
30
|
+
return path.resolve(
|
|
31
|
+
normalizedCwd == null || normalizedCwd.length === 0 ? process.cwd() : normalizedCwd,
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeRemixVersion(remixVersion: string | undefined): string | undefined {
|
|
36
|
+
let normalizedVersion = remixVersion?.trim()
|
|
37
|
+
return normalizedVersion == null || normalizedVersion.length === 0 ? undefined : normalizedVersion
|
|
38
|
+
}
|