@finggujadhav/compiler 0.9.6 β†’ 0.9.7

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.
Files changed (4) hide show
  1. package/cli.js +44 -2
  2. package/engine.js +1 -1
  3. package/package.json +6 -3
  4. package/snapshot.js +384 -0
package/cli.js CHANGED
@@ -1,13 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * FingguFlux CLI
4
- * v0.9.6 Theme Engine Stabilization & Contract Freeze
4
+ * v0.9.7-RC Release Candidate Preparation
5
5
  */
6
6
  import fs from 'fs';
7
7
  import path from 'path';
8
8
  import { scanFiles, getProjectFiles } from './scanner.js';
9
9
  import { CompilerEngine } from './engine.js';
10
10
  import { runThemeCheck, printThemeCheckReport } from './theme-check.js';
11
+ import {
12
+ generateSnapshot, writeSnapshot, runSnapshotCompare, printSnapshotReport
13
+ } from './snapshot.js';
11
14
 
12
15
  const args = process.argv.slice(2);
13
16
  const command = args[0] || 'build';
@@ -38,9 +41,12 @@ async function main() {
38
41
  case 'theme-check':
39
42
  await runThemeCheckCommand();
40
43
  break;
44
+ case 'snapshot':
45
+ await runSnapshotCommand();
46
+ break;
41
47
  default:
42
48
  console.error(`Unknown command: ${command}`);
43
- console.log('Available commands: build, analyze, doctor, a11y, theme-check');
49
+ console.log('Available commands: build, analyze, doctor, a11y, theme-check, snapshot');
44
50
  process.exit(1);
45
51
  }
46
52
  }
@@ -253,6 +259,42 @@ async function runThemeCheckCommand() {
253
259
  if (!result.pass) process.exit(1);
254
260
  }
255
261
 
