@fatagnus/convex-sync-check 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/convex-sync-check.mjs +5 -0
- package/dist/chunk-HARLMPMI.js +222 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +286 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +6 -0
- package/package.json +46 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// src/scanner/backend.ts
|
|
2
|
+
import { readdirSync, readFileSync, statSync } from "fs";
|
|
3
|
+
import { join, relative, sep } from "path";
|
|
4
|
+
var BUILTIN_PUBLIC = /* @__PURE__ */ new Set(["query", "mutation", "action"]);
|
|
5
|
+
var BUILTIN_INTERNAL = /* @__PURE__ */ new Set([
|
|
6
|
+
"internalQuery",
|
|
7
|
+
"internalMutation",
|
|
8
|
+
"internalAction"
|
|
9
|
+
]);
|
|
10
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["_generated", "node_modules"]);
|
|
11
|
+
var SKIP_FILE_PATTERNS = [/\.test\.ts$/, /\.spec\.ts$/];
|
|
12
|
+
function scanBackend(convexDir, functionWrappers) {
|
|
13
|
+
const files = collectTsFiles(convexDir);
|
|
14
|
+
const defs = [];
|
|
15
|
+
const allPublic = new Set(BUILTIN_PUBLIC);
|
|
16
|
+
const allInternal = new Set(BUILTIN_INTERNAL);
|
|
17
|
+
if (functionWrappers) {
|
|
18
|
+
for (const [name, visibility] of Object.entries(functionWrappers)) {
|
|
19
|
+
if (visibility === "public") allPublic.add(name);
|
|
20
|
+
else allInternal.add(name);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const typePattern = [...allPublic, ...allInternal].map((t) => escapeRegex(t)).join("|");
|
|
24
|
+
const regex = new RegExp(
|
|
25
|
+
`^export\\s+const\\s+(\\w+)\\s*=\\s*(${typePattern})\\s*\\(`,
|
|
26
|
+
"gm"
|
|
27
|
+
);
|
|
28
|
+
for (const file of files) {
|
|
29
|
+
const content = readFileSync(file, "utf-8");
|
|
30
|
+
const relPath = relative(convexDir, file);
|
|
31
|
+
const modulePath = relPath.replace(/\.ts$/, "").split(sep).join(".");
|
|
32
|
+
let match;
|
|
33
|
+
regex.lastIndex = 0;
|
|
34
|
+
while ((match = regex.exec(content)) !== null) {
|
|
35
|
+
const name = match[1];
|
|
36
|
+
const fnType = match[2];
|
|
37
|
+
if (fnType === "httpAction") continue;
|
|
38
|
+
const line = content.substring(0, match.index).split("\n").length;
|
|
39
|
+
defs.push({
|
|
40
|
+
name,
|
|
41
|
+
apiPath: `${modulePath}.${name}`,
|
|
42
|
+
type: fnType,
|
|
43
|
+
isInternal: allInternal.has(fnType),
|
|
44
|
+
file,
|
|
45
|
+
line
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return defs;
|
|
50
|
+
}
|
|
51
|
+
function collectTsFiles(dir) {
|
|
52
|
+
const results = [];
|
|
53
|
+
function walk(current) {
|
|
54
|
+
const entries = readdirSync(current);
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const fullPath = join(current, entry);
|
|
57
|
+
const stat = statSync(fullPath);
|
|
58
|
+
if (stat.isDirectory()) {
|
|
59
|
+
if (!SKIP_DIRS.has(entry)) walk(fullPath);
|
|
60
|
+
} else if (entry.endsWith(".ts") && !SKIP_FILE_PATTERNS.some((p) => p.test(entry))) {
|
|
61
|
+
results.push(fullPath);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
walk(dir);
|
|
66
|
+
return results;
|
|
67
|
+
}
|
|
68
|
+
function escapeRegex(s) {
|
|
69
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/scanner/frontend.ts
|
|
73
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
74
|
+
import { join as join2 } from "path";
|
|
75
|
+
var EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
76
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", "__tests__", "_generated"]);
|
|
77
|
+
var SKIP_FILE_PATTERNS2 = [/\.test\.\w+$/, /\.spec\.\w+$/];
|
|
78
|
+
var API_PATTERN = /\bapi\.([a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)+)/g;
|
|
79
|
+
function scanFrontend(frontendDirs) {
|
|
80
|
+
const refs = [];
|
|
81
|
+
for (const dir of frontendDirs) {
|
|
82
|
+
const files = collectSourceFiles(dir);
|
|
83
|
+
for (const file of files) {
|
|
84
|
+
const content = readFileSync2(file, "utf-8");
|
|
85
|
+
const lines = content.split("\n");
|
|
86
|
+
for (let i = 0; i < lines.length; i++) {
|
|
87
|
+
const line = lines[i];
|
|
88
|
+
const trimmed = line.trim();
|
|
89
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const stripped = line.replace(/\/\/.*$/, "").replace(/\/\*.*?\*\//g, "");
|
|
93
|
+
let match;
|
|
94
|
+
API_PATTERN.lastIndex = 0;
|
|
95
|
+
while ((match = API_PATTERN.exec(stripped)) !== null) {
|
|
96
|
+
refs.push({
|
|
97
|
+
apiPath: match[1],
|
|
98
|
+
file,
|
|
99
|
+
line: i + 1
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return refs;
|
|
106
|
+
}
|
|
107
|
+
function collectSourceFiles(dir) {
|
|
108
|
+
const results = [];
|
|
109
|
+
function walk(current) {
|
|
110
|
+
let entries;
|
|
111
|
+
try {
|
|
112
|
+
entries = readdirSync2(current);
|
|
113
|
+
} catch {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
const fullPath = join2(current, entry);
|
|
118
|
+
let stat;
|
|
119
|
+
try {
|
|
120
|
+
stat = statSync2(fullPath);
|
|
121
|
+
} catch {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (stat.isDirectory()) {
|
|
125
|
+
if (!SKIP_DIRS2.has(entry)) walk(fullPath);
|
|
126
|
+
} else {
|
|
127
|
+
const ext = entry.slice(entry.lastIndexOf("."));
|
|
128
|
+
if (EXTENSIONS.has(ext) && !SKIP_FILE_PATTERNS2.some((p) => p.test(entry))) {
|
|
129
|
+
results.push(fullPath);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
walk(dir);
|
|
135
|
+
return results;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/analyzer.ts
|
|
139
|
+
function analyze(defs, refs) {
|
|
140
|
+
const defMap = /* @__PURE__ */ new Map();
|
|
141
|
+
for (const def of defs) {
|
|
142
|
+
defMap.set(def.apiPath, def);
|
|
143
|
+
}
|
|
144
|
+
const refGroups = /* @__PURE__ */ new Map();
|
|
145
|
+
for (const ref of refs) {
|
|
146
|
+
const group = refGroups.get(ref.apiPath) ?? [];
|
|
147
|
+
group.push({ file: ref.file, line: ref.line });
|
|
148
|
+
refGroups.set(ref.apiPath, group);
|
|
149
|
+
}
|
|
150
|
+
const errors = [];
|
|
151
|
+
for (const [apiPath, locations] of refGroups) {
|
|
152
|
+
const def = defMap.get(apiPath);
|
|
153
|
+
if (!def) {
|
|
154
|
+
errors.push({
|
|
155
|
+
type: "MISSING_DEFINITION",
|
|
156
|
+
apiPath,
|
|
157
|
+
locations
|
|
158
|
+
});
|
|
159
|
+
} else if (def.isInternal) {
|
|
160
|
+
errors.push({
|
|
161
|
+
type: "INTERNAL_CALLED_FROM_FRONTEND",
|
|
162
|
+
apiPath,
|
|
163
|
+
locations
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const warnings = [];
|
|
168
|
+
for (const def of defs) {
|
|
169
|
+
if (!def.isInternal && !refGroups.has(def.apiPath)) {
|
|
170
|
+
warnings.push({
|
|
171
|
+
type: "UNREFERENCED",
|
|
172
|
+
apiPath: def.apiPath,
|
|
173
|
+
definition: {
|
|
174
|
+
type: def.type,
|
|
175
|
+
file: def.file,
|
|
176
|
+
line: def.line
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
errors,
|
|
183
|
+
warnings,
|
|
184
|
+
passed: errors.length === 0
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/index.ts
|
|
189
|
+
function checkConvexSync(options) {
|
|
190
|
+
const defs = scanBackend(options.convexDir, options.functionWrappers);
|
|
191
|
+
const refs = scanFrontend(options.frontendDirs);
|
|
192
|
+
const { errors, warnings, passed } = analyze(defs, refs);
|
|
193
|
+
const publicFunctions = defs.filter((d) => !d.isInternal).length;
|
|
194
|
+
const internalFunctions = defs.filter((d) => d.isInternal).length;
|
|
195
|
+
const uniqueRefs = new Set(refs.map((r) => r.apiPath));
|
|
196
|
+
const uniqueModules = new Set(
|
|
197
|
+
defs.map((d) => {
|
|
198
|
+
const parts = d.apiPath.split(".");
|
|
199
|
+
return parts.slice(0, -1).join(".");
|
|
200
|
+
})
|
|
201
|
+
);
|
|
202
|
+
const filteredWarnings = options.suppressWarnings ? warnings.filter((w) => !options.suppressWarnings.includes(w.type)) : warnings;
|
|
203
|
+
return {
|
|
204
|
+
project: "",
|
|
205
|
+
convexDir: options.convexDir,
|
|
206
|
+
frontendDirs: options.frontendDirs,
|
|
207
|
+
stats: {
|
|
208
|
+
definedFunctions: defs.length,
|
|
209
|
+
publicFunctions,
|
|
210
|
+
internalFunctions,
|
|
211
|
+
frontendRefs: uniqueRefs.size,
|
|
212
|
+
backendModules: uniqueModules.size
|
|
213
|
+
},
|
|
214
|
+
errors,
|
|
215
|
+
warnings: filteredWarnings,
|
|
216
|
+
passed
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export {
|
|
221
|
+
checkConvexSync
|
|
222
|
+
};
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
interface CliArgs {
|
|
2
|
+
convexDir?: string;
|
|
3
|
+
frontendDir?: string;
|
|
4
|
+
deployed: boolean;
|
|
5
|
+
json: boolean;
|
|
6
|
+
noWarnings: boolean;
|
|
7
|
+
ignore?: string;
|
|
8
|
+
config?: string;
|
|
9
|
+
verbose: boolean;
|
|
10
|
+
version: boolean;
|
|
11
|
+
help: boolean;
|
|
12
|
+
}
|
|
13
|
+
declare function parseCliArgs(argv: string[]): CliArgs;
|
|
14
|
+
declare function main(): Promise<void>;
|
|
15
|
+
|
|
16
|
+
export { main, parseCliArgs };
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import {
|
|
2
|
+
checkConvexSync
|
|
3
|
+
} from "./chunk-HARLMPMI.js";
|
|
4
|
+
|
|
5
|
+
// src/cli.ts
|
|
6
|
+
import { parseArgs } from "util";
|
|
7
|
+
import { resolve } from "path";
|
|
8
|
+
import { existsSync as existsSync3 } from "fs";
|
|
9
|
+
|
|
10
|
+
// src/config.ts
|
|
11
|
+
import { existsSync, readFileSync } from "fs";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
var DEFAULT_CONFIG_NAME = ".convex-sync-check.json";
|
|
14
|
+
function loadConfig(cwd, configPath) {
|
|
15
|
+
const path = configPath ?? join(cwd, DEFAULT_CONFIG_NAME);
|
|
16
|
+
if (!existsSync(path)) {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const content = readFileSync(path, "utf-8");
|
|
21
|
+
return JSON.parse(content);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
throw new Error(`Failed to parse config file ${path}: ${err.message}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/detector.ts
|
|
28
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
|
|
29
|
+
import { join as join2, basename } from "path";
|
|
30
|
+
var FRONTEND_DIRS = ["src", "app", "pages"];
|
|
31
|
+
function detectLayout(cwd) {
|
|
32
|
+
const project = getProjectName(cwd);
|
|
33
|
+
const convexDir = findConvexDir(cwd);
|
|
34
|
+
const frontendDirs = findFrontendDirs(cwd);
|
|
35
|
+
return { project, convexDir, frontendDirs };
|
|
36
|
+
}
|
|
37
|
+
function getProjectName(cwd) {
|
|
38
|
+
const pkgPath = join2(cwd, "package.json");
|
|
39
|
+
if (existsSync2(pkgPath)) {
|
|
40
|
+
try {
|
|
41
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
42
|
+
if (pkg.name) return pkg.name;
|
|
43
|
+
} catch {
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return basename(cwd);
|
|
47
|
+
}
|
|
48
|
+
function findConvexDir(cwd) {
|
|
49
|
+
const candidate = join2(cwd, "convex");
|
|
50
|
+
if (existsSync2(candidate) && statSync(candidate).isDirectory() && existsSync2(join2(candidate, "_generated"))) {
|
|
51
|
+
return candidate;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
function findFrontendDirs(cwd) {
|
|
56
|
+
const dirs = [];
|
|
57
|
+
for (const name of FRONTEND_DIRS) {
|
|
58
|
+
const candidate = join2(cwd, name);
|
|
59
|
+
if (existsSync2(candidate) && statSync(candidate).isDirectory()) {
|
|
60
|
+
if (containsSourceFiles(candidate)) {
|
|
61
|
+
dirs.push(candidate);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return dirs;
|
|
66
|
+
}
|
|
67
|
+
function containsSourceFiles(dir) {
|
|
68
|
+
try {
|
|
69
|
+
const entries = readdirSync(dir);
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
if (/\.(tsx?|jsx?)$/.test(entry)) return true;
|
|
72
|
+
const full = join2(dir, entry);
|
|
73
|
+
if (statSync(full).isDirectory() && entry !== "node_modules") {
|
|
74
|
+
if (containsSourceFiles(full)) return true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/reporter/human.ts
|
|
83
|
+
function formatHuman(result) {
|
|
84
|
+
const lines = [];
|
|
85
|
+
lines.push("");
|
|
86
|
+
lines.push("\u2550\u2550\u2550 Convex Function Sync Check \u2550\u2550\u2550");
|
|
87
|
+
lines.push("");
|
|
88
|
+
lines.push(` Project: ${result.project}`);
|
|
89
|
+
lines.push(` Convex dir: ${result.convexDir}`);
|
|
90
|
+
lines.push(` Frontend dir: ${result.frontendDirs.join(", ")}`);
|
|
91
|
+
lines.push(
|
|
92
|
+
` Defined functions: ${result.stats.definedFunctions} (${result.stats.publicFunctions} public, ${result.stats.internalFunctions} internal)`
|
|
93
|
+
);
|
|
94
|
+
lines.push(` Frontend refs: ${result.stats.frontendRefs} unique api paths`);
|
|
95
|
+
lines.push(` Backend modules: ${result.stats.backendModules}`);
|
|
96
|
+
lines.push("");
|
|
97
|
+
const missingDefs = result.errors.filter(
|
|
98
|
+
(e) => e.type === "MISSING_DEFINITION"
|
|
99
|
+
);
|
|
100
|
+
const internalLeaks = result.errors.filter(
|
|
101
|
+
(e) => e.type === "INTERNAL_CALLED_FROM_FRONTEND"
|
|
102
|
+
);
|
|
103
|
+
const notDeployed = result.errors.filter(
|
|
104
|
+
(e) => e.type === "NOT_DEPLOYED"
|
|
105
|
+
);
|
|
106
|
+
if (missingDefs.length > 0) {
|
|
107
|
+
lines.push(
|
|
108
|
+
`\u2717 ${missingDefs.length} function(s) referenced but NOT defined:`
|
|
109
|
+
);
|
|
110
|
+
lines.push("");
|
|
111
|
+
for (const err of missingDefs) {
|
|
112
|
+
lines.push(` \u2717 api.${err.apiPath}`);
|
|
113
|
+
for (const loc of err.locations) {
|
|
114
|
+
lines.push(` \u2192 ${loc.file}:${loc.line}`);
|
|
115
|
+
}
|
|
116
|
+
lines.push("");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (internalLeaks.length > 0) {
|
|
120
|
+
lines.push(
|
|
121
|
+
`\u2717 ${internalLeaks.length} internal function(s) called from frontend:`
|
|
122
|
+
);
|
|
123
|
+
lines.push("");
|
|
124
|
+
for (const err of internalLeaks) {
|
|
125
|
+
lines.push(` \u2717 api.${err.apiPath} (internal \u2014 should not be called from frontend)`);
|
|
126
|
+
for (const loc of err.locations) {
|
|
127
|
+
lines.push(` \u2192 ${loc.file}:${loc.line}`);
|
|
128
|
+
}
|
|
129
|
+
lines.push("");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (notDeployed.length > 0) {
|
|
133
|
+
lines.push(
|
|
134
|
+
`\u2717 ${notDeployed.length} function(s) defined but NOT deployed:`
|
|
135
|
+
);
|
|
136
|
+
lines.push("");
|
|
137
|
+
for (const err of notDeployed) {
|
|
138
|
+
lines.push(` \u2717 api.${err.apiPath}`);
|
|
139
|
+
for (const loc of err.locations) {
|
|
140
|
+
lines.push(` \u2192 ${loc.file}:${loc.line}`);
|
|
141
|
+
}
|
|
142
|
+
lines.push("");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (result.warnings.length > 0) {
|
|
146
|
+
lines.push(
|
|
147
|
+
`\u26A0 ${result.warnings.length} public function(s) defined but not referenced from frontend:`
|
|
148
|
+
);
|
|
149
|
+
lines.push("");
|
|
150
|
+
for (const warn of result.warnings) {
|
|
151
|
+
lines.push(` \u25CB api.${warn.apiPath} (${warn.definition.type})`);
|
|
152
|
+
lines.push(` ${warn.definition.file}:${warn.definition.line}`);
|
|
153
|
+
}
|
|
154
|
+
lines.push("");
|
|
155
|
+
}
|
|
156
|
+
if (result.passed) {
|
|
157
|
+
lines.push("PASS \u2014 All Convex function references are in sync.");
|
|
158
|
+
} else {
|
|
159
|
+
lines.push("FAIL \u2014 Convex function sync issues found.");
|
|
160
|
+
}
|
|
161
|
+
lines.push("");
|
|
162
|
+
return lines.join("\n");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/reporter/json.ts
|
|
166
|
+
function formatJson(result) {
|
|
167
|
+
return JSON.stringify(result, null, 2);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/cli.ts
|
|
171
|
+
function parseCliArgs(argv) {
|
|
172
|
+
const { values } = parseArgs({
|
|
173
|
+
args: argv,
|
|
174
|
+
options: {
|
|
175
|
+
"convex-dir": { type: "string" },
|
|
176
|
+
"frontend-dir": { type: "string" },
|
|
177
|
+
deployed: { type: "boolean", default: false },
|
|
178
|
+
json: { type: "boolean", default: false },
|
|
179
|
+
"no-warnings": { type: "boolean", default: false },
|
|
180
|
+
ignore: { type: "string" },
|
|
181
|
+
config: { type: "string" },
|
|
182
|
+
verbose: { type: "boolean", short: "v", default: false },
|
|
183
|
+
version: { type: "boolean", default: false },
|
|
184
|
+
help: { type: "boolean", default: false }
|
|
185
|
+
},
|
|
186
|
+
strict: true
|
|
187
|
+
});
|
|
188
|
+
return {
|
|
189
|
+
convexDir: values["convex-dir"],
|
|
190
|
+
frontendDir: values["frontend-dir"],
|
|
191
|
+
deployed: values.deployed,
|
|
192
|
+
json: values.json,
|
|
193
|
+
noWarnings: values["no-warnings"],
|
|
194
|
+
ignore: values.ignore,
|
|
195
|
+
config: values.config,
|
|
196
|
+
verbose: values.verbose,
|
|
197
|
+
version: values.version,
|
|
198
|
+
help: values.help
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
var HELP_TEXT = `
|
|
202
|
+
Usage: convex-sync-check [options]
|
|
203
|
+
|
|
204
|
+
Options:
|
|
205
|
+
--convex-dir <path> Path to convex/ directory (auto-detected if omitted)
|
|
206
|
+
--frontend-dir <path> Path to frontend source directory (auto-detected if omitted)
|
|
207
|
+
--deployed Also check deployed functions via \`npx convex functions\`
|
|
208
|
+
--json Output as JSON (for programmatic consumption)
|
|
209
|
+
--no-warnings Suppress unreferenced function warnings
|
|
210
|
+
--ignore <patterns> Comma-separated glob patterns to ignore
|
|
211
|
+
--config <path> Path to config file (default: .convex-sync-check.json)
|
|
212
|
+
-v, --verbose Show detailed scan progress
|
|
213
|
+
--version Show version
|
|
214
|
+
--help Show help
|
|
215
|
+
`.trim();
|
|
216
|
+
async function main() {
|
|
217
|
+
const args = parseCliArgs(process.argv.slice(2));
|
|
218
|
+
if (args.help) {
|
|
219
|
+
console.log(HELP_TEXT);
|
|
220
|
+
process.exit(0);
|
|
221
|
+
}
|
|
222
|
+
if (args.version) {
|
|
223
|
+
const { readFileSync: readFileSync3 } = await import("fs");
|
|
224
|
+
const { fileURLToPath } = await import("url");
|
|
225
|
+
const { join: join3, dirname } = await import("path");
|
|
226
|
+
const pkgPath = join3(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
227
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
228
|
+
console.log(pkg.version);
|
|
229
|
+
process.exit(0);
|
|
230
|
+
}
|
|
231
|
+
const cwd = process.cwd();
|
|
232
|
+
const config = loadConfig(cwd, args.config);
|
|
233
|
+
const layout = detectLayout(cwd);
|
|
234
|
+
const convexDir = resolve(
|
|
235
|
+
cwd,
|
|
236
|
+
args.convexDir ?? config.convexDir ?? (layout.convexDir ?? "")
|
|
237
|
+
);
|
|
238
|
+
if (!convexDir || !existsSync3(convexDir)) {
|
|
239
|
+
console.error(
|
|
240
|
+
"Error: Could not find convex/ directory. Use --convex-dir to specify."
|
|
241
|
+
);
|
|
242
|
+
process.exit(2);
|
|
243
|
+
}
|
|
244
|
+
const frontendDirs = args.frontendDir ? [resolve(cwd, args.frontendDir)] : (config.frontendDirs ?? []).length > 0 ? config.frontendDirs.map((d) => resolve(cwd, d)) : layout.frontendDirs;
|
|
245
|
+
if (frontendDirs.length === 0) {
|
|
246
|
+
console.error(
|
|
247
|
+
"Error: Could not find frontend source directory. Use --frontend-dir to specify."
|
|
248
|
+
);
|
|
249
|
+
process.exit(2);
|
|
250
|
+
}
|
|
251
|
+
const functionWrappers = {
|
|
252
|
+
...config.functionWrappers
|
|
253
|
+
};
|
|
254
|
+
const suppressWarnings = args.noWarnings ? ["UNREFERENCED"] : config.suppressWarnings;
|
|
255
|
+
if (args.verbose) {
|
|
256
|
+
console.log(`Scanning backend: ${convexDir}`);
|
|
257
|
+
console.log(`Scanning frontend: ${frontendDirs.join(", ")}`);
|
|
258
|
+
}
|
|
259
|
+
const result = checkConvexSync({
|
|
260
|
+
convexDir,
|
|
261
|
+
frontendDirs,
|
|
262
|
+
functionWrappers: Object.keys(functionWrappers).length > 0 ? functionWrappers : void 0,
|
|
263
|
+
suppressWarnings,
|
|
264
|
+
verbose: args.verbose
|
|
265
|
+
});
|
|
266
|
+
result.project = layout.project;
|
|
267
|
+
result.convexDir = convexDir;
|
|
268
|
+
result.frontendDirs = frontendDirs;
|
|
269
|
+
if (args.json) {
|
|
270
|
+
console.log(formatJson(result));
|
|
271
|
+
} else {
|
|
272
|
+
console.log(formatHuman(result));
|
|
273
|
+
}
|
|
274
|
+
process.exit(result.passed ? 0 : 1);
|
|
275
|
+
}
|
|
276
|
+
var isDirectRun = typeof process !== "undefined" && process.argv[1] && (process.argv[1].endsWith("/cli.js") || process.argv[1].endsWith("/convex-sync-check.mjs"));
|
|
277
|
+
if (isDirectRun) {
|
|
278
|
+
main().catch((err) => {
|
|
279
|
+
console.error("Unexpected error:", err.message);
|
|
280
|
+
process.exit(2);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
export {
|
|
284
|
+
main,
|
|
285
|
+
parseCliArgs
|
|
286
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
type ErrorType = "MISSING_DEFINITION" | "INTERNAL_CALLED_FROM_FRONTEND" | "NOT_DEPLOYED";
|
|
2
|
+
type WarningType = "UNREFERENCED";
|
|
3
|
+
interface SyncError {
|
|
4
|
+
type: ErrorType;
|
|
5
|
+
apiPath: string;
|
|
6
|
+
locations: Array<{
|
|
7
|
+
file: string;
|
|
8
|
+
line: number;
|
|
9
|
+
}>;
|
|
10
|
+
}
|
|
11
|
+
interface SyncWarning {
|
|
12
|
+
type: WarningType;
|
|
13
|
+
apiPath: string;
|
|
14
|
+
definition: {
|
|
15
|
+
type: string;
|
|
16
|
+
file: string;
|
|
17
|
+
line: number;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
interface ScanStats {
|
|
21
|
+
definedFunctions: number;
|
|
22
|
+
publicFunctions: number;
|
|
23
|
+
internalFunctions: number;
|
|
24
|
+
frontendRefs: number;
|
|
25
|
+
backendModules: number;
|
|
26
|
+
}
|
|
27
|
+
interface CheckResult {
|
|
28
|
+
project: string;
|
|
29
|
+
convexDir: string;
|
|
30
|
+
frontendDirs: string[];
|
|
31
|
+
stats: ScanStats;
|
|
32
|
+
errors: SyncError[];
|
|
33
|
+
warnings: SyncWarning[];
|
|
34
|
+
passed: boolean;
|
|
35
|
+
}
|
|
36
|
+
interface CheckOptions {
|
|
37
|
+
convexDir?: string;
|
|
38
|
+
frontendDirs?: string[];
|
|
39
|
+
functionWrappers?: Record<string, "public" | "internal">;
|
|
40
|
+
ignore?: {
|
|
41
|
+
backend?: string[];
|
|
42
|
+
frontend?: string[];
|
|
43
|
+
};
|
|
44
|
+
suppressWarnings?: WarningType[];
|
|
45
|
+
deployed?: boolean;
|
|
46
|
+
verbose?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
declare function checkConvexSync(options: CheckOptions & {
|
|
50
|
+
convexDir: string;
|
|
51
|
+
frontendDirs: string[];
|
|
52
|
+
}): CheckResult;
|
|
53
|
+
|
|
54
|
+
export { type CheckOptions, type CheckResult, checkConvexSync };
|
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fatagnus/convex-sync-check",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Zero-config CLI to check Convex frontend/backend function sync",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"convex-sync-check": "./bin/convex-sync-check.mjs"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"bin"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup",
|
|
23
|
+
"dev": "tsup --watch",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:watch": "vitest",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"prepublishOnly": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"convex",
|
|
31
|
+
"sync",
|
|
32
|
+
"check",
|
|
33
|
+
"lint",
|
|
34
|
+
"static-analysis"
|
|
35
|
+
],
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^25.3.2",
|
|
39
|
+
"tsup": "^8.0.0",
|
|
40
|
+
"typescript": "^5.4.0",
|
|
41
|
+
"vitest": "^3.0.0"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18"
|
|
45
|
+
}
|
|
46
|
+
}
|