@remix-run/cli 0.4.0 → 0.6.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 +52 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/lib/cli-context.d.ts.map +1 -1
- package/dist/lib/cli-context.js +5 -2
- package/dist/lib/cli.d.ts +1 -1
- package/dist/lib/cli.js +5 -1
- package/dist/lib/commands/assets.d.ts +4 -0
- package/dist/lib/commands/assets.d.ts.map +1 -0
- package/dist/lib/commands/assets.js +90 -0
- package/dist/lib/commands/db.d.ts.map +1 -1
- package/dist/lib/commands/db.js +63 -5
- package/dist/lib/commands/help.d.ts.map +1 -1
- package/dist/lib/commands/help.js +7 -0
- package/dist/lib/completion.d.ts.map +1 -1
- package/dist/lib/completion.js +25 -1
- package/dist/lib/database-command.d.ts +5 -1
- package/dist/lib/database-command.d.ts.map +1 -1
- package/dist/lib/database-command.js +1 -0
- package/dist/lib/errors.d.ts +6 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +10 -0
- package/dist/lib/remix-config.d.ts +22 -0
- package/dist/lib/remix-config.d.ts.map +1 -1
- package/dist/lib/remix-config.js +80 -4
- package/package.json +8 -7
- package/schema/remix.json +49 -1
- package/src/index.ts +10 -0
- package/src/lib/cli-context.ts +10 -2
- package/src/lib/cli.ts +6 -1
- package/src/lib/commands/assets.ts +109 -0
- package/src/lib/commands/db.ts +75 -5
- package/src/lib/commands/help.ts +8 -0
- package/src/lib/completion.ts +41 -1
- package/src/lib/database-command.ts +6 -1
- package/src/lib/errors.ts +11 -0
- package/src/lib/remix-config.ts +128 -4
- package/template/.agents/skills/remix/SKILL.md +7 -5
- package/template/.agents/skills/remix/references/assets-and-browser-modules.md +8 -13
- package/template/.agents/skills/remix/references/auth-and-sessions.md +1 -15
- package/template/.agents/skills/remix/references/component-model.md +18 -16
- package/template/.agents/skills/remix/references/hydration-frames-navigation.md +43 -48
- package/template/.agents/skills/remix/references/middleware-and-server.md +4 -1
- package/template/.agents/skills/remix/references/mixins-styling-events.md +1 -1
- package/template/.agents/skills/remix/references/routing-and-controllers.md +1 -1
- package/template/.agents/skills/remix/references/testing-patterns.md +1 -1
- package/template/AGENTS.md +2 -3
- package/template/README.md +2 -3
- package/template/app/actions/controller.tsx +2 -4
- package/template/app/assets.ts +4 -7
- package/template/app/router.ts +5 -3
- package/template/app/middleware/render.tsx +0 -72
package/dist/lib/errors.js
CHANGED
|
@@ -4,6 +4,11 @@ export const CLI_ERROR_DEFINITIONS = {
|
|
|
4
4
|
title: 'Could not determine an app name',
|
|
5
5
|
fix: 'Pass --app-name or choose a target directory name that can become an app name.',
|
|
6
6
|
},
|
|
7
|
+
assetsConfigRequired: {
|
|
8
|
+
code: 'RMX_ASSETS_CONFIG_REQUIRED',
|
|
9
|
+
title: 'Asset configuration is required',
|
|
10
|
+
fix: 'Add an assets configuration to remix.json.',
|
|
11
|
+
},
|
|
7
12
|
dbConfigRequired: {
|
|
8
13
|
code: 'RMX_DB_CONFIG_REQUIRED',
|
|
9
14
|
title: 'Database configuration is required',
|
|
@@ -165,6 +170,11 @@ export function appNameUnavailable(targetDir) {
|
|
|
165
170
|
message: 'Could not determine an app name from the target directory.',
|
|
166
171
|
});
|
|
167
172
|
}
|
|
173
|
+
export function assetsConfigRequired() {
|
|
174
|
+
return createCliError(CLI_ERROR_DEFINITIONS.assetsConfigRequired, {
|
|
175
|
+
message: 'Asset configuration is missing from remix.json.',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
168
178
|
export function dbConfigRequired(filePath) {
|
|
169
179
|
return createCliError(CLI_ERROR_DEFINITIONS.dbConfigRequired, {
|
|
170
180
|
context: { filePath },
|
|
@@ -1,14 +1,28 @@
|
|
|
1
|
+
import type { AssetServerOptions } from '@remix-run/assets';
|
|
1
2
|
import type { RemixTestPool } from '@remix-run/test/cli';
|
|
2
3
|
declare const reporters: readonly ['spec', 'files', 'tap', 'dot'];
|
|
3
4
|
declare const testTypes: readonly ['server', 'browser', 'e2e'];
|
|
4
5
|
type Reporter = (typeof reporters)[number];
|
|
5
6
|
type TestPool = RemixTestPool;
|
|
6
7
|
type TestType = (typeof testTypes)[number];
|
|
8
|
+
/** Validated configuration loaded from a Remix project config file. */
|
|
7
9
|
export interface RemixConfig {
|
|
10
|
+
/** Shared asset mapping and browser access configuration. */
|
|
11
|
+
assets?: RemixAssetsConfig;
|
|
12
|
+
/** Database command configuration. */
|
|
8
13
|
db?: RemixDbCommandConfig;
|
|
14
|
+
/** Project health-check configuration. */
|
|
9
15
|
doctor?: RemixDoctorCommandConfig;
|
|
16
|
+
/** Test runner configuration. */
|
|
10
17
|
test?: RemixTestCommandConfig;
|
|
11
18
|
}
|
|
19
|
+
/** JSON-compatible asset server configuration loaded from `remix.json`. */
|
|
20
|
+
export interface RemixAssetsConfig extends Pick<AssetServerOptions, 'allowFiles' | 'allowPackages' | 'basePath' | 'denyFiles' | 'mounts'> {
|
|
21
|
+
/** Leaf file asset configuration. */
|
|
22
|
+
files?: Pick<NonNullable<AssetServerOptions['files']>, 'extensions'>;
|
|
23
|
+
/** Absolute root directory used to resolve asset file paths. */
|
|
24
|
+
rootDir: string;
|
|
25
|
+
}
|
|
12
26
|
export type RemixDbString = string | {
|
|
13
27
|
env: string;
|
|
14
28
|
default?: string;
|
|
@@ -70,6 +84,14 @@ export interface RemixTestCommandConfig {
|
|
|
70
84
|
type?: TestType[];
|
|
71
85
|
watch?: boolean;
|
|
72
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Loads the nearest Remix project configuration or an explicitly selected config file.
|
|
89
|
+
*
|
|
90
|
+
* @param from A config file or directory from which to search upward for `remix.json`. Defaults to
|
|
91
|
+
* `process.cwd()`.
|
|
92
|
+
* @returns The validated Remix project configuration, or an empty object when no config is found.
|
|
93
|
+
*/
|
|
94
|
+
export declare function loadConfig(from?: string | URL): Promise<RemixConfig>;
|
|
73
95
|
export declare function loadRemixConfig(cwd: string, configPath: string | undefined): Promise<RemixConfig>;
|
|
74
96
|
export {};
|
|
75
97
|
//# sourceMappingURL=remix-config.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remix-config.d.ts","sourceRoot":"","sources":["../../src/lib/remix-config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"remix-config.d.ts","sourceRoot":"","sources":["../../src/lib/remix-config.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/remix-config.js
CHANGED
|
@@ -1,10 +1,40 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
+
import * as process from 'node:process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
3
5
|
import { findNodeAtLocation, getNodeValue, parseTree, printParseErrorCode, } from 'jsonc-parser';
|
|
6
|
+
import { findAppRoot } from './app-root.js';
|
|
4
7
|
import { invalidRemixConfig, remixConfigNotFound } from './errors.js';
|
|
5
8
|
const reporters = ['spec', 'files', 'tap', 'dot'];
|
|
6
9
|
const testPools = ['forks', 'threads'];
|
|
7
10
|
const testTypes = ['server', 'browser', 'e2e'];
|
|
11
|
+
/**
|
|
12
|
+
* Loads the nearest Remix project configuration or an explicitly selected config file.
|
|
13
|
+
*
|
|
14
|
+
* @param from A config file or directory from which to search upward for `remix.json`. Defaults to
|
|
15
|
+
* `process.cwd()`.
|
|
16
|
+
* @returns The validated Remix project configuration, or an empty object when no config is found.
|
|
17
|
+
*/
|
|
18
|
+
export async function loadConfig(from = process.cwd()) {
|
|
19
|
+
let fromPath = path.resolve(from instanceof URL ? fileURLToPath(from) : from);
|
|
20
|
+
let stat;
|
|
21
|
+
try {
|
|
22
|
+
stat = await fs.stat(fromPath);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (isNodeError(error) && error.code === 'ENOENT')
|
|
26
|
+
throw remixConfigNotFound(fromPath);
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
if (stat.isFile()) {
|
|
30
|
+
return loadRemixConfig(path.dirname(fromPath), path.basename(fromPath));
|
|
31
|
+
}
|
|
32
|
+
if (!stat.isDirectory()) {
|
|
33
|
+
throw new TypeError(`Expected a Remix config file or directory: ${fromPath}`);
|
|
34
|
+
}
|
|
35
|
+
let configDir = await findAppRoot(fromPath, 'remix.json');
|
|
36
|
+
return configDir === null ? {} : loadRemixConfig(configDir, undefined);
|
|
37
|
+
}
|
|
8
38
|
export async function loadRemixConfig(cwd, configPath) {
|
|
9
39
|
let filePath = path.resolve(cwd, configPath ?? 'remix.json');
|
|
10
40
|
let text;
|
|
@@ -44,11 +74,14 @@ export async function loadRemixConfig(cwd, configPath) {
|
|
|
44
74
|
}
|
|
45
75
|
function parseConfig(value, source, configDir, cwd) {
|
|
46
76
|
let object = requireObject(value, source, []);
|
|
47
|
-
requireKnownProperties(object, ['$schema', 'db', 'doctor', 'test'], source, []);
|
|
77
|
+
requireKnownProperties(object, ['$schema', 'assets', 'db', 'doctor', 'test'], source, []);
|
|
48
78
|
if (object.$schema !== undefined) {
|
|
49
79
|
requireString(object.$schema, source, ['$schema']);
|
|
50
80
|
}
|
|
51
81
|
let config = {};
|
|
82
|
+
if (object.assets !== undefined) {
|
|
83
|
+
config.assets = parseAssetsConfig(object.assets, source, configDir);
|
|
84
|
+
}
|
|
52
85
|
if (object.db !== undefined) {
|
|
53
86
|
config.db = parseDbConfig(object.db, source, configDir);
|
|
54
87
|
}
|
|
@@ -60,6 +93,39 @@ function parseConfig(value, source, configDir, cwd) {
|
|
|
60
93
|
}
|
|
61
94
|
return config;
|
|
62
95
|
}
|
|
96
|
+
function parseAssetsConfig(value, source, configDir) {
|
|
97
|
+
let objectPath = ['assets'];
|
|
98
|
+
let object = requireObject(value, source, objectPath);
|
|
99
|
+
requireKnownProperties(object, ['allowFiles', 'allowPackages', 'basePath', 'denyFiles', 'files', 'mounts', 'rootDir'], source, objectPath);
|
|
100
|
+
let config = {
|
|
101
|
+
allowFiles: requireStringArray(object.allowFiles, source, [...objectPath, 'allowFiles']),
|
|
102
|
+
basePath: requireString(object.basePath, source, [...objectPath, 'basePath']),
|
|
103
|
+
rootDir: path.resolve(configDir, optionalString(object.rootDir, source, [...objectPath, 'rootDir']) ?? '.'),
|
|
104
|
+
};
|
|
105
|
+
let allowPackages = optionalStringArray(object.allowPackages, source, [
|
|
106
|
+
...objectPath,
|
|
107
|
+
'allowPackages',
|
|
108
|
+
]);
|
|
109
|
+
let denyFiles = optionalStringArray(object.denyFiles, source, [...objectPath, 'denyFiles']);
|
|
110
|
+
let mounts = object.mounts === undefined
|
|
111
|
+
? undefined
|
|
112
|
+
: requireStringRecord(object.mounts, source, [...objectPath, 'mounts']);
|
|
113
|
+
if (allowPackages !== undefined)
|
|
114
|
+
config.allowPackages = allowPackages;
|
|
115
|
+
if (denyFiles !== undefined)
|
|
116
|
+
config.denyFiles = denyFiles;
|
|
117
|
+
if (mounts !== undefined)
|
|
118
|
+
config.mounts = mounts;
|
|
119
|
+
if (object.files !== undefined) {
|
|
120
|
+
let filesPath = [...objectPath, 'files'];
|
|
121
|
+
let files = requireObject(object.files, source, filesPath);
|
|
122
|
+
requireKnownProperties(files, ['extensions'], source, filesPath);
|
|
123
|
+
config.files = {
|
|
124
|
+
extensions: requireStringArray(files.extensions, source, [...filesPath, 'extensions']),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return config;
|
|
128
|
+
}
|
|
63
129
|
function parseDbConfig(value, source, configDir) {
|
|
64
130
|
let objectPath = ['db'];
|
|
65
131
|
let object = requireObject(value, source, objectPath);
|
|
@@ -315,13 +381,23 @@ function requireString(value, source, propertyPath) {
|
|
|
315
381
|
throwConfigError(source, propertyPath, 'Expected a string');
|
|
316
382
|
return value;
|
|
317
383
|
}
|
|
318
|
-
function
|
|
319
|
-
if (value === undefined)
|
|
320
|
-
return undefined;
|
|
384
|
+
function requireStringArray(value, source, propertyPath) {
|
|
321
385
|
if (!Array.isArray(value))
|
|
322
386
|
throwConfigError(source, propertyPath, 'Expected an array of strings');
|
|
323
387
|
return value.map((item, index) => requireString(item, source, [...propertyPath, index]));
|
|
324
388
|
}
|
|
389
|
+
function requireStringRecord(value, source, propertyPath) {
|
|
390
|
+
let object = requireObject(value, source, propertyPath);
|
|
391
|
+
return Object.fromEntries(Object.entries(object).map(([key, item]) => [
|
|
392
|
+
key,
|
|
393
|
+
requireString(item, source, [...propertyPath, key]),
|
|
394
|
+
]));
|
|
395
|
+
}
|
|
396
|
+
function optionalStringArray(value, source, propertyPath) {
|
|
397
|
+
if (value === undefined)
|
|
398
|
+
return undefined;
|
|
399
|
+
return requireStringArray(value, source, propertyPath);
|
|
400
|
+
}
|
|
325
401
|
function optionalEnum(input, values, source, propertyPath) {
|
|
326
402
|
if (input === undefined)
|
|
327
403
|
return undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remix-run/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Command-line interface for Remix",
|
|
5
5
|
"author": "Michael Jackson <mjijackson@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,12 +33,13 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"jsonc-parser": "^3.3.1",
|
|
35
35
|
"semver": "^7.7.4",
|
|
36
|
-
"@remix-run/data-table": "^0.
|
|
37
|
-
"@remix-run/data-table-
|
|
38
|
-
"@remix-run/data-table-
|
|
39
|
-
"@remix-run/
|
|
40
|
-
"@remix-run/
|
|
41
|
-
"@remix-run/test": "^0.6.0"
|
|
36
|
+
"@remix-run/data-table": "^0.5.0",
|
|
37
|
+
"@remix-run/data-table-postgres": "^0.5.1",
|
|
38
|
+
"@remix-run/data-table-mysql": "^0.5.1",
|
|
39
|
+
"@remix-run/data-table-sqlite": "^0.6.1",
|
|
40
|
+
"@remix-run/assets": "^0.6.0",
|
|
41
|
+
"@remix-run/test": "^0.6.0",
|
|
42
|
+
"@remix-run/terminal": "^0.1.1"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@types/node": "^24.6.0",
|
package/schema/remix.json
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "https://remix.run/schemas/remix.json",
|
|
4
3
|
"title": "Remix CLI configuration",
|
|
5
4
|
"description": "Configuration for the Remix command-line interface.",
|
|
6
5
|
"type": "object",
|
|
@@ -10,6 +9,9 @@
|
|
|
10
9
|
"type": "string",
|
|
11
10
|
"description": "JSON Schema used by editors to validate this file."
|
|
12
11
|
},
|
|
12
|
+
"assets": {
|
|
13
|
+
"$ref": "#/$defs/assets"
|
|
14
|
+
},
|
|
13
15
|
"db": {
|
|
14
16
|
"$ref": "#/$defs/db"
|
|
15
17
|
},
|
|
@@ -21,6 +23,52 @@
|
|
|
21
23
|
}
|
|
22
24
|
},
|
|
23
25
|
"$defs": {
|
|
26
|
+
"assets": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"additionalProperties": false,
|
|
29
|
+
"required": ["allowFiles", "basePath"],
|
|
30
|
+
"properties": {
|
|
31
|
+
"allowFiles": {
|
|
32
|
+
"$ref": "#/$defs/stringArray",
|
|
33
|
+
"description": "File paths and glob patterns that may be served."
|
|
34
|
+
},
|
|
35
|
+
"allowPackages": {
|
|
36
|
+
"$ref": "#/$defs/stringArray",
|
|
37
|
+
"description": "Package names whose files and dependencies may be served."
|
|
38
|
+
},
|
|
39
|
+
"basePath": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "Public URL prefix for served assets."
|
|
42
|
+
},
|
|
43
|
+
"denyFiles": {
|
|
44
|
+
"$ref": "#/$defs/stringArray",
|
|
45
|
+
"description": "File paths and glob patterns that must not be served."
|
|
46
|
+
},
|
|
47
|
+
"mounts": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"description": "Directories keyed by public URL paths relative to basePath.",
|
|
50
|
+
"additionalProperties": { "type": "string" }
|
|
51
|
+
},
|
|
52
|
+
"files": {
|
|
53
|
+
"$ref": "#/$defs/assetsFiles"
|
|
54
|
+
},
|
|
55
|
+
"rootDir": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"description": "Root directory for asset file paths, relative to this config file."
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"assetsFiles": {
|
|
62
|
+
"type": "object",
|
|
63
|
+
"additionalProperties": false,
|
|
64
|
+
"required": ["extensions"],
|
|
65
|
+
"properties": {
|
|
66
|
+
"extensions": {
|
|
67
|
+
"$ref": "#/$defs/stringArray",
|
|
68
|
+
"description": "Additional file extensions served without script or style compilation."
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
24
72
|
"db": {
|
|
25
73
|
"type": "object",
|
|
26
74
|
"additionalProperties": false,
|
package/src/index.ts
CHANGED
|
@@ -1 +1,11 @@
|
|
|
1
1
|
export { runRemix, type RunRemixOptions } from './lib/cli.ts'
|
|
2
|
+
export {
|
|
3
|
+
loadConfig,
|
|
4
|
+
type RemixAssetsConfig,
|
|
5
|
+
type RemixConfig,
|
|
6
|
+
type RemixDbAdapterConfig,
|
|
7
|
+
type RemixDbCommandConfig,
|
|
8
|
+
type RemixDbString,
|
|
9
|
+
type RemixDoctorCommandConfig,
|
|
10
|
+
type RemixTestCommandConfig,
|
|
11
|
+
} from './lib/remix-config.ts'
|
package/src/lib/cli-context.ts
CHANGED
|
@@ -2,7 +2,11 @@ import * as path from 'node:path'
|
|
|
2
2
|
import * as process from 'node:process'
|
|
3
3
|
|
|
4
4
|
import { resolveDefaultRemixVersion } from './remix-version.ts'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
loadConfig as loadProjectConfig,
|
|
7
|
+
loadRemixConfig,
|
|
8
|
+
type RemixConfig,
|
|
9
|
+
} from './remix-config.ts'
|
|
6
10
|
|
|
7
11
|
interface ResolveCliContextOptions {
|
|
8
12
|
configPath?: string
|
|
@@ -26,7 +30,11 @@ export async function resolveCliContext(
|
|
|
26
30
|
let remixVersion = normalizeRemixVersion(options.remixVersion)
|
|
27
31
|
|
|
28
32
|
let configPromise: Promise<RemixConfig> | undefined
|
|
29
|
-
let loadConfig = () =>
|
|
33
|
+
let loadConfig = () =>
|
|
34
|
+
(configPromise ??=
|
|
35
|
+
options.configPath === undefined
|
|
36
|
+
? loadProjectConfig(cwd)
|
|
37
|
+
: loadRemixConfig(cwd, options.configPath))
|
|
30
38
|
|
|
31
39
|
// Validate an explicitly selected config file up front so every command
|
|
32
40
|
// fails fast on a bad --config path. The default remix.json is loaded
|
package/src/lib/cli.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface RunRemixOptions {
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Entry point for the `remix` CLI. Parses `argv`, dispatches to the matching
|
|
26
|
-
* subcommand (`new`, `db`, `doctor`, `routes`, `test`, `version`,
|
|
26
|
+
* subcommand (`new`, `assets`, `db`, `doctor`, `routes`, `test`, `version`,
|
|
27
27
|
* `completion`, `help`), and resolves with the exit code the process should
|
|
28
28
|
* use.
|
|
29
29
|
*
|
|
@@ -103,6 +103,11 @@ async function runCommand(command: string, argv: string[], context: CliContext):
|
|
|
103
103
|
return runCompletionCommand(argv)
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
if (command === 'assets') {
|
|
107
|
+
let { runAssetsCommand } = await import('./commands/assets.ts')
|
|
108
|
+
return runAssetsCommand(argv, context)
|
|
109
|
+
}
|
|
110
|
+
|
|
106
111
|
if (command === 'doctor') {
|
|
107
112
|
let { runDoctorCommand } = await import('./commands/doctor.ts')
|
|
108
113
|
return runDoctorCommand(argv, context)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import * as fs from 'node:fs'
|
|
2
|
+
import * as path from 'node:path'
|
|
3
|
+
import * as process from 'node:process'
|
|
4
|
+
import { createAssetServer, type AssetDetails } from '@remix-run/assets'
|
|
5
|
+
|
|
6
|
+
import type { CliContext } from '../cli-context.ts'
|
|
7
|
+
import {
|
|
8
|
+
assetsConfigRequired,
|
|
9
|
+
invalidOptionValue,
|
|
10
|
+
renderCliError,
|
|
11
|
+
toCliError,
|
|
12
|
+
unknownCommand,
|
|
13
|
+
} from '../errors.ts'
|
|
14
|
+
import { formatHelpText } from '../help-text.ts'
|
|
15
|
+
import { parseArgs } from '../parse-args.ts'
|
|
16
|
+
|
|
17
|
+
export async function runAssetsCommand(argv: string[], context: CliContext): Promise<number> {
|
|
18
|
+
if (argv.includes('-h') || argv.includes('--help')) {
|
|
19
|
+
process.stdout.write(getAssetsCommandHelpText())
|
|
20
|
+
return 0
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
let invocation = parseAssetsCommandArgs(argv)
|
|
25
|
+
let config = await context.loadConfig()
|
|
26
|
+
if (config.assets === undefined) throw assetsConfigRequired()
|
|
27
|
+
|
|
28
|
+
let assetServer = createAssetServer({ ...config.assets, watch: false })
|
|
29
|
+
try {
|
|
30
|
+
let rootDir = fs.realpathSync(config.assets.rootDir)
|
|
31
|
+
|
|
32
|
+
if (invocation.command === 'list') {
|
|
33
|
+
process.stdout.write(formatAssetList(await assetServer.getAssets(), rootDir))
|
|
34
|
+
} else {
|
|
35
|
+
process.stdout.write(
|
|
36
|
+
formatAssetDetails(await assetServer.getAssetDetails(invocation.input), rootDir),
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
} finally {
|
|
40
|
+
await assetServer.close()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return 0
|
|
44
|
+
} catch (error) {
|
|
45
|
+
process.stderr.write(
|
|
46
|
+
renderCliError(toCliError(error), { helpText: getAssetsCommandHelpText(process.stderr) }),
|
|
47
|
+
)
|
|
48
|
+
return 1
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getAssetsCommandHelpText(target: NodeJS.WriteStream = process.stdout): string {
|
|
53
|
+
return formatHelpText(
|
|
54
|
+
{
|
|
55
|
+
description: 'List or inspect browser-reachable assets.',
|
|
56
|
+
commands: [{ description: 'Inspect one asset URL or file', label: 'inspect <url-or-file>' }],
|
|
57
|
+
examples: [
|
|
58
|
+
'remix assets',
|
|
59
|
+
'remix assets inspect /assets/app/actions/public/entry.ts',
|
|
60
|
+
'remix assets inspect app/actions/public/entry.ts',
|
|
61
|
+
],
|
|
62
|
+
usage: ['remix assets', 'remix assets inspect <url-or-file>'],
|
|
63
|
+
},
|
|
64
|
+
target,
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type AssetsCommandInvocation = { command: 'list' } | { command: 'inspect'; input: string }
|
|
69
|
+
|
|
70
|
+
function parseAssetsCommandArgs(argv: string[]): AssetsCommandInvocation {
|
|
71
|
+
let parsed = parseArgs(argv, {}, { maxPositionals: 2 })
|
|
72
|
+
let [command, input] = parsed.positionals
|
|
73
|
+
|
|
74
|
+
if (command === undefined) return { command: 'list' }
|
|
75
|
+
if (command !== 'inspect') throw unknownCommand(`assets ${command}`)
|
|
76
|
+
if (input === undefined) {
|
|
77
|
+
throw invalidOptionValue('`remix assets inspect` requires a URL or file path.')
|
|
78
|
+
}
|
|
79
|
+
return { command, input }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function formatAssetList(assets: readonly AssetDetails[], rootDir: string): string {
|
|
83
|
+
if (assets.length === 0) return 'No assets.\n'
|
|
84
|
+
return `${assets
|
|
85
|
+
.map((asset) => `${asset.url} -> ${formatFilePath(asset.filePath, rootDir)}`)
|
|
86
|
+
.join('\n')}\n`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatAssetDetails(details: AssetDetails, rootDir: string): string {
|
|
90
|
+
let lines = [`Status: ${details.status}`]
|
|
91
|
+
if (details.url !== undefined) lines.push(`URL: ${details.url}`)
|
|
92
|
+
if (details.filePath !== undefined) {
|
|
93
|
+
lines.push(`File: ${formatFilePath(details.filePath, rootDir)}`)
|
|
94
|
+
}
|
|
95
|
+
if (details.access?.deniedBy !== undefined) {
|
|
96
|
+
lines.push(`Denied by: ${details.access.deniedBy}`)
|
|
97
|
+
}
|
|
98
|
+
return `${lines.join('\n')}\n`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function formatFilePath(filePath: string | undefined, rootDir: string): string {
|
|
102
|
+
if (filePath === undefined) return ''
|
|
103
|
+
let relativePath = path.relative(rootDir, filePath)
|
|
104
|
+
return relativePath === '' ||
|
|
105
|
+
relativePath.startsWith(`..${path.sep}`) ||
|
|
106
|
+
path.isAbsolute(relativePath)
|
|
107
|
+
? filePath
|
|
108
|
+
: relativePath.split(path.sep).join('/')
|
|
109
|
+
}
|
package/src/lib/commands/db.ts
CHANGED
|
@@ -25,7 +25,6 @@ import {
|
|
|
25
25
|
import { formatHelpText } from '../help-text.ts'
|
|
26
26
|
import { parseArgs } from '../parse-args.ts'
|
|
27
27
|
import {
|
|
28
|
-
loadRemixConfig,
|
|
29
28
|
type RemixDbAdapterConfig,
|
|
30
29
|
type RemixDbCommandConfig,
|
|
31
30
|
type RemixDbString,
|
|
@@ -63,6 +62,8 @@ export function getDbCommandHelpText(target: NodeJS.WriteStream = process.stdout
|
|
|
63
62
|
'remix db wipe --force',
|
|
64
63
|
'remix db migrate',
|
|
65
64
|
'remix db migrate --to 20260715123000_add_users',
|
|
65
|
+
'remix db rollback',
|
|
66
|
+
'remix db rollback --step 2 --dry-run',
|
|
66
67
|
'remix db status',
|
|
67
68
|
'remix db seed',
|
|
68
69
|
'remix db reset --force',
|
|
@@ -72,24 +73,35 @@ export function getDbCommandHelpText(target: NodeJS.WriteStream = process.stdout
|
|
|
72
73
|
description: 'Read the database connection from an environment variable',
|
|
73
74
|
label: '--connection-env <name>',
|
|
74
75
|
},
|
|
76
|
+
{
|
|
77
|
+
description: 'Report what would be reverted without running it (rollback only)',
|
|
78
|
+
label: '--dry-run',
|
|
79
|
+
},
|
|
75
80
|
{ description: 'Confirm a destructive command (wipe and reset only)', label: '--force' },
|
|
76
81
|
{
|
|
77
|
-
description: 'Use a migration journal table (migrate, status, and reset only)',
|
|
82
|
+
description: 'Use a migration journal table (migrate, rollback, status, and reset only)',
|
|
78
83
|
label: '--journal-table <name>',
|
|
79
84
|
},
|
|
80
85
|
{
|
|
81
|
-
description:
|
|
86
|
+
description:
|
|
87
|
+
'Load migrations from a directory (migrate, rollback, status, and reset only)',
|
|
82
88
|
label: '--migrations <path>',
|
|
83
89
|
},
|
|
84
90
|
{ description: 'Run a SQL seed file (seed and reset only)', label: '--seed <path>' },
|
|
85
91
|
{
|
|
86
|
-
description: '
|
|
92
|
+
description: 'Revert this many migrations, newest first (rollback only)',
|
|
93
|
+
label: '--step <count>',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
description:
|
|
97
|
+
'Stop after applying the specified migration, or revert back through it (migrate and rollback only)',
|
|
87
98
|
label: '--to <migration>',
|
|
88
99
|
},
|
|
89
100
|
],
|
|
90
101
|
usage: [
|
|
91
102
|
'remix db wipe --force [options]',
|
|
92
103
|
'remix db migrate [--to <migration>] [options]',
|
|
104
|
+
'remix db rollback [--step <count> | --to <migration>] [--dry-run] [options]',
|
|
93
105
|
'remix db status [options]',
|
|
94
106
|
'remix db seed [options]',
|
|
95
107
|
'remix db reset --force [options]',
|
|
@@ -120,6 +132,28 @@ function parseDbCommandArgs(argv: string[]): DatabaseCommandInvocation {
|
|
|
120
132
|
return { command, ...parsed.options }
|
|
121
133
|
}
|
|
122
134
|
|
|
135
|
+
if (command === 'rollback') {
|
|
136
|
+
let parsed = parseArgs(
|
|
137
|
+
commandArgv,
|
|
138
|
+
{
|
|
139
|
+
connectionEnv: connectionOption,
|
|
140
|
+
dryRun: { flag: '--dry-run', type: 'boolean' },
|
|
141
|
+
journalTable: journalOption,
|
|
142
|
+
migrations: migrationsOption,
|
|
143
|
+
step: { flag: '--step', type: 'string' },
|
|
144
|
+
to: { flag: '--to', type: 'string' },
|
|
145
|
+
},
|
|
146
|
+
{ maxPositionals: 0 },
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if (parsed.options.step !== undefined && parsed.options.to !== undefined) {
|
|
150
|
+
throw invalidOptionValue('Options --step and --to are mutually exclusive')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let { step: rawStep, ...options } = parsed.options
|
|
154
|
+
return { command, ...options, step: parseStepOption(rawStep) }
|
|
155
|
+
}
|
|
156
|
+
|
|
123
157
|
if (command === 'reset') {
|
|
124
158
|
let parsed = parseArgs(
|
|
125
159
|
commandArgv,
|
|
@@ -171,6 +205,17 @@ function parseDbCommandArgs(argv: string[]): DatabaseCommandInvocation {
|
|
|
171
205
|
return { command, ...parsed.options }
|
|
172
206
|
}
|
|
173
207
|
|
|
208
|
+
function parseStepOption(value: string | undefined): number | undefined {
|
|
209
|
+
if (value === undefined) return undefined
|
|
210
|
+
|
|
211
|
+
let step = Number(value)
|
|
212
|
+
if (!Number.isInteger(step) || step < 1) {
|
|
213
|
+
throw invalidOptionValue(`Option --step expects a positive integer, received "${value}"`)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return step
|
|
217
|
+
}
|
|
218
|
+
|
|
174
219
|
async function resolveDbConfig(
|
|
175
220
|
context: CliContext,
|
|
176
221
|
): Promise<{ config: RemixDbCommandConfig; configDir: string }> {
|
|
@@ -184,7 +229,7 @@ async function resolveDbConfig(
|
|
|
184
229
|
let configDir = await findAppRoot(context.cwd, 'remix.json')
|
|
185
230
|
if (configDir === null) throw remixConfigNotFound(path.join(context.cwd, 'remix.json'))
|
|
186
231
|
configPath = path.join(configDir, 'remix.json')
|
|
187
|
-
config = await
|
|
232
|
+
config = await context.loadConfig()
|
|
188
233
|
}
|
|
189
234
|
|
|
190
235
|
if (config.db === undefined) throw dbConfigRequired(configPath)
|
|
@@ -206,6 +251,7 @@ function resolveDatabaseCommandPlan(
|
|
|
206
251
|
if (
|
|
207
252
|
(invocation.command === 'migrate' ||
|
|
208
253
|
invocation.command === 'reset' ||
|
|
254
|
+
invocation.command === 'rollback' ||
|
|
209
255
|
invocation.command === 'status') &&
|
|
210
256
|
migrations === undefined
|
|
211
257
|
) {
|
|
@@ -221,9 +267,11 @@ function resolveDatabaseCommandPlan(
|
|
|
221
267
|
return {
|
|
222
268
|
adapter,
|
|
223
269
|
command: invocation.command,
|
|
270
|
+
dryRun: invocation.dryRun,
|
|
224
271
|
journalTable,
|
|
225
272
|
migrations,
|
|
226
273
|
seed,
|
|
274
|
+
step: invocation.step,
|
|
227
275
|
to: invocation.to,
|
|
228
276
|
}
|
|
229
277
|
}
|
|
@@ -305,6 +353,28 @@ async function executeDatabaseCommand(plan: DatabaseCommandPlan, db: Database):
|
|
|
305
353
|
})
|
|
306
354
|
}
|
|
307
355
|
|
|
356
|
+
if (plan.command === 'rollback') {
|
|
357
|
+
if (plan.to !== undefined) {
|
|
358
|
+
return runRemixDb({
|
|
359
|
+
command: plan.command,
|
|
360
|
+
db,
|
|
361
|
+
migrations,
|
|
362
|
+
to: plan.to,
|
|
363
|
+
dryRun: plan.dryRun,
|
|
364
|
+
journalTable: plan.journalTable,
|
|
365
|
+
})
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return runRemixDb({
|
|
369
|
+
command: plan.command,
|
|
370
|
+
db,
|
|
371
|
+
migrations,
|
|
372
|
+
step: plan.step,
|
|
373
|
+
dryRun: plan.dryRun,
|
|
374
|
+
journalTable: plan.journalTable,
|
|
375
|
+
})
|
|
376
|
+
}
|
|
377
|
+
|
|
308
378
|
if (plan.command === 'reset') {
|
|
309
379
|
let seed = plan.seed === undefined ? undefined : await loadSeed(plan.seed)
|
|
310
380
|
return runRemixDb({
|
package/src/lib/commands/help.ts
CHANGED
|
@@ -24,6 +24,7 @@ export function getCliHelpText(target: NodeJS.WriteStream = process.stdout): str
|
|
|
24
24
|
return formatHelpText(
|
|
25
25
|
{
|
|
26
26
|
commands: [
|
|
27
|
+
{ description: 'List or inspect browser-reachable assets', label: 'assets [command]' },
|
|
27
28
|
{ description: 'Print shell completion scripts for Remix', label: 'completion' },
|
|
28
29
|
{ description: 'Show help for Remix commands', label: 'help [command]' },
|
|
29
30
|
{ description: 'Create a new Remix project', label: 'new <name>' },
|
|
@@ -34,6 +35,7 @@ export function getCliHelpText(target: NodeJS.WriteStream = process.stdout): str
|
|
|
34
35
|
{ description: 'Show the current Remix version', label: 'version' },
|
|
35
36
|
],
|
|
36
37
|
examples: [
|
|
38
|
+
'remix assets',
|
|
37
39
|
'remix completion bash',
|
|
38
40
|
'remix help',
|
|
39
41
|
'remix help completion',
|
|
@@ -64,6 +66,7 @@ export function getHelpCommandHelpText(target: NodeJS.WriteStream = process.stdo
|
|
|
64
66
|
description: 'Show help for Remix commands.',
|
|
65
67
|
examples: [
|
|
66
68
|
'remix help',
|
|
69
|
+
'remix help assets',
|
|
67
70
|
'remix help completion',
|
|
68
71
|
'remix help db',
|
|
69
72
|
'remix help doctor',
|
|
@@ -94,6 +97,11 @@ async function getCommandHelpText(argv: string[]): Promise<string> {
|
|
|
94
97
|
return getNewCommandHelpText()
|
|
95
98
|
}
|
|
96
99
|
|
|
100
|
+
if (command === 'assets' && rest.length === 0) {
|
|
101
|
+
let { getAssetsCommandHelpText } = await import('./assets.ts')
|
|
102
|
+
return getAssetsCommandHelpText()
|
|
103
|
+
}
|
|
104
|
+
|
|
97
105
|
if (command === 'completion' && rest.length === 0) {
|
|
98
106
|
let { getCompletionCommandHelpText } = await import('./completion.ts')
|
|
99
107
|
return getCompletionCommandHelpText()
|