262
+ async function runSnapshotCommand() {
263
+ const doWrite = args.includes('--write');
264
+ const doCompare = args.includes('--compare');
265
+ const verbose = args.includes('--verbose');
266
+
267
+ if (!doWrite && !doCompare) {
268
+ console.error('Usage: finggu snapshot --write (generate/update baseline)');
269
+ console.error(' finggu snapshot --compare (CI break-guard diff)');
270
+ process.exit(1);
271
+ }
272
+
273
+ if (doWrite) {
274
+ console.log('\nπŸ“Έ Generating API surface snapshot…');
275
+ const snap = writeSnapshot();
276
+ const dim = snap.surface;
277
+ console.log(` βœ… CSS tokens : ${dim.cssTokens.length}`);
278
+ console.log(` βœ… JS exports : ${dim.jsExports.length}`);
279
+ console.log(` βœ… CLI commands : ${dim.cliCommands.length}`);
280
+ console.log(` βœ… Component CSS : ${dim.componentFiles.length}`);
281
+ console.log(`\n✨ Snapshot written β†’ packages/core/API_SURFACE_SNAPSHOT.json (v${snap.version})\n`);
282
+ return;
283
+ }
284
+
285
+ if (doCompare) {
286
+ let report;
287
+ try {
288
+ report = runSnapshotCompare();
289
+ } catch (e) {
290
+ console.error('\n❌', e.message);
291
+ process.exit(1);
292
+ }
293
+ printSnapshotReport(report, verbose);
294
+ if (!report.pass) process.exit(1);
295
+ }
296
+ }
297
+
256
298
  main().catch(err => {
257
299
  console.error('Command failed:', err);
258
300
  process.exit(1);
package/engine.js CHANGED
@@ -158,7 +158,7 @@ export class CompilerEngine {
158
158
  generateReport() {
159
159
  return {
160
160
  timestamp: new Date().toISOString(),
161
- engineVersion: "0.9.6",
161
+ engineVersion: "0.9.7",
162
162
  mode: this.mode,
163
163
  stats: this.getStats(),
164
164
  mapping: this.getMapping()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finggujadhav/compiler",
3
- "version": "0.9.6",
3
+ "version": "0.9.7",
4
4
  "description": "The FingguFlux static analysis and selector hardening engine.",
5
5
  "main": "engine.js",
6
6
  "bin": {
@@ -11,12 +11,15 @@
11
11
  "scanner.js",
12
12
  "cli.js",
13
13
  "a11y.js",
14
- "theme-check.js"
14
+ "theme-check.js",
15
+ "snapshot.js"
15
16
  ],
16
17
  "type": "module",
17
18
  "scripts": {
18
19
  "test": "node --test test/theme-check.test.js",
19
- "test:all": "node --test test/**/*.test.js"
20
+ "test:snapshot": "node --test test/snapshot.test.js",
21
+ "test:all": "node --test test/**/*.test.js",
22
+ "release-check": "node cli.js theme-check && node cli.js snapshot --compare"
20
23
  },
21
24
  "engines": {
22
25
  "node": ">=16.0.0"
package/snapshot.js ADDED
@@ -0,0 +1,384 @@
1
+ /**
2
+ * FingguFlux API Surface Snapshot Engine
3
+ * v0.9.7-RC – Release Candidate Preparation
4
+ *
5
+ * Generates an authoritative, machine-readable snapshot of the ENTIRE
6
+ * public API surface and diffs it against a stored baseline to detect:
7
+ * β€’ BREAKING: removed CSS tokens
8
+ * β€’ BREAKING: removed JS exports
9
+ * β€’ BREAKING: removed CLI commands
10
+ * β€’ BREAKING: removed component CSS files
11
+ * β€’ WARNING: added tokens (unregistered additions)
12
+ * β€’ WARNING: deprecated APIs approaching removal date
13
+ *
14
+ * Zero runtime overhead β€” this module is a build/CI tool only.
15
+ * It produces no browser-bound code.
16
+ */
17
+
18
+ import fs from 'fs';
19
+ import path from 'path';
20
+ import { fileURLToPath } from 'url';
21
+
22
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
+
24
+ // ─── Paths ────────────────────────────────────────────────────────────────────
25
+
26
+ const CORE_DIR = path.resolve(__dirname, '../core');
27
+ const COMPILER_DIR = path.resolve(__dirname, '../compiler');
28
+ const JS_HELPER_DIR = path.resolve(__dirname, '../js-helper');
29
+ const SNAPSHOT_PATH = path.resolve(CORE_DIR, 'API_SURFACE_SNAPSHOT.json');
30
+ const REGISTRY_PATH = path.resolve(CORE_DIR, 'TOKENS_REGISTRY.json');
31
+ const DEPRECATION_PATH = path.resolve(CORE_DIR, 'DEPRECATION_LOG.json');
32
+
33
+ // ─── CSS token extractor ──────────────────────────────────────────────────────
34
+
35
+ /**
36
+ * Extract all --ff-* CSS custom property definitions from a CSS string.
37
+ * Excludes var() references (same logic as theme-check.js).
38
+ * @param {string} css
39
+ * @returns {string[]} sorted token names
40
+ */
41
+ export function extractCSSTokens(css) {
42
+ const tokens = new Set();
43
+ const re = /(--ff-[\w-]+)\s*:/g;
44
+ let m;
45
+ while ((m = re.exec(css)) !== null) {
46
+ const before = css.slice(0, m.index);
47
+ const lastVar = before.lastIndexOf('var(');
48
+ const lastClose = before.lastIndexOf(')');
49
+ if (lastVar !== -1 && lastVar > lastClose) continue;
50
+ tokens.add(m[1]);
51
+ }
52
+ return [...tokens].sort();
53
+ }
54
+
55
+ /**
56
+ * Collect all --ff-* CSS tokens from tokens.css (includes dark overrides).
57
+ * @returns {string[]}
58
+ */
59
+ export function collectCSSTokens() {
60
+ const tokensCss = path.join(CORE_DIR, 'tokens.css');
61
+ if (!fs.existsSync(tokensCss)) return [];
62
+ return extractCSSTokens(fs.readFileSync(tokensCss, 'utf8'));
63
+ }
64
+
65
+ // ─── Component surface collector ─────────────────────────────────────────────
66
+
67
+ /**
68
+ * Collect all component CSS filenames published in @finggujadhav/core.
69
+ * @returns {string[]} sorted list of component file basenames (e.g. "button.css")
70
+ */
71
+ export function collectComponentFiles() {
72
+ const compDir = path.join(CORE_DIR, 'components');
73
+ if (!fs.existsSync(compDir)) return [];
74
+ return fs.readdirSync(compDir)
75
+ .filter(f => f.endsWith('.css'))
76
+ .sort();
77
+ }
78
+
79
+ // ─── JS export collector ─────────────────────────────────────────────────────
80
+
81
+ /**
82
+ * Extract named exports from a TypeScript source file.
83
+ * Handles: export function/class/const/type/interface/enum declarations
84
+ * and `export { ... }` re-export blocks.
85
+ * @param {string} src file content
86
+ * @returns {string[]} sorted export names
87
+ */
88
+ export function extractTSExports(src) {
89
+ const names = new Set();
90
+
91
+ // Named declarations: export function foo / export const foo / export type Foo / etc.
92
+ const declRe = /^export\s+(?:async\s+)?(?:function|class|const|let|var|type|interface|enum)\s+([\w$]+)/gm;
93
+ let m;
94
+ while ((m = declRe.exec(src)) !== null) names.add(m[1]);
95
+
96
+ // Re-export blocks: export { foo, bar as baz }
97
+ const blockRe = /export\s*\{([^}]+)\}/g;
98
+ while ((m = blockRe.exec(src)) !== null) {
99
+ m[1].split(',').forEach(part => {
100
+ const alias = part.trim().split(/\s+as\s+/);
101
+ const name = (alias[1] || alias[0]).trim();
102
+ if (name) names.add(name);
103
+ });
104
+ }
105
+
106
+ return [...names].sort();
107
+ }
108
+
109
+ /**
110
+ * Collect all public JS exports from @finggujadhav/js-helper src.
111
+ * @returns {string[]}
112
+ */
113
+ export function collectJSExports() {
114
+ const srcDir = path.join(JS_HELPER_DIR, 'src');
115
+ if (!fs.existsSync(srcDir)) return [];
116
+
117
+ const allExports = new Set();
118
+ const files = fs.readdirSync(srcDir).filter(f => f.endsWith('.ts') && f !== 'index.ts');
119
+
120
+ for (const file of files) {
121
+ const src = fs.readFileSync(path.join(srcDir, file), 'utf8');
122
+ extractTSExports(src).forEach(e => allExports.add(e));
123
+ }
124
+ return [...allExports].sort();
125
+ }
126
+
127
+ // ─── CLI command surface collector ───────────────────────────────────────────
128
+
129
+ /**
130
+ * Extract CLI command names from cli.js by scanning the switch statement.
131
+ * @returns {string[]} sorted command names
132
+ */
133
+ export function collectCLICommands() {
134
+ const cliPath = path.join(COMPILER_DIR, 'cli.js');
135
+ if (!fs.existsSync(cliPath)) return [];
136
+
137
+ const src = fs.readFileSync(cliPath, 'utf8');
138
+ const commands = new Set();
139
+ const re = /case\s+'([\w:-]+)'\s*:/g;
140
+ let m;
141
+ while ((m = re.exec(src)) !== null) commands.add(m[1]);
142
+ return [...commands].sort();
143
+ }
144
+
145
+ // ─── Snapshot builder ─────────────────────────────────────────────────────────
146
+
147
+ /**
148
+ * Generate a complete API surface snapshot object.
149
+ * @returns {{ generatedAt: string, version: string, surface: object }}
150
+ */
151
+ export function generateSnapshot() {
152
+ // Read version from core package.json
153
+ const corePkg = JSON.parse(fs.readFileSync(path.join(CORE_DIR, 'package.json'), 'utf8'));
154
+
155
+ return {
156
+ $schema: 'https://fingguflux.dev/schemas/api-surface-snapshot.json',
157
+ generatedAt: new Date().toISOString(),
158
+ version: corePkg.version,
159
+ surface: {
160
+ cssTokens: collectCSSTokens(),
161
+ jsExports: collectJSExports(),
162
+ cliCommands: collectCLICommands(),
163
+ componentFiles: collectComponentFiles(),
164
+ },
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Write the snapshot to disk at packages/core/API_SURFACE_SNAPSHOT.json.
170
+ * @param {object} [snapshotOverride] optionally pass a pre-built snapshot
171
+ * @returns {object} the written snapshot
172
+ */
173
+ export function writeSnapshot(snapshotOverride) {
174
+ const snap = snapshotOverride ?? generateSnapshot();
175
+ fs.writeFileSync(SNAPSHOT_PATH, JSON.stringify(snap, null, 4), 'utf8');
176
+ return snap;
177
+ }
178
+
179
+ // ─── Snapshot comparator ──────────────────────────────────────────────────────
180
+
181
+ /**
182
+ * Compare two API surface sets and produce structured diff.
183
+ * @param {string[]} baseline items in the stored snapshot
184
+ * @param {string[]} current items in the freshly generated snapshot
185
+ * @param {string} label human-readable label for reporting
186
+ * @returns {{ removed: string[], added: string[] }}
187
+ */
188
+ export function diffSurface(baseline, current, label) {
189
+ const baseSet = new Set(baseline);
190
+ const currentSet = new Set(current);
191
+
192
+ const removed = [...baseSet].filter(x => !currentSet.has(x)).sort();
193
+ const added = [...currentSet].filter(x => !baseSet.has(x)).sort();
194
+
195
+ return { label, removed, added };
196
+ }
197
+
198
+ /**
199
+ * Load the deprecation log.
200
+ * @returns {Array<{name:string, type:string, since:string, until:string, replacement:string|null, reason:string}>}
201
+ */
202
+ export function loadDeprecationLog() {
203
+ if (!fs.existsSync(DEPRECATION_PATH)) return [];
204
+ return JSON.parse(fs.readFileSync(DEPRECATION_PATH, 'utf8')).deprecations ?? [];
205
+ }
206
+
207
+ /**
208
+ * Check for deprecated items that are past their `until` version and
209
+ * should have been removed β€” these are breaking-change guards.
210
+ * @param {string} currentVersion e.g. "0.9.7"
211
+ * @returns {Array} overdue deprecations
212
+ */
213
+ export function detectOverdueDeprecations(currentVersion) {
214
+ const log = loadDeprecationLog();
215
+ return log.filter(d => {
216
+ if (!d.until) return false;
217
+ return compareVersions(d.until, currentVersion) < 0;
218
+ });
219
+ }
220
+
221
+ /** Simple semver comparator (major.minor.patch only). */
222
+ function compareVersions(a, b) {
223
+ const pa = a.split('.').map(Number);
224
+ const pb = b.split('.').map(Number);
225
+ for (let i = 0; i < 3; i++) {
226
+ if ((pa[i] ?? 0) > (pb[i] ?? 0)) return 1;
227
+ if ((pa[i] ?? 0) < (pb[i] ?? 0)) return -1;
228
+ }
229
+ return 0;
230
+ }
231
+
232
+ // ─── Snapshot comparison runner ───────────────────────────────────────────────
233
+
234
+ /**
235
+ * Full snapshot comparison pipeline.
236
+ * Loads the stored baseline, generates a fresh snapshot, diffs all surface
237
+ * dimensions, detects overdue deprecations, and returns a structured report.
238
+ *
239
+ * @param {object} [opts]
240
+ * @param {object} [opts.currentSnapshot] override the live snapshot (for tests)
241
+ * @param {object} [opts.baselineSnapshot] override the stored baseline (for tests)
242
+ * @param {string} [opts.currentVersion] override version string (for tests)
243
+ * @returns {{
244
+ * pass: boolean,
245
+ * breakingChanges: string[],
246
+ * warnings: string[],
247
+ * diffs: object[],
248
+ * overdueDeprecations: object[]
249
+ * }}
250
+ */
251
+ export function runSnapshotCompare(opts = {}) {
252
+ // --- Load baseline ---
253
+ const baseline = opts.baselineSnapshot ?? (() => {
254
+ if (!fs.existsSync(SNAPSHOT_PATH)) {
255
+ throw new Error(
256
+ 'API_SURFACE_SNAPSHOT.json not found. ' +
257
+ "Run 'finggu snapshot --write' to generate the initial baseline."
258
+ );
259
+ }
260
+ return JSON.parse(fs.readFileSync(SNAPSHOT_PATH, 'utf8'));
261
+ })();
262
+
263
+ // --- Generate current state ---
264
+ const current = opts.currentSnapshot ?? generateSnapshot();
265
+
266
+ // --- Diff each surface dimension ---
267
+ const diffs = [
268
+ diffSurface(baseline.surface.cssTokens, current.surface.cssTokens, 'CSS Tokens'),
269
+ diffSurface(baseline.surface.jsExports, current.surface.jsExports, 'JS Exports'),
270
+ diffSurface(baseline.surface.cliCommands, current.surface.cliCommands, 'CLI Commands'),
271
+ diffSurface(baseline.surface.componentFiles, current.surface.componentFiles, 'Component Files'),
272
+ ];
273
+
274
+ const breakingChanges = [];
275
+ const warnings = [];
276
+
277
+ for (const diff of diffs) {
278
+ // Removals are BREAKING
279
+ for (const item of diff.removed) {
280
+ breakingChanges.push(`[BREAKING] ${diff.label}: '${item}' was removed from the public API surface.`);
281
+ }
282
+ // Additions are warnings (undocumented surface growth)
283
+ for (const item of diff.added) {
284
+ warnings.push(`[WARN] ${diff.label}: '${item}' is new β€” ensure it is intentional and documented.`);
285
+ }
286
+ }
287
+
288
+ // --- Overdue deprecations (items past their `until` date are breaking) ---
289
+ const currentVersion = opts.currentVersion ?? current.version;
290
+ const overdue = detectOverdueDeprecations(currentVersion);
291
+ for (const d of overdue) {
292
+ breakingChanges.push(
293
+ `[BREAKING] Deprecation overdue: '${d.name}' (${d.type}) was scheduled for removal in v${d.until} ` +
294
+ `but is still present. Remove it or extend the deprecation window.`
295
+ );
296
+ }
297
+
298
+ return {
299
+ pass: breakingChanges.length === 0,
300
+ breakingChanges,
301
+ warnings,
302
+ diffs,
303
+ overdueDeprecations: overdue,
304
+ baselineVersion: baseline.version,
305
+ currentVersion,
306
+ };
307
+ }
308
+
309
+ // ─── CLI printer ──────────────────────────────────────────────────────────────
310
+
311
+ const C = {
312
+ reset: '\x1b[0m',
313
+ bold: '\x1b[1m',
314
+ red: '\x1b[31m',
315
+ green: '\x1b[32m',
316
+ yellow: '\x1b[33m',
317
+ cyan: '\x1b[36m',
318
+ gray: '\x1b[90m',
319
+ };
320
+
321
+ const line = (ch = '─', n = 54) => ch.repeat(n);
322
+
323
+ export function printSnapshotReport(report, verbose = false) {
324
+ const { pass, breakingChanges, warnings, diffs, overdueDeprecations,
325
+ baselineVersion, currentVersion } = report;
326
+
327
+ console.log('');
328
+ console.log(`${C.bold}${C.cyan}πŸ“Έ FingguFlux API Surface Snapshot β€” Drift Audit${C.reset}`);
329
+ console.log(C.gray + line() + C.reset);
330
+ console.log(` Baseline : ${C.bold}v${baselineVersion}${C.reset}`);
331
+ console.log(` Current : ${C.bold}v${currentVersion}${C.reset}`);
332
+ console.log('');
333
+
334
+ // Surface summary
335
+ for (const diff of diffs) {
336
+ const removedCount = diff.removed.length;
337
+ const addedCount = diff.added.length;
338
+ const icon = removedCount ? C.red + 'βœ–' :
339
+ addedCount ? C.yellow + '⚠' :
340
+ C.green + 'βœ”';
341
+ console.log(` ${icon}${C.reset} ${diff.label.padEnd(18)} ` +
342
+ `${removedCount ? C.red + removedCount + ' removed' + C.reset : 'β€”'} ` +
343
+ `${addedCount ? C.yellow + addedCount + ' added' + C.reset : ''}`);
344
+ }
345
+
346
+ // Breaking changes
347
+ if (breakingChanges.length > 0) {
348
+ console.log('');
349
+ console.log(C.red + C.bold + ` β›” ${breakingChanges.length} Breaking Change(s) Detected` + C.reset);
350
+ for (const msg of breakingChanges) {
351
+ console.log(` ${C.red}${msg}${C.reset}`);
352
+ }
353
+ }
354
+
355
+ // Warnings
356
+ if (warnings.length > 0 && (verbose || breakingChanges.length === 0)) {
357
+ console.log('');
358
+ console.log(C.yellow + C.bold + ` ⚠ ${warnings.length} Warning(s)` + C.reset);
359
+ for (const msg of warnings) {
360
+ console.log(` ${C.yellow}${msg}${C.reset}`);
361
+ }
362
+ }
363
+
364
+ // Overdue deprecations
365
+ if (overdueDeprecations.length > 0) {
366
+ console.log('');
367
+ console.log(C.red + C.bold + ` 🚨 ${overdueDeprecations.length} Overdue Deprecation(s)` + C.reset);
368
+ for (const d of overdueDeprecations) {
369
+ console.log(` ${C.red}β€’ ${d.name} (${d.type}) β€” scheduled removal: v${d.until}${C.reset}`);
370
+ if (d.replacement) console.log(` ${C.gray}β†’ replacement: ${d.replacement}${C.reset}`);
371
+ }
372
+ }
373
+
374
+ console.log('');
375
+ console.log(C.gray + line() + C.reset);
376
+
377
+ if (pass) {
378
+ console.log(`${C.green}${C.bold}✨ API surface STABLE. No breaking changes detected.${C.reset}`);
379
+ } else {
380
+ console.log(`${C.red}${C.bold}🚫 API surface has BREAKING CHANGES. Release blocked.${C.reset}`);
381
+ console.log(`${C.gray} Fix all [BREAKING] issues before bumping version.${C.reset}`);
382
+ }
383
+ console.log('');
384
+ }