This is an automated email from the ASF dual-hosted git repository. sciabarracom pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/openserverless-configurator.git
commit cd8b30d370f073956ac8a5d4bcc22c9adaf13644 Author: Michele Sciabarra <[email protected]> AuthorDate: Mon Sep 28 20:54:14 2026 +0100 configurator sources --- README.md | 58 ++++ configurator/bun.lock | 41 +++ configurator/configurator.ts | 389 +++++++++++++++++++++++++ configurator/index.ts | 22 ++ configurator/package.json | 19 ++ configurator/tests/fixtures/bad.json | 1 + configurator/tests/fixtures/multi-keys.json | 22 ++ configurator/tests/fixtures/not-json.txt | 16 + configurator/tests/fixtures/valid.json | 11 + configurator/tests/fixtures/with-defaults.json | 26 ++ configurator/tests/index.test.ts | 144 +++++++++ configurator/tsconfig.json | 27 ++ 12 files changed, 776 insertions(+) diff --git a/README.md b/README.md new file mode 100644 index 0000000..d1e3c35 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one + ~ or more contributor license agreements. See the NOTICE file + ~ distributed with this work for additional information + ~ regarding copyright ownership. The ASF licenses this file + ~ to you under the Apache License, Version 2.0 (the + ~ "License"); you may not use this file except in compliance + ~ with the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, + ~ software distributed under the License is distributed on an + ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + ~ KIND, either express or implied. See the License for the + ~ specific language governing permissions and limitations + ~ under the License. +--> + +## Openserverless Configurator task + +### Development: + +The configurator is inside the `configurator` folder. +The entrypoint is the `configurator/index.ts` file. It simply calls the configurator main function. + +All the logic is inside the `configurator/configurator.ts` file. Of course, you can add more files +and change the structure as you see fit. + +Inside the `configurator` folder, first install dependencies: + +```bash +bun install +``` + +In the `package.json` file there are a couple of scripts. The `start` script will run the configurator: + +```bash +bun run start +``` + +### Tests: + +There are unit tests inside the `configurator/test` folder. You can run them with the following command: + +```bash +bun run test +``` + +### Building: + +To build the configurator and generate the `configurator.js` file, run the following command: + +```bash +bun run build +``` + +It will generate the `configurator.js` file and move it in the parent, where it can be used by the opsfile task. diff --git a/configurator/bun.lock b/configurator/bun.lock new file mode 100644 index 0000000..9031cbc --- /dev/null +++ b/configurator/bun.lock @@ -0,0 +1,41 @@ +{ + "lockfileVersion": 1, + "configVersion": 0, + "workspaces": { + "": { + "name": "prompt", + "dependencies": { + "@clack/prompts": "^0.7.0", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5.0.0", + }, + }, + }, + "packages": { + "@clack/core": ["@clack/[email protected]", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-H4hxZDXgHtWTwV3RAVenqcC4VbJZNegbBjlPvzOzCouXtS2y3sDvlO3IsbrPNWuLWPPlYVYPghQdSF64683Ldw=="], + + "@clack/prompts": ["@clack/[email protected]", "", { "dependencies": { "@clack/core": "^0.3.3", "is-unicode-supported": "*", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-0MhX9/B4iL6Re04jPrttDm+BsP8y6mS7byuv0BvXgdXhbV5PdlsHt55dvNsuBCPZ7xq1oTAOOuotR9NFbQyMSA=="], + + "@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.1.17" } }, "sha512-uJgKjTdX0GkWEHZzQzFsJkWp5+43ZS7HC8sZPFnOwnSo1AsNl2q9o2bFeS23disNDqbggEgyFkKCHl/w8iZsMA=="], + + "@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg=="], + + "@types/ws": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ=="], + + "bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "~20.12.8", "@types/ws": "~8.5.10" } }, "sha512-Z4+OplcSd/YZq7ZsrfD00DKJeCwuNY96a1IDJyR73+cTBaFIS7SC6LhpY/W3AMEXO9iYq5NJ58WAwnwL1p5vKg=="], + + "is-unicode-supported": ["[email protected]", "", {}, "sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q=="], + + "picocolors": ["[email protected]", "", {}, "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew=="], + + "sisteransi": ["[email protected]", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q=="], + + "undici-types": ["[email protected]", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + } +} diff --git a/configurator/configurator.ts b/configurator/configurator.ts new file mode 100644 index 0000000..70a5f13 --- /dev/null +++ b/configurator/configurator.ts @@ -0,0 +1,389 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { $ } from "bun"; + +import { + intro, + outro, + select, + isCancel, + cancel, + text, + password, +} from "@clack/prompts"; +import color from "picocolors"; + +import { parseArgs } from "util"; + +type Prompts = Record<string, PromptData>; + +type PromptData = { + label?: string; + type: string | string[]; + default?: string; + value?: string; +}; + +const OPS = process.env.OPS || "ops"; + +export const AdditionalArgsMsg = "Additional arguments will be ignored."; +export const NotValidJsonMsg = "Not a valid JSON file"; +export const FileNotFoundJsonMsg = "The JSON file was not found. Pathname: "; +export const BadConfigMsg = + "Bad configuration file. Check the help message (-h) to see the expected format."; + +export const HelpMsg = ` +Usage: config <configjson> [-o | --override] + +Description: +Prompt the user for configuration data defined in the config.json file. +The script will ignore the keys from the input config that are already set in ops +and only prompt the user for the missing data, unless the override flag is set. +Then they will be saved in the ops config. You can also define a default value. + +The config.json file must be a JSON file with the following structure: + + { + "KEY": { + "type": "string" + }, + "OTHER_KEY": { + "label": "An optional custom message", + "type": "int", + "default": "42" + }, + ... + } + +The keys must be uppercase words (separated by underscores). +The value for the "type" key must be either string with the following values: +- string +- int +- float +- bool +- password +- an array of strings with specific values (an enum). +`; + +export default async function main() { + const options: { showhelp: { type: 'boolean' }, override: { type: 'boolean' } } = { + showhelp: { + type: 'boolean', + }, + override: { + type: 'boolean', + }, + }; + const { values, positionals } = parseArgs({ + args: Bun.argv, + options: options, + strict: true, + allowPositionals: true, + + }); + + if (values.showhelp) { + console.log(HelpMsg); + return process.exit(0); + } + + const override = values.override || false; + + // 1. Read input config json + const readPosRes = readPositionalFile(positionals); + if (!readPosRes.success) { + cancel(readPosRes.message); + return process.exit(1); + } + + if (readPosRes.help) { + console.log(readPosRes.help); + return process.exit(0); + } + + if (readPosRes.message) { + console.warn(readPosRes.message); + } + + // 2. Parse the json + const jsonRes = await parsePositionalFile(readPosRes.jsonFilePath!); + + if (!jsonRes.success) { + cancel(jsonRes.message); + return process.exit(1); + } + + const config = jsonRes.body; + + // 3. Validate the given config json + if (!isInputConfigValid(config)) { + cancel(BadConfigMsg); + return process.exit(1); + } + + // 4. Run OPS to get the available config data + const { exitCode, stderr, stdout } = await $`${OPS} -config -d`.quiet(); + if (exitCode !== 0) { + cancel(stderr.toString()); + return process.exit(1); + } + + const opsCurrentConfig = stdout.toString(); + + // 5. Remove the keys from config that are already in the opsConfig + let missingData = config; + if (!override) { + missingData = findMissingConfig(config, opsCurrentConfig); + } + + // 6. Ask the user for the missing data + console.log(); + intro(color.inverse(" ops configurator ")); + + await askMissingData(missingData, override); + + // 7. Save the data to the config? + console.log(); + + outro("You're all set!"); +} + +async function askMissingData(missingData: Prompts, override: boolean = false) { + if (Object.keys(missingData).length === 0) { + outro("Configuration set from ops"); + process.exit(0); + } + console.log(); + + if (!override) { + console.log("Configuration partially set from ops. Need a few more:"); + } + + for (const key in missingData) { + let inputFromPrompt: string; + const prompt: PromptData = missingData[key]; + // let askedForPassword = false; + + if (Array.isArray(prompt.type)) { + const defaultValueOK = prompt.default && prompt.type.includes(prompt.default); + if (prompt.default && !defaultValueOK) { + console.log(); + console.warn(`The default value ${prompt.default} is not in the enum values.`); + } + + const selected = await select({ + message: prompt.label || `Pick a value for '${key}'`, + options: prompt.type.map((v) => ({ label: v, value: v })), + initialValue: defaultValueOK ? prompt.default : undefined, + }); + + if (!selected || isCancel(selected)) { + cancel("Operation cancelled"); + process.exit(0); + } + + inputFromPrompt = selected.toString(); + + // inputConfigs[key] = { ...prompt, value: selected.toString() }; + } else if (prompt.type === "bool") { + const selected = await select({ + initialValue: prompt.default === "true" || prompt.default as unknown as boolean === true ? "true" : "false", + message: prompt.label || `Pick a true/false for '${key}'`, + options: [ + { label: "true", value: "true" }, + { label: "false", value: "false" }, + ], + }); + + if (!selected || isCancel(selected)) { + cancel("Operation cancelled"); + process.exit(0); + } + + inputFromPrompt = selected.toString(); + // inputConfigs[key] = { ...prompt, value: selected.toString() }; + } else if (prompt.type === "password") { + if (prompt.default) { + console.log(); + console.warn("Default password value is not supported. Please enter the password manually."); + } + const input = await password({ + message: prompt.label || `Enter password value for ${key}`, + validate: (value) => { + if (!value) { + return "Password cannot be empty"; + } + } + }); + + if (isCancel(input)) { + cancel("Operation cancelled"); + process.exit(0); + } + inputFromPrompt = input; + // askedForPassword = true; + } else { + const defaultMsgFragment = prompt.default ? `(default: ${prompt.default})` : ""; + const message = `Enter value for ${key} ${defaultMsgFragment} (${prompt.type})`; + + let input = await text({ + message: prompt.label || message, + // defaultValue: prompt.default?.toString(), + initialValue: prompt.default?.toString(), + validate: (value) => { + switch (prompt.type) { + case "int": + if (!Number.isInteger(Number(value))) { + return `Value for ${key} must be an integer number`; + } + break; + case "float": + if (!Number(value)) { + return `Value for ${key} must be a number`; + } + } + return; + } + }) as string; + + if (isCancel(input)) { + cancel("Operation cancelled"); + process.exit(0); + } + + + inputFromPrompt = input; + // inputConfigs[key] = { ...prompt, value: input }; + } + + // if (!askedForPassword) { + // console.log("Setting", key, "to", inputFromPrompt); + // } else { + // console.log("Setting", key, "to", "*".repeat(inputFromPrompt.length)); + // } + + console.log(`Setting ${key} to ${inputFromPrompt}`); + const { exitCode, stderr } = + await $`${OPS} -config ${key}=${inputFromPrompt}`.nothrow(); + if (exitCode !== 0) { + cancel(stderr.toString()); + return process.exit(1); + } + } +} + +export function findMissingConfig( + config: Record<string, any>, + opsCurrentConfig: string +): Prompts { + let newConfig: Record<string, any> = {}; + + let opsConfigKeys = opsCurrentConfig.split("\n").map((line) => line.split("=")[0]); + + for (const key in config) { + if (opsConfigKeys.includes(key)) { + continue; + } + newConfig[key] = config[key]; + } + + return newConfig; +} + +export function readPositionalFile(positionals: string[]): { + success: boolean; + message?: string; + help?: string; + jsonFilePath?: string; +} { + + if (positionals.length < 2) { + console.error("This should not happen"); + return { success: false, message: "This should not happen" }; + } + + if (positionals.length === 2) { + return { success: true, help: HelpMsg }; + } + + if (positionals.length > 3) { + return { + success: true, + message: AdditionalArgsMsg, + jsonFilePath: positionals[2], + }; + } + + return { success: true, jsonFilePath: positionals[2] }; +} + +export async function parsePositionalFile( + path: string +): Promise<{ success: boolean; message?: string; body?: any }> { + + const file = Bun.file(Bun.pathToFileURL(path)); + + if (!await file.exists()) { + return { success: false, message: FileNotFoundJsonMsg + Bun.pathToFileURL(path) }; + } + + + try { + const contents = await file.json(); + return { success: true, body: contents }; + } catch (error) { + return { success: false, message: NotValidJsonMsg }; + } +} + +export function isInputConfigValid(body: Record<string, any>): boolean { + // 1. If the body is empty, return false + if (Object.keys(body).length === 0) { + return false; + } + + // 2. Check that each key in the body has the keys as the Prompt type + for (const key in body) { + const value = body[key]; + if (typeof value !== "object") { + return false; + } + + if (!value.type) { + return false; + } + + if ( + !["string", "int", "float", "bool", "password"].includes(value.type) && + !Array.isArray(value.type) + ) { + return false; + } + } + + // 3. Check that all the keys are uppercase and can only have underscores not at the beginning or end + for (const key in body) { + if (!/^[A-Z][A-Z_]|[0-9]*[A-Z]|[0-9]$/.test(key)) { + return false; + } + } + + return true; +} diff --git a/configurator/index.ts b/configurator/index.ts new file mode 100644 index 0000000..18e0159 --- /dev/null +++ b/configurator/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import main from "./configurator"; + +main().catch(console.error); diff --git a/configurator/package.json b/configurator/package.json new file mode 100644 index 0000000..bbd833c --- /dev/null +++ b/configurator/package.json @@ -0,0 +1,19 @@ +{ + "name": "prompt", + "module": "index.ts", + "type": "module", + "scripts": { + "start": "bun index.ts", + "test": "bun test tests/index.test.ts", + "build": "bun build --target=bun index.ts > configurator.js && mv configurator.js ../configurator.js" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "@clack/prompts": "^0.7.0" + } +} diff --git a/configurator/tests/fixtures/bad.json b/configurator/tests/fixtures/bad.json new file mode 100644 index 0000000..ff30235 --- /dev/null +++ b/configurator/tests/fixtures/bad.json @@ -0,0 +1 @@ +} \ No newline at end of file diff --git a/configurator/tests/fixtures/multi-keys.json b/configurator/tests/fixtures/multi-keys.json new file mode 100644 index 0000000..9ba80cf --- /dev/null +++ b/configurator/tests/fixtures/multi-keys.json @@ -0,0 +1,22 @@ +{ + "HELLO": { + "label": "Set a greeting", + "type": "string" + }, + "TESTINT": { + "type": "int" + }, + "TESTFLOAT": { + "label": "Set a float", + "type": "float" + }, + "TESTBOOL": { + "type": "bool" + }, + "TESTPASSWORD": { + "type": "password" + }, + "TESTSELECT": { + "type": ["one", "two", "three", "four options"] + } +} diff --git a/configurator/tests/fixtures/not-json.txt b/configurator/tests/fixtures/not-json.txt new file mode 100644 index 0000000..13a8339 --- /dev/null +++ b/configurator/tests/fixtures/not-json.txt @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/configurator/tests/fixtures/valid.json b/configurator/tests/fixtures/valid.json new file mode 100644 index 0000000..50a753e --- /dev/null +++ b/configurator/tests/fixtures/valid.json @@ -0,0 +1,11 @@ +{ + "HELLO": { + "type": "string" + }, + "HELLO3": { + "type": "string" + }, + "ANOTHER": { + "type": "string" + } +} diff --git a/configurator/tests/fixtures/with-defaults.json b/configurator/tests/fixtures/with-defaults.json new file mode 100644 index 0000000..ff94d44 --- /dev/null +++ b/configurator/tests/fixtures/with-defaults.json @@ -0,0 +1,26 @@ +{ + "HELLO": { + "type": "string", + "default": "world" + }, + "HELLO3": { + "type": "int", + "default": 3 + }, + "A_FLOAT": { + "type": "float", + "default": 3.14 + }, + "A_BOOL": { + "type": "bool", + "default": false + }, + "A_PASSWORD": { + "type": "password", + "default": "mypass" + }, + "A_SELECT": { + "type": ["one", "two", "three", "four options"], + "default": "two" + } +} diff --git a/configurator/tests/index.test.ts b/configurator/tests/index.test.ts new file mode 100644 index 0000000..20f6acd --- /dev/null +++ b/configurator/tests/index.test.ts @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { expect, test, describe } from "bun:test"; +import { + readPositionalFile, + parsePositionalFile, + NotValidJsonMsg, + HelpMsg, + AdditionalArgsMsg, + isInputConfigValid, + findMissingConfig, + FileNotFoundJsonMsg, +} from "../configurator.ts"; + +describe("findMissingConfig", () => { + test("config has some opsConfig key", async () => { + const config: any = { hello: "world", world: "hello" }; + const opsConfig = "hello=world"; + const newC: any = findMissingConfig(config, opsConfig); + expect(newC).toEqual({ world: "hello" }); + }); + + test("config has no opsConfig key", async () => { + const config: any = { hello: "world", world: "hello" }; + const opsConfig = "not=existing"; + const newC: any = findMissingConfig(config, opsConfig); + expect(newC).toEqual({ hello: "world", world: "hello" }); + }); + + test("opsConfig has no key", async () => { + const config: any = { hello: "world", world: "hello" }; + const opsConfig = ""; + const newC: any = findMissingConfig(config, opsConfig); + expect(newC).toEqual({ hello: "world", world: "hello" }); + }); + + test("config and opsConfig are the same", async () => { + const config: any = { hello: "world", world: "hello" }; + const opsConfig = "hello=world\nworld=hello"; + const newC = findMissingConfig(config, opsConfig); + expect(newC).toEqual({}); + }); +}); + +describe("validateConfigJson", () => { + test("empty config", async () => { + const res = isInputConfigValid({}); + expect(res).toBe(false); + }); + + test("wrong json", async () => { + const res = isInputConfigValid({ + some: "key", + properties: { hello: { type: "string" } }, + }); + expect(res).toBe(false); + }); + + test("valid json", async () => { + const res = isInputConfigValid({ + HELLO: { type: "string" }, + HELLO3: { type: "string" }, + HELLO33: { type: "string" }, + HELLO_3: { type: "string" }, + HELLO_33: { type: "string" }, + HELLO_A_1_B_2: { type: "string" }, + HELLO_A1B2: { type: "string" }, + HELLO_INT: { type: "int" }, + HELLO_FLOAT: { type: "float" }, + HELLO_BOOL: { type: "bool" }, + HELLOPASS: { type: "password" }, + HELLOENUM: { type: ["a", "b", "c"] }, + HELLOENUM3: { type: ["a", "b", "c"] }, + }); + expect(res).toBe(true); + }); +}); + +describe("readPositionalFile", () => { + test("no positional arg provided", async () => { + const res = readPositionalFile(["bun", "index.ts"]); + expect(res.success).toBe(true); + expect(res.help).toBe(HelpMsg); + }); + + test("too many positional arg provided", async () => { + const res = readPositionalFile(["bun", "index.ts", "config.json", "extra"]); + expect(res.success).toBe(true); + expect(res.jsonFilePath).toBe("config.json"); + expect(res.message).toBe(AdditionalArgsMsg); + }); + + test("positional arg provided", async () => { + const res = readPositionalFile(["bun", "index.ts", "config.json"]); + expect(res.success).toBe(true); + expect(res.jsonFilePath).toBe("config.json"); + }); +}); + +describe("parsePositionalFile", () => { + test("file does not exist", async () => { + const filename = "not-existing.json"; + const jsonRes = await parsePositionalFile(filename); + expect(jsonRes.success).toBe(false); + expect(jsonRes.message).toBe(FileNotFoundJsonMsg + Bun.pathToFileURL(filename)); + }); + + test("not a json file", async () => { + const jsonRes = await parsePositionalFile("tests/fixtures/not-json.txt"); + expect(jsonRes.success).toBe(false); + expect(jsonRes.message).toBe(NotValidJsonMsg); + }); + + test("not a valid json", async () => { + const jsonRes = await parsePositionalFile("tests/fixtures/bad.json"); + expect(jsonRes.success).toBe(false); + expect(jsonRes.message).toBe(NotValidJsonMsg); + }); + + test("valid json", async () => { + const jsonRes = await parsePositionalFile("tests//fixtures/valid.json"); + expect(jsonRes.success).toBe(true); + expect(jsonRes.body).toHaveProperty("HELLO", { type: "string" } ); + expect(jsonRes.body).toHaveProperty("HELLO3", { type: "string" } ); + expect(jsonRes.body).toHaveProperty("HELLO_3", { type: "string" }); + }); +}); diff --git a/configurator/tsconfig.json b/configurator/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/configurator/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}
