diff --git a/.github/workflows/extract.yml b/.github/workflows/extract.yml deleted file mode 100644 index e480297..0000000 --- a/.github/workflows/extract.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Extract - -on: - push: - branches: [main] - pull_request: - -jobs: - extract-spec: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - run: bun install - - - run: bun run bin/cli.js extract-spec --file /tmp/extracted-spec.md - - run: bun run bin/cli.js extract-schema --folder /tmp/extracted-schemas - - - name: Compare extracted spec with core-spec/v1/spec.md - run: diff core-spec/v1/spec.md /tmp/extracted-spec.md - - - name: Compare extracted schemas with core-spec/v1/schemas - run: diff -r core-spec/v1/schemas /tmp/extracted-schemas diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..33bc9c7 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,37 @@ +name: Lint + +on: + push: + branches: [main] + pull_request: + +jobs: + type-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - run: bun install + - run: bun run type-check + + lint-eslint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - run: bun install + - run: bun run lint-eslint + + lint-format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - run: bun install + - run: bun run lint-format diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 221bc9f..48ee57b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,6 +19,17 @@ jobs: node-version: "20" registry-url: "https://registry.npmjs.org" + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Build + run: bun run build + - name: Compare local and published versions id: check run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..0dc6b3d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,17 @@ +name: Test + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - run: bun install + - run: bun run test diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 06e60ab..16ddcb5 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -17,4 +17,4 @@ jobs: - run: bun install - - run: bun run bin/cli.js validate-schema --folder examples/v1 + - run: bun run bin/cli.ts validate-schema --folder examples/v1 diff --git a/.gitignore b/.gitignore index a0ca357..733217c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ Desktop.ini *.swp *.swo node_modules/ +dist/ *.tgz diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 0000000..3865a02 --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,4 @@ +{ + "printWidth": 80, + "trailingComma": "all" +} diff --git a/README.md b/README.md index 836dc26..9704c5b 100644 --- a/README.md +++ b/README.md @@ -57,10 +57,32 @@ npx @metabase/representations extract-schema --folder ./schemas Omit `--folder` to extract into the current directory. +### Generating entity IDs + +Print one or more 21-character NanoID entity IDs (one per line): + +```sh +npx @metabase/representations generate-entity-id +npx @metabase/representations generate-entity-id --count 5 +``` + +### Generating UUIDs + +Print one or more v4 UUIDs (one per line): + +```sh +npx @metabase/representations generate-uuid +npx @metabase/representations generate-uuid --count 5 +``` + ### Programmatic ```js -import { validateSchema } from "@metabase/representations"; +import { + generateEntityId, + generateUuid, + validateSchema, +} from "@metabase/representations"; const { results, passed, failed } = validateSchema({ folder: "./my-export", @@ -71,6 +93,9 @@ for (const result of results) { console.log(result.file, result.errors); } } + +const entityId = generateEntityId(); // "LZfXLFzPPR4NNrgjlWDxn" +const uuid = generateUuid(); // "1d4e9fdf-49ae-4fbe-ae27-05e7c6a5cfe8" ``` ## Publishing to NPM diff --git a/bin/cli.js b/bin/cli.js deleted file mode 100755 index 07f7f48..0000000 --- a/bin/cli.js +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env node - -import { parseArgs } from "node:util"; -import { relative } from "path"; - -import { extractSchema } from "../src/extract-schema.js"; -import { extractSpec } from "../src/extract-spec.js"; -import { validateSchema } from "../src/validate-schema.js"; - -const { values, positionals } = parseArgs({ - allowPositionals: true, - options: { - folder: { type: "string" }, - file: { type: "string" }, - help: { type: "boolean", short: "h", default: false }, - }, -}); - -const command = positionals[0]; - -const HELP = `Usage: representations [options] - -Commands: - validate-schema Validate YAML files against Metabase representation schemas - --folder Folder to validate (default: cwd) - - extract-spec Copy the bundled spec.md into a target file - --file Destination file (default: ./spec.md) - - extract-schema Copy bundled schemas into a target folder - --folder Destination folder (default: cwd) - -Options: - -h, --help Show this help message`; - -if (values.help || !command) { - console.log(HELP); - process.exit(command ? 0 : 1); -} - -if (command === "validate-schema") { - const folder = values.folder ?? process.cwd(); - const { results, passed, failed } = validateSchema({ folder }); - - if (results.length === 0) { - console.error(`No YAML files found in ${folder}`); - process.exit(1); - } - - for (const result of results) { - const path = relative(process.cwd(), `${folder}/${result.file}`); - if (result.status === "ok") { - console.log(`OK ${path} (${result.model})`); - } else { - console.error(`FAIL ${path}${result.model ? ` (${result.model})` : ""}`); - for (const error of result.errors) { - console.error(` ${error.path} ${error.message}`); - } - } - } - - console.log(`\n${passed} passed, ${failed} failed`); - process.exit(failed > 0 ? 1 : 0); -} - -if (command === "extract-spec") { - const { target } = extractSpec({ file: values.file ?? "spec.md" }); - console.log(`Spec extracted to ${target}`); - process.exit(0); -} - -if (command === "extract-schema") { - const { target } = extractSchema({ folder: values.folder ?? process.cwd() }); - console.log(`Schemas extracted to ${target}`); - process.exit(0); -} - -console.error(`Unknown command: ${command}`); -process.exit(1); diff --git a/bin/cli.test.ts b/bin/cli.test.ts new file mode 100644 index 0000000..2d4ba2a --- /dev/null +++ b/bin/cli.test.ts @@ -0,0 +1,188 @@ +import { afterEach, beforeEach, describe, expect, it } from "bun:test"; +import { existsSync, mkdtempSync, readdirSync, rmSync } from "fs"; +import { tmpdir } from "os"; +import { join, resolve } from "path"; + +const REPO_ROOT = resolve(import.meta.dirname, ".."); +const CLI = "bin/cli.ts"; + +type RunResult = { + stdout: string; + stderr: string; + exitCode: number; +}; + +function runCli(args: string[]): RunResult { + const proc = Bun.spawnSync({ + cmd: ["bun", "run", CLI, ...args], + cwd: REPO_ROOT, + }); + return { + stdout: proc.stdout.toString(), + stderr: proc.stderr.toString(), + exitCode: proc.exitCode ?? 0, + }; +} + +const UUID_REGEX = /^[0-9a-f-]{36}$/i; +const NANOID_REGEX = /^[\w-]{21}$/; + +describe("cli", () => { + describe("help", () => { + it("prints help and exits 1 with no args", () => { + const { stdout, exitCode } = runCli([]); + expect(stdout).toContain("Usage: representations"); + expect(exitCode).toBe(1); + }); + + it("prints help and exits 0 with --help", () => { + const { stdout, exitCode } = runCli(["--help"]); + expect(stdout).toContain("Usage: representations"); + expect(exitCode).toBe(0); + }); + + it("errors on an unknown command", () => { + const { stderr, exitCode } = runCli(["bogus"]); + expect(stderr).toContain("Unknown command: bogus"); + expect(exitCode).toBe(1); + }); + }); + + describe("generate-entity-id", () => { + it("prints one NanoID by default", () => { + const { stdout, exitCode } = runCli(["generate-entity-id"]); + const lines = stdout.trim().split("\n"); + expect(exitCode).toBe(0); + expect(lines).toHaveLength(1); + expect(lines[0]).toMatch(NANOID_REGEX); + }); + + it("prints --count lines, each a unique NanoID", () => { + const { stdout, exitCode } = runCli([ + "generate-entity-id", + "--count", + "5", + ]); + const lines = stdout.trim().split("\n"); + expect(exitCode).toBe(0); + expect(lines).toHaveLength(5); + for (const line of lines) { + expect(line).toMatch(NANOID_REGEX); + } + expect(new Set(lines).size).toBe(5); + }); + + it("rejects --count 0", () => { + const { stderr, exitCode } = runCli([ + "generate-entity-id", + "--count", + "0", + ]); + expect(stderr).toContain("Invalid --count"); + expect(exitCode).toBe(1); + }); + + it("rejects non-integer --count", () => { + const { stderr, exitCode } = runCli([ + "generate-entity-id", + "--count", + "abc", + ]); + expect(stderr).toContain("Invalid --count"); + expect(exitCode).toBe(1); + }); + }); + + describe("generate-uuid", () => { + it("prints one v4 UUID by default", () => { + const { stdout, exitCode } = runCli(["generate-uuid"]); + const lines = stdout.trim().split("\n"); + expect(exitCode).toBe(0); + expect(lines).toHaveLength(1); + expect(lines[0]).toMatch(UUID_REGEX); + }); + + it("prints --count lines, each a unique UUID", () => { + const { stdout, exitCode } = runCli(["generate-uuid", "--count", "3"]); + const lines = stdout.trim().split("\n"); + expect(exitCode).toBe(0); + expect(lines).toHaveLength(3); + for (const line of lines) { + expect(line).toMatch(UUID_REGEX); + } + expect(new Set(lines).size).toBe(3); + }); + }); + + describe("extract-spec", () => { + let workdir: string; + + beforeEach(() => { + workdir = mkdtempSync(join(tmpdir(), "representations-cli-spec-")); + }); + + afterEach(() => { + rmSync(workdir, { recursive: true, force: true }); + }); + + it("copies the spec to --file", () => { + const target = join(workdir, "spec.md"); + const { stdout, exitCode } = runCli(["extract-spec", "--file", target]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Spec extracted to"); + expect(existsSync(target)).toBe(true); + }); + }); + + describe("extract-schema", () => { + let workdir: string; + + beforeEach(() => { + workdir = mkdtempSync(join(tmpdir(), "representations-cli-schema-")); + }); + + afterEach(() => { + rmSync(workdir, { recursive: true, force: true }); + }); + + it("copies schema files to --folder", () => { + const target = join(workdir, "schemas"); + const { stdout, exitCode } = runCli([ + "extract-schema", + "--folder", + target, + ]); + expect(exitCode).toBe(0); + expect(stdout).toContain("Schemas extracted to"); + expect(existsSync(target)).toBe(true); + expect(readdirSync(target).length).toBeGreaterThan(0); + }); + }); + + describe("validate-schema", () => { + it("passes on the bundled examples", () => { + const { stdout, exitCode } = runCli([ + "validate-schema", + "--folder", + "examples/v1", + ]); + expect(exitCode).toBe(0); + expect(stdout).toMatch(/\d+ passed, 0 failed/); + }); + + it("errors when the folder has no YAML files", () => { + const empty = mkdtempSync(join(tmpdir(), "representations-cli-empty-")); + try { + const { stderr, exitCode } = runCli([ + "validate-schema", + "--folder", + empty, + ]); + expect(exitCode).toBe(1); + expect(stderr).toContain("No YAML files found"); + } finally { + rmSync(empty, { recursive: true, force: true }); + } + }); + }); +}); diff --git a/bin/cli.ts b/bin/cli.ts new file mode 100644 index 0000000..aac2f2f --- /dev/null +++ b/bin/cli.ts @@ -0,0 +1,139 @@ +#!/usr/bin/env node + +import { parseArgs } from "node:util"; +import { relative } from "path"; + +import { extractSchema } from "../src/extract-schema.js"; +import { extractSpec } from "../src/extract-spec.js"; +import { generateEntityId } from "../src/generate-entity-id.js"; +import { generateUuid } from "../src/generate-uuid.js"; +import { validateSchema } from "../src/validate-schema.js"; + +type ParsedValues = { + folder?: string; + file?: string; + count?: string; + help?: boolean; +}; + +const HELP = `Usage: representations [options] + +Commands: + validate-schema Validate YAML files against Metabase representation schemas + --folder Folder to validate (default: cwd) + + extract-spec Copy the bundled spec.md into a target file + --file Destination file (default: ./spec.md) + + extract-schema Copy bundled schemas into a target folder + --folder Destination folder (default: cwd) + + generate-entity-id Generate one or more NanoID entity IDs (one per line) + --count Number to generate (default: 1) + + generate-uuid Generate one or more UUIDs (one per line) + --count Number to generate (default: 1) + +Options: + -h, --help Show this help message`; + +function parseArguments() { + return parseArgs({ + allowPositionals: true, + options: { + folder: { type: "string" }, + file: { type: "string" }, + count: { type: "string" }, + help: { type: "boolean", short: "h", default: false }, + }, + }); +} + +function parseCount(raw: string | undefined): number { + const count = raw === undefined ? 1 : Number(raw); + if (!Number.isInteger(count) || count < 1) { + console.error(`Invalid --count: ${raw} (expected a positive integer)`); + process.exit(1); + } + return count; +} + +function handleValidateSchema(values: ParsedValues): void { + const folder = values.folder ?? process.cwd(); + const { results, passed, failed } = validateSchema({ folder }); + + if (results.length === 0) { + console.error(`No YAML files found in ${folder}`); + process.exit(1); + } + + for (const result of results) { + if (result.status === "ok") { + continue; + } + const path = relative(process.cwd(), `${folder}/${result.file}`); + console.error(`FAIL ${path}${result.model ? ` (${result.model})` : ""}`); + for (const error of result.errors) { + console.error(` ${error.path} ${error.message}`); + } + } + + console.log(`\n${passed} passed, ${failed} failed`); + process.exit(failed > 0 ? 1 : 0); +} + +function handleExtractSpec(values: ParsedValues): void { + const { target } = extractSpec({ file: values.file ?? "spec.md" }); + console.log(`Spec extracted to ${target}`); + process.exit(0); +} + +function handleExtractSchema(values: ParsedValues): void { + const { target } = extractSchema({ folder: values.folder ?? process.cwd() }); + console.log(`Schemas extracted to ${target}`); + process.exit(0); +} + +function handleGenerateEntityId(values: ParsedValues): void { + const count = parseCount(values.count); + for (let i = 0; i < count; i++) { + console.log(generateEntityId()); + } + process.exit(0); +} + +function handleGenerateUuid(values: ParsedValues): void { + const count = parseCount(values.count); + for (let i = 0; i < count; i++) { + console.log(generateUuid()); + } + process.exit(0); +} + +function main(): void { + const { values, positionals } = parseArguments(); + const command = positionals[0]; + + if (values.help || !command) { + console.log(HELP); + process.exit(values.help ? 0 : 1); + } + + switch (command) { + case "validate-schema": + return handleValidateSchema(values); + case "extract-spec": + return handleExtractSpec(values); + case "extract-schema": + return handleExtractSchema(values); + case "generate-entity-id": + return handleGenerateEntityId(values); + case "generate-uuid": + return handleGenerateUuid(values); + default: + console.error(`Unknown command: ${command}`); + process.exit(1); + } +} + +main(); diff --git a/bun.lock b/bun.lock index 250b925..89a87fb 100644 --- a/bun.lock +++ b/bun.lock @@ -9,12 +9,123 @@ "ajv-formats": "^3.0.1", "glob": "^11.0.1", "js-yaml": "^4.1.0", + "nanoid": "^5.1.9", + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/bun": "^1.3.12", + "@types/js-yaml": "^4.0.9", + "@types/node": "^25.6.0", + "eslint": "^10.2.1", + "oxfmt": "^0.45.0", + "typescript": "^6.0.3", + "typescript-eslint": "^8.58.2", }, }, }, "packages": { + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.5.5", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w=="], + + "@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="], + + "@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="], + + "@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="], + + "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], + + "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="], + + "@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + "@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="], + "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.45.0", "", { "os": "android", "cpu": "arm" }, "sha512-A/UMxFob1fefCuMeGxQBulGfFE38g2Gm23ynr3u6b+b7fY7/ajGbNsa3ikMIkGMLJW/TRoQaMoP1kME7S+815w=="], + + "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.45.0", "", { "os": "android", "cpu": "arm64" }, "sha512-L63z4uZmHjgvvqvMJD7mwff8aSBkM0+X4uFr6l6U5t6+Qc9DCLVZWIunJ7Gm4fn4zHPdSq6FFQnhu9yqqobxIg=="], + + "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.45.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UV34dd623FzqT+outIGndsCA/RBB+qgB3XVQhgmmJ9PJwa37NzPC9qzgKeOhPKxVk2HW+JKldQrVL54zs4Noww=="], + + "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.45.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-pMNJv0CMa1pDefVPeNbuQxibh8ITpWDFEhMC/IBB9Zlu76EbgzYwrzI4Cb11mqX2+rIYN70UTrh3z06TM59ptQ=="], + + "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.45.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-xTcRoxbbo61sW2+ZRPeH+vp/o9G8gkdhiVumFU+TpneiPm14c79l6GFlxPXlCE9bNWikigbsrvJw46zCVAQFfg=="], + + "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.45.0", "", { "os": "linux", "cpu": "arm" }, "sha512-hWL8Hdni+3U1mPFx1UtWeGp3tNb6EhBAUHRMbKUxVkOp3WwoJbpVO2bfUVbS4PfpledviXXNHSTl1veTa6FhkQ=="], + + "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.45.0", "", { "os": "linux", "cpu": "arm" }, "sha512-6Blt/0OBT7vvfQpqYuYbpbFLPqSiaYpEJzUUWhinPEuADypDbtV1+LdjM0vYBNGPvnj85ex7lTerEX6JGcPt9w=="], + + "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.45.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jLjoLfe+hGfjhA8hNBSdw85yCA8ePKq7ME4T+g6P9caQXvmt6IhE2X7iVjnVdkmYUWEzZrxlh4p6RkDmAMJY/A=="], + + "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.45.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-XQKXZIKYJC3GQJ8FnD3iMntpw69Wd9kDDK/Xt79p6xnFYlGGxSNv2vIBvRTDg5CKByWFWWZLCRDOXoP/m6YN4g=="], + + "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.45.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+g5RiG+xOkdrCWkKodv407nTvMq4vYM18Uox2MhZBm/YoqFxxJpWKsloskFFG5NU13HGPw1wzYjjOVcyd9moCA=="], + + "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.45.0", "", { "os": "linux", "cpu": "none" }, "sha512-V7dXKoSyEbWAkkSF4JJNtF+NJZDmJoSarSoP30WCsB3X636Rehd3CvxBj49FIJxEBFWhvcUjGSHVeU8Erck1bQ=="], + + "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.45.0", "", { "os": "linux", "cpu": "none" }, "sha512-Vdelft1sAEYojVGgcODEFXSWYQYlIvoyIGWebKCuUibd1tvS1TjTx413xG2ZLuHpYj45CkN/ztMLMX6jrgqpgg=="], + + "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.45.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-RR7xKgNpqwENnK0aYCGYg0JycY2n93J0reNjHyes+I9Gq52dH95x+CBlnlAQHCPfz6FGnKA9HirgUl14WO6o7w=="], + + "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.45.0", "", { "os": "linux", "cpu": "x64" }, "sha512-U/QQ0+BQNSHxjuXR/utvXnQ50Vu5kUuqEomZvQ1/3mhgbBiMc2WU9q5kZ5WwLp3gnFIx9ibkveoRSe2EZubkqg=="], + + "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.45.0", "", { "os": "linux", "cpu": "x64" }, "sha512-o5TLOUCF0RWQjsIS06yVC+kFgp092/yLe6qBGSUvtnmTVw9gxjpdQSXc3VN5Cnive4K11HNstEZF8ROKHfDFSw=="], + + "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.45.0", "", { "os": "none", "cpu": "arm64" }, "sha512-RnGcV3HgPuOjsGx/k9oyRNKmOp+NBLGzZTdPDYbc19r7NGeYPplnUU/BfU35bX2Y/O4ejvHxcfkvW2WoYL/gsg=="], + + "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.45.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-v3Vj7iKKsUFwt9w5hsqIIoErKVoENC6LoqfDlteOQ5QMDCXihlqLoxpmviUhXnNncg4zV6U9BPwlBbwa+qm4wg=="], + + "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.45.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-N8yotPBX6ph0H3toF4AEpdCeVPrdcSetj+8eGiZGsrLsng3bs/Q5HPu4bbSxip5GBPx5hGbGHrZwH4+rcrjhHA=="], + + "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.45.0", "", { "os": "win32", "cpu": "x64" }, "sha512-w5MMTRCK1dpQeRA+HHqXQXyN33DlG/N2LOYxJmaT4fJjcmZrbNnqw7SmIk7I2/a2493PPLZ+2E/Ar6t2iKVMug=="], + + "@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="], + + "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/type-utils": "8.58.2", "@typescript-eslint/utils": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.2", "@typescript-eslint/types": "^8.58.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2" } }, "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/utils": "8.58.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.58.2", "", {}, "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.58.2", "@typescript-eslint/tsconfig-utils": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], @@ -25,44 +136,154 @@ "brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@10.2.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.5.5", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q=="], + + "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], "glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="], "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="], "minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@5.1.9", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "oxfmt": ["oxfmt@0.45.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.45.0", "@oxfmt/binding-android-arm64": "0.45.0", "@oxfmt/binding-darwin-arm64": "0.45.0", "@oxfmt/binding-darwin-x64": "0.45.0", "@oxfmt/binding-freebsd-x64": "0.45.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.45.0", "@oxfmt/binding-linux-arm-musleabihf": "0.45.0", "@oxfmt/binding-linux-arm64-gnu": "0.45.0", "@oxfmt/binding-linux-arm64-musl": "0.45.0", "@oxfmt/binding-linux-ppc64-gnu": "0.45.0", "@oxfmt/binding-linux-riscv64-gnu": "0.45.0", "@oxfmt/binding-linux-riscv64-musl": "0.45.0", "@oxfmt/binding-linux-s390x-gnu": "0.45.0", "@oxfmt/binding-linux-x64-gnu": "0.45.0", "@oxfmt/binding-linux-x64-musl": "0.45.0", "@oxfmt/binding-openharmony-arm64": "0.45.0", "@oxfmt/binding-win32-arm64-msvc": "0.45.0", "@oxfmt/binding-win32-ia32-msvc": "0.45.0", "@oxfmt/binding-win32-x64-msvc": "0.45.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-0o/COoN9fY50bjVeM7PQsNgbhndKurBIeTIcspW033OumksjJJmIVDKjAk5HMwU/GHTxSOdGDdhJ6BRzGPmsHg=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + + "tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="], + + "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], + + "typescript-eslint": ["typescript-eslint@8.58.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.2", "@typescript-eslint/parser": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/utils": "8.58.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ=="], + + "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "eslint/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + + "eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], } } diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..51f0330 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,48 @@ +// @ts-check +import js from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default [ + { + ignores: ["node_modules/**", "dist/**", "core-spec/**", "examples/**"], + }, + js.configs.recommended, + ...tseslint.configs.recommended.map((config) => ({ + ...config, + files: ["**/*.ts"], + })), + { + files: ["**/*.ts"], + languageOptions: { + parser: tseslint.parser, + ecmaVersion: 2022, + sourceType: "module", + globals: { + console: "readonly", + process: "readonly", + }, + }, + rules: { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_.+$", + varsIgnorePattern: "^_.+$", + ignoreRestSiblings: true, + destructuredArrayIgnorePattern: "^_.+$", + caughtErrors: "none", + }, + ], + "@typescript-eslint/consistent-type-imports": [ + "error", + { fixStyle: "inline-type-imports" }, + ], + "@typescript-eslint/no-import-type-side-effects": "error", + "@typescript-eslint/no-explicit-any": "off", + "prefer-const": ["warn", { destructuring: "all" }], + eqeqeq: ["warn", "smart"], + curly: ["warn", "all"], + }, + }, +]; diff --git a/package.json b/package.json index 8f3eab2..2ec963f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metabase/representations", - "version": "1.1.6", + "version": "1.1.7", "description": "Metabase representation format specification and schema validator", "license": "SEE LICENSE IN LICENSE.txt", "repository": { @@ -14,25 +14,44 @@ }, "type": "module", "files": [ - "src", - "bin", + "dist", "core-spec/v1/schemas", "core-spec/v1/spec.md" ], - "main": "src/index.js", + "main": "dist/src/index.js", + "types": "dist/src/index.d.ts", "exports": { - ".": "./src/index.js" + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } }, "bin": { - "representations": "bin/cli.js" + "representations": "dist/bin/cli.js" }, "scripts": { - "validate-schema": "bun run bin/cli.js validate-schema --folder examples/v1" + "build": "tsc -p tsconfig.build.json && cp -r core-spec dist/core-spec", + "type-check": "tsc --noEmit", + "lint-eslint": "eslint --max-warnings 0 --report-unused-disable-directives bin src", + "lint-format": "oxfmt --check bin src", + "test": "bun test", + "validate-schema": "bun run bin/cli.ts validate-schema --folder examples/v1" }, "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "glob": "^11.0.1", - "js-yaml": "^4.1.0" + "js-yaml": "^4.1.0", + "nanoid": "^5.1.9" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/bun": "^1.3.12", + "@types/js-yaml": "^4.0.9", + "@types/node": "^25.6.0", + "eslint": "^10.2.1", + "oxfmt": "^0.45.0", + "typescript": "^6.0.3", + "typescript-eslint": "^8.58.2" } } diff --git a/src/extract-schema.test.ts b/src/extract-schema.test.ts new file mode 100644 index 0000000..5418f80 --- /dev/null +++ b/src/extract-schema.test.ts @@ -0,0 +1,37 @@ +import { afterEach, beforeEach, describe, expect, it } from "bun:test"; +import { existsSync, mkdtempSync, readdirSync, rmSync } from "fs"; +import { tmpdir } from "os"; +import { join } from "path"; + +import { extractSchema } from "./extract-schema.js"; + +describe("extractSchema", () => { + let workdir: string; + + beforeEach(() => { + workdir = mkdtempSync(join(tmpdir(), "representations-extract-schema-")); + }); + + afterEach(() => { + rmSync(workdir, { recursive: true, force: true }); + }); + + it("copies all schema files to the target folder", () => { + const target = join(workdir, "schemas"); + const result = extractSchema({ folder: target }); + + expect(result.target).toBe(target); + expect(existsSync(target)).toBe(true); + + const files = readdirSync(target); + expect(files.length).toBeGreaterThan(0); + expect(files.some((f) => f.endsWith(".yaml"))).toBe(true); + }); + + it("creates the target folder if it does not exist", () => { + const target = join(workdir, "nested", "schemas"); + extractSchema({ folder: target }); + + expect(existsSync(target)).toBe(true); + }); +}); diff --git a/src/extract-schema.js b/src/extract-schema.ts similarity index 62% rename from src/extract-schema.js rename to src/extract-schema.ts index 70c7ecf..5f5d7d2 100644 --- a/src/extract-schema.js +++ b/src/extract-schema.ts @@ -3,7 +3,18 @@ import { resolve } from "path"; const PACKAGE_ROOT = resolve(import.meta.dirname, ".."); -export function extractSchema({ folder }) { +export type ExtractSchemaOptions = { + folder: string; +}; + +export type ExtractSchemaResult = { + source: string; + target: string; +}; + +export function extractSchema({ + folder, +}: ExtractSchemaOptions): ExtractSchemaResult { const schemasDir = resolve(PACKAGE_ROOT, "core-spec/v1/schemas"); const target = resolve(folder); mkdirSync(target, { recursive: true }); diff --git a/src/extract-spec.test.ts b/src/extract-spec.test.ts new file mode 100644 index 0000000..9f3e8a3 --- /dev/null +++ b/src/extract-spec.test.ts @@ -0,0 +1,37 @@ +import { afterEach, beforeEach, describe, expect, it } from "bun:test"; +import { existsSync, mkdtempSync, readFileSync, rmSync, statSync } from "fs"; +import { tmpdir } from "os"; +import { join } from "path"; + +import { extractSpec } from "./extract-spec.js"; + +describe("extractSpec", () => { + let workdir: string; + + beforeEach(() => { + workdir = mkdtempSync(join(tmpdir(), "representations-extract-spec-")); + }); + + afterEach(() => { + rmSync(workdir, { recursive: true, force: true }); + }); + + it("copies the bundled spec to the target file", () => { + const target = join(workdir, "spec.md"); + const result = extractSpec({ file: target }); + + expect(result.target).toBe(target); + expect(existsSync(target)).toBe(true); + expect(statSync(target).size).toBeGreaterThan(0); + expect(readFileSync(target, "utf8")).toContain( + "Metabase Representation Format", + ); + }); + + it("creates missing parent directories", () => { + const target = join(workdir, "nested", "dir", "spec.md"); + extractSpec({ file: target }); + + expect(existsSync(target)).toBe(true); + }); +}); diff --git a/src/extract-spec.js b/src/extract-spec.ts similarity index 63% rename from src/extract-spec.js rename to src/extract-spec.ts index da1e36e..fea318e 100644 --- a/src/extract-spec.js +++ b/src/extract-spec.ts @@ -3,7 +3,16 @@ import { dirname, resolve } from "path"; const PACKAGE_ROOT = resolve(import.meta.dirname, ".."); -export function extractSpec({ file }) { +export type ExtractSpecOptions = { + file: string; +}; + +export type ExtractSpecResult = { + source: string; + target: string; +}; + +export function extractSpec({ file }: ExtractSpecOptions): ExtractSpecResult { const source = resolve(PACKAGE_ROOT, "core-spec/v1/spec.md"); const target = resolve(file); mkdirSync(dirname(target), { recursive: true }); diff --git a/src/generate-entity-id.test.ts b/src/generate-entity-id.test.ts new file mode 100644 index 0000000..323c1ff --- /dev/null +++ b/src/generate-entity-id.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "bun:test"; + +import { generateEntityId } from "./generate-entity-id.js"; + +describe("generateEntityId", () => { + it("returns a 21-character string", () => { + const id = generateEntityId(); + expect(typeof id).toBe("string"); + expect(id).toHaveLength(21); + }); + + it("uses the NanoID alphabet (A-Za-z0-9_-)", () => { + for (let i = 0; i < 20; i++) { + expect(generateEntityId()).toMatch(/^[\w-]{21}$/); + } + }); + + it("returns different values on subsequent calls", () => { + const ids = new Set(Array.from({ length: 100 }, () => generateEntityId())); + expect(ids.size).toBe(100); + }); +}); diff --git a/src/generate-entity-id.ts b/src/generate-entity-id.ts new file mode 100644 index 0000000..b839385 --- /dev/null +++ b/src/generate-entity-id.ts @@ -0,0 +1,5 @@ +import { nanoid } from "nanoid"; + +export function generateEntityId(): string { + return nanoid(); +} diff --git a/src/generate-uuid.test.ts b/src/generate-uuid.test.ts new file mode 100644 index 0000000..9cd1c36 --- /dev/null +++ b/src/generate-uuid.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "bun:test"; + +import { generateUuid } from "./generate-uuid.js"; + +const UUID_REGEX = /^[0-9a-f-]{36}$/i; + +describe("generateUuid", () => { + it("returns a UUID-shaped string", () => { + const id = generateUuid(); + expect(typeof id).toBe("string"); + expect(id).toMatch(UUID_REGEX); + }); + + it("returns different values on subsequent calls", () => { + const ids = new Set(Array.from({ length: 100 }, () => generateUuid())); + expect(ids.size).toBe(100); + }); +}); diff --git a/src/generate-uuid.ts b/src/generate-uuid.ts new file mode 100644 index 0000000..e9d43d1 --- /dev/null +++ b/src/generate-uuid.ts @@ -0,0 +1,5 @@ +import { randomUUID } from "node:crypto"; + +export function generateUuid(): string { + return randomUUID(); +} diff --git a/src/index.js b/src/index.js deleted file mode 100644 index e86f096..0000000 --- a/src/index.js +++ /dev/null @@ -1 +0,0 @@ -export { validateSchema } from "./validate-schema.js"; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..c9f819c --- /dev/null +++ b/src/index.ts @@ -0,0 +1,19 @@ +export { + extractSchema, + type ExtractSchemaOptions, + type ExtractSchemaResult, +} from "./extract-schema.js"; +export { + extractSpec, + type ExtractSpecOptions, + type ExtractSpecResult, +} from "./extract-spec.js"; +export { generateEntityId } from "./generate-entity-id.js"; +export { generateUuid } from "./generate-uuid.js"; +export { + validateSchema, + type ValidateSchemaOptions, + type ValidateSchemaResult, + type ValidationError, + type ValidationResult, +} from "./validate-schema.js"; diff --git a/src/validate-schema.test.ts b/src/validate-schema.test.ts new file mode 100644 index 0000000..f0bae81 --- /dev/null +++ b/src/validate-schema.test.ts @@ -0,0 +1,72 @@ +import { afterEach, beforeEach, describe, expect, it } from "bun:test"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs"; +import { tmpdir } from "os"; +import { join, resolve } from "path"; + +import { validateSchema } from "./validate-schema.js"; + +const EXAMPLES = resolve(import.meta.dirname, "..", "examples", "v1"); + +describe("validateSchema", () => { + it("validates all bundled example files", () => { + const { results, passed, failed } = validateSchema({ folder: EXAMPLES }); + expect(results.length).toBeGreaterThan(0); + expect(failed).toBe(0); + expect(passed).toBe(results.length); + }); + + describe("with an ad-hoc folder", () => { + let workdir: string; + + beforeEach(() => { + workdir = mkdtempSync(join(tmpdir(), "representations-validate-")); + }); + + afterEach(() => { + rmSync(workdir, { recursive: true, force: true }); + }); + + it("returns zero results when no YAML files are present", () => { + const { results, passed, failed } = validateSchema({ folder: workdir }); + expect(results).toEqual([]); + expect(passed).toBe(0); + expect(failed).toBe(0); + }); + + it("fails files missing serdes/meta", () => { + mkdirSync(join(workdir, "collections", "main"), { recursive: true }); + writeFileSync( + join(workdir, "collections", "main", "broken.yaml"), + "name: Broken\n", + ); + + const { passed, failed, results } = validateSchema({ folder: workdir }); + expect(passed).toBe(0); + expect(failed).toBe(1); + expect(results[0].status).toBe("fail"); + if (results[0].status === "fail") { + expect(results[0].errors[0].message).toMatch(/serdes\/meta/); + } + }); + + it("fails files with an unknown model", () => { + mkdirSync(join(workdir, "collections", "main"), { recursive: true }); + writeFileSync( + join(workdir, "collections", "main", "unknown.yaml"), + [ + "name: Unknown", + "serdes/meta:", + "- id: abc", + " model: NotARealModel", + "", + ].join("\n"), + ); + + const { failed, results } = validateSchema({ folder: workdir }); + expect(failed).toBe(1); + if (results[0].status === "fail") { + expect(results[0].errors[0].message).toMatch(/unknown model/); + } + }); + }); +}); diff --git a/src/validate-schema.js b/src/validate-schema.ts similarity index 50% rename from src/validate-schema.js rename to src/validate-schema.ts index 319a8f5..c01bec3 100644 --- a/src/validate-schema.js +++ b/src/validate-schema.ts @@ -1,9 +1,12 @@ +import { Ajv2020, type ValidateFunction } from "ajv/dist/2020.js"; +import type { FormatsPlugin } from "ajv-formats"; import { readFileSync } from "fs"; -import { resolve } from "path"; import { globSync } from "glob"; import yaml from "js-yaml"; -import Ajv from "ajv/dist/2020.js"; -import addFormats from "ajv-formats"; +import { createRequire } from "node:module"; +import { resolve } from "path"; + +const addFormats: FormatsPlugin = createRequire(import.meta.url)("ajv-formats"); const PACKAGE_ROOT = resolve(import.meta.dirname, ".."); @@ -18,36 +21,70 @@ const IMPORT_PATHS = [ "transforms/**/*.yaml", ]; -function extractModel(schema) { +export type ValidateSchemaOptions = { + folder: string; +}; + +export type ValidationError = { + path: string; + message: string; +}; + +export type ValidationResult = + | { file: string; model: string; status: "ok" } + | { + file: string; + model?: string; + status: "fail"; + errors: ValidationError[]; + }; + +export type ValidateSchemaResult = { + results: ValidationResult[]; + passed: number; + failed: number; +}; + +function extractModel(schema: any): string | null { const items = schema?.properties?.["serdes/meta"]?.items; const props = items?.properties ?? items?.items?.properties; return props?.model?.const ?? null; } -function getModel(doc) { +function getModel(doc: any): string | null { const meta = doc?.["serdes/meta"]; - if (!Array.isArray(meta) || meta.length === 0) return null; + if (!Array.isArray(meta) || meta.length === 0) { + return null; + } return meta[meta.length - 1]?.model ?? null; } -export function validateSchema({ folder }) { +export function validateSchema({ + folder, +}: ValidateSchemaOptions): ValidateSchemaResult { const schemasDir = resolve(PACKAGE_ROOT, "core-spec/v1/schemas"); - const ajv = new Ajv({ allErrors: true, strictTuples: false, allowUnionTypes: true }); + const ajv = new Ajv2020({ + allErrors: true, + strictTuples: false, + allowUnionTypes: true, + }); addFormats(ajv); - // Load common schemas (non-entity schemas referenced via common/ prefix) for (const file of globSync("common/*.yaml", { cwd: schemasDir })) { - const raw = yaml.load(readFileSync(resolve(schemasDir, file), "utf8")); - const { $schema, ...body } = raw; + const raw = yaml.load( + readFileSync(resolve(schemasDir, file), "utf8"), + ) as any; + const { $schema: _$schema, ...body } = raw; ajv.addSchema(body, file); } - // Load entity schemas - const schemas = {}; + const schemas: Record = {}; for (const file of globSync("*.yaml", { cwd: schemasDir })) { - const raw = yaml.load(readFileSync(resolve(schemasDir, file), "utf8")); - const { $schema, ...body } = raw; + const raw = yaml.load( + readFileSync(resolve(schemasDir, file), "utf8"), + ) as any; + const { $schema: _$schema, ...body } = raw; const model = extractModel(raw); if (model) { schemas[model] = body; @@ -56,7 +93,7 @@ export function validateSchema({ folder }) { } } - const validators = {}; + const validators: Record = {}; for (const [model, schema] of Object.entries(schemas)) { validators[model] = ajv.compile(schema); } @@ -67,37 +104,49 @@ export function validateSchema({ folder }) { return { results: [], passed: 0, failed: 0 }; } - const results = []; + const results: ValidationResult[] = []; for (const file of files.sort()) { const fullPath = resolve(folder, file); - let doc; + let doc: any; try { doc = yaml.load(readFileSync(fullPath, "utf8")); - } catch (e) { - results.push({ file, status: "fail", errors: [{ path: "/", message: `invalid YAML: ${e.message}` }] }); + } catch (e: any) { + results.push({ + file, + status: "fail", + errors: [{ path: "/", message: `invalid YAML: ${e.message}` }], + }); continue; } const model = getModel(doc); if (!model) { - results.push({ file, status: "fail", errors: [{ path: "/", message: "missing serdes/meta or model" }] }); + results.push({ + file, + status: "fail", + errors: [{ path: "/", message: "missing serdes/meta or model" }], + }); continue; } const validate = validators[model]; if (!validate) { - results.push({ file, status: "fail", errors: [{ path: "/", message: `unknown model "${model}"` }] }); + results.push({ + file, + status: "fail", + errors: [{ path: "/", message: `unknown model "${model}"` }], + }); continue; } if (validate(doc)) { results.push({ file, model, status: "ok" }); } else { - const errors = validate.errors.map((e) => ({ + const errors = (validate.errors ?? []).map((e) => ({ path: e.instancePath || "/", - message: e.message, + message: e.message ?? "", })); results.push({ file, model, status: "fail", errors }); } diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..49a23bc --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..813fec3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2023"], + "types": ["node", "bun"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "declaration": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "." + }, + "include": ["src/**/*", "bin/**/*"], + "exclude": ["node_modules", "dist"] +}