diff --git a/.gitignore b/.gitignore index 67132b6f..bb182d2d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ # Environment variable files with local overrides. /.env*.local +# Superset configuration for agentic tasks. +/.superset/ + # Developement logs generated by pm2. /logs/**/*.log diff --git a/apps/admin/fly.toml b/apps/admin/fly.toml index 9ccf573a..c7b7dcb7 100644 --- a/apps/admin/fly.toml +++ b/apps/admin/fly.toml @@ -1,7 +1,7 @@ # See https://fly.io/docs/reference/configuration/ for information about how to use this file. app = "animeaux-admin-3ac2" -primary_region = "ams" +primary_region = "cdg" kill_signal = "SIGINT" kill_timeout = "5s" swap_size_mb = 512 @@ -36,7 +36,7 @@ swap_size_mb = 512 [[services.http_checks]] interval = "1m" - timeout = "2s" + timeout = "5s" grace_period = "1m" restart_limit = 0 method = "get" diff --git a/apps/admin/scripts/generate-icon-sprite.ts b/apps/admin/scripts/generate-icon-sprite.ts index 2f999628..c0662567 100644 --- a/apps/admin/scripts/generate-icon-sprite.ts +++ b/apps/admin/scripts/generate-icon-sprite.ts @@ -2,11 +2,10 @@ import { mkdir, readdir, readFile, writeFile } from "node:fs/promises" import { basename, dirname, join, relative, resolve } from "node:path" import { fileURLToPath } from "node:url" +import { relativeToCwd, safelyReadFile } from "@animeaux/dev-tools" import { watch } from "chokidar" import { parse } from "node-html-parser" -import { relativeToCwd, safelyReadFile } from "./shared.js" - const FILENAME = fileURLToPath(import.meta.url) const DIRNAME = dirname(FILENAME) const SCRIPT_NAME = relativeToCwd(FILENAME) diff --git a/apps/admin/scripts/generate-theme.ts b/apps/admin/scripts/generate-theme.ts index 760e8f6e..4a5fc491 100644 --- a/apps/admin/scripts/generate-theme.ts +++ b/apps/admin/scripts/generate-theme.ts @@ -2,12 +2,14 @@ import { mkdir, readFile, writeFile } from "node:fs/promises" import { dirname, resolve } from "node:path" import { fileURLToPath } from "node:url" +import { + formatBreakpoints, + formatSpacing, + relativeToCwd, + safelyReadFile, +} from "@animeaux/dev-tools" import { __unstable__loadDesignSystem as loadDesignSystem } from "@tailwindcss/node" -import { orderBy } from "es-toolkit/array" import type { __unstable__loadDesignSystem as loadDesignSystemBase } from "tailwindcss" -import invariant from "tiny-invariant" - -import { relativeToCwd, safelyReadFile } from "./shared.js" const FILENAME = fileURLToPath(import.meta.url) const DIRNAME = dirname(FILENAME) @@ -17,14 +19,6 @@ const MAIN_STYLE_FILE = resolve(STYLES_DIRECTORY, "main.css") const DEST_DIRECTORY = resolve(DIRNAME, "../src/generated") const DEST_FILE = resolve(DEST_DIRECTORY, "theme.ts") -/** - * Matches values for the `` CSS type. - * We only support unitless `0`, rem and px. - * - * @see https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/length - */ -const CSS_LENGTH_REGEXP = /(?-?\d*\.?\d+)(?.*)/ - console.log("Building...") const mainCss = await readFile(MAIN_STYLE_FILE, "utf-8") @@ -40,11 +34,11 @@ const design = (await loadDesignSystem(mainCss, { await mkdir(DEST_DIRECTORY, { recursive: true }) const content = `// This file is generated by ${SCRIPT_NAME} -${getBreakpoints()} +${formatBreakpoints(design.theme.namespace("--breakpoint"))} ${getColors()} -${getSpacing()} +${formatSpacing(design.theme.namespace("--spacing"))} ` const currentContent = await safelyReadFile(DEST_FILE) @@ -59,36 +53,6 @@ if (currentContent === content) { await writeFile(DEST_FILE, content) console.info(`Built theme (${relativeToCwd(DEST_FILE)})`) -function getBreakpoints() { - const breakpoints = design.theme.namespace("--breakpoint") - - const sortedEntries = orderBy( - Array.from(breakpoints.entries()) - // `null` is used as key for a property with the namespace's name (e.g. - // `--breakpoint: `). - // For breakpoints this shouldn't happen. - .filter((entry): entry is [string, string] => entry[0] != null) - .map( - ([breakpoint, length]) => - [breakpoint, getLengthValuePx(length)] as const, - ), - [([_breakpoint, valuePx]) => valuePx], - ["desc"], - ) - - return `export type Breakpoint = typeof Breakpoint.names[number] - -export namespace Breakpoint { - /** Breakpoint names sorted by descending value. */ - export const names = ${JSON.stringify( - sortedEntries.map(([breakpoint]) => breakpoint), - )} as const - - /** Breakpoint values in pixels. */ - export const value = ${JSON.stringify(Object.fromEntries(sortedEntries), null, 2)} as const -}` -} - function getColors() { const colors = design.theme.namespace("--color") @@ -97,54 +61,3 @@ function getColors() { export const white = ${JSON.stringify(colors.get("white"))} }` } - -function getSpacing() { - const spacings = design.theme.namespace("--spacing") - - // A property with the namespace's name has `null` as key. (e.g. - // `--spacing: `). - const spacing = spacings.get(null) - - invariant(spacing != null, "A `--spacing` definition is missing in the theme") - - return `export namespace Spacing { - export const unitPx = ${getLengthValuePx(spacing)} -}` -} - -/** - * Returns the given CSS length in pixels. - * - * @param length expressed with the `` CSS type. - * @returns The value in pixels - * @see https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/length - */ -function getLengthValuePx(length: string) { - length = length.trim() - - const match = length.match(CSS_LENGTH_REGEXP) - - if (match == null || match?.groups?.value == null) { - throw new Error(`Invalid CSS length from theme: "${length}"`) - } - - const value = Number(match.groups.value) - - // The unit is optional when the value is `0`. - // See https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/length#syntax - const unit = match.groups.unit || "px" - - switch (unit) { - case "px": { - return value - } - - case "rem": { - return 16 * value - } - - default: { - throw new Error(`Unsupported length unit: "${unit}" in "${length}"`) - } - } -} diff --git a/apps/show/fly.toml b/apps/show/fly.toml index 0c13a083..3ed89546 100644 --- a/apps/show/fly.toml +++ b/apps/show/fly.toml @@ -1,7 +1,7 @@ # See https://fly.io/docs/reference/configuration/ for information about how to use this file. app = "animeaux-show-3ac2" -primary_region = "ams" +primary_region = "cdg" kill_signal = "SIGINT" kill_timeout = "5s" @@ -33,16 +33,10 @@ kill_timeout = "5s" hard_limit = 100 soft_limit = 80 - [[services.tcp_checks]] - interval = "15s" - timeout = "2s" - grace_period = "1s" - restart_limit = 0 - [[services.http_checks]] - interval = "10s" - timeout = "2s" - grace_period = "5s" + interval = "1m" + timeout = "5s" + grace_period = "1m" restart_limit = 0 method = "get" path = "/healthcheck" diff --git a/apps/show/scripts/generate-icon-sprite.ts b/apps/show/scripts/generate-icon-sprite.ts index e23c85c0..1f140f77 100644 --- a/apps/show/scripts/generate-icon-sprite.ts +++ b/apps/show/scripts/generate-icon-sprite.ts @@ -2,11 +2,14 @@ import { mkdir, readdir, readFile, writeFile } from "node:fs/promises" import { basename, dirname, join, relative, resolve } from "node:path" import { fileURLToPath } from "node:url" +import { + oneAtTheTime, + relativeToCwd, + safelyReadFile, +} from "@animeaux/dev-tools" import { watch } from "chokidar" import { parse } from "node-html-parser" -import { oneAtTheTime, relativeToCwd, safelyReadFile } from "./shared.js" - const FILENAME = fileURLToPath(import.meta.url) const DIRNAME = dirname(FILENAME) const SCRIPT_NAME = relativeToCwd(FILENAME) diff --git a/apps/show/scripts/generate-pictogram-sprite.ts b/apps/show/scripts/generate-pictogram-sprite.ts index 2a553374..028e9d30 100644 --- a/apps/show/scripts/generate-pictogram-sprite.ts +++ b/apps/show/scripts/generate-pictogram-sprite.ts @@ -2,11 +2,14 @@ import { mkdir, readdir, readFile, writeFile } from "node:fs/promises" import { basename, dirname, join, relative, resolve } from "node:path" import { fileURLToPath } from "node:url" +import { + oneAtTheTime, + relativeToCwd, + safelyReadFile, +} from "@animeaux/dev-tools" import { watch } from "chokidar" import { parse } from "node-html-parser" -import { oneAtTheTime, relativeToCwd, safelyReadFile } from "./shared.js" - const FILENAME = fileURLToPath(import.meta.url) const DIRNAME = dirname(FILENAME) const SCRIPT_NAME = relativeToCwd(FILENAME) diff --git a/apps/show/scripts/generate-theme.ts b/apps/show/scripts/generate-theme.ts index 75fd4c56..9afc922c 100644 --- a/apps/show/scripts/generate-theme.ts +++ b/apps/show/scripts/generate-theme.ts @@ -2,14 +2,17 @@ import { mkdir, readFile, writeFile } from "node:fs/promises" import { dirname, resolve } from "node:path" import { fileURLToPath } from "node:url" +import { + formatBreakpoints, + formatSpacing, + relativeToCwd, + safelyReadFile, +} from "@animeaux/dev-tools" import { __unstable__loadDesignSystem as loadDesignSystem } from "@tailwindcss/node" import { formatHex } from "culori" -import { orderBy } from "es-toolkit/array" import type { __unstable__loadDesignSystem as loadDesignSystemBase } from "tailwindcss" import invariant from "tiny-invariant" -import { relativeToCwd, safelyReadFile } from "./shared.js" - const FILENAME = fileURLToPath(import.meta.url) const DIRNAME = dirname(FILENAME) const SCRIPT_NAME = relativeToCwd(FILENAME) @@ -37,13 +40,13 @@ const design = (await loadDesignSystem(mainCss, { await mkdir(DEST_DIRECTORY, { recursive: true }) const content = `// This file is generated by ${SCRIPT_NAME} -${getBreakpoints()} +${formatBreakpoints(design.theme.namespace("--breakpoint"))} ${getFonts()} ${getColors()} -${getSpacing()} +${formatSpacing(design.theme.namespace("--spacing"))} ` const currentContent = await safelyReadFile(DEST_FILE) @@ -58,36 +61,6 @@ if (currentContent === content) { await writeFile(DEST_FILE, content) console.info(`Built theme (${relativeToCwd(DEST_FILE)})`) -function getBreakpoints() { - const breakpoints = design.theme.namespace("--breakpoint") - - const sortedEntries = orderBy( - Array.from(breakpoints.entries()) - // `null` is used as key for a property with the namespace's name (e.g. - // `--breakpoint: `). - // For breakpoints this shouldn't happen. - .filter((entry): entry is [string, string] => entry[0] != null) - .map( - ([breakpoint, length]) => - [breakpoint, getLengthValuePx(length)] as const, - ), - [([_breakpoint, valuePx]) => valuePx], - ["desc"], - ) - - return `export type Breakpoint = typeof Breakpoint.names[number] - -export namespace Breakpoint { - /** Breakpoint names sorted by descending value. */ - export const names = ${JSON.stringify( - sortedEntries.map(([breakpoint]) => breakpoint), - )} as const - - /** Breakpoint values in pixels. */ - export const value = ${JSON.stringify(Object.fromEntries(sortedEntries), null, 2)} as const -}` -} - function getColors() { const colors = design.theme.namespace("--color") @@ -121,7 +94,7 @@ function getFonts() { export const sans = ${JSON.stringify(sans, null, 2)} as const export const serif = ${JSON.stringify(serif, null, 2)} as const - + export const all = [sans, serif] }` } @@ -206,63 +179,3 @@ function parseFontFamily( return { name, fallbacks, variants } } - -function getSpacing() { - const spacings = design.theme.namespace("--spacing") - - // A property with the namespace's name has `null` as key. (e.g. - // `--spacing: `). - const spacing = spacings.get(null) - - invariant(spacing != null, "A `--spacing` definition is missing in the theme") - - return `export namespace Spacing { - export const unitPx = ${getLengthValuePx(spacing)} -}` -} - -/** - * Returns the given CSS length in pixels. - * - * @param length expressed with the `` CSS type. - * @returns The value in pixels - * @see https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/length - */ -function getLengthValuePx(length: string) { - length = length.trim() - - /** - * Matches values for the `` CSS type. - * We only support unitless `0`, rem and px. - * - * @see https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/length - */ - const CSS_LENGTH_REGEXP = /(?-?\d*\.?\d+)(?.*)/ - - const match = length.match(CSS_LENGTH_REGEXP) - - invariant( - match?.groups?.value != null, - `Invalid CSS length from theme: "${length}"`, - ) - - const value = Number(match.groups.value) - - // The unit is optional when the value is `0`. - // See https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/length#syntax - const unit = match.groups.unit || "px" - - switch (unit) { - case "px": { - return value - } - - case "rem": { - return 16 * value - } - - default: { - throw new Error(`Unsupported length unit: "${unit}" in "${length}"`) - } - } -} diff --git a/apps/show/scripts/shared.ts b/apps/show/scripts/shared.ts deleted file mode 100644 index cb9c0293..00000000 --- a/apps/show/scripts/shared.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { readFile } from "node:fs/promises" - -import { relative } from "path/posix" - -export async function safelyReadFile(path: string) { - let content: undefined | string - try { - content = await readFile(path, "utf-8") - } catch (_error) {} - - return content -} - -export function relativeToCwd(filePath: string): string { - return relative(process.cwd(), filePath) -} -export function oneAtTheTime(fn: () => Promise) { - let currentCall: null | Promise = null - - return async () => { - if (currentCall != null) { - return await currentCall - } - - try { - currentCall = fn() - await currentCall - } finally { - currentCall = null - } - } -} diff --git a/apps/website/fly.toml b/apps/website/fly.toml index 27d4bfd6..a401ab60 100644 --- a/apps/website/fly.toml +++ b/apps/website/fly.toml @@ -1,7 +1,7 @@ # See https://fly.io/docs/reference/configuration/ for information about how to use this file. app = "animeaux-website-3ac2" -primary_region = "ams" +primary_region = "cdg" kill_signal = "SIGINT" kill_timeout = "5s" @@ -33,16 +33,10 @@ kill_timeout = "5s" hard_limit = 100 soft_limit = 80 - [[services.tcp_checks]] - interval = "15s" - timeout = "2s" - grace_period = "1s" - restart_limit = 0 - [[services.http_checks]] - interval = "10s" - timeout = "2s" - grace_period = "5s" + interval = "1m" + timeout = "5s" + grace_period = "1m" restart_limit = 0 method = "get" path = "/healthcheck" diff --git a/apps/website/scripts/generate-icon-sprite.ts b/apps/website/scripts/generate-icon-sprite.ts index ae3d1d69..1f140f77 100644 --- a/apps/website/scripts/generate-icon-sprite.ts +++ b/apps/website/scripts/generate-icon-sprite.ts @@ -2,11 +2,14 @@ import { mkdir, readdir, readFile, writeFile } from "node:fs/promises" import { basename, dirname, join, relative, resolve } from "node:path" import { fileURLToPath } from "node:url" +import { + oneAtTheTime, + relativeToCwd, + safelyReadFile, +} from "@animeaux/dev-tools" import { watch } from "chokidar" import { parse } from "node-html-parser" -import { relativeToCwd, safelyReadFile } from "./shared.js" - const FILENAME = fileURLToPath(import.meta.url) const DIRNAME = dirname(FILENAME) const SCRIPT_NAME = relativeToCwd(FILENAME) @@ -210,20 +213,3 @@ async function generateSymbolForIcon(icon: IconDescriptor) { svg.removeAttribute("xmlns:xlink") return svg.toString() } - -function oneAtTheTime(fn: () => Promise) { - let currentCall: null | Promise = null - - return async () => { - if (currentCall != null) { - return await currentCall - } - - try { - currentCall = fn() - await currentCall - } finally { - currentCall = null - } - } -} diff --git a/apps/website/scripts/generate-theme.ts b/apps/website/scripts/generate-theme.ts index ced211a4..b8623a9b 100644 --- a/apps/website/scripts/generate-theme.ts +++ b/apps/website/scripts/generate-theme.ts @@ -2,13 +2,14 @@ import { mkdir, readFile, writeFile } from "node:fs/promises" import { dirname, resolve } from "node:path" import { fileURLToPath } from "node:url" +import { + formatBreakpoints, + relativeToCwd, + safelyReadFile, +} from "@animeaux/dev-tools" import { __unstable__loadDesignSystem as loadDesignSystem } from "@tailwindcss/node" import { formatHex } from "culori" -import { orderBy } from "es-toolkit" import type { __unstable__loadDesignSystem as loadDesignSystemBase } from "tailwindcss" -import invariant from "tiny-invariant" - -import { relativeToCwd, safelyReadFile } from "./shared.js" const FILENAME = fileURLToPath(import.meta.url) const DIRNAME = dirname(FILENAME) @@ -33,7 +34,7 @@ const design = (await loadDesignSystem(mainCss, { await mkdir(DEST_DIRECTORY, { recursive: true }) const content = `// This file is generated by ${SCRIPT_NAME} -${getBreakpoints()} +${formatBreakpoints(design.theme.namespace("--breakpoint"))} ${getColors()} ` @@ -50,36 +51,6 @@ if (currentContent === content) { await writeFile(DEST_FILE, content) console.info(`Built theme (${relativeToCwd(DEST_FILE)})`) -function getBreakpoints() { - const breakpoints = design.theme.namespace("--breakpoint") - - const sortedEntries = orderBy( - Array.from(breakpoints.entries()) - // `null` is used as key for a property with the namespace's name (e.g. - // `--breakpoint: `). - // For breakpoints this shouldn't happen. - .filter((entry): entry is [string, string] => entry[0] != null) - .map( - ([breakpoint, length]) => - [breakpoint, getLengthValuePx(length)] as const, - ), - [([_breakpoint, valuePx]) => valuePx], - ["desc"], - ) - - return `export type Breakpoint = typeof Breakpoint.names[number] - -export namespace Breakpoint { - /** Breakpoint names sorted by descending value. */ - export const names = ${JSON.stringify( - sortedEntries.map(([breakpoint]) => breakpoint), - )} as const - - /** Breakpoint values in pixels. */ - export const value = ${JSON.stringify(Object.fromEntries(sortedEntries), null, 2)} as const -}` -} - function getColors() { const colors = design.theme.namespace("--color") @@ -91,56 +62,10 @@ function getColors() { export const brandBlueHex = ${JSON.stringify(formatHex(brandBlue))} export const gray50 = ${JSON.stringify(gray50)} export const gray50Hex = ${JSON.stringify(formatHex(gray50))} - + export const background = Color.gray50 export const backgroundHex = Color.gray50Hex export const primary = Color.brandBlue export const primaryHex = Color.brandBlueHex }` } - -/** - * Returns the given CSS length in pixels. - * - * @param length expressed with the `` CSS type. - * @returns The value in pixels - * @see https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/length - */ -function getLengthValuePx(length: string) { - length = length.trim() - - /** - * Matches values for the `` CSS type. - * We only support unitless `0`, rem and px. - * - * @see https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/length - */ - const CSS_LENGTH_REGEXP = /(?-?\d*\.?\d+)(?.*)/ - - const match = length.match(CSS_LENGTH_REGEXP) - - invariant( - match?.groups?.value != null, - `Invalid CSS length from theme: "${length}"`, - ) - - const value = Number(match.groups.value) - - // The unit is optional when the value is `0`. - // See https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/length#syntax - const unit = match.groups.unit || "px" - - switch (unit) { - case "px": { - return value - } - - case "rem": { - return 16 * value - } - - default: { - throw new Error(`Unsupported length unit: "${unit}" in "${length}"`) - } - } -} diff --git a/apps/website/scripts/shared.ts b/apps/website/scripts/shared.ts deleted file mode 100644 index 9fa423a5..00000000 --- a/apps/website/scripts/shared.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { readFile } from "node:fs/promises" - -import { relative } from "path/posix" - -export async function safelyReadFile(path: string) { - let content: undefined | string - try { - content = await readFile(path, "utf-8") - } catch (_error) {} - - return content -} - -export function relativeToCwd(filePath: string): string { - return relative(process.cwd(), filePath) -} diff --git a/libs/dev-tools/.gitignore b/libs/dev-tools/.gitignore index fbc47d3a..7652daf0 100644 --- a/libs/dev-tools/.gitignore +++ b/libs/dev-tools/.gitignore @@ -1,2 +1,5 @@ +# Compiled code by tsc. +/build/ + # Dependencies installed by pnpm. /node_modules/ diff --git a/libs/dev-tools/.prettierignore b/libs/dev-tools/.prettierignore new file mode 100644 index 00000000..2e36e3be --- /dev/null +++ b/libs/dev-tools/.prettierignore @@ -0,0 +1,2 @@ +# Compiled code by TypeScript. +/build/ diff --git a/libs/dev-tools/package.json b/libs/dev-tools/package.json index 8b4fe0c2..5e6c58c8 100644 --- a/libs/dev-tools/package.json +++ b/libs/dev-tools/package.json @@ -5,6 +5,7 @@ "sideEffects": false, "type": "module", "files": [ + "build", "eslint.config.js", "prettier.config.js", "reset.d.ts", @@ -13,6 +14,10 @@ "tsconfig.lib.json" ], "exports": { + ".": { + "types": "./build/src/index.d.ts", + "default": "./build/src/index.js" + }, "./eslint": "./eslint.config.js", "./prettier": "./prettier.config.js", "./reset": "./reset.d.ts", @@ -20,9 +25,20 @@ "./tsconfig.lib": "./tsconfig.lib.json" }, "scripts": { + "prebuild": "pnpm clean", + "build": "run-s --print-label build:src build:paths", + "build:paths": "tsc-alias -p ./tsconfig.build.json", + "build:src": "tsc -p ./tsconfig.build.json", + "postbuild": "pnpm sync-workspace", + "clean": "rm -rf ./build", + "dev": "run-p --continue-on-error --print-label dev:*", + "dev:paths": "pnpm build:paths --watch", + "dev:src": "pnpm build:src --watch", + "dev:sync-workspace": "chokidar ./build --debounce --silent -c 'pnpm sync-workspace'", "lint": "run-p --continue-on-error --print-label lint:*", "lint:eslint": "eslint --cache --cache-location ./node_modules/.cache/eslint --max-warnings 0 .", "lint:prettier": "prettier --list-different .", + "lint:tsc": "tsc --noEmit", "lint-fix": "run-s --print-label lint-fix:eslint lint-fix:prettier", "lint-fix:eslint": "eslint --cache --cache-location ./node_modules/.cache/eslint --fix .", "lint-fix:prettier": "prettier --write .", @@ -30,6 +46,7 @@ }, "dependencies": { "@total-typescript/ts-reset": "catalog:", + "es-toolkit": "catalog:", "eslint": "catalog:", "eslint-plugin-better-tailwindcss": "catalog:", "eslint-plugin-import-x": "catalog:", @@ -40,9 +57,14 @@ "eslint-plugin-unused-imports": "catalog:", "globals": "catalog:", "prettier": "catalog:", + "tiny-invariant": "catalog:", "typescript-eslint": "catalog:" }, "devDependencies": { - "npm-run-all": "catalog:" + "@types/node": "catalog:", + "chokidar-cli": "catalog:", + "npm-run-all": "catalog:", + "tsc-alias": "catalog:", + "typescript": "catalog:" } } diff --git a/libs/dev-tools/src/format-breakpoints.ts b/libs/dev-tools/src/format-breakpoints.ts new file mode 100644 index 00000000..78bacf81 --- /dev/null +++ b/libs/dev-tools/src/format-breakpoints.ts @@ -0,0 +1,31 @@ +import { orderBy } from "es-toolkit/array" + +import { getLengthValuePx } from "#i/get-length-value-px.js" + +export function formatBreakpoints(breakpoints: Map) { + const sortedEntries = orderBy( + Array.from(breakpoints.entries()) + // `null` is used as key for a property with the namespace's name (e.g. + // `--breakpoint: `). + // For breakpoints this shouldn't happen. + .filter((entry): entry is [string, string] => entry[0] != null) + .map( + ([breakpoint, length]) => + [breakpoint, getLengthValuePx(length)] as const, + ), + [([_breakpoint, valuePx]) => valuePx], + ["desc"], + ) + + return `export type Breakpoint = typeof Breakpoint.names[number] + +export namespace Breakpoint { + /** Breakpoint names sorted by descending value. */ + export const names = ${JSON.stringify( + sortedEntries.map(([breakpoint]) => breakpoint), + )} as const + + /** Breakpoint values in pixels. */ + export const value = ${JSON.stringify(Object.fromEntries(sortedEntries), null, 2)} as const +}` +} diff --git a/libs/dev-tools/src/format-spacing.ts b/libs/dev-tools/src/format-spacing.ts new file mode 100644 index 00000000..26ea2846 --- /dev/null +++ b/libs/dev-tools/src/format-spacing.ts @@ -0,0 +1,15 @@ +import invariant from "tiny-invariant" + +import { getLengthValuePx } from "#i/get-length-value-px.js" + +export function formatSpacing(spacings: Map) { + // A property with the namespace's name has `null` as key. (e.g. + // `--spacing: `). + const spacing = spacings.get(null) + + invariant(spacing != null, "A `--spacing` definition is missing in the theme") + + return `export namespace Spacing { + export const unitPx = ${getLengthValuePx(spacing)} +}` +} diff --git a/libs/dev-tools/src/get-length-value-px.ts b/libs/dev-tools/src/get-length-value-px.ts new file mode 100644 index 00000000..e2e5d5cc --- /dev/null +++ b/libs/dev-tools/src/get-length-value-px.ts @@ -0,0 +1,44 @@ +/** + * Matches values for the `` CSS type. + * We only support unitless `0`, rem and px. + * + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/length + */ +const CSS_LENGTH_REGEXP = /(?-?\d*\.?\d+)(?.*)/ + +/** + * Returns the given CSS length in pixels. + * + * @param length expressed with the `` CSS type. + * @returns The value in pixels + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/length + */ +export function getLengthValuePx(length: string) { + length = length.trim() + + const match = length.match(CSS_LENGTH_REGEXP) + + if (match?.groups?.value == null) { + throw new Error(`Invalid CSS length from theme: "${length}"`) + } + + const value = Number(match.groups.value) + + // The unit is optional when the value is `0`. + // See https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/length#syntax + const unit = match.groups.unit || "px" + + switch (unit) { + case "px": { + return value + } + + case "rem": { + return 16 * value + } + + default: { + throw new Error(`Unsupported length unit: "${unit}" in "${length}"`) + } + } +} diff --git a/libs/dev-tools/src/index.ts b/libs/dev-tools/src/index.ts new file mode 100644 index 00000000..88a11cea --- /dev/null +++ b/libs/dev-tools/src/index.ts @@ -0,0 +1,6 @@ +export * from "#i/format-breakpoints.js" +export * from "#i/format-spacing.js" +export * from "#i/get-length-value-px.js" +export * from "#i/one-at-the-time.js" +export * from "#i/relative-to-cwd.js" +export * from "#i/safely-read-file.js" diff --git a/libs/dev-tools/src/one-at-the-time.ts b/libs/dev-tools/src/one-at-the-time.ts new file mode 100644 index 00000000..1eb331e4 --- /dev/null +++ b/libs/dev-tools/src/one-at-the-time.ts @@ -0,0 +1,16 @@ +export function oneAtTheTime(fn: () => Promise) { + let currentCall: null | Promise = null + + return async () => { + if (currentCall != null) { + return await currentCall + } + + try { + currentCall = fn() + await currentCall + } finally { + currentCall = null + } + } +} diff --git a/libs/dev-tools/src/relative-to-cwd.ts b/libs/dev-tools/src/relative-to-cwd.ts new file mode 100644 index 00000000..0bf42032 --- /dev/null +++ b/libs/dev-tools/src/relative-to-cwd.ts @@ -0,0 +1,5 @@ +import { relative } from "path/posix" + +export function relativeToCwd(filePath: string) { + return relative(process.cwd(), filePath) +} diff --git a/apps/admin/scripts/shared.ts b/libs/dev-tools/src/safely-read-file.ts similarity index 61% rename from apps/admin/scripts/shared.ts rename to libs/dev-tools/src/safely-read-file.ts index 9fa423a5..a71ab382 100644 --- a/apps/admin/scripts/shared.ts +++ b/libs/dev-tools/src/safely-read-file.ts @@ -1,7 +1,5 @@ import { readFile } from "node:fs/promises" -import { relative } from "path/posix" - export async function safelyReadFile(path: string) { let content: undefined | string try { @@ -10,7 +8,3 @@ export async function safelyReadFile(path: string) { return content } - -export function relativeToCwd(filePath: string): string { - return relative(process.cwd(), filePath) -} diff --git a/libs/dev-tools/tsconfig.build.json b/libs/dev-tools/tsconfig.build.json new file mode 100644 index 00000000..49ad09e1 --- /dev/null +++ b/libs/dev-tools/tsconfig.build.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + + // Only build files under these folders. + "include": ["./src/**/*"] +} diff --git a/libs/dev-tools/tsconfig.json b/libs/dev-tools/tsconfig.json index 82121f30..acc6c10b 100644 --- a/libs/dev-tools/tsconfig.json +++ b/libs/dev-tools/tsconfig.json @@ -2,11 +2,13 @@ "extends": ["./tsconfig.lib.json"], "include": ["./**/*.ts"], - "exclude": ["./node_modules"], + "exclude": ["./node_modules", "./build"], "compilerOptions": { "rootDir": "./", + "outDir": "./build", + "paths": { "#i/*": ["./src/*"] }, - "types": ["./reset.d.ts"] + "types": ["node", "./reset.d.ts"] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c5f5030..ee21b540 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1061,6 +1061,9 @@ importers: '@total-typescript/ts-reset': specifier: 'catalog:' version: 0.6.1 + es-toolkit: + specifier: 'catalog:' + version: 1.45.1 eslint: specifier: 'catalog:' version: 9.39.3(jiti@2.6.1) @@ -1091,13 +1094,28 @@ importers: prettier: specifier: 'catalog:' version: 3.8.2 + tiny-invariant: + specifier: 'catalog:' + version: 1.3.3 typescript-eslint: specifier: 'catalog:' version: 8.58.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.7.3) devDependencies: + '@types/node': + specifier: 'catalog:' + version: 22.8.7 + chokidar-cli: + specifier: 'catalog:' + version: 3.0.0 npm-run-all: specifier: 'catalog:' version: 4.1.5 + tsc-alias: + specifier: 'catalog:' + version: 1.8.16 + typescript: + specifier: 'catalog:' + version: 5.7.3 libs/file-storage: dependencies: @@ -9638,6 +9656,7 @@ snapshots: '@animeaux/dev-tools@file:libs/dev-tools(@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.7.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.7.3))(@typescript-eslint/utils@8.58.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.7.3))(jiti@2.6.1)(tailwindcss@4.2.4)(typescript@5.7.3)': dependencies: '@total-typescript/ts-reset': 0.6.1 + es-toolkit: 1.45.1 eslint: 9.39.3(jiti@2.6.1) eslint-plugin-better-tailwindcss: 4.5.0(eslint@9.39.3(jiti@2.6.1))(tailwindcss@4.2.4)(typescript@5.7.3) eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.58.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.7.3))(eslint@9.39.3(jiti@2.6.1)) @@ -9648,6 +9667,7 @@ snapshots: eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.7.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.7.3))(eslint@9.39.3(jiti@2.6.1)) globals: 15.15.0 prettier: 3.8.2 + tiny-invariant: 1.3.3 typescript-eslint: 8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.7.3) transitivePeerDependencies: - '@eslint/css'