feat: adds proper logic to handle syncing rulesets for git sync/cloud sync projects

This commit is contained in:
Fares Osman
2026-05-08 00:24:21 -04:00
parent 53673f16a3
commit 2f14bc7fc8

View File

@@ -191,14 +191,15 @@ const Component = ({ params }: Route.ComponentProps) => {
const editor = useRef<CodeEditorHandle>(null);
const { submit: updateApiSpec } = useSpecUpdateActionFetcher();
const generateRequestCollectionFetcher = useSpecGenerateRequestCollectionActionFetcher();
const gitVersion = useGitVCSVersion();
const [isLintPaneOpen, setIsLintPaneOpen] = useState(false);
const [isSpecPaneOpen, setIsSpecPaneOpen] = useState(Boolean(parsedSpec));
const [rulesetPath, setRulesetPath] = useState<string>('');
// This determines what path to write the ruleset content to when the user selects a custom ruleset file or when the rulesetContent in the DB changes.
// For git sync projects, this will be the git working directory so that it appears in the staging modal and can be committed.
// For cloud/local projects, this will be a per-workspace path — rulesetContent in the DB handles syncing for these projects.
// Spectral requires a file path on disk to lint with a ruleset. Ref: lint-process.mjs.
// For git sync projects, write .spectral.yaml directly to the git working directory so it
// appears in the staging modal and can be committed/pushed/pulled like any other file.
// For cloud/local projects, write to a per-workspace scratch path — rulesetContent in NeDB handles syncing.
const rulesetWritePath = useMemo(
() =>
gitSyncRulesetPath ||
@@ -261,32 +262,42 @@ const Component = ({ params }: Route.ComponentProps) => {
editor.current?.tryToSetOption('lint', { ...lintOptions });
}, [rulesetPath]);
// For git sync projects, .spectral.yaml on disk is the source of truth.
// Runs on mount and whenever gitVersion changes (e.g. after a pull)
// to detect whether .spectral.yaml exists and update rulesetPath accordingly.
useEffect(() => {
const syncRulesetToDisk = async () => {
const syncGitRuleset = async () => {
if (gitSyncRulesetPath) {
try {
const content = await window.main.insecureReadFile({ path: gitSyncRulesetPath });
setRulesetPath(content ? gitSyncRulesetPath : '');
} catch {
setRulesetPath('');
}
}
};
syncGitRuleset();
}, [gitSyncRulesetPath, gitVersion]);
// For cloud/local projects, rulesetContent in NeDB is the source of truth.
// Write to the /workspace/${workspaceId} path for Spectral and update rulesetPath accordingly.
useEffect(() => {
const syncCloudRuleset = async () => {
if (gitSyncRulesetPath) return;
if (apiSpec.rulesetContent) {
// Write the stored content to the correct path for this project type and use it for linting.
await window.main.writeFile({ path: rulesetWritePath, content: apiSpec.rulesetContent });
setRulesetPath(rulesetWritePath);
} else {
// No ruleset content — for git sync, fall back to any .spectral.yaml already present in the repo (e.g. committed by another user).
// For cloud/local projects, default to OAS ruleset.
setRulesetPath(gitSyncRulesetPath || '');
await window.main.deleteFile({ path: rulesetWritePath });
setRulesetPath('');
}
};
syncRulesetToDisk();
syncCloudRuleset();
}, [apiSpec.rulesetContent, rulesetWritePath, gitSyncRulesetPath]);
useEffect(() => {
const syncRulesetForGitProject = async () => {
if (gitSyncRulesetPath && !apiSpec.rulesetContent) {
const content = await window.main.insecureReadFile({ path: gitSyncRulesetPath });
await services.apiSpec.update(apiSpec, { rulesetContent: content });
}
};
syncRulesetForGitProject();
}, []);
reactUse.useUnmount(() => {
// delete the helper to avoid it run multiple times when user enter the page next time
CodeMirror.registerHelper('lint', 'openapi', () => {});
@@ -449,7 +460,15 @@ const Component = ({ params }: Route.ComponentProps) => {
const content = await window.main.insecureReadFile({ path: filePath });
await window.main.writeFile({ path: rulesetWritePath, content });
await services.apiSpec.update(apiSpec, { rulesetContent: content });
// We do not write rulesetContent to NeDB for git sync projects because it would
// be serialized into insomnia.{workspaceId}.yaml, creating a duplicate representation.
// Instead, .spectral.yaml is written directly to the git working directory and
// tracked by git like any other file — committed, pushed, and pulled normally.
if (!gitSyncRulesetPath) {
await services.apiSpec.update(apiSpec, { rulesetContent: content });
}
setRulesetPath(rulesetWritePath);
};
@@ -463,9 +482,11 @@ const Component = ({ params }: Route.ComponentProps) => {
noText: 'Cancel',
onDone: async (confirmed: boolean) => {
if (confirmed) {
await services.apiSpec.update(apiSpec, { rulesetContent: undefined });
setRulesetPath('');
if (!gitSyncRulesetPath) {
await services.apiSpec.update(apiSpec, { rulesetContent: undefined });
}
await window.main.deleteFile({ path: rulesetWritePath });
setRulesetPath('');
}
},
});
@@ -521,7 +542,6 @@ const Component = ({ params }: Route.ComponentProps) => {
const disabledKeys = specActionList.filter(item => item.isDisabled).map(item => item.id);
const gitVersion = useGitVCSVersion();
const uniquenessKey = `${apiSpec?._id}::${apiSpec?.created}::${gitVersion}::${vcsVersion}`;
const [direction, setDirection] = useState<'horizontal' | 'vertical'>(