Skip to content

Commit 44dc9ab

Browse files
feat(flagd-core): pre-compile AJV validators for edge runtime compatibility (1/2) (#1481)
Signed-off-by: Norris <jonathan.norris@dynatrace.com>
1 parent 1e8bcb1 commit 44dc9ab

9 files changed

Lines changed: 185 additions & 28 deletions

File tree

libs/providers/flagd-web/project.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,12 @@
6060
"target": "generate"
6161
},
6262
{
63-
"target": "flagd-core:pullTestHarness"
63+
"projects": ["flagd-core"],
64+
"target": "pullTestHarness"
65+
},
66+
{
67+
"projects": ["flagd-core"],
68+
"target": "buildValidators"
6469
}
6570
]
6671
},

libs/providers/flagd/project.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,12 @@
6060
"target": "generate"
6161
},
6262
{
63-
"target": "flagd-core:pullTestHarness"
63+
"projects": ["flagd-core"],
64+
"target": "pullTestHarness"
65+
},
66+
{
67+
"projects": ["flagd-core"],
68+
"target": "buildValidators"
6469
}
6570
]
6671
},

libs/shared/flagd-core/.eslintrc.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"extends": ["../../../.eslintrc.json"],
3-
"ignorePatterns": ["!**/*", "test-harness", "flagd-schemas", "spec"],
3+
"ignorePatterns": ["!**/*", "test-harness", "flagd-schemas", "spec", "src/lib/generated"],
44
"overrides": [
55
{
66
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
@@ -18,7 +18,12 @@
1818
"files": ["*.json"],
1919
"parser": "jsonc-eslint-parser",
2020
"rules": {
21-
"@nx/dependency-checks": "error"
21+
"@nx/dependency-checks": [
22+
"error",
23+
{
24+
"ignoredFiles": ["{projectRoot}/scripts/**", "{projectRoot}/src/lib/generated/**"]
25+
}
26+
]
2227
}
2328
}
2429
]

libs/shared/flagd-core/package-lock.json

Lines changed: 59 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libs/shared/flagd-core/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,20 @@
88
},
99
"license": "Apache-2.0",
1010
"scripts": {
11+
"build:validators": "node scripts/build-validators.js",
1112
"publish-if-not-exists": "cp $NPM_CONFIG_USERCONFIG .npmrc && if [ \"$(npm show $npm_package_name@$npm_package_version version)\" = \"$(npm run current-version -s)\" ]; then echo 'already published, skipping'; else npm publish --access public; fi",
1213
"current-version": "echo $npm_package_version"
1314
},
1415
"peerDependencies": {
1516
"@openfeature/core": ">=1.6.0"
1617
},
1718
"dependencies": {
18-
"ajv": "^8.12.0",
1919
"object-hash": "^3.0.0",
2020
"imurmurhash": "^0.1.4",
2121
"semver": "^7.6.3",
2222
"json-logic-engine": "^4.0.2"
23+
},
24+
"devDependencies": {
25+
"ajv": "^8.12.0"
2326
}
2427
}

