diff --git a/src/__tests__/engine.spec.ts b/engine/__tests__/engine.spec.ts similarity index 99% rename from src/__tests__/engine.spec.ts rename to engine/__tests__/engine.spec.ts index 86afd5d..873bcce 100644 --- a/src/__tests__/engine.spec.ts +++ b/engine/__tests__/engine.spec.ts @@ -3,8 +3,8 @@ import { evaluateFlag, evaluateRolloutStep, evaluateRolloutSteps, -} from "../engine"; -import type { FeatureFlagInputSchema, FlagEvaluationInput } from "../schema"; +} from "../engine.ts"; +import type { FeatureFlagInputSchema, FlagEvaluationInput } from "../types.ts"; function createMockInput( overrides?: Partial, diff --git a/src/__tests__/example.spec.ts b/engine/__tests__/example.spec.ts similarity index 99% rename from src/__tests__/example.spec.ts rename to engine/__tests__/example.spec.ts index 603d2af..305791f 100644 --- a/src/__tests__/example.spec.ts +++ b/engine/__tests__/example.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "vitest"; -import { evaluateFlag } from "../engine"; -import type { FlagEvaluationInput } from "../schema"; +import { evaluateFlag } from "../engine.ts"; +import type { FlagEvaluationInput } from "../types.ts"; /** * Helper to create properly structured evaluation input diff --git a/src/engine.ts b/engine/engine.ts similarity index 99% rename from src/engine.ts rename to engine/engine.ts index 9bc7ed2..10fa5e8 100644 --- a/src/engine.ts +++ b/engine/engine.ts @@ -3,7 +3,7 @@ import type { FeatureFlagInputSchema, FlagEvaluationInput, FlagResultSchema, -} from "./schema"; +} from "./types"; jexl.addTransform("split", (val, char) => val.split(char)); jexl.addTransform("lower", (val) => val.toLowerCase()); diff --git a/engine/index.ts b/engine/index.ts new file mode 100644 index 0000000..3df7a48 --- /dev/null +++ b/engine/index.ts @@ -0,0 +1,2 @@ +export * from "./engine"; +export * from "./types"; diff --git a/engine/package.json b/engine/package.json new file mode 100644 index 0000000..4cfca93 --- /dev/null +++ b/engine/package.json @@ -0,0 +1,34 @@ +{ + "name": "@flaggly/engine", + "version": "0.2.0", + "description": "Flaggly feature flag evaluation engine", + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "files": [ + "dist", + "README.md" + ], + "keywords": [ + "feature-flags", + "engine", + "typescript", + "cloudflare" + ], + "author": "butttons", + "license": "MIT", + "dependencies": { + "jexl": "^2.3.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/butttons/flaggly" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + } +} diff --git a/engine/tsconfig.engine.json b/engine/tsconfig.engine.json new file mode 100644 index 0000000..9435683 --- /dev/null +++ b/engine/tsconfig.engine.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": false, + "outDir": "../../dist/engine", + "rootDir": "../", + "strict": true, + "strictNullChecks": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "types": [] + }, + "include": ["./**/*.ts"], + "exclude": ["**/*.test.ts", "**/*.spec.ts", "node_modules"] +} diff --git a/engine/types.ts b/engine/types.ts new file mode 100644 index 0000000..3457bfe --- /dev/null +++ b/engine/types.ts @@ -0,0 +1,98 @@ +export type RolloutStep = { + start: string; + percentage?: number; + segment?: string; +}; + +export type FeatureFlagVariation = { + id: string; + label?: string; + weight: number; + payload?: unknown; +}; + +export type FeatureFlagInputSchema = + | { + id: string; + label?: string; + description?: string; + enabled: boolean; + type: "boolean"; + rules: string[]; + segments: string[]; + rollout: number; + rollouts: RolloutStep[]; + isTrackable: boolean; + } + | { + id: string; + label?: string; + description?: string; + enabled: boolean; + type: "payload"; + payload: unknown; + rules: string[]; + segments: string[]; + rollout: number; + rollouts: RolloutStep[]; + isTrackable: boolean; + } + | { + id: string; + label?: string; + description?: string; + enabled: boolean; + type: "variant"; + variations: FeatureFlagVariation[]; + rules: string[]; + segments: string[]; + rollout: number; + rollouts: RolloutStep[]; + isTrackable: boolean; + }; + +export type FlagEvaluationInput = { + user?: unknown; + id?: string; + request: { + headers: Record; + }; + page: { + url?: string | null; + }; + geo: { + country?: string; + isEUCountry?: boolean; + continent?: string; + city?: string; + postalCode?: string; + latitude?: string; + longitude?: string; + timezone?: string; + region?: string; + regionCode?: string; + metroCode?: string; + }; +}; + +export type FlagResultSchema = + | { + type: "boolean"; + result: boolean; + isEval: boolean; + } + | { + type: "payload"; + result: unknown; + isEval: boolean; + } + | { + type: "variant"; + result: unknown; + isEval: boolean; + }; + +export type AppData = { + flags: Record; + segments: Record; +}; diff --git a/package.json b/package.json index 113fb59..542e0de 100644 --- a/package.json +++ b/package.json @@ -11,12 +11,14 @@ "test": "vitest", "test:run": "vitest run", "build:sdk": "tsup sdk/index.ts --dts --format esm,cjs --tsconfig sdk/tsconfig.sdk.json --out-dir sdk/dist", - "sandbox:sdk": "tsx sdk/sandbox.ts" + "sandbox:sdk": "tsx sdk/sandbox.ts", + "build:engine": "tsup engine/index.ts --dts --format esm,cjs --tsconfig engine/tsconfig.engine.json --out-dir engine/dist" }, "dependencies": { "hono": "^4.9.9", "jexl": "^2.3.0", - "zod": "^4.1.11" + "zod": "^4.1.11", + "@flaggly/engine": "workspace:*" }, "devDependencies": { "@types/jexl": "^2.3.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2ca91a..0ed18fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@flaggly/engine': + specifier: workspace:* + version: link:engine hono: specifier: ^4.9.9 version: 4.9.9 @@ -46,6 +49,34 @@ importers: specifier: ^4.40.2 version: 4.40.2(@cloudflare/workers-types@4.20250927.0) + engine: + dependencies: + jexl: + specifier: ^2.3.0 + version: 2.3.0 + zod: + specifier: ^4.1.11 + version: 4.1.11 + + playground: + dependencies: + '@flaggly/engine': + specifier: file:../engine + version: link:../engine + devDependencies: + tsx: + specifier: ^4.20.6 + version: 4.20.6 + typescript: + specifier: ^5.9.2 + version: 5.9.2 + + sdk: + dependencies: + nanostores: + specifier: ^1.0.1 + version: 1.0.1 + packages: '@babel/runtime@7.28.4': @@ -281,67 +312,79 @@ packages: resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.0.5': resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.0.4': resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.0.4': resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.0.4': resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.0.4': resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.33.5': resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.33.5': resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.33.5': resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.33.5': resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.33.5': resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.33.5': resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.33.5': resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} @@ -427,56 +470,67 @@ packages: resolution: {integrity: sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.52.3': resolution: {integrity: sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.52.3': resolution: {integrity: sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.52.3': resolution: {integrity: sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.52.3': resolution: {integrity: sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.52.3': resolution: {integrity: sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.52.3': resolution: {integrity: sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.52.3': resolution: {integrity: sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.52.3': resolution: {integrity: sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.52.3': resolution: {integrity: sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.52.3': resolution: {integrity: sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openharmony-arm64@4.52.3': resolution: {integrity: sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..7e62b45 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,5 @@ +packages: + - "." + - "sdk" + - "engine" + - "playground" diff --git a/src/routes/admin.ts b/src/routes/admin.ts index 0426256..edd11d9 100644 --- a/src/routes/admin.ts +++ b/src/routes/admin.ts @@ -2,8 +2,8 @@ import { jwt } from "hono/jwt"; import { validator } from "hono/validator"; import { object, string } from "zod/v4-mini"; import { FlagglyError } from "../error"; +import { inputFeatureFlagSchema } from "../schema"; import { - inputFeatureFlagSchema, segmentInputSchema, syncInputSchema, updateableFeatureFlagSchema, diff --git a/src/routes/api.ts b/src/routes/api.ts index 378069d..28c88ed 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -2,9 +2,10 @@ import { jwt } from "hono/jwt"; import { validator } from "hono/validator"; import { omit } from "zod/v4-mini"; -import { evaluateFlag } from "../engine"; +import { evaluateFlag } from "@flaggly/engine"; +import { evaluateInputSchema, requestGeoSchema } from "../schema"; import { FlagglyError } from "../error"; -import { evaluateInputSchema, paramSchema, requestGeoSchema } from "../schema"; +import { paramSchema } from "../schema"; import { createApp } from "./_app"; export const api = createApp(); diff --git a/src/routes/ui.tsx b/src/routes/ui.tsx index 501bb0f..e1e67de 100644 --- a/src/routes/ui.tsx +++ b/src/routes/ui.tsx @@ -1,7 +1,8 @@ import { html } from "hono/html"; import type { FC } from "hono/jsx"; import type { AppData, FeatureFlagInputSchema } from "../schema"; -import { inputFeatureFlagSchema, segmentInputSchema } from "../schema"; +import { inputFeatureFlagSchema } from "../schema"; +import { segmentInputSchema } from "../schema"; import { buildUrl, Layout } from "../components/Layout"; import { createApp } from "./_app"; diff --git a/src/storage.ts b/src/storage.ts index 5ec6de4..d5b72e1 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -1,7 +1,6 @@ import { FlagglyError, tryPromise } from "./error"; +import type { AppData, FeatureFlagInputSchema } from "./schema"; import type { - AppData, - FeatureFlagInputSchema, SegmentInputSchema, SyncInput, UpdatableFeatureFlagSchema, diff --git a/tsconfig.json b/tsconfig.json index 9404c61..7649fb9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,8 +25,11 @@ "./worker-configuration.d.ts", "@types/node", "@types/service-worker-mock" - ] + ], + "paths": { + "@flaggly/engine": ["./engine/index.ts"] + } }, - "exclude": ["node_modules", "dist", "tests"], - "include": ["src", "worker-configuration.d.ts"] + "exclude": ["node_modules", "dist", "tests", "engine/dist", "**/*.spec.ts"], + "include": ["src", "engine", "worker-configuration.d.ts"] }