Skip to content

Commit b1bfa86

Browse files
Copilothotlong
andcommitted
Add spec compliance audit script and fix audit-log dependency
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 6be3338 commit b1bfa86

3 files changed

Lines changed: 757 additions & 1 deletion

File tree

packages/plugins/audit-log/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"test": "jest --passWithNoTests"
1111
},
1212
"dependencies": {
13-
"@objectstack/runtime": "^1.0.0"
13+
"@objectstack/runtime": "^1.0.0",
14+
"@objectstack/spec": "1.0.0"
1415
},
1516
"devDependencies": {
1617
"@types/jest": "^30.0.0",

scripts/audit-spec-compliance.mjs

Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Audit Script for @objectstack/spec Compliance
4+
*
5+
* This script scans all packages in the monorepo to ensure they comply
6+
* with @objectstack/spec protocol requirements.
7+
*/
8+
9+
import fs from 'fs';
10+
import path from 'path';
11+
import { fileURLToPath } from 'url';
12+
13+
const __filename = fileURLToPath(import.meta.url);
14+
const __dirname = path.dirname(__filename);
15+
16+
class SpecComplianceAuditor {
17+
constructor(rootDir) {
18+
this.packagesDir = path.join(rootDir, 'packages');
19+
this.results = [];
20+
this.issues = [];
21+
}
22+
23+
/**
24+
* Find all package.json files in the packages directory
25+
*/
26+
findPackages() {
27+
const packages = [];
28+
29+
const scanDir = (dir) => {
30+
const entries = fs.readdirSync(dir, { withFileTypes: true });
31+
32+
for (const entry of entries) {
33+
const fullPath = path.join(dir, entry.name);
34+
35+
if (entry.isDirectory() && entry.name !== 'node_modules' && entry.name !== 'dist') {
36+
scanDir(fullPath);
37+
} else if (entry.name === 'package.json') {
38+
packages.push(fullPath);
39+
}
40+
}
41+
};
42+
43+
scanDir(this.packagesDir);
44+
return packages;
45+
}
46+
47+
/**
48+
* Read and parse package.json
49+
*/
50+
readPackageJson(packagePath) {
51+
try {
52+
const content = fs.readFileSync(packagePath, 'utf-8');
53+
return JSON.parse(content);
54+
} catch (error) {
55+
console.error(`Error reading ${packagePath}:`, error);
56+
return null;
57+
}
58+
}
59+
60+
/**
61+
* Check if package has @objectstack/spec dependency
62+
*/
63+
hasSpecDependency(pkg) {
64+
const allDeps = {
65+
...pkg.dependencies,
66+
...pkg.devDependencies,
67+
...pkg.peerDependencies,
68+
};
69+
return '@objectstack/spec' in allDeps;
70+
}
71+
72+
/**
73+
* Check if package has @objectstack/runtime dependency
74+
*/
75+
hasRuntimeDependency(pkg) {
76+
const allDeps = {
77+
...pkg.dependencies,
78+
...pkg.devDependencies,
79+
...pkg.peerDependencies,
80+
};
81+
return '@objectstack/runtime' in allDeps;
82+
}
83+
84+
/**
85+
* Find all TypeScript files in package directory
86+
*/
87+
findTypeScriptFiles(packageDir) {
88+
const tsFiles = [];
89+
90+
const scanDir = (dir) => {
91+
if (!fs.existsSync(dir)) return;
92+
93+
const entries = fs.readdirSync(dir, { withFileTypes: true });
94+
95+
for (const entry of entries) {
96+
const fullPath = path.join(dir, entry.name);
97+
98+
if (entry.isDirectory() && entry.name !== 'node_modules' && entry.name !== 'dist') {
99+
scanDir(fullPath);
100+
} else if (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx')) {
101+
tsFiles.push(fullPath);
102+
}
103+
}
104+
};
105+
106+
const srcDir = path.join(packageDir, 'src');
107+
scanDir(srcDir);
108+
return tsFiles;
109+
}
110+
111+
/**
112+
* Extract @objectstack/spec imports from TypeScript file
113+
*/
114+
extractSpecImports(filePath) {
115+
const imports = [];
116+
117+
try {
118+
const content = fs.readFileSync(filePath, 'utf-8');
119+
const importRegex = /import\s+.*?from\s+['"]@objectstack\/spec[^'"]*['"]/g;
120+
const matches = content.match(importRegex);
121+
122+
if (matches) {
123+
imports.push(...matches);
124+
}
125+
} catch (error) {
126+
// Ignore read errors
127+
}
128+
129+
return imports;
130+
}
131+
132+
/**
133+
* Check if package has Plugin implementation
134+
*/
135+
hasPluginImplementation(packageDir) {
136+
const tsFiles = this.findTypeScriptFiles(packageDir);
137+
138+
for (const file of tsFiles) {
139+
try {
140+
const content = fs.readFileSync(file, 'utf-8');
141+
142+
// Check for Plugin interface implementation
143+
if (content.includes('implements Plugin')) {
144+
return true;
145+
}
146+
147+
// Check for Plugin class or type import
148+
if (content.includes('Plugin') &&
149+
(content.includes('from \'@objectstack/runtime\'') ||
150+
content.includes('from "@objectstack/runtime"'))) {
151+
return true;
152+
}
153+
} catch (error) {
154+
// Ignore read errors
155+
}
156+
}
157+
158+
return false;
159+
}
160+
161+
/**
162+
* Audit a single package
163+
*/
164+
auditPackage(packagePath) {
165+
const packageDir = path.dirname(packagePath);
166+
const pkg = this.readPackageJson(packagePath);
167+
168+
if (!pkg) {
169+
return {
170+
package: 'unknown',
171+
packagePath,
172+
issues: [{
173+
package: packagePath,
174+
severity: 'error',
175+
category: 'parsing',
176+
message: 'Failed to parse package.json'
177+
}],
178+
hasSpecDependency: false,
179+
hasRuntimeDependency: false,
180+
hasPluginImplementation: false,
181+
specImports: []
182+
};
183+
}
184+
185+
const result = {
186+
package: pkg.name,
187+
packagePath,
188+
issues: [],
189+
hasSpecDependency: this.hasSpecDependency(pkg),
190+
hasRuntimeDependency: this.hasRuntimeDependency(pkg),
191+
hasPluginImplementation: this.hasPluginImplementation(packageDir),
192+
specImports: []
193+
};
194+
195+
// Collect all spec imports
196+
const tsFiles = this.findTypeScriptFiles(packageDir);
197+
for (const file of tsFiles) {
198+
const imports = this.extractSpecImports(file);
199+
result.specImports.push(...imports);
200+
}
201+
202+
// Check compliance rules
203+
this.checkCompliance(result, pkg);
204+
205+
return result;
206+
}
207+
208+
/**
209+
* Check compliance rules for a package
210+
*/
211+
checkCompliance(result, pkg) {
212+
const isPlugin = result.package.includes('plugin-') || result.hasPluginImplementation;
213+
const isAdapter = result.package.includes('adapter-');
214+
215+
// Rule 1: Plugins should have @objectstack/runtime dependency
216+
if (isPlugin && !result.hasRuntimeDependency) {
217+
result.issues.push({
218+
package: result.package,
219+
severity: 'error',
220+
category: 'dependencies',
221+
message: 'Plugin package must declare @objectstack/runtime dependency'
222+
});
223+
}
224+
225+
// Rule 2: Packages using spec imports should declare spec dependency
226+
if (result.specImports.length > 0 && !result.hasSpecDependency) {
227+
result.issues.push({
228+
package: result.package,
229+
severity: 'warning',
230+
category: 'dependencies',
231+
message: `Package imports from @objectstack/spec but doesn't declare it as dependency. Found ${result.specImports.length} import(s)`
232+
});
233+
}
234+
235+
// Rule 3: Plugin packages should have Plugin implementation
236+
if (isPlugin && !result.hasPluginImplementation) {
237+
result.issues.push({
238+
package: result.package,
239+
severity: 'warning',
240+
category: 'implementation',
241+
message: 'Plugin package should implement Plugin interface'
242+
});
243+
}
244+
245+
// Rule 4: Check version consistency
246+
if (result.hasSpecDependency) {
247+
const specVersion = pkg.dependencies?.['@objectstack/spec'] ||
248+
pkg.devDependencies?.['@objectstack/spec'] ||
249+
pkg.peerDependencies?.['@objectstack/spec'];
250+
251+
if (specVersion !== '1.0.0' && specVersion !== '^1.0.0') {
252+
result.issues.push({
253+
package: result.package,
254+
severity: 'info',
255+
category: 'dependencies',
256+
message: `@objectstack/spec version is ${specVersion}, expected 1.0.0 or ^1.0.0`
257+
});
258+
}
259+
}
260+
261+
// Rule 5: Check runtime version consistency
262+
if (result.hasRuntimeDependency) {
263+
const runtimeVersion = pkg.dependencies?.['@objectstack/runtime'] ||
264+
pkg.devDependencies?.['@objectstack/runtime'] ||
265+
pkg.peerDependencies?.['@objectstack/runtime'];
266+
267+
if (runtimeVersion !== '^1.0.0' && runtimeVersion !== '1.0.0') {
268+
result.issues.push({
269+
package: result.package,
270+
severity: 'info',
271+
category: 'dependencies',
272+
message: `@objectstack/runtime version is ${runtimeVersion}, expected ^1.0.0`
273+
});
274+
}
275+
}
276+
}
277+
278+
/**
279+
* Run the audit
280+
*/
281+
async audit() {
282+
console.log('🔍 Scanning packages for @objectstack/spec compliance...\n');
283+
284+
const packages = this.findPackages();
285+
console.log(`Found ${packages.length} packages to audit\n`);
286+
287+
for (const packagePath of packages) {
288+
const result = this.auditPackage(packagePath);
289+
this.results.push(result);
290+
this.issues.push(...result.issues);
291+
}
292+
293+
this.printResults();
294+
}
295+
296+
/**
297+
* Print audit results
298+
*/
299+
printResults() {
300+
console.log('='.repeat(80));
301+
console.log('AUDIT RESULTS');
302+
console.log('='.repeat(80));
303+
console.log();
304+
305+
// Summary
306+
const totalPackages = this.results.length;
307+
const packagesWithIssues = this.results.filter(r => r.issues.length > 0).length;
308+
const totalIssues = this.issues.length;
309+
const errors = this.issues.filter(i => i.severity === 'error').length;
310+
const warnings = this.issues.filter(i => i.severity === 'warning').length;
311+
const infos = this.issues.filter(i => i.severity === 'info').length;
312+
313+
console.log('📊 Summary:');
314+
console.log(` Total packages: ${totalPackages}`);
315+
console.log(` Packages with issues: ${packagesWithIssues}`);
316+
console.log(` Total issues: ${totalIssues}`);
317+
console.log(` - Errors: ${errors}`);
318+
console.log(` - Warnings: ${warnings}`);
319+
console.log(` - Info: ${infos}`);
320+
console.log();
321+
322+
// Package details
323+
console.log('📦 Package Details:');
324+
console.log();
325+
326+
for (const result of this.results) {
327+
const hasIssues = result.issues.length > 0;
328+
const icon = hasIssues ? '❌' : '✅';
329+
330+
console.log(`${icon} ${result.package}`);
331+
console.log(` Path: ${path.relative(process.cwd(), result.packagePath)}`);
332+
console.log(` Has @objectstack/spec: ${result.hasSpecDependency ? '✓' : '✗'}`);
333+
console.log(` Has @objectstack/runtime: ${result.hasRuntimeDependency ? '✓' : '✗'}`);
334+
console.log(` Has Plugin implementation: ${result.hasPluginImplementation ? '✓' : '✗'}`);
335+
console.log(` Spec imports: ${result.specImports.length}`);
336+
337+
if (result.issues.length > 0) {
338+
console.log(' Issues:');
339+
for (const issue of result.issues) {
340+
const severityIcon = issue.severity === 'error' ? '🔴' :
341+
issue.severity === 'warning' ? '🟡' : '🔵';
342+
console.log(` ${severityIcon} [${issue.category}] ${issue.message}`);
343+
}
344+
}
345+
console.log();
346+
}
347+
348+
// Exit code based on errors
349+
if (errors > 0) {
350+
console.log(`\n❌ Audit failed with ${errors} error(s)\n`);
351+
process.exit(1);
352+
} else if (warnings > 0) {
353+
console.log(`\n⚠️ Audit completed with ${warnings} warning(s)\n`);
354+
} else {
355+
console.log('\n✅ All packages are compliant!\n');
356+
}
357+
}
358+
}
359+
360+
// Run the auditor
361+
const rootDir = process.cwd();
362+
const auditor = new SpecComplianceAuditor(rootDir);
363+
auditor.audit().catch(error => {
364+
console.error('Audit failed:', error);
365+
process.exit(1);
366+
});

0 commit comments

Comments
 (0)