Skip to content

Commit 3ff2e8a

Browse files
committed
update to prisma v6 and ditch installGlobals
1 parent 070600f commit 3ff2e8a

File tree

527 files changed

+3939
-2927
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

527 files changed

+3939
-2927
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,14 @@ Vitest renamed `SpyInstance` to `MockInstance` so the videos will use
2323
- SpyInstance<Parameters<typeof console.error>>
2424
+ MockInstance<typeof console.error>
2525
```
26+
27+
## Prisma v6
28+
29+
Updated everything to use Prisma v6. The only substantial change is instead of
30+
using `Buffer.from` to convert a file to a `Uint8Array`, we now use
31+
`new Uint8Array(await file.arrayBuffer())`.
32+
33+
## `installGlobals`
34+
35+
`installGlobals` is no longer needed in modern versions of Node.js and
36+
references to it have been removed from the exercise content.

epicshop/package-lock.json

Lines changed: 819 additions & 858 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

epicshop/package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
"type": "module",
33
"dependencies": {
44
"@epic-web/workshop-app": "^6.43.0",
5+
"@epic-web/workshop-cli": "^6.43.0",
6+
"@epic-web/workshop-utils": "^6.43.0",
57
"chokidar": "^3.6.0",
6-
"execa": "^9.3.0",
8+
"enquirer": "^2.4.1",
9+
"execa": "^9.6.1",
710
"fs-extra": "^11.2.0",
8-
"@epic-web/workshop-cli": "^6.43.0",
9-
"@epic-web/workshop-utils": "^6.43.0"
11+
"match-sorter": "^8.2.0",
12+
"p-limit": "^7.2.0"
1013
}
1114
}

epicshop/test.js

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
import path from 'node:path'
2+
import { performance } from 'perf_hooks'
3+
import { fileURLToPath } from 'url'
4+
import {
5+
getApps,
6+
getAppDisplayName,
7+
} from '@epic-web/workshop-utils/apps.server'
8+
import enquirer from 'enquirer'
9+
import { execa } from 'execa'
10+
import { matchSorter } from 'match-sorter'
11+
import pLimit from 'p-limit'
12+
13+
const { prompt } = enquirer
14+
15+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
16+
17+
function captureOutput() {
18+
const output = []
19+
return {
20+
write: (chunk, streamType) => {
21+
output.push({ chunk: chunk.toString(), streamType })
22+
},
23+
replay: () => {
24+
for (const { chunk, streamType } of output) {
25+
if (streamType === 'stderr') {
26+
process.stderr.write(chunk)
27+
} else {
28+
process.stdout.write(chunk)
29+
}
30+
}
31+
},
32+
hasOutput: () => output.length > 0,
33+
}
34+
}
35+
36+
function printTestSummary(results) {
37+
const label = '--- Test Summary ---'
38+
console.log(`\n${label}`)
39+
for (const [appPath, { result, duration }] of results) {
40+
let emoji
41+
switch (result) {
42+
case 'Passed':
43+
emoji = '✅'
44+
break
45+
case 'Failed':
46+
emoji = '❌'
47+
break
48+
case 'Error':
49+
emoji = '💥'
50+
break
51+
case 'Incomplete':
52+
emoji = '⏳'
53+
break
54+
default:
55+
emoji = '❓'
56+
}
57+
console.log(`${emoji} ${appPath} (${duration.toFixed(2)}s)`)
58+
}
59+
console.log(`${'-'.repeat(label.length)}\n`)
60+
}
61+
62+
async function main() {
63+
const allApps = await getApps()
64+
const allAppsWithTests = allApps.filter((app) => app.test?.type === 'script')
65+
66+
if (allAppsWithTests.length === 0) {
67+
console.error(
68+
'❌ No apps with tests were found. Ensure your apps have a test script defined in the package.json. Exiting.',
69+
)
70+
process.exit(1)
71+
}
72+
73+
let selectedApps
74+
let additionalArgs = []
75+
76+
// Parse command-line arguments
77+
const argIndex = process.argv.indexOf('--')
78+
if (argIndex !== -1) {
79+
additionalArgs = process.argv.slice(argIndex + 1)
80+
process.argv = process.argv.slice(0, argIndex)
81+
}
82+
83+
if (process.argv[2]) {
84+
const patterns = process.argv[2].toLowerCase().split(',')
85+
selectedApps = allAppsWithTests.filter((app) => {
86+
const { exerciseNumber, stepNumber, type } = app
87+
88+
return patterns.some((pattern) => {
89+
let [patternExercise = '*', patternStep = '*', patternType = '*'] =
90+
pattern.split('.')
91+
92+
patternExercise ||= '*'
93+
patternStep ||= '*'
94+
patternType ||= '*'
95+
96+
return (
97+
(patternExercise === '*' ||
98+
exerciseNumber === Number(patternExercise)) &&
99+
(patternStep === '*' || stepNumber === Number(patternStep)) &&
100+
(patternType === '*' || type.includes(patternType))
101+
)
102+
})
103+
})
104+
} else {
105+
const displayNameMap = new Map(
106+
allAppsWithTests.map((app) => [
107+
getAppDisplayName(app, allAppsWithTests),
108+
app,
109+
]),
110+
)
111+
const choices = displayNameMap.keys()
112+
113+
const response = await prompt({
114+
type: 'autocomplete',
115+
name: 'appDisplayNames',
116+
message: 'Select apps to test:',
117+
choices: ['All', ...choices],
118+
multiple: true,
119+
suggest: (input, choices) => {
120+
return matchSorter(choices, input, { keys: ['name'] })
121+
},
122+
})
123+
124+
selectedApps = response.appDisplayNames.includes('All')
125+
? allAppsWithTests
126+
: response.appDisplayNames.map((appDisplayName) =>
127+
displayNameMap.get(appDisplayName),
128+
)
129+
130+
// Update this block to use process.argv
131+
const appPattern =
132+
selectedApps.length === allAppsWithTests.length
133+
? '*'
134+
: selectedApps
135+
.map((app) => `${app.exerciseNumber}.${app.stepNumber}.${app.type}`)
136+
.join(',')
137+
const additionalArgsString =
138+
additionalArgs.length > 0 ? ` -- ${additionalArgs.join(' ')}` : ''
139+
console.log(`\nℹ️ To skip the prompt next time, use this command:`)
140+
console.log(`npm test -- ${appPattern}${additionalArgsString}\n`)
141+
}
142+
143+
if (selectedApps.length === 0) {
144+
console.log('⚠️ No apps selected. Exiting.')
145+
return
146+
}
147+
148+
if (selectedApps.length === 1) {
149+
const app = selectedApps[0]
150+
console.log(`🚀 Running tests for ${app.relativePath}\n\n`)
151+
const startTime = performance.now()
152+
try {
153+
await execa('npm', ['run', 'test', '--silent', '--', ...additionalArgs], {
154+
cwd: app.fullPath,
155+
stdio: 'inherit',
156+
env: {
157+
...process.env,
158+
PORT: app.dev.portNumber,
159+
},
160+
})
161+
const duration = (performance.now() - startTime) / 1000
162+
console.log(
163+
`✅ Finished tests for ${app.relativePath} (${duration.toFixed(2)}s)`,
164+
)
165+
} catch {
166+
const duration = (performance.now() - startTime) / 1000
167+
console.error(
168+
`❌ Tests failed for ${app.relativePath} (${duration.toFixed(2)}s)`,
169+
)
170+
process.exit(1)
171+
}
172+
} else {
173+
const limit = pLimit(1)
174+
let hasFailures = false
175+
const runningProcesses = new Map()
176+
let isShuttingDown = false
177+
const results = new Map()
178+
179+
const shutdownHandler = () => {
180+
if (isShuttingDown) return
181+
isShuttingDown = true
182+
console.log('\nGracefully shutting down. Please wait...')
183+
console.log('Outputting results of running tests:')
184+
for (const [app, output] of runningProcesses.entries()) {
185+
if (output.hasOutput()) {
186+
console.log(`\nPartial results for ${app.relativePath}:\n\n`)
187+
output.replay()
188+
console.log('\n\n')
189+
} else {
190+
console.log(`ℹ️ No output captured for ${app.relativePath}`)
191+
}
192+
// Set result for incomplete tests
193+
if (!results.has(app.relativePath)) {
194+
results.set(app.relativePath, 'Incomplete')
195+
}
196+
}
197+
printTestSummary(results)
198+
// Allow some time for output to be written before exiting
199+
setTimeout(() => process.exit(1), 100)
200+
}
201+
202+
process.on('SIGINT', shutdownHandler)
203+
process.on('SIGTERM', shutdownHandler)
204+
205+
const tasks = selectedApps.map((app) =>
206+
limit(async () => {
207+
if (isShuttingDown) return
208+
console.log(`🚀 Starting tests for ${app.relativePath}`)
209+
const output = captureOutput()
210+
runningProcesses.set(app, output)
211+
const startTime = performance.now()
212+
try {
213+
const subprocess = execa(
214+
'npm',
215+
['run', 'test', '--silent', '--', ...additionalArgs],
216+
{
217+
cwd: path.join(__dirname, '..', app.relativePath),
218+
reject: false,
219+
env: {
220+
...process.env,
221+
PORT: app.dev.portNumber,
222+
},
223+
},
224+
)
225+
226+
subprocess.stdout.on('data', (chunk) => output.write(chunk, 'stdout'))
227+
subprocess.stderr.on('data', (chunk) => output.write(chunk, 'stderr'))
228+
229+
const { exitCode } = await subprocess
230+
const duration = (performance.now() - startTime) / 1000
231+
232+
runningProcesses.delete(app)
233+
234+
if (exitCode !== 0) {
235+
hasFailures = true
236+
console.error(
237+
`\n❌ Tests failed for ${app.relativePath} (${duration.toFixed(2)}s):\n\n`,
238+
)
239+
output.replay()
240+
console.log('\n\n')
241+
results.set(app.relativePath, { result: 'Failed', duration })
242+
// Set result for incomplete tests
243+
if (!results.has(app.relativePath)) {
244+
results.set(app.relativePath, 'Incomplete')
245+
}
246+
} else {
247+
console.log(
248+
`✅ Finished tests for ${app.relativePath} (${duration.toFixed(2)}s)`,
249+
)
250+
results.set(app.relativePath, { result: 'Passed', duration })
251+
}
252+
} catch (error) {
253+
const duration = (performance.now() - startTime) / 1000
254+
runningProcesses.delete(app)
255+
hasFailures = true
256+
console.error(
257+
`\n❌ An error occurred while running tests for ${app.relativePath} (${duration.toFixed(2)}s):\n\n`,
258+
)
259+
console.error(error.message)
260+
output.replay()
261+
console.log('\n\n')
262+
results.set(app.relativePath, { result: 'Error', duration })
263+
}
264+
}),
265+
)
266+
267+
await Promise.all(tasks)
268+
269+
// Print summary output
270+
printTestSummary(results)
271+
272+
if (hasFailures) {
273+
process.exit(1)
274+
}
275+
}
276+
}
277+
278+
main().catch((error) => {
279+
if (error) {
280+
console.error('❌ An error occurred:', error)
281+
}
282+
setTimeout(() => process.exit(1), 100)
283+
})

epicshop/update-deps.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
npx npm-check-updates --dep prod,dev --upgrade --workspaces --root --reject eslint,@conform-to/react,@conform-to/zod
1+
npx npm-check-updates --dep prod,dev --upgrade --workspaces --root --reject eslint,@conform-to/react,@conform-to/zod,remix-auth-github,@remix-run/react,@remix-run/node,@remix-run/express,@remix-run/serve,@remix-run/css-bundle
22
cd epicshop && npx npm-check-updates --dep prod,dev --upgrade --root
33
cd ..
44
rm -rf node_modules package-lock.json ./epicshop/package-lock.json ./epicshop/node_modules ./exercises/**/node_modules
55
npm install
66
npm run setup
77
npm run typecheck
8-
npm run lint --fix
8+
npm run lint -- --fix

exercises/01.e2e/01.problem.playwright/app/routes/resources+/note-images.$imageId.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export async function loader({ params }: LoaderFunctionArgs) {
1414
return new Response(image.blob, {
1515
headers: {
1616
'content-type': image.contentType,
17-
'content-length': Buffer.byteLength(image.blob).toString(),
17+
'content-length': image.blob.length.toString(),
1818
'content-disposition': `inline; filename="${params.imageId}"`,
1919
'cache-control': 'public, max-age=31536000, immutable',
2020
},

exercises/01.e2e/01.problem.playwright/app/routes/resources+/user-images.$imageId.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export async function loader({ params }: LoaderFunctionArgs) {
1414
return new Response(image.blob, {
1515
headers: {
1616
'content-type': image.contentType,
17-
'content-length': Buffer.byteLength(image.blob).toString(),
17+
'content-length': image.blob.length.toString(),
1818
'content-disposition': `inline; filename="${params.imageId}"`,
1919
'cache-control': 'public, max-age=31536000, immutable',
2020
},

exercises/01.e2e/01.problem.playwright/app/routes/settings+/profile.photo.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export async function action({ request }: ActionFunctionArgs) {
7676
return {
7777
image: {
7878
contentType: data.photoFile.type,
79-
blob: Buffer.from(await data.photoFile.arrayBuffer()),
79+
blob: new Uint8Array(await data.photoFile.arrayBuffer()),
8080
},
8181
}
8282
}),

exercises/01.e2e/01.problem.playwright/app/routes/users+/$username_+/__note-editor.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
9696
id: i.id,
9797
altText: i.altText,
9898
contentType: i.file.type,
99-
blob: Buffer.from(await i.file.arrayBuffer()),
99+
blob: new Uint8Array(await i.file.arrayBuffer()),
100100
}
101101
} else {
102102
return { id: i.id, altText: i.altText }
@@ -111,7 +111,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
111111
return {
112112
altText: image.altText,
113113
contentType: image.file.type,
114-
blob: Buffer.from(await image.file.arrayBuffer()),
114+
blob: new Uint8Array(await image.file.arrayBuffer()),
115115
}
116116
}),
117117
),

exercises/01.e2e/01.problem.playwright/app/utils/misc.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ export async function downloadFile(url: string, retries: number = 0) {
303303
throw new Error(`Failed to fetch image with status ${response.status}`)
304304
}
305305
const contentType = response.headers.get('content-type') ?? 'image/jpg'
306-
const blob = Buffer.from(await response.arrayBuffer())
306+
const blob = new Uint8Array(await response.arrayBuffer())
307307
return { contentType, blob }
308308
} catch (e) {
309309
if (retries > MAX_RETRIES) throw e

0 commit comments

Comments
 (0)