diff --git a/package-lock.json b/package-lock.json index fa1787c..3d16d99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,7 +70,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.14", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -111,7 +113,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.14", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -295,7 +299,6 @@ "version": "6.21.0", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -481,7 +484,6 @@ "version": "8.16.0", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -592,7 +594,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.1.0", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -907,7 +911,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -985,7 +988,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.14", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -1490,8 +1495,20 @@ } }, "node_modules/js-yaml": { - "version": "4.1.1", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -2191,7 +2208,9 @@ } }, "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.14", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -2495,7 +2514,6 @@ "version": "5.9.3", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 5ed74fd..a675986 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,7 @@ "watch": "tsc -watch -p ./", "pretest": "npm run compile", "lint": "eslint src --ext ts", - "test": "node --test tests/smoke.test.js" + "test": "node --test tests/*.test.js" }, "devDependencies": { "@types/node": "^20.0.0", diff --git a/src/cli.ts b/src/cli.ts index 2e34e9a..cf4b697 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode'; import { execFile, execFileSync } from 'child_process'; +import { SCHEMA_FORMATS } from './formats'; /** * Get the schemaforge CLI path from settings or default to 'schemaforge'. @@ -99,6 +100,6 @@ export async function getAvailableFormats(): Promise { return JSON.parse(result); } catch { // Fallback - return ['sql', 'prisma', 'drizzle', 'typeorm', 'django', 'sqlalchemy', 'alembic', 'json_schema', 'graphql', 'ef', 'scala']; + return SCHEMA_FORMATS; } } diff --git a/src/commands/convert.ts b/src/commands/convert.ts index a1ecf53..a7f9313 100644 --- a/src/commands/convert.ts +++ b/src/commands/convert.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import { execSchemaForge } from '../cli'; import { getOutputChannel } from '../output'; +import { SCHEMA_FORMATS, normalizeFormat } from '../formats'; export class ConvertCommand { static async run(uri?: vscode.Uri) { @@ -23,10 +24,10 @@ export class ConvertCommand { // Detect format first const detectResult = await execSchemaForge(['detect', sourcePath]); - const detectedFormat = detectResult.trim(); + const detectedFormat = normalizeFormat(detectResult) ?? ''; // Let user pick target format - const formats = ['sql', 'prisma', 'drizzle', 'typeorm', 'django', 'sqlalchemy', 'alembic', 'json_schema', 'graphql', 'ef', 'scala']; + const formats = SCHEMA_FORMATS; const target = await vscode.window.showQuickPick( formats.filter(f => f !== detectedFormat), { placeHolder: `Source: ${detectedFormat}. Pick target format:`, canPickMany: false } diff --git a/src/formats.ts b/src/formats.ts new file mode 100644 index 0000000..9482d6b --- /dev/null +++ b/src/formats.ts @@ -0,0 +1,50 @@ +/** + * Canonical list of schema formats SchemaForge converts between. + * + * Previously this list was inlined in three places (convert.ts, + * previewPanel.ts, and cli.ts's getAvailableFormats fallback). Keeping a + * single source of truth prevents the list from silently drifting between + * call sites — a recurring failure class in this extension (see PR #9, where + * the quickConvert default fallback had already diverged from package.json). + */ +export const SCHEMA_FORMATS: string[] = [ + 'sql', + 'prisma', + 'drizzle', + 'typeorm', + 'django', + 'sqlalchemy', + 'alembic', + 'json_schema', + 'graphql', + 'ef', + 'scala', +]; + +const FORMAT_SET = new Set(SCHEMA_FORMATS); + +/** + * Normalize a format identifier produced by the schemaforge CLI (e.g. from + * `detect`) into the canonical token the `convert --from/--to` flags expect. + * + * The CLI sometimes emits mixed-case or padded labels (e.g. "SQL", " Prisma "). + * Passing those verbatim to `convert` fails with an opaque CLI error while the + * command appears to "do nothing" — the classic silent-failure trap. Here we + * trim + lowercase and, when the value matches a known format, return its + * canonical spelling so the conversion actually runs. Unknown values are passed + * through unchanged so the CLI still produces a clear, actionable error. + */ +export function normalizeFormat(fmt: string | undefined | null): string | undefined { + if (fmt === undefined || fmt === null) { + return undefined; + } + const trimmed = fmt.trim(); + if (trimmed.length === 0) { + return undefined; + } + const lower = trimmed.toLowerCase(); + if (FORMAT_SET.has(lower)) { + return lower; + } + return trimmed; +} diff --git a/src/panels/previewPanel.ts b/src/panels/previewPanel.ts index 608464d..97a91cb 100644 --- a/src/panels/previewPanel.ts +++ b/src/panels/previewPanel.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode'; import { execSchemaForge } from '../cli'; +import { SCHEMA_FORMATS, normalizeFormat } from '../formats'; /** * Generate a random nonce so the webview's Content-Security-Policy can @@ -87,10 +88,10 @@ export class SchemaPreviewPanel { try { // Detect format const detectResult = await execSchemaForge(['detect', this.currentFile]); - const sourceFormat = detectResult.trim(); + const sourceFormat = normalizeFormat(detectResult) ?? ''; // Get all formats - const allFormats = ['sql', 'prisma', 'drizzle', 'typeorm', 'django', 'sqlalchemy', 'alembic', 'json_schema', 'graphql', 'ef', 'scala']; + const allFormats = SCHEMA_FORMATS; const targetFormats = allFormats.filter(f => f !== sourceFormat); // Convert to all other formats (limit to 5 most relevant to keep it fast) diff --git a/tests/formats.test.js b/tests/formats.test.js new file mode 100644 index 0000000..5db3e14 --- /dev/null +++ b/tests/formats.test.js @@ -0,0 +1,49 @@ +const test = require("node:test"); +const assert = require("node:assert"); +const path = require("node:path"); +const fs = require("node:fs"); + +// Load the compiled module (the `pretest` script compiles src -> out). +const formats = require(path.join(__dirname, "..", "out", "formats.js")); + +test("normalizeFormat lowercases and trims detected labels", () => { + assert.strictEqual(formats.normalizeFormat("SQL"), "sql"); + assert.strictEqual(formats.normalizeFormat(" Prisma "), "prisma"); + assert.strictEqual(formats.normalizeFormat("JSON_SCHEMA"), "json_schema"); +}); + +test("normalizeFormat maps to the canonical token for known formats", () => { + for (const f of formats.SCHEMA_FORMATS) { + assert.strictEqual(formats.normalizeFormat(f.toUpperCase()), f); + } +}); + +test("normalizeFormat passes unknown formats through (so the CLI errors clearly)", () => { + assert.strictEqual(formats.normalizeFormat("mongodb"), "mongodb"); +}); + +test("normalizeFormat handles empty / undefined / null input", () => { + assert.strictEqual(formats.normalizeFormat(""), undefined); + assert.strictEqual(formats.normalizeFormat(" "), undefined); + assert.strictEqual(formats.normalizeFormat(undefined), undefined); + assert.strictEqual(formats.normalizeFormat(null), undefined); +}); + +test("SCHEMA_FORMATS matches the package.json defaultTargetFormat enum", () => { + const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf-8")); + const enumVals = pkg.contributes.configuration.properties["schemaforge.defaultTargetFormat"].enum; + assert.deepStrictEqual(formats.SCHEMA_FORMATS.slice().sort(), enumVals.slice().sort()); +}); + +test("source call sites import SCHEMA_FORMATS instead of inlining the list", () => { + const root = path.join(__dirname, ".."); + const files = ["src/commands/convert.ts", "src/panels/previewPanel.ts", "src/cli.ts"]; + for (const rel of files) { + const src = fs.readFileSync(path.join(root, rel), "utf-8"); + assert.doesNotMatch( + src, + /\['sql', 'prisma', 'drizzle', 'typeorm', 'django', 'sqlalchemy', 'alembic', 'json_schema', 'graphql', 'ef', 'scala'\]/, + `${rel} should import SCHEMA_FORMATS instead of inlining the list` + ); + } +});