@remix-run/cli 0.3.3 → 0.3.4
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/dist/lib/doctor/fixes.d.ts.map +1 -1
- package/dist/lib/doctor/fixes.js +45 -6
- package/package.json +3 -3
- package/src/lib/doctor/fixes.ts +55 -6
- package/template/.agents/skills/remix/SKILL.md +1 -1
- package/template/app/assets/entry.ts +9 -0
- package/template/app/assets.ts +5 -7
- package/template/app/middleware/render.tsx +29 -1
- package/template/app/ui/scaffold-home-page.tsx +1 -1
- package/template/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fixes.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/fixes.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAEjE,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,aAAa,EAAE,GACxB,OAAO,CAAC,gBAAgB,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"fixes.d.ts","sourceRoot":"","sources":["../../../src/lib/doctor/fixes.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAEjE,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,aAAa,EAAE,GACxB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAkC7B"}
|
package/dist/lib/doctor/fixes.js
CHANGED
|
@@ -4,7 +4,7 @@ import { resolveContainedPath } from "../contained-path.js";
|
|
|
4
4
|
export async function applyDoctorFixPlans(appRoot, fixPlans) {
|
|
5
5
|
let appliedFixes = [];
|
|
6
6
|
for (let fixPlan of dedupeDoctorFixPlans(fixPlans)) {
|
|
7
|
-
let absolutePath = resolveDoctorFixPath(appRoot, fixPlan.path);
|
|
7
|
+
let absolutePath = await resolveDoctorFixPath(appRoot, fixPlan.path);
|
|
8
8
|
if (fixPlan.kind === 'create-directory') {
|
|
9
9
|
await fs.mkdir(absolutePath, { recursive: true });
|
|
10
10
|
appliedFixes.push(toAppliedDoctorFix(fixPlan));
|
|
@@ -20,8 +20,7 @@ export async function applyDoctorFixPlans(appRoot, fixPlans) {
|
|
|
20
20
|
await fs.writeFile(absolutePath, fixPlan.contents ?? '', { encoding: 'utf8', flag: 'wx' });
|
|
21
21
|
}
|
|
22
22
|
catch (error) {
|
|
23
|
-
|
|
24
|
-
if (nodeError.code !== 'EEXIST') {
|
|
23
|
+
if (!isErrorWithCode(error, 'EEXIST')) {
|
|
25
24
|
throw error;
|
|
26
25
|
}
|
|
27
26
|
continue;
|
|
@@ -52,14 +51,54 @@ function toAppliedDoctorFix(fixPlan) {
|
|
|
52
51
|
suite: fixPlan.suite,
|
|
53
52
|
};
|
|
54
53
|
}
|
|
55
|
-
function resolveDoctorFixPath(appRoot, fixPath) {
|
|
54
|
+
async function resolveDoctorFixPath(appRoot, fixPath) {
|
|
55
|
+
let absolutePath;
|
|
56
56
|
try {
|
|
57
|
-
|
|
57
|
+
absolutePath = resolveContainedPath(appRoot, fixPath);
|
|
58
58
|
}
|
|
59
59
|
catch (error) {
|
|
60
60
|
if (error instanceof Error && error.message.includes('escapes the allowed root')) {
|
|
61
|
-
|
|
61
|
+
throwDoctorFixPathOutsideRoot(fixPath);
|
|
62
62
|
}
|
|
63
63
|
throw error;
|
|
64
64
|
}
|
|
65
|
+
let rootPath = path.resolve(appRoot);
|
|
66
|
+
let rootRealPath = await fs.realpath(rootPath);
|
|
67
|
+
let relativePath = path.relative(rootPath, absolutePath);
|
|
68
|
+
let pathParts = relativePath === '' ? [] : relativePath.split(path.sep);
|
|
69
|
+
let currentPath = rootPath;
|
|
70
|
+
let currentRealPath = rootRealPath;
|
|
71
|
+
for (let [index, pathPart] of pathParts.entries()) {
|
|
72
|
+
currentPath = path.join(currentPath, pathPart);
|
|
73
|
+
let stats = await lstatIfExists(currentPath);
|
|
74
|
+
if (stats == null) {
|
|
75
|
+
return path.join(currentRealPath, ...pathParts.slice(index));
|
|
76
|
+
}
|
|
77
|
+
currentRealPath = await fs.realpath(currentPath);
|
|
78
|
+
if (!isPathInsideRoot(rootRealPath, currentRealPath)) {
|
|
79
|
+
throwDoctorFixPathOutsideRoot(fixPath);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return currentRealPath;
|
|
83
|
+
}
|
|
84
|
+
async function lstatIfExists(filePath) {
|
|
85
|
+
try {
|
|
86
|
+
return await fs.lstat(filePath);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (isErrorWithCode(error, 'ENOENT')) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function isPathInsideRoot(rootPath, filePath) {
|
|
96
|
+
let relativePath = path.relative(rootPath, filePath);
|
|
97
|
+
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
|
|
98
|
+
}
|
|
99
|
+
function isErrorWithCode(error, code) {
|
|
100
|
+
return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
|
|
101
|
+
}
|
|
102
|
+
function throwDoctorFixPathOutsideRoot(fixPath) {
|
|
103
|
+
throw new Error(`Doctor fix path resolves outside the app root: ${fixPath}`);
|
|
65
104
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remix-run/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Command-line interface for Remix",
|
|
5
5
|
"author": "Michael Jackson <mjijackson@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"semver": "^7.7.4",
|
|
34
|
-
"@remix-run/
|
|
35
|
-
"@remix-run/
|
|
34
|
+
"@remix-run/test": "^0.5.0",
|
|
35
|
+
"@remix-run/terminal": "^0.1.1"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^24.6.0",
|
package/src/lib/doctor/fixes.ts
CHANGED
|
@@ -11,7 +11,7 @@ export async function applyDoctorFixPlans(
|
|
|
11
11
|
let appliedFixes: DoctorAppliedFix[] = []
|
|
12
12
|
|
|
13
13
|
for (let fixPlan of dedupeDoctorFixPlans(fixPlans)) {
|
|
14
|
-
let absolutePath = resolveDoctorFixPath(appRoot, fixPlan.path)
|
|
14
|
+
let absolutePath = await resolveDoctorFixPath(appRoot, fixPlan.path)
|
|
15
15
|
|
|
16
16
|
if (fixPlan.kind === 'create-directory') {
|
|
17
17
|
await fs.mkdir(absolutePath, { recursive: true })
|
|
@@ -30,8 +30,7 @@ export async function applyDoctorFixPlans(
|
|
|
30
30
|
try {
|
|
31
31
|
await fs.writeFile(absolutePath, fixPlan.contents ?? '', { encoding: 'utf8', flag: 'wx' })
|
|
32
32
|
} catch (error) {
|
|
33
|
-
|
|
34
|
-
if (nodeError.code !== 'EEXIST') {
|
|
33
|
+
if (!isErrorWithCode(error, 'EEXIST')) {
|
|
35
34
|
throw error
|
|
36
35
|
}
|
|
37
36
|
|
|
@@ -71,14 +70,64 @@ function toAppliedDoctorFix(fixPlan: DoctorFixPlan): DoctorAppliedFix {
|
|
|
71
70
|
}
|
|
72
71
|
}
|
|
73
72
|
|
|
74
|
-
function resolveDoctorFixPath(appRoot: string, fixPath: string): string {
|
|
73
|
+
async function resolveDoctorFixPath(appRoot: string, fixPath: string): Promise<string> {
|
|
74
|
+
let absolutePath: string
|
|
75
|
+
|
|
75
76
|
try {
|
|
76
|
-
|
|
77
|
+
absolutePath = resolveContainedPath(appRoot, fixPath)
|
|
77
78
|
} catch (error) {
|
|
78
79
|
if (error instanceof Error && error.message.includes('escapes the allowed root')) {
|
|
79
|
-
|
|
80
|
+
throwDoctorFixPathOutsideRoot(fixPath)
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
throw error
|
|
83
84
|
}
|
|
85
|
+
|
|
86
|
+
let rootPath = path.resolve(appRoot)
|
|
87
|
+
let rootRealPath = await fs.realpath(rootPath)
|
|
88
|
+
let relativePath = path.relative(rootPath, absolutePath)
|
|
89
|
+
let pathParts = relativePath === '' ? [] : relativePath.split(path.sep)
|
|
90
|
+
let currentPath = rootPath
|
|
91
|
+
let currentRealPath = rootRealPath
|
|
92
|
+
|
|
93
|
+
for (let [index, pathPart] of pathParts.entries()) {
|
|
94
|
+
currentPath = path.join(currentPath, pathPart)
|
|
95
|
+
|
|
96
|
+
let stats = await lstatIfExists(currentPath)
|
|
97
|
+
if (stats == null) {
|
|
98
|
+
return path.join(currentRealPath, ...pathParts.slice(index))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
currentRealPath = await fs.realpath(currentPath)
|
|
102
|
+
if (!isPathInsideRoot(rootRealPath, currentRealPath)) {
|
|
103
|
+
throwDoctorFixPathOutsideRoot(fixPath)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return currentRealPath
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function lstatIfExists(filePath: string) {
|
|
111
|
+
try {
|
|
112
|
+
return await fs.lstat(filePath)
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (isErrorWithCode(error, 'ENOENT')) {
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
throw error
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isPathInsideRoot(rootPath: string, filePath: string): boolean {
|
|
123
|
+
let relativePath = path.relative(rootPath, filePath)
|
|
124
|
+
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isErrorWithCode(error: unknown, code: string): boolean {
|
|
128
|
+
return typeof error === 'object' && error !== null && 'code' in error && error.code === code
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function throwDoctorFixPathOutsideRoot(fixPath: string): never {
|
|
132
|
+
throw new Error(`Doctor fix path resolves outside the app root: ${fixPath}`)
|
|
84
133
|
}
|
|
@@ -245,7 +245,7 @@ Use this map to find the right package quickly. Each entry says what the package
|
|
|
245
245
|
- `remix/ui` — the component runtime: components, core mixins, `clientEntry`, `run`, `<Frame>`, navigation helpers, and `createRoot`. Use for app UI behavior
|
|
246
246
|
- `remix/ui/server` — server rendering: `renderToStream`, `renderToString`. Use in the `app/actions/render.tsx` helper that returns HTML responses
|
|
247
247
|
- `remix/ui/animation` — animation APIs: `animateEntrance`, `animateExit`, `animateLayout`, `spring`, `tween`, and `easings`
|
|
248
|
-
- `remix/ui/<primitive>` — UI primitives, mixins,
|
|
248
|
+
- `remix/ui/<primitive>` — UI primitives, mixins, and component helpers. Current subpaths include `remix/ui/accordion`, `remix/ui/anchor`, `remix/ui/button`, `remix/ui/checkbox`, `remix/ui/combobox`, `remix/ui/input`, `remix/ui/listbox`, `remix/ui/menu`, `remix/ui/popover`, and `remix/ui/select`
|
|
249
249
|
- `remix/ui/test` — component test rendering helpers such as `render`
|
|
250
250
|
- `remix/ui/jsx-runtime` and `remix/ui/jsx-dev-runtime` — JSX transform targets. Configured in `tsconfig.json`, rarely imported directly
|
|
251
251
|
- `remix/html-template` — escaped HTML template literals. Use when generating HTML outside the component system (RSS feeds, email bodies, error pages)
|
|
@@ -5,4 +5,13 @@ run({
|
|
|
5
5
|
let mod = await import(moduleUrl)
|
|
6
6
|
return mod[exportName]
|
|
7
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
|
+
},
|
|
8
17
|
})
|
package/template/app/assets.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createAssetServer } from 'remix/assets'
|
|
2
2
|
|
|
3
3
|
const rootDir = process.cwd()
|
|
4
|
+
const nodeEnv = process.env.NODE_ENV ?? 'development'
|
|
5
|
+
const isDevelopment = nodeEnv === 'development'
|
|
4
6
|
|
|
5
7
|
export const assetServer = createAssetServer({
|
|
6
8
|
basePath: '/assets',
|
|
@@ -10,11 +12,7 @@ export const assetServer = createAssetServer({
|
|
|
10
12
|
'node_modules/*path': 'node_modules/*path',
|
|
11
13
|
},
|
|
12
14
|
allow: ['app/assets/**', 'node_modules/**'],
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
define: {
|
|
17
|
-
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV ?? 'development'),
|
|
18
|
-
},
|
|
19
|
-
},
|
|
15
|
+
sourceMaps: isDevelopment ? 'external' : undefined,
|
|
16
|
+
minify: !isDevelopment,
|
|
17
|
+
watch: false,
|
|
20
18
|
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as path from 'node:path'
|
|
2
2
|
|
|
3
|
+
import type { Router } from 'remix/router'
|
|
3
4
|
import { renderWith } from 'remix/middleware/render'
|
|
4
5
|
import { createHtmlResponse } from 'remix/response/html'
|
|
5
6
|
import type { RemixNode } from 'remix/ui'
|
|
@@ -9,10 +10,12 @@ import { assetServer } from '../assets.ts'
|
|
|
9
10
|
|
|
10
11
|
export function render() {
|
|
11
12
|
return renderWith(
|
|
12
|
-
({ request }) =>
|
|
13
|
+
({ request, router }) =>
|
|
13
14
|
function render(node: RemixNode, init?: ResponseInit) {
|
|
14
15
|
let stream = renderToStream(node, {
|
|
16
|
+
frameSrc: request.url,
|
|
15
17
|
signal: request.signal,
|
|
18
|
+
resolveFrame: (src) => resolveFrame(router, request, src),
|
|
16
19
|
// Server rendering turns client entries into browser module URLs.
|
|
17
20
|
async resolveClientEntry(entryId, component) {
|
|
18
21
|
if (!entryId.startsWith('file://')) {
|
|
@@ -33,6 +36,31 @@ export function render() {
|
|
|
33
36
|
)
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
async function resolveFrame(router: Router, request: Request, src: string) {
|
|
40
|
+
let url = new URL(src, request.url)
|
|
41
|
+
|
|
42
|
+
let headers = new Headers()
|
|
43
|
+
headers.set('Accept', 'text/html')
|
|
44
|
+
|
|
45
|
+
let cookie = request.headers.get('Cookie')
|
|
46
|
+
if (cookie) headers.set('Cookie', cookie)
|
|
47
|
+
|
|
48
|
+
let response = await router.fetch(
|
|
49
|
+
new Request(url, {
|
|
50
|
+
method: 'GET',
|
|
51
|
+
headers,
|
|
52
|
+
signal: request.signal,
|
|
53
|
+
}),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
return `<pre>Frame error: ${response.status} ${response.statusText}</pre>`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (response.body) return response.body
|
|
61
|
+
return await response.text()
|
|
62
|
+
}
|
|
63
|
+
|
|
36
64
|
function titleCaseFileName(fileUrl: string): string {
|
|
37
65
|
let url = new URL(fileUrl)
|
|
38
66
|
let fileName = path.basename(url.pathname, path.extname(url.pathname))
|
|
@@ -197,7 +197,7 @@ function CodingWithAiCard() {
|
|
|
197
197
|
<PromptButton text="I want to build a simple headless Shopify store, what does Remix have available to help scaffold this?" />
|
|
198
198
|
<PromptButton text="Add a sqlite database with a users table and scaffold a signup flow" />
|
|
199
199
|
<PromptButton text="Make a copy to clipboard component that confirms to the user it was copied then resets after a few seconds" />
|
|
200
|
-
<PromptButton text="Add
|
|
200
|
+
<PromptButton text="Add a page with a remix/ui/select component and remix/ui/button variants" />
|
|
201
201
|
<PromptButton text="Add compression middleware" />
|
|
202
202
|
</div>
|
|
203
203
|
</div>
|
package/template/package.json
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"dev": "node --watch --import remix/node-tsx server.ts",
|
|
7
|
-
"start": "node --import remix/node-tsx server.ts",
|
|
8
|
-
"test": "node --import remix/node-tsx --test",
|
|
6
|
+
"dev": "NODE_ENV=development node --watch --import remix/node-tsx server.ts",
|
|
7
|
+
"start": "NODE_ENV=production node --import remix/node-tsx server.ts",
|
|
8
|
+
"test": "NODE_ENV=test node --import remix/node-tsx --test",
|
|
9
9
|
"typecheck": "tsc --noEmit"
|
|
10
10
|
},
|
|
11
11
|
"engines": {
|