Add script to generate TS for Nimbus API response
This commit is contained in:
Родитель
b546e7b705
Коммит
1c5e868fb7
|
@ -15,6 +15,7 @@ jobs:
|
|||
node-version: '20.11.x'
|
||||
- run: npm ci
|
||||
- run: npm run build-glean
|
||||
- run: npm run build-nimbus
|
||||
- run: cp .env-dist .env
|
||||
# Mirror old linter from CircleCI, verifies that linter succeeds
|
||||
- run: npm run lint
|
||||
|
|
|
@ -86,6 +86,12 @@ We track commits that are largely style/formatting via `.git-blame-ignore-revs`.
|
|||
npm run build-glean
|
||||
```
|
||||
|
||||
6. Generate required Nimbus files (needs re-ran anytime Nimbus' `config/nimbus.yaml` file is updated):
|
||||
|
||||
```sh
|
||||
npm run build-nimbus
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
1. To run the server similar to production using a build phase, which includes minified and bundled assets:
|
||||
|
|
|
@ -21,6 +21,3 @@ features:
|
|||
- channel: production
|
||||
value: { "something": "wicked" }
|
||||
|
||||
types:
|
||||
objects: {}
|
||||
enums: {}
|
||||
|
|
|
@ -99,7 +99,8 @@
|
|||
"stylelint-config-recommended-scss": "^14.0.0",
|
||||
"stylelint-scss": "^6.2.1",
|
||||
"ts-jest": "^29.1.2",
|
||||
"typescript": "^5.4.2"
|
||||
"typescript": "^5.4.2",
|
||||
"yaml": "^2.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20.12.x",
|
||||
|
@ -12403,6 +12404,15 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/cosmiconfig/node_modules/yaml": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
|
||||
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/create-ecdh": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
|
||||
|
@ -27705,12 +27715,15 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
|
||||
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz",
|
||||
"integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev --port=6060",
|
||||
"build": "npm run get-location-data && npm run build-glean && next build",
|
||||
"build": "npm run get-location-data && npm run build-glean && npm run build-nimbus && next build",
|
||||
"start": "next start",
|
||||
"lint": "stylelint '**/*.scss' && prettier --check './src' && next lint --max-warnings=0",
|
||||
"fix": "prettier --write './src' && next lint --fix && stylelint --fix '**/*.scss'",
|
||||
|
@ -24,6 +24,7 @@
|
|||
"build-storybook": "npm run build-glean && storybook build",
|
||||
"create-location-data": "node src/scripts/build/uploadAutoCompleteLocations.js",
|
||||
"get-location-data": "node src/scripts/build/getAutoCompleteLocations.js",
|
||||
"build-nimbus": "node src/scripts/build/nimbusTypes.js",
|
||||
"build-glean": "glean translate src/telemetry/metrics.yaml --format typescript --output src/telemetry/generated && npm run build-glean-types",
|
||||
"build-glean-types": "node src/scripts/build/gleanTypes.js",
|
||||
"build-glean-docs": "glean translate src/telemetry/metrics.yaml --format markdown --output docs"
|
||||
|
@ -129,6 +130,7 @@
|
|||
"stylelint-config-recommended-scss": "^14.0.0",
|
||||
"stylelint-scss": "^6.2.1",
|
||||
"ts-jest": "^29.1.2",
|
||||
"typescript": "^5.4.2"
|
||||
"typescript": "^5.4.2",
|
||||
"yaml": "^2.4.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
|
||||
import { captureException } from "@sentry/node";
|
||||
import { logger } from "./logging";
|
||||
import {
|
||||
ExperimentData,
|
||||
defaultExperimentData,
|
||||
} from "../../../telemetry/generated/nimbus/experiments";
|
||||
|
||||
/**
|
||||
* Call the Cirrus sidecar, which returns a list of eligible experiments for the current user.
|
||||
|
@ -14,7 +18,7 @@ import { logger } from "./logging";
|
|||
*/
|
||||
export async function getExperiments(
|
||||
userId: string | undefined,
|
||||
): Promise<unknown> {
|
||||
): Promise<ExperimentData> {
|
||||
if (["stage", "production"].includes(process.env.APP_ENV ?? "local")) {
|
||||
const serverUrl = process.env.NIMBUS_SIDECAR_URL;
|
||||
if (!serverUrl) {
|
||||
|
@ -22,7 +26,7 @@ export async function getExperiments(
|
|||
}
|
||||
|
||||
try {
|
||||
const features = await fetch(`${serverUrl}/v1/features`, {
|
||||
const response = await fetch(`${serverUrl}/v1/features`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
|
@ -33,10 +37,12 @@ export async function getExperiments(
|
|||
}),
|
||||
});
|
||||
|
||||
return features?.json();
|
||||
const experimentData = (await response.json()) as ExperimentData;
|
||||
return experimentData ?? defaultExperimentData;
|
||||
} catch (ex) {
|
||||
logger.error(`Could not connect to Cirrus on ${serverUrl}`, ex);
|
||||
captureException(ex);
|
||||
}
|
||||
}
|
||||
return defaultExperimentData;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,202 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { parse } from "yaml";
|
||||
|
||||
run();
|
||||
|
||||
/**
|
||||
* See https://experimenter.info/fml-spec/#additional-types
|
||||
* @typedef {"String" | "Boolean" | "Int" | "Text" | "Image" | `Option<${Type}>` | `List<${Type}>` | `Map<${Type}, ${Type}>`} Type
|
||||
*/
|
||||
/**
|
||||
* @typedef {"staging" | "production"} Channel
|
||||
*/
|
||||
/**
|
||||
* @typedef {Record<
|
||||
* string,
|
||||
* {
|
||||
* description: string;
|
||||
* type: Type;
|
||||
* default: unknown;
|
||||
* string-alias?: string;
|
||||
* }
|
||||
* >} Variables
|
||||
*/
|
||||
/**
|
||||
* @typedef {{
|
||||
* about: { description: string; };
|
||||
* channels: Channel[];
|
||||
* features: Record<string, {
|
||||
* description: string;
|
||||
* variables: Variables;
|
||||
* defaults: Array<{
|
||||
* channel: Channel;
|
||||
* values: Record<keyof Variables, unknown>;
|
||||
* }>;
|
||||
* }>;
|
||||
* enums?: Record<string, {
|
||||
* description: string;
|
||||
* variants: Record<string, string>;
|
||||
* }>;
|
||||
* objects?: Record<string, {
|
||||
* description: string;
|
||||
* fields: Record<string, {
|
||||
* description: string;
|
||||
* type: Type;
|
||||
* default: unknown;
|
||||
* }>;
|
||||
* }>;
|
||||
* }} NimbusConfig
|
||||
*/
|
||||
|
||||
async function run() {
|
||||
const nimbusConfigSource = await readFile("config/nimbus.yaml", "utf-8");
|
||||
/** @type {NimbusConfig} */
|
||||
const nimbusConfig = parse(nimbusConfigSource);
|
||||
|
||||
const typedef =
|
||||
"/* This Source Code Form is subject to the terms of the Mozilla Public\n" +
|
||||
" * License, v. 2.0. If a copy of the MPL was not distributed with this\n" +
|
||||
" * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\n" +
|
||||
"// AUTOGENERATED `npm run build-nimbus`. DO NOT EDIT. DO NOT COMMIT.\n\n" +
|
||||
getFeaturesTypeDef(nimbusConfig) +
|
||||
"\n" +
|
||||
getFallbackObject(nimbusConfig);
|
||||
await mkdir("src/telemetry/generated/nimbus/", { recursive: true });
|
||||
await writeFile("src/telemetry/generated/nimbus/experiments.ts", typedef);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NimbusConfig} nimbusConfig
|
||||
* @returns string
|
||||
*/
|
||||
function getFallbackObject(nimbusConfig) {
|
||||
const featureFallbackDefs = Object.keys(nimbusConfig.features).map(
|
||||
(featureId) => {
|
||||
const variableNames = Object.keys(
|
||||
nimbusConfig.features[featureId].variables,
|
||||
);
|
||||
const variableFallbackDefs = variableNames.map((variableName) => {
|
||||
return ` "${variableName}": ${nimbusConfig.features[featureId].variables[variableName].default},\n`;
|
||||
});
|
||||
|
||||
return ` "${featureId}": {\n${variableFallbackDefs.join("")} },\n`;
|
||||
},
|
||||
);
|
||||
|
||||
return `export const defaultExperimentData: ExperimentData = {\n${featureFallbackDefs.join("\n")}};\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NimbusConfig} nimbusConfig
|
||||
* @returns string
|
||||
*/
|
||||
function getFeaturesTypeDef(nimbusConfig) {
|
||||
const featureDefs = Object.keys(nimbusConfig.features).map((featureId) => {
|
||||
const variableNames = Object.keys(
|
||||
nimbusConfig.features[featureId].variables,
|
||||
);
|
||||
const variableDefs = variableNames.map((variableName) => {
|
||||
return ` "${variableName}": ${getType(nimbusConfig.features[featureId].variables[variableName].type)};\n`;
|
||||
});
|
||||
return ` "${featureId}": {\n${variableDefs.join("")} };\n`;
|
||||
});
|
||||
const experimentDataTypeDef = `/** Status of experiments, as setup in Experimenter */\nexport type ExperimentData = {\n${featureDefs.join("")}};\n`;
|
||||
const featureTypeDef =
|
||||
getStringAliases(nimbusConfig) +
|
||||
"\n\n" +
|
||||
getTypeAliases(nimbusConfig) +
|
||||
"\n\n" +
|
||||
experimentDataTypeDef;
|
||||
return featureTypeDef;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NimbusConfig} nimbusConfig
|
||||
* @returns string
|
||||
*/
|
||||
function getStringAliases(nimbusConfig) {
|
||||
const features = Object.values(nimbusConfig.features);
|
||||
const stringAliasDefs = features.flatMap((feature) => {
|
||||
const variablesWithStringAlias = Object.values(feature.variables).filter(
|
||||
(variable) => {
|
||||
return typeof variable["string-alias"] === "string";
|
||||
},
|
||||
);
|
||||
return variablesWithStringAlias.map(
|
||||
(variable) => `type ${variable["string-alias"]} = string;\n`,
|
||||
);
|
||||
});
|
||||
|
||||
return `/* Nimbus string aliases */\n${stringAliasDefs.join("")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NimbusConfig} nimbusConfig
|
||||
* @returns string
|
||||
*/
|
||||
function getTypeAliases(nimbusConfig) {
|
||||
const objects = nimbusConfig.objects ?? {};
|
||||
const objectDefs = Object.keys(objects).map((typeAlias) => {
|
||||
const propertyNames = Object.keys(nimbusConfig.objects[typeAlias].fields);
|
||||
const propertyDefs = propertyNames.map((propertyName) => {
|
||||
// TODO: Add descriptions as TSDoc comment?
|
||||
return ` "${propertyName}": ${getType(nimbusConfig.objects[typeAlias].fields[propertyName].type)};\n`;
|
||||
});
|
||||
// TODO: Add description as TSDoc comment?
|
||||
return `type ${typeAlias} = {\n${propertyDefs.join("")}};\n`;
|
||||
});
|
||||
|
||||
const enums = nimbusConfig.enums ?? {};
|
||||
const enumDefs = Object.keys(enums).map((typeAlias) => {
|
||||
// TODO: Add values as TSDoc comment?
|
||||
const unionOfStrings = Object.keys(nimbusConfig.enums[typeAlias].variants)
|
||||
.map((variant) => `"${variant}"`)
|
||||
.join(" | ");
|
||||
return `type ${typeAlias} = ${unionOfStrings};`;
|
||||
});
|
||||
|
||||
return (
|
||||
"/* Nimbus object types */\n" +
|
||||
objectDefs.join("\n") +
|
||||
"\n\n/* Nimbus enum types */\n" +
|
||||
enumDefs.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
* @returns string
|
||||
*/
|
||||
function getType(type) {
|
||||
if (type === "String") {
|
||||
return "string";
|
||||
}
|
||||
if (type === "Boolean") {
|
||||
return "boolean";
|
||||
}
|
||||
if (type === "Int") {
|
||||
return "number";
|
||||
}
|
||||
if (type === "Text" || type === "Image") {
|
||||
return "string";
|
||||
}
|
||||
if (type.startsWith("Option<")) {
|
||||
const t = type.substring("Option<".length, type.length - 1).trim();
|
||||
return `null | ${getType(t)}`;
|
||||
}
|
||||
if (type.startsWith("List<")) {
|
||||
const t = type.substring("List<".length, type.length - 1).trim();
|
||||
return `Array<${getType(t)}>`;
|
||||
}
|
||||
if (type.startsWith("Map<")) {
|
||||
const kv = type.substring("Map<".length, type.length - 1).trim();
|
||||
const [k, v] = kv.split(",").map((part) => part.trim());
|
||||
return `Record<${getType(k)}, ${getType(v)}>`;
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
Загрузка…
Ссылка в новой задаче