| 1 | // Alchemy modifications are licensed under Apache-2.0. |
| 2 | // This file includes third-party code; see /THIRD_PARTY_LICENSES.md. |
| 3 | import ignore from "@alchemy.run/node-utils/ignore"; |
| 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as FileSystem from "effect/FileSystem"; |
| 6 | import * as Path from "effect/Path"; |
| 7 | import mime from "mime"; |
| 8 | import { isAbsolute, sep } from "node:path"; |
| 9 | import { |
| 10 | CF_ASSETS_IGNORE_FILENAME, |
| 11 | HEADERS_FILENAME, |
| 12 | REDIRECTS_FILENAME, |
| 13 | } from "../shared/constants.ts"; |
| 14 | |
| 15 | /** normalises sep for windows and prefix with `/` */ |
| 16 | export const normalizeFilePath = (relativeFilepath: string) => { |
| 17 | if (isAbsolute(relativeFilepath)) { |
| 18 | throw new Error(`Expected relative path`); |
| 19 | } |
| 20 | return "/" + relativeFilepath.split(sep).join("/"); |
| 21 | }; |
| 22 | |
| 23 | export const getContentType = (absFilePath: string) => { |
| 24 | let contentType = mime.getType(absFilePath); |
| 25 | if ( |
| 26 | contentType && |
| 27 | contentType.startsWith("text/") && |
| 28 | !contentType.includes("charset") |
| 29 | ) { |
| 30 | contentType = `${contentType}; charset=utf-8`; |
| 31 | } |
| 32 | return contentType; |
| 33 | }; |
| 34 | |
| 35 | /** |
| 36 | * Generate a function that can match relative filepaths against a list of gitignore formatted patterns. |
| 37 | */ |
| 38 | export function createPatternMatcher( |
| 39 | patterns: Array<string>, |
| 40 | exclude: boolean, |
| 41 | ): (filePath: string) => boolean { |
| 42 | if (patterns.length === 0) { |
| 43 | return (_filePath) => !exclude; |
| 44 | } else { |
| 45 | const ignorer = ignore().add(patterns); |
| 46 | return (filePath) => ignorer.test(filePath).ignored; |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | /** |
| 51 | * Create a function for filtering out ignored assets. |
| 52 | * |
| 53 | * The generated function takes an asset path, relative to the asset directory, |
| 54 | * and returns true if the asset should not be ignored. |
| 55 | */ |
| 56 | export const createAssetsIgnoreFunction = Effect.fn(function* (dir: string) { |
| 57 | const fs = yield* FileSystem.FileSystem; |
| 58 | const path = yield* Path.Path; |
| 59 | const cfAssetIgnorePath = path.join(dir, CF_ASSETS_IGNORE_FILENAME); |
| 60 | |
| 61 | const ignorePatterns = [ |
| 62 | // Ignore the `.assetsignore` file and other metafiles by default. |
| 63 | // The ignore lib expects unix-style paths for its patterns |
| 64 | `/${CF_ASSETS_IGNORE_FILENAME}`, |
| 65 | `/${REDIRECTS_FILENAME}`, |
| 66 | `/${HEADERS_FILENAME}`, |
| 67 | ]; |
| 68 | |
| 69 | const assetsIgnore = yield* fs.readFileString(cfAssetIgnorePath).pipe( |
| 70 | Effect.catchIf( |
| 71 | (error) => error.reason._tag === "NotFound", |
| 72 | () => Effect.succeed(undefined), |
| 73 | ), |
| 74 | ); |
| 75 | if (assetsIgnore !== undefined) { |
| 76 | ignorePatterns.push(...assetsIgnore.split("\n")); |
| 77 | } |
| 78 | |
| 79 | return { |
| 80 | assetsIgnoreFunction: createPatternMatcher(ignorePatterns, true), |
| 81 | assetsIgnoreFilePresent: assetsIgnore !== undefined, |
| 82 | }; |
| 83 | }); |