| 1 | import { |
| 2 | packageWebsiteArtifact, |
| 3 | stageWebsiteArtifact, |
| 4 | } from "@/Neon/Website/Artifact.ts"; |
| 5 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 6 | import { expect, it } from "alchemy-test"; |
| 7 | import * as Effect from "effect/Effect"; |
| 8 | import * as FileSystem from "effect/FileSystem"; |
| 9 | import * as Path from "effect/Path"; |
| 10 | import * as Result from "effect/Result"; |
| 11 | import * as Stream from "effect/Stream"; |
| 12 | import * as ChildProcess from "effect/process/ChildProcess"; |
| 13 | import { unzipSync } from "fflate"; |
| 14 | import * as FetchHttpClient from "effect/http/FetchHttpClient"; |
| 15 | import { createHash } from "node:crypto"; |
| 16 | import { gzipSync } from "node:zlib"; |
| 17 | |
| 18 | const fixture = Effect.gen(function* () { |
| 19 | const fs = yield* FileSystem.FileSystem; |
| 20 | const path = yield* Path.Path; |
| 21 | const root = yield* fs.makeTempDirectoryScoped({ |
| 22 | prefix: "neon-website-test-", |
| 23 | }); |
| 24 | const dist = path.join(root, "dist"); |
| 25 | yield* fs.makeDirectory(dist); |
| 26 | yield* fs.writeFileString(path.join(dist, "index.html"), "<h1>Neon</h1>"); |
| 27 | return { fs, path, root, dist }; |
| 28 | }); |
| 29 | |
| 30 | it.effect( |
| 31 | "static archives are deterministic and omit env and source maps", |
| 32 | () => |
| 33 | Effect.gen(function* () { |
| 34 | const { fs, path, root, dist } = yield* fixture; |
| 35 | yield* fs.writeFileString( |
| 36 | path.join(dist, ".env.production"), |
| 37 | "TOKEN=do-not-package", |
| 38 | ); |
| 39 | yield* fs.writeFileString(path.join(dist, "app.js.map"), "source-secret"); |
| 40 | const props = { |
| 41 | root, |
| 42 | distDir: dist, |
| 43 | static: { notFoundHandling: "spa" as const }, |
| 44 | }; |
| 45 | const first = yield* packageWebsiteArtifact(props); |
| 46 | const second = yield* packageWebsiteArtifact(props); |
| 47 | expect(first.hash).toBe(second.hash); |
| 48 | const files = yield* Effect.sync(() => unzipSync(first.archive)); |
| 49 | expect(files["index.mjs"]).toBeDefined(); |
| 50 | expect( |
| 51 | Object.keys(files).some( |
| 52 | (name) => name.includes(".env") || name.endsWith(".map"), |
| 53 | ), |
| 54 | ).toBe(false); |
| 55 | const source = yield* Effect.sync(() => |
| 56 | new TextDecoder().decode(files["index.mjs"]), |
| 57 | ); |
| 58 | expect(source).toContain("export default"); |
| 59 | expect(source).not.toContain(".listen("); |
| 60 | yield* fs.writeFileString( |
| 61 | path.join(dist, "index.html"), |
| 62 | "<h1>Updated</h1>", |
| 63 | ); |
| 64 | expect((yield* packageWebsiteArtifact(props)).hash).not.toBe(first.hash); |
| 65 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), |
| 66 | { tags: ["unit", "provider:neon", "provider:neon:website", "local"] }, |
| 67 | ); |
| 68 | |
| 69 | for (const kind of [ |
| 70 | "escape", |
| 71 | "native", |
| 72 | "executable", |
| 73 | "secret-alias", |
| 74 | "cycle", |
| 75 | "error-page", |
| 76 | ] as const) { |
| 77 | it.effect( |
| 78 | `rejects unsafe ${kind} output`, |
| 79 | () => |
| 80 | Effect.gen(function* () { |
| 81 | const { fs, path, root, dist } = yield* fixture; |
| 82 | if (kind === "escape") { |
| 83 | yield* fs.writeFileString(path.join(root, "secret.txt"), "private"); |
| 84 | yield* fs.symlink("../secret.txt", path.join(dist, "leak.txt")); |
| 85 | } else if (kind === "native") |
| 86 | yield* fs.writeFileString(path.join(dist, "addon.node"), "native"); |
| 87 | else if (kind === "executable") |
| 88 | yield* fs.writeFile( |
| 89 | path.join(dist, "binary"), |
| 90 | new Uint8Array([0xcf, 0xfa, 0xed, 0xfe]), |
| 91 | ); |
| 92 | else if (kind === "secret-alias") { |
| 93 | yield* fs.writeFileString(path.join(dist, ".env"), "SECRET=hidden"); |
| 94 | yield* fs.symlink(".env", path.join(dist, "public.txt")); |
| 95 | } else if (kind === "cycle") |
| 96 | yield* fs.symlink(".", path.join(dist, "cycle")); |
| 97 | const result = yield* stageWebsiteArtifact({ |
| 98 | root, |
| 99 | distDir: dist, |
| 100 | static: { |
| 101 | errorPage: kind === "error-page" ? "../secret.txt" : undefined, |
| 102 | }, |
| 103 | }).pipe(Effect.result); |
| 104 | expect(Result.isFailure(result)).toBe(true); |
| 105 | if (Result.isFailure(result)) |
| 106 | expect(result.failure._tag).toBe("WebsiteArtifactError"); |
| 107 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), |
| 108 | { tags: ["unit", "provider:neon", "provider:neon:website", "local"] }, |
| 109 | ); |
| 110 | } |
| 111 | |
| 112 | for (const sensitive of [ |
| 113 | ".env.production", |
| 114 | ".alchemy/state file.json", |
| 115 | "private.pem", |
| 116 | ]) |
| 117 | for (const alias of [false, true]) |
| 118 | it.effect( |
| 119 | `rejects traced ${JSON.stringify(sensitive)}${alias ? " through a symlink" : ""} with a sanitized filename`, |
| 120 | () => |
| 121 | Effect.gen(function* () { |
| 122 | const { fs, path, root, dist } = yield* fixture; |
| 123 | const secret = path.join(root, sensitive); |
| 124 | yield* fs.makeDirectory(path.dirname(secret), { recursive: true }); |
| 125 | yield* fs.writeFileString(secret, "fixture-secret-do-not-log"); |
| 126 | const dependency = alias ? "runtime-data.txt" : sensitive; |
| 127 | if (alias) yield* fs.symlink(secret, path.join(root, dependency)); |
| 128 | const serverEntry = path.join(dist, "serve.mjs"); |
| 129 | yield* fs.writeFileString( |
| 130 | serverEntry, |
| 131 | `import { readFileSync } from "node:fs"; |
| 132 | const value = readFileSync(new URL(${JSON.stringify(`../${dependency}`)}, import.meta.url), "utf8"); |
| 133 | export default { fetch: () => new Response(value) };`, |
| 134 | ); |
| 135 | const result = yield* stageWebsiteArtifact({ |
| 136 | root, |
| 137 | distDir: dist, |
| 138 | serverEntry, |
| 139 | }).pipe(Effect.result); |
| 140 | expect(Result.isFailure(result)).toBe(true); |
| 141 | if (Result.isFailure(result)) { |
| 142 | expect(result.failure._tag).toBe("WebsiteArtifactError"); |
| 143 | expect(result.failure.message).toBe( |
| 144 | `A traced dependency selects a sensitive file: ${sensitive.replace(/[^a-zA-Z0-9_./@+-]/g, "_")}`, |
| 145 | ); |
| 146 | expect(result.failure.message).not.toContain(root); |
| 147 | expect(result.failure.message).not.toContain( |
| 148 | "fixture-secret-do-not-log", |
| 149 | ); |
| 150 | } |
| 151 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), |
| 152 | { tags: ["unit", "provider:neon", "provider:neon:website", "local"] }, |
| 153 | ); |
| 154 | |
| 155 | it.effect( |
| 156 | "traces referenced source data without selecting neighboring state or secrets", |
| 157 | () => |
| 158 | Effect.gen(function* () { |
| 159 | const { fs, path, root, dist } = yield* fixture; |
| 160 | yield* fs.makeDirectory(path.join(root, "src")); |
| 161 | yield* fs.makeDirectory(path.join(root, ".alchemy")); |
| 162 | yield* fs.writeFileString(path.join(root, "src/guide.mdx"), "# Guide"); |
| 163 | yield* fs.writeFileString(path.join(root, ".env"), "fixture-secret"); |
| 164 | yield* fs.writeFileString(path.join(root, "private.key"), "fixture-key"); |
| 165 | yield* fs.writeFileString( |
| 166 | path.join(root, ".alchemy/state.json"), |
| 167 | '{"private":"fixture-state"}', |
| 168 | ); |
| 169 | const serverEntry = path.join(dist, "serve.mjs"); |
| 170 | yield* fs.writeFileString( |
| 171 | serverEntry, |
| 172 | 'import { readFileSync } from "node:fs"; const guide = readFileSync(new URL("../src/guide.mdx", import.meta.url), "utf8"); export default { fetch: () => new Response(guide) };', |
| 173 | ); |
| 174 | const props = { root, distDir: dist, serverEntry }; |
| 175 | const first = yield* packageWebsiteArtifact(props); |
| 176 | const archive = yield* Effect.sync(() => unzipSync(first.archive)); |
| 177 | expect(Object.keys(archive).sort()).toEqual([ |
| 178 | "files/dist/index.html", |
| 179 | "files/dist/serve.mjs", |
| 180 | "files/src/guide.mdx", |
| 181 | "index.mjs", |
| 182 | ]); |
| 183 | yield* fs.writeFileString( |
| 184 | path.join(root, "src/guide.mdx"), |
| 185 | "# Updated guide", |
| 186 | ); |
| 187 | expect((yield* packageWebsiteArtifact(props)).hash).not.toBe(first.hash); |
| 188 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), |
| 189 | { tags: ["unit", "provider:neon", "provider:neon:website", "local"] }, |
| 190 | ); |
| 191 | |
| 192 | for (const alias of [false, true]) |
| 193 | it.effect( |
| 194 | `rejects a traced workspace escape${alias ? " through a symlink" : ""}`, |
| 195 | () => |
| 196 | Effect.gen(function* () { |
| 197 | const { fs, path, root, dist } = yield* fixture; |
| 198 | const outside = yield* fs.makeTempDirectoryScoped(); |
| 199 | const dependency = path.join(outside, "runtime-data.txt"); |
| 200 | yield* fs.writeFileString(dependency, "outside-workspace"); |
| 201 | const selected = alias |
| 202 | ? path.join(root, "runtime-data.txt") |
| 203 | : dependency; |
| 204 | if (alias) yield* fs.symlink(dependency, selected); |
| 205 | const serverEntry = path.join(dist, "serve.mjs"); |
| 206 | yield* fs.writeFileString( |
| 207 | serverEntry, |
| 208 | `import { readFileSync } from "node:fs"; const value = readFileSync(${JSON.stringify(selected)}, "utf8"); export default { fetch: () => new Response(value) };`, |
| 209 | ); |
| 210 | const result = yield* stageWebsiteArtifact({ |
| 211 | root, |
| 212 | distDir: dist, |
| 213 | serverEntry, |
| 214 | }).pipe(Effect.result); |
| 215 | expect(Result.isFailure(result)).toBe(true); |
| 216 | if (Result.isFailure(result)) { |
| 217 | expect(result.failure._tag).toBe("WebsiteArtifactError"); |
| 218 | expect(result.failure.message).toBe( |
| 219 | "A traced dependency escapes the application workspace.", |
| 220 | ); |
| 221 | } |
| 222 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), |
| 223 | { tags: ["unit", "provider:neon", "provider:neon:website", "local"] }, |
| 224 | ); |
| 225 | |
| 226 | for (const mode of ["import", "require"] as const) |
| 227 | it.effect( |
| 228 | `resolves pnpm ${mode} aliases without copying untraced dependencies`, |
| 229 | () => |
| 230 | Effect.gen(function* () { |
| 231 | const { fs, path, root, dist } = yield* fixture; |
| 232 | const store = path.join( |
| 233 | root, |
| 234 | "node_modules", |
| 235 | ".pnpm", |
| 236 | "answer@1", |
| 237 | "node_modules", |
| 238 | "answer", |
| 239 | ); |
| 240 | yield* fs.makeDirectory(store, { recursive: true }); |
| 241 | yield* fs.writeFileString( |
| 242 | path.join(root, "package.json"), |
| 243 | '{"type":"module"}', |
| 244 | ); |
| 245 | yield* fs.writeFileString( |
| 246 | path.join(store, "package.json"), |
| 247 | '{"name":"answer","type":"module","exports":"./index.js"}', |
| 248 | ); |
| 249 | yield* fs.writeFileString( |
| 250 | path.join(store, "index.js"), |
| 251 | "export const answer = 42;", |
| 252 | ); |
| 253 | yield* fs.writeFileString(path.join(store, "unused.txt"), "not-traced"); |
| 254 | yield* fs.symlink( |
| 255 | ".pnpm/answer@1/node_modules/answer", |
| 256 | path.join(root, "node_modules", "answer"), |
| 257 | ); |
| 258 | const other = path.join( |
| 259 | root, |
| 260 | "node_modules/.pnpm/answer@2/node_modules/answer", |
| 261 | ); |
| 262 | yield* fs.makeDirectory(other, { recursive: true }); |
| 263 | yield* fs.writeFileString( |
| 264 | path.join(other, "package.json"), |
| 265 | '{"name":"answer","type":"module","exports":"./index.js"}', |
| 266 | ); |
| 267 | yield* fs.writeFileString( |
| 268 | path.join(other, "index.js"), |
| 269 | "export const answer = 99;", |
| 270 | ); |
| 271 | yield* fs.makeDirectory( |
| 272 | path.join(root, "node_modules/.pnpm/node_modules"), |
| 273 | ); |
| 274 | yield* fs.symlink( |
| 275 | "../answer@2/node_modules/answer", |
| 276 | path.join(root, "node_modules/.pnpm/node_modules/answer"), |
| 277 | ); |
| 278 | yield* fs.writeFileString( |
| 279 | path.join(dist, "other.mjs"), |
| 280 | 'import "../node_modules/.pnpm/node_modules/answer/index.js";', |
| 281 | ); |
| 282 | const serverEntry = path.join(dist, "serve-neon.mjs"); |
| 283 | yield* fs.writeFileString( |
| 284 | serverEntry, |
| 285 | (mode === "import" |
| 286 | ? 'import { answer } from "answer";' |
| 287 | : 'import { createRequire } from "node:module"; const require = createRequire(import.meta.url); const { answer } = require("answer");') + |
| 288 | " export default { fetch() { return new Response(String(answer)); } };", |
| 289 | ); |
| 290 | const props = { root, distDir: dist, serverEntry }; |
| 291 | const first = yield* packageWebsiteArtifact(props); |
| 292 | const files = yield* Effect.sync(() => unzipSync(first.archive)); |
| 293 | expect( |
| 294 | Object.keys(files).some((name) => |
| 295 | name.endsWith("node_modules/answer/index.js"), |
| 296 | ), |
| 297 | ).toBe(true); |
| 298 | expect( |
| 299 | Object.keys(files).some((name) => name.endsWith("unused.txt")), |
| 300 | ).toBe(false); |
| 301 | const staged = yield* stageWebsiteArtifact(props); |
| 302 | const proc = yield* ChildProcess.make("node", [ |
| 303 | "--input-type=module", |
| 304 | "-e", |
| 305 | `const {default: handler} = await import(${JSON.stringify(path.join(staged.directory, "index.mjs"))}); console.log(await handler.fetch(new Request("http://localhost/")).text());`, |
| 306 | ]); |
| 307 | const [code, stdout, stderr] = yield* Effect.all( |
| 308 | [ |
| 309 | proc.exitCode, |
| 310 | proc.stdout.pipe(Stream.decodeText, Stream.mkString), |
| 311 | proc.stderr.pipe(Stream.decodeText, Stream.mkString), |
| 312 | ], |
| 313 | { concurrency: "unbounded" }, |
| 314 | ); |
| 315 | expect({ code, stderr }).toEqual({ code: 0, stderr: "" }); |
| 316 | expect(stdout.trim()).toBe("42"); |
| 317 | yield* fs.writeFileString( |
| 318 | path.join(store, "index.js"), |
| 319 | "export const answer = 43;", |
| 320 | ); |
| 321 | expect((yield* packageWebsiteArtifact(props)).hash).not.toBe( |
| 322 | first.hash, |
| 323 | ); |
| 324 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), |
| 325 | { tags: ["unit", "provider:neon", "provider:neon:website", "local"] }, |
| 326 | ); |
| 327 | |
| 328 | const elf = (machine = 183) => { |
| 329 | const bytes = new Uint8Array(64); |
| 330 | bytes.set([0x7f, 0x45, 0x4c, 0x46, 2, 1, 1]); |
| 331 | bytes[16] = 3; |
| 332 | bytes[18] = machine; |
| 333 | return bytes; |
| 334 | }; |
| 335 | |
| 336 | for (const machine of [183, 62]) |
| 337 | it.effect( |
| 338 | `validates ELF target architecture ${machine}`, |
| 339 | () => |
| 340 | Effect.gen(function* () { |
| 341 | const { fs, path, root, dist } = yield* fixture; |
| 342 | yield* fs.writeFile( |
| 343 | path.join(dist, "addon.node"), |
| 344 | yield* Effect.sync(() => elf(machine)), |
| 345 | ); |
| 346 | const result = yield* stageWebsiteArtifact({ |
| 347 | root, |
| 348 | distDir: dist, |
| 349 | static: {}, |
| 350 | }).pipe(Effect.result); |
| 351 | expect(Result.isSuccess(result)).toBe(machine === 183); |
| 352 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), |
| 353 | { tags: ["unit", "provider:neon", "provider:neon:website", "local"] }, |
| 354 | ); |
| 355 | |
| 356 | it.effect( |
| 357 | "Sharp metadata-only traces do not materialize native packages", |
| 358 | () => |
| 359 | Effect.gen(function* () { |
| 360 | const { fs, path, root, dist } = yield* fixture; |
| 361 | const sharp = path.join(root, "node_modules/sharp"); |
| 362 | yield* fs.makeDirectory(sharp, { recursive: true }); |
| 363 | yield* fs.writeFileString( |
| 364 | path.join(sharp, "package.json"), |
| 365 | JSON.stringify({ name: "sharp", version: "0.35.4" }), |
| 366 | ); |
| 367 | const serverEntry = path.join(dist, "serve.mjs"); |
| 368 | yield* fs.writeFileString( |
| 369 | serverEntry, |
| 370 | 'import manifest from "sharp/package.json" with { type: "json" }; export default { fetch: () => new Response(manifest.version) };', |
| 371 | ); |
| 372 | const artifact = yield* stageWebsiteArtifact({ |
| 373 | root, |
| 374 | distDir: dist, |
| 375 | serverEntry, |
| 376 | }); |
| 377 | expect( |
| 378 | yield* fs.exists( |
| 379 | path.join( |
| 380 | artifact.directory, |
| 381 | "files/node_modules/sharp/package.json", |
| 382 | ), |
| 383 | ), |
| 384 | ).toBe(true); |
| 385 | expect( |
| 386 | yield* fs.exists( |
| 387 | path.join(artifact.directory, "files/node_modules/@img"), |
| 388 | ), |
| 389 | ).toBe(false); |
| 390 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), |
| 391 | { tags: ["unit", "provider:neon", "provider:neon:website", "local"] }, |
| 392 | ); |
| 393 | |
| 394 | // Minimal ustar fixtures exercise the registry boundary without downloading packages. |
| 395 | const tarball = (files: [string, Uint8Array, string?][]) => { |
| 396 | const blocks: Buffer[] = []; |
| 397 | for (const [name, content, type = "0"] of files) { |
| 398 | const header = Buffer.alloc(512); |
| 399 | header.write(name, 0); |
| 400 | header.write("0000644\0", 100); |
| 401 | header.write(content.length.toString(8).padStart(11, "0") + "\0", 124); |
| 402 | header.fill(32, 148, 156); |
| 403 | header.write(type, 156); |
| 404 | header.write("ustar\0", 257); |
| 405 | const checksum = header.reduce( |
| 406 | (sum: number, byte: number) => sum + byte, |
| 407 | 0, |
| 408 | ); |
| 409 | header.write(checksum.toString(8).padStart(6, "0") + "\0 ", 148); |
| 410 | blocks.push( |
| 411 | header, |
| 412 | Buffer.from(content), |
| 413 | Buffer.alloc((512 - (content.length % 512)) % 512), |
| 414 | ); |
| 415 | } |
| 416 | return gzipSync(Buffer.concat([...blocks, Buffer.alloc(1024)])); |
| 417 | }; |
| 418 | |
| 419 | for (const scenario of [ |
| 420 | "valid", |
| 421 | "integrity", |
| 422 | "identity", |
| 423 | "origin", |
| 424 | "traversal", |
| 425 | "symlink", |
| 426 | "architecture", |
| 427 | "musl", |
| 428 | ] as const) |
| 429 | it.effect( |
| 430 | `Sharp platform replacement verifies ${scenario}`, |
| 431 | () => |
| 432 | Effect.gen(function* () { |
| 433 | const { fs, path, root, dist } = yield* fixture; |
| 434 | const sharp = path.join(root, "node_modules/sharp"); |
| 435 | yield* fs.makeDirectory(sharp, { recursive: true }); |
| 436 | yield* fs.writeFileString( |
| 437 | path.join(sharp, "package.json"), |
| 438 | JSON.stringify({ |
| 439 | name: "sharp", |
| 440 | version: "0.34.5", |
| 441 | main: "index.js", |
| 442 | optionalDependencies: { |
| 443 | "@img/sharp-linux-arm64": "0.34.5", |
| 444 | "@img/sharp-libvips-linux-arm64": "1.2.4", |
| 445 | }, |
| 446 | }), |
| 447 | ); |
| 448 | yield* fs.writeFileString( |
| 449 | path.join(sharp, "index.js"), |
| 450 | "module.exports = 42;", |
| 451 | ); |
| 452 | const serverEntry = path.join(dist, "serve.mjs"); |
| 453 | yield* fs.writeFileString( |
| 454 | serverEntry, |
| 455 | 'import sharp from "sharp"; export default { fetch: () => new Response(String(sharp)) };', |
| 456 | ); |
| 457 | const requested: string[] = []; |
| 458 | const fetch = ((input: string | URL | Request) => |
| 459 | Effect.runPromise( |
| 460 | Effect.sync(() => { |
| 461 | const url = String(input); |
| 462 | requested.push(url); |
| 463 | const name = url.includes("libvips") |
| 464 | ? "@img/sharp-libvips-linux-arm64" |
| 465 | : "@img/sharp-linux-arm64"; |
| 466 | const version = name.includes("libvips") ? "1.2.4" : "0.34.5"; |
| 467 | const manifest = { |
| 468 | name, |
| 469 | version, |
| 470 | os: ["linux"], |
| 471 | cpu: ["arm64"], |
| 472 | libc: [scenario === "musl" ? "musl" : "glibc"], |
| 473 | }; |
| 474 | const binary = name.includes("libvips") |
| 475 | ? "lib/libvips.so.42" |
| 476 | : "lib/sharp-linux-arm64.node"; |
| 477 | const archive = tarball([ |
| 478 | ["package/package.json", Buffer.from(JSON.stringify(manifest))], |
| 479 | [ |
| 480 | scenario === "traversal" |
| 481 | ? "package/../escape" |
| 482 | : `package/${binary}`, |
| 483 | elf(scenario === "architecture" ? 62 : 183), |
| 484 | scenario === "symlink" ? "2" : "0", |
| 485 | ], |
| 486 | ]); |
| 487 | if (url.endsWith(".tgz")) return new Response(archive); |
| 488 | return Response.json({ |
| 489 | ...manifest, |
| 490 | version: scenario === "identity" ? "99.0.0" : version, |
| 491 | dist: { |
| 492 | integrity: `sha512-${createHash("sha512") |
| 493 | .update(scenario === "integrity" ? "wrong" : archive) |
| 494 | .digest("base64")}`, |
| 495 | tarball: `${scenario === "origin" ? "https://untrusted.invalid" : "https://registry.npmjs.org"}/${name}/-/${name.split("/")[1]}-${version}.tgz`, |
| 496 | }, |
| 497 | }); |
| 498 | }), |
| 499 | )) as typeof globalThis.fetch; |
| 500 | const result = yield* stageWebsiteArtifact({ |
| 501 | root, |
| 502 | distDir: dist, |
| 503 | serverEntry, |
| 504 | }).pipe( |
| 505 | Effect.provideService(FetchHttpClient.Fetch, fetch), |
| 506 | Effect.result, |
| 507 | ); |
| 508 | expect(Result.isSuccess(result)).toBe(scenario === "valid"); |
| 509 | expect(requested.length).toBeGreaterThan(0); |
| 510 | if (Result.isFailure(result)) |
| 511 | expect(result.failure._tag).toBe("WebsiteArtifactError"); |
| 512 | if (Result.isSuccess(result)) { |
| 513 | const packageFile = path.join( |
| 514 | result.success.directory, |
| 515 | "files/node_modules/@img/sharp-linux-arm64/package.json", |
| 516 | ); |
| 517 | expect( |
| 518 | JSON.parse(yield* fs.readFileString(packageFile)).version, |
| 519 | ).toBe("0.34.5"); |
| 520 | expect( |
| 521 | JSON.parse( |
| 522 | yield* fs.readFileString(path.join(sharp, "package.json")), |
| 523 | ).version, |
| 524 | ).toBe("0.34.5"); |
| 525 | } |
| 526 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), |
| 527 | { tags: ["unit", "provider:neon", "provider:neon:website", "local"] }, |
| 528 | ); |