libs/shared/flagd-core/project.json

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,32 @@
2020
"parallel": false
2121
}
2222
},
23+
"buildValidators": {
24+
"executor": "nx:run-commands",
25+
"options": {
26+
"commands": ["git submodule update --init flagd-schemas", "node scripts/build-validators.js"],
27+
"cwd": "libs/shared/flagd-core",
28+
"parallel": false
29+
},
30+
"dependsOn": ["pullTestHarness"]
31+
},
2332
"lint": {
2433
"executor": "@nx/eslint:lint",
25-
"outputs": ["{options.outputFile}"]
34+
"outputs": ["{options.outputFile}"],
35+
"dependsOn": ["buildValidators"]
2636
},
2737
"test": {
2838
"executor": "@nx/jest:jest",
2939
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
40+
"dependsOn": ["buildValidators"],
3041
"options": {
3142
"jestConfig": "libs/shared/flagd-core/jest.config.ts"
3243
}
3344
},
3445
"package": {
3546
"executor": "@nx/rollup:rollup",
3647
"outputs": ["{options.outputPath}"],
37-
"dependsOn": ["pullTestHarness"],
48+
"dependsOn": ["pullTestHarness", "buildValidators"],
3849
"options": {
3950
"project": "libs/shared/flagd-core/package.json",
4051
"outputPath": "dist/libs/shared/flagd-core",
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* Build script to generate pre-compiled ajv validators.
3+
*
4+
* This generates static validation functions that don't use `new Function()`,
5+
* making them compatible with Cloudflare Workers and other restricted environments.
6+
*
7+
* Usage: node scripts/build-validators.js
8+
*/
9+
10+
const Ajv = require('ajv');
11+
const standaloneCode = require('ajv/dist/standalone').default;
12+
const fs = require('fs');
13+
const path = require('path');
14+
15+
const flagsSchema = require('../flagd-schemas/json/flags.json');
16+
const targetingSchema = require('../flagd-schemas/json/targeting.json');
17+
18+
// Configure ajv with source code generation enabled.
19+
// CJS output (esm: false, the default) so the file can be directly required
20+
// in both Node.js and Jest (CJS) environments. When bundled for Workers with
21+
// a bundler like esbuild/rollup, the CJS export will be rewritten to ESM
22+
// automatically by the bundler.
23+
// strict: false is required — the upstream flagd schema uses 1-tuple `items`
24+
// without additionalItems/minItems/maxItems (e.g. varRule), which AJV strict
25+
// mode rejects.
26+
const ajv = new Ajv({
27+
strict: false,
28+
code: { source: true, esm: false },
29+
});
30+
31+
// Add targeting schema first (referenced by flags schema)
32+
ajv.addSchema(targetingSchema, 'targeting');
33+
34+
// Compile the flags schema
35+
const validate = ajv.compile(flagsSchema);
36+
37+
// Generate standalone code
38+
const moduleCode = standaloneCode(ajv, validate);
39+
40+
// Create output directory
41+
const outputDir = path.join(__dirname, '../src/lib/generated');
42+
fs.mkdirSync(outputDir, { recursive: true });
43+
44+
// Write the generated validators
45+
const outputPath = path.join(outputDir, 'validators.js');
46+
fs.writeFileSync(
47+
outputPath,
48+
`// AUTO-GENERATED FILE - DO NOT EDIT
49+
// Generated by scripts/build-validators.js
50+
// Run 'npm run build:validators' to regenerate
51+
//
52+
// These pre-compiled validators are compatible with Cloudflare Workers
53+
// and other environments that restrict dynamic code generation.
54+
55+
${moduleCode}
56+
`,
57+
);
58+
59+
// Also create a TypeScript declaration file.
60+
// The type is inlined (rather than importing ValidateFunction from ajv) so that
61+
// downstream consumers don't need ajv installed for type resolution.
62+
const dtsPath = path.join(outputDir, 'validators.d.ts');
63+
fs.writeFileSync(
64+
dtsPath,
65+
`// AUTO-GENERATED FILE - DO NOT EDIT
66+
// Type declarations for pre-compiled validators
67+
68+
interface ValidateFunction {
69+
(data: unknown): boolean;
70+
errors?: null | Array<Record<string, unknown>>;
71+
}
72+
73+
declare const validate: ValidateFunction;
74+
export = validate;
75+
`,
76+
);
77+
78+
console.log('Generated pre-compiled validators:');
79+
console.log(' -', outputPath);
80+
console.log(' -', dtsPath);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Pre-compiled AJV validators (generated by scripts/build-validators.js)
2+
validators.js
3+
validators.d.ts

libs/shared/flagd-core/src/lib/parser.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import type { Logger, FlagMetadata } from '@openfeature/core';
22
import { ParseError } from '@openfeature/core';
3-
import Ajv from 'ajv';
4-
import flagsSchema from '../../flagd-schemas/json/flags.json';
5-
import targetingSchema from '../../flagd-schemas/json/targeting.json';
63
import type { Flag } from './feature-flag';
74
import { FeatureFlag } from './feature-flag';
85

6+
// Pre-compiled validators generated by scripts/build-validators.js.
7+
// These avoid new Function() and are compatible with all environments
8+
// including Cloudflare Workers, Deno Deploy, and Vercel Edge Runtime.
9+
import precompiledValidate from './generated/validators';
10+
911
type FlagConfig = {
1012
flags: { [key: string]: Flag };
1113
metadata?: FlagMetadata;
@@ -16,9 +18,6 @@ type FlagSet = {
1618
metadata: FlagMetadata;
1719
};
1820

19-
const ajv = new Ajv({ strict: false });
20-
const validate = ajv.addSchema(targetingSchema).compile(flagsSchema);
21-
2221
const evaluatorKey = '$evaluators';
2322
const bracketReplacer = new RegExp('^[^{]*\\{|}[^}]*$', 'g');
2423

@@ -36,9 +35,9 @@ export function parse(flagConfig: string, strictValidation: boolean, logger: Log
3635
const transformed = transform(flagConfig);
3736
const parsedFlagConfig: FlagConfig = JSON.parse(transformed);
3837

39-
const isValid = validate(parsedFlagConfig);
38+
const isValid = precompiledValidate(parsedFlagConfig);
4039
if (!isValid) {
41-
const message = `${errorMessages}: ${JSON.stringify(validate.errors, undefined, 2)}`;
40+
const message = `${errorMessages}: ${JSON.stringify(precompiledValidate.errors, undefined, 2)}`;
4241
if (strictValidation) {
4342
throw new ParseError(message);
4443
} else {

0 commit comments

Comments
 (0)