fix: address release v2 reviews
This commit is contained in:
1 parent
4905ea6738
commit
32732680e5
55 files changed
+925
-294
No files matched your search
@@ -155,10 +155,22 @@ export const createFeatureCheckPlan = (root: string, kind: string): FeatureCheck
|
||||
executable: "node",
|
||||
args: ["--experimental-strip-types", "scripts/check-feature-contracts.mts"],
|
||||
},
|
||||
{ label: "Focused fixture and unit tests", executable: "pnpm", args: ["exec", "vitest", "run", ...sortedTests] },
|
||||
...(sortedTests.length > 0
|
||||
? [
|
||||
{
|
||||
label: "Focused fixture and unit tests",
|
||||
executable: "pnpm",
|
||||
args: ["exec", "vitest", "run", ...sortedTests],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
packageCommand("Affected package typechecks", sortedPackages),
|
||||
{ label: "Affected file lint", executable: "pnpm", args: ["exec", "oxlint", ...lintFiles] },
|
||||
{ label: "Affected file formatting", executable: "pnpm", args: ["exec", "oxfmt", "--check", ...sortedFiles] },
|
||||
...(lintFiles.length > 0
|
||||
? [{ label: "Affected file lint", executable: "pnpm", args: ["exec", "oxlint", ...lintFiles] }]
|
||||
: []),
|
||||
...(sortedFiles.length > 0
|
||||
? [{ label: "Affected file formatting", executable: "pnpm", args: ["exec", "oxfmt", "--check", ...sortedFiles] }]
|
||||
: []),
|
||||
];
|
||||
|
||||
return {
|
||||
|
||||
@@ -74,10 +74,12 @@ const compareKeys = (
|
||||
});
|
||||
};
|
||||
|
||||
const ignoredDirectories = new Set([".git", ".turbo", ".next", "node_modules", "dist", "build", "coverage"]);
|
||||
|
||||
const walkFiles = (directory: string): string[] => {
|
||||
if (!existsSync(directory)) return [];
|
||||
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
||||
if (entry.isDirectory() && [".git", ".turbo", "node_modules"].includes(entry.name)) return [];
|
||||
if (entry.isDirectory() && ignoredDirectories.has(entry.name)) return [];
|
||||
const path = join(directory, entry.name);
|
||||
return entry.isDirectory() ? walkFiles(path) : [path];
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { test } from "node:test";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import { checkFeatureContracts } from "./contracts.mts";
|
||||
import { formatFeatureChanges, planFeatureGeneration } from "./generate-feature.mts";
|
||||
import { formatFeatureChanges, getIntegrationGeneratorChoices, planFeatureGeneration } from "./generate-feature.mts";
|
||||
// oxlint-disable-next-line import/no-unassigned-import -- Registers shared contract tests in this suite.
|
||||
import "./check-feature.contract.mts";
|
||||
|
||||
@@ -13,6 +13,55 @@ test("the checked-in feature contracts are consistent", () => {
|
||||
assert.deepEqual(checkFeatureContracts(root), []);
|
||||
});
|
||||
|
||||
test("reads generator choices from the canonical integration definitions", () => {
|
||||
const choices = getIntegrationGeneratorChoices(root);
|
||||
|
||||
assert.ok(choices.categories.includes("miscellaneous"));
|
||||
assert.ok(choices.secretKinds.includes("apiKey"));
|
||||
});
|
||||
|
||||
test("adds translations to empty JSON registries without a leading comma", async () => {
|
||||
const fs = await import("node:fs");
|
||||
const os = await import("node:os");
|
||||
const path = await import("node:path");
|
||||
const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "homarr-feature-generator-"));
|
||||
try {
|
||||
for (const sourcePath of [
|
||||
"packages/definitions/src/integration.ts",
|
||||
"packages/definitions/src/widget.ts",
|
||||
"packages/definitions/src/widget-integration-map.ts",
|
||||
"packages/definitions/src/docs/widget-doc-slugs.ts",
|
||||
"packages/widgets/src/manifest.ts",
|
||||
"packages/widgets/src/catalog.ts",
|
||||
"packages/widgets/src/index.tsx",
|
||||
]) {
|
||||
const target = path.join(temporaryRoot, sourcePath);
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||
fs.copyFileSync(path.join(root, sourcePath), target);
|
||||
}
|
||||
const translationPath = path.join(temporaryRoot, "packages/translation/src/lang/en.json");
|
||||
fs.mkdirSync(path.dirname(translationPath), { recursive: true });
|
||||
fs.writeFileSync(translationPath, '{\n "integration": {},\n "widget": {}\n}\n');
|
||||
|
||||
const changes = planFeatureGeneration(temporaryRoot, {
|
||||
widget: {
|
||||
kind: "exampleEmptyRegistry",
|
||||
name: "Example empty registry",
|
||||
slug: "example-empty-registry",
|
||||
description: "Verifies empty translation registry insertion.",
|
||||
icon: "IconBox",
|
||||
supportedIntegrations: [],
|
||||
},
|
||||
});
|
||||
const translation = changes.find((change) => change.path === "packages/translation/src/lang/en.json");
|
||||
|
||||
assert.ok(translation);
|
||||
assert.doesNotThrow(() => JSON.parse(translation.content));
|
||||
} finally {
|
||||
fs.rmSync(temporaryRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("paired generation plans every formatter-clean contract without writing", async () => {
|
||||
const description =
|
||||
"A deliberately long feature description that verifies generated source stays formatter-clean when contributor input exceeds the configured print width.";
|
||||
|
||||
@@ -91,7 +91,20 @@ const addJsonProperty = (source: string, property: "integration" | "widget", nam
|
||||
.join("\n");
|
||||
const beforeClose = source.slice(0, end);
|
||||
const trimmed = beforeClose.trimEnd();
|
||||
return `${trimmed},\n ${JSON.stringify(name)}: ${serialized}\n ${source.slice(end)}`;
|
||||
const separator = body.trim().length === 0 ? "" : ",";
|
||||
return `${trimmed}${separator}\n ${JSON.stringify(name)}: ${serialized}\n ${source.slice(end)}`;
|
||||
};
|
||||
|
||||
export const getIntegrationGeneratorChoices = (root: string) => {
|
||||
const source = read(root, "packages/definitions/src/integration.ts");
|
||||
const categoryRange = containerRange(source, "const integrationCategories", "[");
|
||||
const secretRange = containerRange(source, "const integrationSecretKindObject", "{");
|
||||
return {
|
||||
categories: [...source.slice(categoryRange.start, categoryRange.end).matchAll(/"([^"]+)"/g)].map(
|
||||
(match) => match[1],
|
||||
),
|
||||
secretKinds: [...source.slice(secretRange.start, secretRange.end).matchAll(/^ (\w+):/gm)].map((match) => match[1]),
|
||||
};
|
||||
};
|
||||
|
||||
const addChange = (
|
||||
@@ -123,19 +136,11 @@ const validate = (request: FeatureRequest) => {
|
||||
|
||||
const planIntegration = (root: string, feature: IntegrationFeature, changes: PlannedChange[]) => {
|
||||
const definitionSource = read(root, "packages/definitions/src/integration.ts");
|
||||
const categoryRange = containerRange(definitionSource, "const integrationCategories", "[");
|
||||
const categories = new Set(
|
||||
[...definitionSource.slice(categoryRange.start, categoryRange.end).matchAll(/"([^"]+)"/g)].map((match) => match[1]),
|
||||
);
|
||||
if (!categories.has(feature.category)) {
|
||||
const choices = getIntegrationGeneratorChoices(root);
|
||||
if (!choices.categories.includes(feature.category)) {
|
||||
throw new Error(`Generator stopped: ${feature.category} is not a current integration category.`);
|
||||
}
|
||||
const secretObject = definitionSource.slice(
|
||||
definitionSource.indexOf("const integrationSecretKindObject"),
|
||||
definitionSource.indexOf("satisfies Record<string", definitionSource.indexOf("const integrationSecretKindObject")),
|
||||
);
|
||||
const validSecrets = new Set([...secretObject.matchAll(/^ (\w+):/gm)].map((match) => match[1]));
|
||||
const unknownSecrets = feature.secretKinds.filter((kind) => !validSecrets.has(kind));
|
||||
const unknownSecrets = feature.secretKinds.filter((kind) => !choices.secretKinds.includes(kind));
|
||||
if (unknownSecrets.length > 0) {
|
||||
throw new Error(`Generator stopped: unknown credential kind(s): ${unknownSecrets.join(", ")}.`);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user