diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fb93fbad1..f42dfa971 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,13 +5,8 @@ on: branches: [master] jobs: - release-test: - if: github.actor != 'renovate[bot]' - uses: ./.github/workflows/test.yml - release-github: name: Github Release - needs: [release-test] runs-on: ubuntu-latest concurrency: group: release diff --git a/deno.lock b/deno.lock index 8d3724b83..d7e4b7ce5 100644 --- a/deno.lock +++ b/deno.lock @@ -1404,6 +1404,7 @@ }, "packages/json-schema": { "dependencies": [ + "jsr:@es-toolkit/es-toolkit@^1.45.1", "jsr:@targetd/api@^8.0.3", "npm:@types/yargs@^17.0.35", "npm:yargs@18", diff --git a/packages/fs/src/watch.ts b/packages/fs/src/watch.ts index dcdc025fb..56328d554 100644 --- a/packages/fs/src/watch.ts +++ b/packages/fs/src/watch.ts @@ -1,6 +1,9 @@ import type { DT } from '@targetd/api' import { debounce, Mutex } from '@es-toolkit/es-toolkit' -import { watch as fsWatch, type WatchOptions } from 'node:fs' +import { + watch as fsWatch, + type WatchOptions as BaseWatchOptions, +} from 'node:fs' import { load, pathIsLoadable } from './load.ts' /** @@ -26,6 +29,17 @@ import { load, pathIsLoadable } from './load.ts' */ export type OnLoad = (error: Error | null, data: D) => any +/** + * Options for watching a directory for rule file changes. + */ +export interface WatchOptions extends BaseWatchOptions { + /** + * Milliseconds to debounce file change events before reloading rules. + * @default 300 + */ + debounceMS?: number +} + /** * Watch a directory for rule file changes and automatically reload. * Provides hot-reloading of targeting rules without restarting the application. @@ -80,7 +94,9 @@ export function watch( optionsOrOnLoad: WatchOptions | OnLoad, onLoadParam?: OnLoad, ) { - const options = onLoadParam ? optionsOrOnLoad as WatchOptions : {} + const { debounceMS = 300, ...fsOptions } = onLoadParam + ? optionsOrOnLoad as WatchOptions + : {} const onLoad = (onLoadParam || optionsOrOnLoad) as OnLoad const mutex = new Mutex() @@ -99,13 +115,13 @@ export function watch( const watcher = fsWatch( dir, - options, + fsOptions, debounce( async (_eventType, filename) => { if (filename && !pathIsLoadable(filename)) return await onChange() }, - 300, + debounceMS, ), ) diff --git a/packages/json-schema/deno.json b/packages/json-schema/deno.json index 428bf67a8..302177a94 100644 --- a/packages/json-schema/deno.json +++ b/packages/json-schema/deno.json @@ -7,6 +7,7 @@ "./cli": "./src/cli.ts" }, "imports": { + "@es-toolkit/es-toolkit": "jsr:@es-toolkit/es-toolkit@^1.45.1", "@targetd/api": "jsr:@targetd/api@^8.0.3", "@types/yargs": "npm:@types/yargs@^17.0.35", "yargs": "npm:yargs@^18.0.0", @@ -31,7 +32,6 @@ "publish": { "include": [ "deno.json", - "package.json", "src/", "LICENSE", "README.md" diff --git a/packages/json-schema/package.json b/packages/json-schema/package.json deleted file mode 100644 index 2f68d3152..000000000 --- a/packages/json-schema/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "@targetd/json-schema", - "bin": "./src/cli.js" -} diff --git a/packages/json-schema/src/cli.ts b/packages/json-schema/src/cli.ts index e9ed12ae0..a25bb226a 100644 --- a/packages/json-schema/src/cli.ts +++ b/packages/json-schema/src/cli.ts @@ -1,6 +1,7 @@ import type { DT } from '@targetd/api' import yargs from 'yargs' import * as path from 'node:path' +import { pathToFileURL } from 'node:url' import { writeFile } from 'node:fs/promises' import { argv, cwd } from 'node:process' import { dataJSONSchemas } from './index.ts' @@ -31,8 +32,7 @@ const { dataExport, inputModule, outputFile } = await yargs() .parseAsync(argv) const input = await import( - // We need a forward slash here so that Deno is aware it's an absolute path - `/${path.resolve(cwd(), inputModule).replace(/^\//, '')}` + pathToFileURL(path.resolve(cwd(), inputModule)).href ) const data = input[dataExport] diff --git a/packages/json-schema/src/index.ts b/packages/json-schema/src/index.ts index 0aa426a22..33c36394f 100644 --- a/packages/json-schema/src/index.ts +++ b/packages/json-schema/src/index.ts @@ -1,3 +1,4 @@ +import { omit } from '@es-toolkit/es-toolkit' import { DataItemParser, DataItemsParser, @@ -26,6 +27,7 @@ import type { $ZodType, JSONSchema } from 'zod/v4/core' */ export function dataJSONSchemas( data: D, + params?: ToJSONSchemaParams, ): JSONSchema.BaseSchema { return toJSONSchema( extend( @@ -36,7 +38,7 @@ export function dataJSONSchemas( ) as any, { $schema: optional(string()) }, ), - params, + toJSONSchemaParams(params), ) } @@ -58,6 +60,7 @@ export function dataJSONSchemas( export function dataJSONSchema( data: D, name: keyof DT.PayloadParsers, + params?: ToJSONSchemaParams, ): JSONSchema.BaseSchema { return toJSONSchema( extend( @@ -69,25 +72,45 @@ export function dataJSONSchema( ) as any, { $schema: optional(string()) }, ), - params, + toJSONSchemaParams(params), ) } -const params: NonNullable[1]> = { - io: 'input', - unrepresentable: 'any', - reused: 'ref', - override(ctx) { - if (isZodSwitch(ctx.zodSchema)) { - const union = switchRegistry.get(ctx.zodSchema) - ?.union - if (union) { - ctx.jsonSchema = toJSONSchema(union as any, params) +type _ToJSONSchemaParams = NonNullable[1]> + +export type ToJSONSchemaParams = _ToJSONSchemaParams & { + override?: ( + ...args: Parameters['override']> + ) => void | boolean +} + +function toJSONSchemaParams(params?: ToJSONSchemaParams): _ToJSONSchemaParams { + return { + io: 'input', + reused: 'ref', + unrepresentable: 'any', + override(ctx) { + if (params?.override?.(ctx)) return + if (isZodSwitch(ctx.zodSchema)) { + const union = switchRegistry.get(ctx.zodSchema) + ?.union + if (union) { + const schema = toJSONSchema( + union as any, + toJSONSchemaParams(params), + ) + // Mutate the jsonSchema in place - ctx.jsonSchema is a reference + // to the actual schema object, so we must modify it directly + for (const key in ctx.jsonSchema) { + delete (ctx.jsonSchema as Record)[key] + } + Object.assign(ctx.jsonSchema, schema) + delete (ctx.jsonSchema as Record).$schema + } } - } else if (ctx.zodSchema._zod.def.type === 'transform') { - ctx.jsonSchema = {} - } - }, + }, + ...(params && omit(params, ['override'])), + } } function isZodSwitch(parser: $ZodType): parser is ZodSwitch { diff --git a/packages/json-schema/test/__snapshots__/index.test.ts.snap b/packages/json-schema/test/__snapshots__/index.test.ts.snap index 87dec096c..78fbe6c05 100644 --- a/packages/json-schema/test/__snapshots__/index.test.ts.snap +++ b/packages/json-schema/test/__snapshots__/index.test.ts.snap @@ -34,7 +34,15 @@ snapshot[`json schema for simple data object 1`] = ` items: { additionalProperties: false, properties: { - payload: {}, + payload: { + anyOf: [ + { + pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", + type: "string", + }, + {}, + ], + }, targeting: { anyOf: [ { @@ -68,6 +76,67 @@ snapshot[`json schema for simple data object 1`] = ` }, type: "object", }, + __schema6: { + additionalProperties: false, + properties: { + browser: { + "\$ref": "#/\$defs/__schema2", + }, + weather: { + "\$ref": "#/\$defs/__schema1", + }, + }, + type: "object", + }, + __schema7: { + type: "string", + }, + __schema8: { + items: { + additionalProperties: false, + properties: { + payload: { + anyOf: [ + { + pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", + type: "string", + }, + {}, + ], + }, + targeting: { + anyOf: [ + { + "\$ref": "#/\$defs/__schema9", + }, + { + items: { + "\$ref": "#/\$defs/__schema9", + }, + type: "array", + }, + ], + }, + }, + required: [ + "payload", + ], + type: "object", + }, + type: "array", + }, + __schema9: { + additionalProperties: false, + properties: { + browser: { + "\$ref": "#/\$defs/__schema2", + }, + weather: { + "\$ref": "#/\$defs/__schema1", + }, + }, + type: "object", + }, }, "\$schema": "https://json-schema.org/draft/2020-12/schema", additionalProperties: false, @@ -75,6 +144,92 @@ snapshot[`json schema for simple data object 1`] = ` "\$schema": { type: "string", }, + bar: { + additionalProperties: false, + properties: { + rules: { + items: { + additionalProperties: false, + properties: { + payload: { + anyOf: [ + { + pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", + type: "string", + }, + { + additionalProperties: false, + properties: { + a: { + anyOf: [ + { + pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", + type: "string", + }, + { + type: "number", + }, + ], + }, + b: { + anyOf: [ + { + pattern: "^\\\\{\\\\{[\\\\s\\\\S]{0,}\\\\}\\\\}\$", + type: "string", + }, + { + items: { + type: "string", + }, + type: "array", + }, + ], + }, + }, + required: [ + "a", + "b", + ], + type: "object", + }, + ], + }, + targeting: { + anyOf: [ + { + "\$ref": "#/\$defs/__schema6", + }, + { + items: { + "\$ref": "#/\$defs/__schema6", + }, + type: "array", + }, + ], + }, + }, + required: [ + "payload", + ], + type: "object", + }, + type: "array", + }, + variables: { + additionalProperties: { + "\$ref": "#/\$defs/__schema8", + }, + propertyNames: { + "\$ref": "#/\$defs/__schema7", + }, + type: "object", + }, + }, + required: [ + "rules", + ], + type: "object", + }, foo: { additionalProperties: false, properties: { diff --git a/packages/json-schema/test/fixtures/data.ts b/packages/json-schema/test/fixtures/data.ts index cef6bf29f..c2c40c491 100644 --- a/packages/json-schema/test/fixtures/data.ts +++ b/packages/json-schema/test/fixtures/data.ts @@ -5,6 +5,10 @@ export const data = await Data.create() .usePayload( { foo: z.string(), + bar: z.strictObject({ + a: z.number(), + b: z.array(z.string()), + }), }, ) .useTargeting({ @@ -13,8 +17,43 @@ export const data = await Data.create() .useFallThroughTargeting({ browser: targetIncludes(z.string()), }) - .addRules('foo', [ + .addRules('foo', { + variables: { + weatherMessage: [ + { + targeting: { + weather: ['sunny'], + }, + payload: 'sunshine', + }, + { + targeting: { + weather: ['rainy'], + }, + payload: 'raindrops', + }, + { + payload: 'weather', + }, + ], + }, + rules: [ + { + targeting: { + weather: ['sunny'], + }, + payload: '{{weatherMessage}}', + }, + { + payload: 'default message', + }, + ], + }) + .addRules('bar', [ { - payload: 'bar', + payload: { + a: 123, + b: ['a', 'b'], + }, }, ])