From ffaefb63a472371c335c94ef85417f1020b535a3 Mon Sep 17 00:00:00 2001 From: Isha Dijcks Date: Tue, 17 Feb 2026 00:28:10 +0100 Subject: [PATCH 1/2] Add Louter --- package-lock.json | 143 +++++++++++++++++++++++++++++++++ package.json | 6 +- src/add.ts | 3 - src/generate/string-type.ts | 25 ++++++ src/index.ts | 3 +- src/louter/Louter.ts | 79 ++++++++++++++++++ src/louter/LouterError.ts | 13 +++ src/louter/LouterIndexation.ts | 12 +++ src/louter/LouterKind.ts | 7 ++ src/subtract.ts | 3 - tests/add.test.ts | 6 -- tests/subtract.test.ts | 6 -- 12 files changed, 285 insertions(+), 21 deletions(-) delete mode 100644 src/add.ts create mode 100644 src/generate/string-type.ts create mode 100644 src/louter/Louter.ts create mode 100644 src/louter/LouterError.ts create mode 100644 src/louter/LouterIndexation.ts create mode 100644 src/louter/LouterKind.ts delete mode 100644 src/subtract.ts delete mode 100644 tests/add.test.ts delete mode 100644 tests/subtract.test.ts diff --git a/package-lock.json b/package-lock.json index 3a6aae6..3c1c55f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,11 @@ "name": "@123ishatest/louter-test2", "version": "0.0.1", "license": "MIT", + "dependencies": { + "glob": "^13.0.3", + "yaml": "^2.8.2", + "zod": "^4.3.6" + }, "devDependencies": { "@changesets/changelog-github": "^0.5.2", "@changesets/cli": "^2.29.8", @@ -865,6 +870,15 @@ "node": "20 || >=22" } }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -2423,6 +2437,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/glob": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.3.tgz", + "integrity": "sha512-/g3B0mC+4x724v1TgtBlBtt2hPi/EWptsIAmXUx9Z2rvBYleQcsrmaOzd5LyL50jf/Soi83ZDJmw2+XqvH/EeA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.0", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -2436,6 +2467,45 @@ "node": ">= 6" } }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.2.tgz", + "integrity": "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==", + "license": "MIT", + "dependencies": { + "jackspeak": "^4.2.3" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", + "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.0.tgz", + "integrity": "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", @@ -2630,6 +2700,21 @@ "dev": true, "license": "ISC" }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jju": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", @@ -2782,6 +2867,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mlly": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", @@ -3002,6 +3096,31 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -3995,6 +4114,30 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true, "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 0031e58..a66b657 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,11 @@ "format": "prettier --write .", "test": "vitest" }, - "dependencies": {}, + "dependencies": { + "glob": "^13.0.3", + "yaml": "^2.8.2", + "zod": "^4.3.6" + }, "devDependencies": { "@changesets/changelog-github": "^0.5.2", "@changesets/cli": "^2.29.8", diff --git a/src/add.ts b/src/add.ts deleted file mode 100644 index c68b57a..0000000 --- a/src/add.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const add = (a: number, b: number) => { - return a + b; -}; diff --git a/src/generate/string-type.ts b/src/generate/string-type.ts new file mode 100644 index 0000000..31e5bd8 --- /dev/null +++ b/src/generate/string-type.ts @@ -0,0 +1,25 @@ +export const generateStringUnionType = (typeName: string, ids: string[]): string => { + if (ids.length === 0) { + return `export type ${typeName} = never;\n`; + } + return ( + `export type ${typeName} =\n` + + ids + .map((id) => { + return ` | '${id}'`; + }) + .join('\n') + + ';' + ); +}; + +export const generateEnumType = (typeName: string, ids: string[]): string => { + const idList = ids.map((id) => `'${id}'`).join(', '); + const idListVariable = typeName + 's'; + let output = ''; + output += `const ${idListVariable} = [` + idList + '] as const;\n'; + output += `export const ${typeName}Schema = z.enum(${idListVariable});\n`; + output += `export type ${typeName} = z.infer;\n`; + output += '\n'; + return output; +}; diff --git a/src/index.ts b/src/index.ts index 11d2f27..9652e9c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1 @@ -export { add } from '@/add.ts'; -export { subtract } from '@/subtract.ts'; +export { Louter } from '@/louter/Louter.ts'; diff --git a/src/louter/Louter.ts b/src/louter/Louter.ts new file mode 100644 index 0000000..9e7381d --- /dev/null +++ b/src/louter/Louter.ts @@ -0,0 +1,79 @@ +import { glob } from 'glob'; +import { readFileSync } from 'node:fs'; +import { parse } from 'yaml'; +import { generateEnumType } from '@/generate/string-type.ts'; +import { LouterKind } from '@/louter/LouterKind.ts'; +import { LouterIndexationMap, LouterIndexationResult } from '@/louter/LouterIndexation.ts'; +import { LouterErrorType } from '@/louter/LouterError.ts'; + +export interface LouterConfig { + root: string; + content: LouterKind[]; + debug: boolean; + idKey: string; +} + +export class Louter { + private readonly _config: LouterConfig; + + // private _content: Record> = {}; + + constructor(config: LouterConfig) { + this._config = config; + } + + public index(): LouterIndexationResult { + const indexationMap: LouterIndexationMap = {}; + const errors = []; + + const filePaths = glob.sync(`${this._config.root}/**/*.{yml,yaml}`); + this.debug(`Reading ${filePaths.length} files from`, this._config.root); + + console.log(filePaths); + filePaths.forEach((filePath) => { + const kind = filePath.replace('.yaml', '').replace('.yml', '').split('.').pop() as string; + const fileName = filePath.replace(this._config.root, ''); + + indexationMap[kind] ??= {}; + + try { + const parsedData = parse(readFileSync(filePath, 'utf8')); + const contentId = parsedData[this._config.idKey]; + indexationMap[kind][contentId] = { + id: contentId, + kind: kind, + source: fileName, + }; + } catch (e) { + errors.push({ + file: filePath, + type: LouterErrorType.InvalidYaml, + message: `Could not parse file '${fileName}'. Is it valid yaml?: ${e}`, + }); + } + }); + + return { + content: indexationMap, + enumCode: this.buildKindEnum(indexationMap), + }; + } + + private buildKindEnum(result: LouterIndexationMap): string { + let output = "import { z } from 'zod';\n\n"; + + this._config.content.forEach((kind) => { + output += generateEnumType( + kind.enumSymbol, + Object.values(result[kind.id] ?? []).map((result) => result.id), + ); + }); + return output; + } + + private debug(...args: unknown[]) { + if (this._config.debug) { + console.log(...args); + } + } +} diff --git a/src/louter/LouterError.ts b/src/louter/LouterError.ts new file mode 100644 index 0000000..b86af67 --- /dev/null +++ b/src/louter/LouterError.ts @@ -0,0 +1,13 @@ +export enum LouterErrorType { + UnrecognizedContentType = 'UnrecognizedContentType', + InvalidYaml = 'InvalidYaml', + MissingGlobalIdKey = 'MissingGlobalIdKey', + ZodValidationFailed = 'ZodValidationFailed', + DuplicateId = 'DuplicateId', +} + +export interface LouterError { + file: string; + type: LouterErrorType; + message: string; +} diff --git a/src/louter/LouterIndexation.ts b/src/louter/LouterIndexation.ts new file mode 100644 index 0000000..2654e14 --- /dev/null +++ b/src/louter/LouterIndexation.ts @@ -0,0 +1,12 @@ +export interface LouterIndexation { + id: string; + kind: string; + source: string; +} + +export type LouterIndexationMap = Record>; + +export interface LouterIndexationResult { + enumCode: string; + content: LouterIndexationMap; +} diff --git a/src/louter/LouterKind.ts b/src/louter/LouterKind.ts new file mode 100644 index 0000000..aa0ae0b --- /dev/null +++ b/src/louter/LouterKind.ts @@ -0,0 +1,7 @@ +import type { ZodType } from 'zod'; + +export interface LouterKind { + id: string; + enumSymbol: string; + schema: ZodType; +} diff --git a/src/subtract.ts b/src/subtract.ts deleted file mode 100644 index a45c6dc..0000000 --- a/src/subtract.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const subtract = (a: number, b: number) => { - return a - b; -}; diff --git a/tests/add.test.ts b/tests/add.test.ts deleted file mode 100644 index 1e2eab3..0000000 --- a/tests/add.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { expect, test } from 'vitest'; -import { add } from '@/add.ts'; - -test('adds 1 + 2 to equal 3', () => { - expect(add(1, 2)).toBe(3); -}); diff --git a/tests/subtract.test.ts b/tests/subtract.test.ts deleted file mode 100644 index 710c974..0000000 --- a/tests/subtract.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { expect, test } from 'vitest'; -import { subtract } from '@/subtract.ts'; - -test('subtracts 4 from 5 to equal 1', () => { - expect(subtract(5, 4)).toBe(1); -}); From ca952e081cc74021c290600a25b1c475fd63e4b6 Mon Sep 17 00:00:00 2001 From: Isha Dijcks Date: Tue, 17 Feb 2026 00:39:18 +0100 Subject: [PATCH 2/2] Add hollow test --- src/example/CurrencySchema.ts | 6 ++++++ src/example/ItemSchema.ts | 7 +++++++ tests/indexation.spec.ts | 29 +++++++++++++++++++++++++++++ tsconfig.json | 4 +++- 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 src/example/CurrencySchema.ts create mode 100644 src/example/ItemSchema.ts create mode 100644 tests/indexation.spec.ts diff --git a/src/example/CurrencySchema.ts b/src/example/CurrencySchema.ts new file mode 100644 index 0000000..5a67b07 --- /dev/null +++ b/src/example/CurrencySchema.ts @@ -0,0 +1,6 @@ +import z from 'zod'; + +export const CurrencySchema = z.strictObject({ + id: z.string(), + name: z.string(), +}); diff --git a/src/example/ItemSchema.ts b/src/example/ItemSchema.ts new file mode 100644 index 0000000..da74b4f --- /dev/null +++ b/src/example/ItemSchema.ts @@ -0,0 +1,7 @@ +import z from 'zod'; + +export const ItemSchema = z.strictObject({ + id: z.string(), + name: z.string(), + description: z.string(), +}); diff --git a/tests/indexation.spec.ts b/tests/indexation.spec.ts new file mode 100644 index 0000000..1c34e4c --- /dev/null +++ b/tests/indexation.spec.ts @@ -0,0 +1,29 @@ +import { expect, it } from 'vitest'; +import path from 'node:path'; +import * as fs from 'node:fs'; +import { Louter } from '@/louter/Louter.ts'; +import { CurrencySchema } from '@/example/CurrencySchema.ts'; +import { ItemSchema } from '@/example/ItemSchema.ts'; + +it('Indexes an schemas', () => { + // Arrange + const louter = new Louter({ + idKey: 'id', + root: path.dirname(__dirname) + '/tests/data', + debug: true, + content: [ + { id: 'currency', schema: CurrencySchema, enumSymbol: 'CurrencyId' }, + { id: 'item', schema: ItemSchema, enumSymbol: 'ItemId' }, + ], + }); + + // Act + const indexation = louter.index(); + + console.log(indexation); + + fs.writeFileSync('tests/louter.ts', indexation.enumCode); + + // Assert + expect(Object.values(indexation.content)).toHaveLength(0); +}); diff --git a/tsconfig.json b/tsconfig.json index 7cb3420..587d6fa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,8 @@ "lib": ["ES2020", "DOM", "DOM.Iterable"], "skipLibCheck": true, "paths": { - "@/*": ["./src/*"] + "@/*": ["./src/*"], + "$louter": ["./.louter/$louter.d.ts"] }, /* Bundler mode */ @@ -22,5 +23,6 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, + "include": ["src", "tests"] }