@chemx/starter-kit 26.9.25-943 → 26.9.26-253

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.
@@ -0,0 +1,392 @@
1
+ import { parse } from '@babel/parser';
2
+ import traverseModule from '@babel/traverse';
3
+
4
+ const traverse = traverseModule.default || traverseModule;
5
+
6
+ /**
7
+ * Strips JavaScript/TypeScript comments while preserving line boundaries where possible.
8
+ *
9
+ * @param {string} code Source code.
10
+ * @returns {string} Code without comments.
11
+ */
12
+ export const stripCodeComments = (code) => {
13
+ return code
14
+ .replace(/\/\*[\s\S]*?\*\//g, '')
15
+ .replace(/(^|[^\:])\/\/.*$/gm, '$1');
16
+ };
17
+
18
+ /**
19
+ * Collapses consecutive blank lines and trims trailing spaces.
20
+ *
21
+ * @param {string} code Source code.
22
+ * @returns {string} Compact code.
23
+ */
24
+ export const compactCode = (code) => {
25
+ return code
26
+ .split('\n')
27
+ .map((line) => line.trimEnd())
28
+ .filter((line, idx, arr) => {
29
+ const isCurrentEmpty = line.trim() === '';
30
+ const isPrevEmpty = idx > 0 && arr[idx - 1].trim() === '';
31
+ return !(isCurrentEmpty && isPrevEmpty);
32
+ })
33
+ .join('\n');
34
+ };
35
+
36
+ export const resolveDeclarationKind = (declaration) => {
37
+ const init = declaration.init;
38
+ if (!init) return 'const';
39
+
40
+ if (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression') {
41
+ return 'function';
42
+ }
43
+
44
+ if (init.type !== 'CallExpression') {
45
+ return 'const';
46
+ }
47
+
48
+ const calleeName = init.callee?.name;
49
+ if (calleeName === 'computed') {
50
+ return 'computed';
51
+ }
52
+ if (calleeName === 'ref') {
53
+ return 'ref';
54
+ }
55
+ if (calleeName && calleeName.startsWith('use')) {
56
+ return 'hook';
57
+ }
58
+
59
+ return 'const';
60
+ };
61
+
62
+ /**
63
+ * Summarizes declarative template bindings, components, and events (Vue, Svelte, JSX).
64
+ *
65
+ * @param {string} code Source code.
66
+ * @param {string} filePath File path.
67
+ * @returns {string|null} Compact template summary.
68
+ */
69
+ export const summarizeTemplate = (code, filePath = '') => {
70
+ const isVue = filePath.endsWith('.vue');
71
+ const isSvelte = filePath.endsWith('.svelte');
72
+ let tpl = '';
73
+
74
+ if (isVue) {
75
+ const match = code.match(/<template[\s\S]*?>([\s\S]*?)<\/template>/i);
76
+ if (match) tpl = match[1];
77
+ } else if (isSvelte) {
78
+ tpl = code
79
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
80
+ .replace(/<style[\s\S]*?<\/style>/gi, '');
81
+ } else if (filePath.endsWith('.tsx') || filePath.endsWith('.jsx')) {
82
+ tpl = code;
83
+ }
84
+
85
+ if (!tpl.trim()) return null;
86
+
87
+ const tagMatches = tpl.matchAll(/<([A-Z][a-zA-Z0-9]+|[a-z]+-[a-z0-9-]+)\b/g);
88
+ const tags = new Set();
89
+ for (const m of tagMatches) {
90
+ tags.add(`<${m[1]}>`);
91
+ }
92
+
93
+ const eventMatches = tpl.matchAll(/(?:@|v-on:|on:)([a-zA-Z0-9_.:-]+)="([^"]+)"/g);
94
+ const events = new Set();
95
+ for (const m of eventMatches) {
96
+ events.add(`@${m[1]}="${m[2]}"`);
97
+ }
98
+
99
+ const reactEventMatches = tpl.matchAll(/\b(on[A-Z][a-zA-Z0-9]*)=\{([^}]+)\}/g);
100
+ for (const m of reactEventMatches) {
101
+ events.add(`${m[1]}={${m[2].trim()}}`);
102
+ }
103
+
104
+ const parts = [];
105
+ if (tags.size > 0) parts.push(`Components: ${Array.from(tags).slice(0, 10).join(', ')}`);
106
+ if (events.size > 0) parts.push(`Events: ${Array.from(events).slice(0, 8).join(', ')}`);
107
+
108
+ return parts.length > 0
109
+ ? `// [Template Summary: ${parts.join(' | ')}]`
110
+ : '// [Template Summary: Static markup]';
111
+ };
112
+
113
+ /**
114
+ * Extracts raw template markup from Vue, Svelte, or JSX files.
115
+ *
116
+ * @param {string} code Source code.
117
+ * @param {string} filePath File path.
118
+ * @returns {string} Extracted template markup.
119
+ */
120
+ export const extractTemplateContent = (code, filePath = '') => {
121
+ const isVue = filePath.endsWith('.vue');
122
+ const isSvelte = filePath.endsWith('.svelte');
123
+
124
+ if (isVue) {
125
+ const match = code.match(/<template[\s\S]*?>([\s\S]*?)<\/template>/i);
126
+ return match ? match[0].trim() : '// No <template> block found in Vue file.';
127
+ }
128
+
129
+ if (isSvelte) {
130
+ const cleaned = code
131
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
132
+ .replace(/<style[\s\S]*?<\/style>/gi, '')
133
+ .trim();
134
+ return cleaned || '// No template markup found in Svelte file.';
135
+ }
136
+
137
+ if (filePath.endsWith('.tsx') || filePath.endsWith('.jsx')) {
138
+ const jsxMatch = code.match(/return\s*\(\s*(<[\s\S]*?>[\s\S]*?)\s*\);/);
139
+ if (jsxMatch) return jsxMatch[1].trim();
140
+ }
141
+
142
+ return `// File ${filePath} is a script/module without declarative template syntax.`;
143
+ };
144
+
145
+ /**
146
+ * Extracts an AST logic skeleton of a file (control flow, state, guards, side-effects, mutations).
147
+ * Preserves 100% of execution semantics while stripping decorative syntax and boilerplate.
148
+ *
149
+ * @param {string} code Source code.
150
+ * @param {string} filePath File path for parser context.
151
+ * @param {object} options Additional options.
152
+ * @returns {string} Compressed logic skeleton.
153
+ */
154
+ export const generateAstLogicSkeleton = (code, filePath, options = {}) => {
155
+ const isVue = filePath.endsWith('.vue');
156
+ const isSvelte = filePath.endsWith('.svelte');
157
+ let scriptContent = code;
158
+
159
+ if (isVue) {
160
+ const scriptMatch = code.match(/<script[\s\S]*?>([\s\S]*?)<\/script>/i);
161
+ scriptContent = scriptMatch ? scriptMatch[1] : '';
162
+ } else if (isSvelte) {
163
+ const scriptMatch = code.match(/<script[\s\S]*?>([\s\S]*?)<\/script>/i);
164
+ scriptContent = scriptMatch ? scriptMatch[1] : '';
165
+ }
166
+
167
+ const scriptLines = scriptContent.split('\n');
168
+ const lines = [];
169
+ lines.push(`// Logic Skeleton: ${filePath}`);
170
+
171
+ try {
172
+ const ast = parse(scriptContent, {
173
+ sourceType: 'module',
174
+ plugins: ['typescript', 'jsx', 'decorators-legacy', 'topLevelAwait'],
175
+ errorRecovery: true,
176
+ });
177
+
178
+ const importsSummary = [];
179
+ const stateDeclarations = [];
180
+ const logicBlocks = [];
181
+
182
+ const isLogicStatement = (stmt) => {
183
+ if (!stmt) return false;
184
+ switch (stmt.type) {
185
+ case 'IfStatement':
186
+ case 'ReturnStatement':
187
+ case 'ThrowStatement':
188
+ case 'SwitchStatement':
189
+ case 'TryStatement':
190
+ case 'WhileStatement':
191
+ case 'ForStatement':
192
+ case 'ForOfStatement':
193
+ case 'ForInStatement':
194
+ return true;
195
+ case 'ExpressionStatement': {
196
+ const expr = stmt.expression;
197
+ if (expr.type === 'AssignmentExpression') return true;
198
+ if (expr.type === 'AwaitExpression') return true;
199
+ if (expr.type === 'CallExpression' || expr.type === 'OptionalCallExpression') {
200
+ const callee = expr.callee;
201
+ if (callee.type === 'MemberExpression' && callee.object?.name === 'console') {
202
+ return false;
203
+ }
204
+ return true;
205
+ }
206
+ return false;
207
+ }
208
+ case 'VariableDeclaration': {
209
+ return stmt.declarations.some((d) => {
210
+ const init = d.init;
211
+ if (!init) return false;
212
+ return (
213
+ init.type === 'CallExpression' ||
214
+ init.type === 'OptionalCallExpression' ||
215
+ init.type === 'AwaitExpression' ||
216
+ init.type === 'LogicalExpression' ||
217
+ init.type === 'BinaryExpression'
218
+ );
219
+ });
220
+ }
221
+ default:
222
+ return false;
223
+ }
224
+ };
225
+
226
+ const sliceNodeLines = (node) => {
227
+ if (!node.loc) return '';
228
+ const start = Math.max(0, node.loc.start.line - 1);
229
+ const end = Math.min(scriptLines.length, node.loc.end.line);
230
+ return scriptLines.slice(start, end).join('\n');
231
+ };
232
+
233
+ const processFunctionLogic = (fnName, fnNode, isExport) => {
234
+ const params = (fnNode.params || []).map((p) => {
235
+ if (p.type === 'AssignmentPattern' && p.left?.name) {
236
+ return `${p.left.name} = ...`;
237
+ }
238
+ return p.name || p.type;
239
+ }).join(', ');
240
+
241
+ const exportPrefix = isExport ? 'export ' : '';
242
+ const isAsync = Boolean(fnNode.async);
243
+ const asyncPrefix = isAsync ? 'async ' : '';
244
+
245
+ const fnLines = [];
246
+ fnLines.push(`${exportPrefix}const ${fnName} = ${asyncPrefix}(${params}) => {`);
247
+
248
+ const body = fnNode.body;
249
+ if (body && body.type === 'BlockStatement') {
250
+ const stmts = body.body || [];
251
+ for (const stmt of stmts) {
252
+ if (stmt.type === 'VariableDeclaration') {
253
+ const hasArrowOrFn = stmt.declarations.some(
254
+ (d) => d.init && (d.init.type === 'ArrowFunctionExpression' || d.init.type === 'FunctionExpression')
255
+ );
256
+
257
+ if (hasArrowOrFn) {
258
+ stmt.declarations.forEach((d) => {
259
+ const dName = d.id?.name;
260
+ const dInit = d.init;
261
+ if (dInit && (dInit.type === 'ArrowFunctionExpression' || dInit.type === 'FunctionExpression')) {
262
+ const innerStmts = (dInit.body?.body || []).filter(isLogicStatement);
263
+ if (innerStmts.length > 0) {
264
+ const innerAsync = dInit.async ? 'async ' : '';
265
+ const innerParams = (dInit.params || []).map((p) => p.name || 'arg').join(', ');
266
+ fnLines.push(` const ${dName} = ${innerAsync}(${innerParams}) => {`);
267
+ innerStmts.forEach((is) => {
268
+ const sliced = sliceNodeLines(is);
269
+ if (sliced) fnLines.push(` ${sliced.trim()}`);
270
+ });
271
+ fnLines.push(` };`);
272
+ } else {
273
+ fnLines.push(` const ${dName} = () => { /* no-op */ };`);
274
+ }
275
+ }
276
+ });
277
+ } else if (isLogicStatement(stmt)) {
278
+ const sliced = sliceNodeLines(stmt);
279
+ if (sliced) fnLines.push(` ${sliced.trim()}`);
280
+ }
281
+ } else if (isLogicStatement(stmt)) {
282
+ if (stmt.type === 'ReturnStatement' && (stmt.argument?.type === 'JSXElement' || stmt.argument?.type === 'JSXFragment')) {
283
+ fnLines.push(` return <JSX: Template Rendered>;`);
284
+ } else {
285
+ const sliced = sliceNodeLines(stmt);
286
+ if (sliced) fnLines.push(` ${sliced.trim()}`);
287
+ }
288
+ }
289
+ }
290
+ } else if (body) {
291
+ const sliced = sliceNodeLines(body);
292
+ if (sliced) fnLines.push(` return ${sliced.trim()};`);
293
+ }
294
+
295
+ fnLines.push(`};`);
296
+ logicBlocks.push(fnLines.join('\n'));
297
+ };
298
+
299
+ traverse(ast, {
300
+ ImportDeclaration(nodePath) {
301
+ if (nodePath.node.importKind === 'type') return;
302
+ const source = nodePath.node.source?.value;
303
+ const specifiers = nodePath.node.specifiers
304
+ .filter((s) => s.importKind !== 'type')
305
+ .map((s) => s.local?.name || s.imported?.name)
306
+ .filter(Boolean);
307
+ if (specifiers.length > 0 && source) {
308
+ importsSummary.push(`// Imports: [${specifiers.join(', ')}] from '${source}'`);
309
+ }
310
+ },
311
+
312
+ ExportNamedDeclaration(nodePath) {
313
+ const decl = nodePath.node.declaration;
314
+ if (!decl) return;
315
+
316
+ if (decl.type === 'VariableDeclaration') {
317
+ decl.declarations.forEach((d) => {
318
+ const name = d.id?.name;
319
+ const init = d.init;
320
+ if (!name || !init) return;
321
+
322
+ if (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression') {
323
+ processFunctionLogic(name, init, true);
324
+ } else {
325
+ const kind = resolveDeclarationKind(d);
326
+ if (kind === 'ref' || kind === 'computed' || kind === 'hook') {
327
+ const nodeStr = sliceNodeLines(d);
328
+ stateDeclarations.push(`export const ${nodeStr}`);
329
+ }
330
+ }
331
+ });
332
+ } else if (decl.type === 'FunctionDeclaration' && decl.id) {
333
+ processFunctionLogic(decl.id.name, decl, true);
334
+ }
335
+ },
336
+
337
+ VariableDeclaration(nodePath) {
338
+ if (nodePath.parent.type !== 'Program') return;
339
+ if (nodePath.parentPath?.parent?.type === 'ExportNamedDeclaration') return;
340
+
341
+ nodePath.node.declarations.forEach((d) => {
342
+ const name = d.id?.name;
343
+ const init = d.init;
344
+ if (!name || !init) return;
345
+
346
+ if (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression') {
347
+ processFunctionLogic(name, init, false);
348
+ } else {
349
+ const kind = resolveDeclarationKind(d);
350
+ if (kind === 'ref' || kind === 'computed' || kind === 'hook') {
351
+ const nodeStr = sliceNodeLines(d);
352
+ stateDeclarations.push(`const ${nodeStr}`);
353
+ }
354
+ }
355
+ });
356
+ },
357
+
358
+ FunctionDeclaration(nodePath) {
359
+ if (nodePath.parent.type !== 'Program') return;
360
+ if (nodePath.parentPath?.parent?.type === 'ExportNamedDeclaration') return;
361
+ if (nodePath.node.id) {
362
+ processFunctionLogic(nodePath.node.id.name, nodePath.node, false);
363
+ }
364
+ }
365
+ });
366
+
367
+ if (importsSummary.length > 0) {
368
+ lines.push(...importsSummary);
369
+ lines.push('');
370
+ }
371
+
372
+ if (stateDeclarations.length > 0) {
373
+ lines.push(...stateDeclarations);
374
+ lines.push('');
375
+ }
376
+
377
+ if (logicBlocks.length > 0) {
378
+ lines.push(logicBlocks.join('\n\n'));
379
+ }
380
+
381
+ const tplSummary = summarizeTemplate(code, filePath);
382
+ if (tplSummary) {
383
+ lines.push('');
384
+ lines.push(tplSummary);
385
+ }
386
+ } catch (err) {
387
+ lines.push(`// [AST Parsing Note: ${err.message}]`);
388
+ lines.push(compactCode(stripCodeComments(scriptContent)));
389
+ }
390
+
391
+ return lines.join('\n');
392
+ };
@@ -0,0 +1,160 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import {
6
+ generateAstLogicSkeleton,
7
+ summarizeTemplate,
8
+ extractTemplateContent,
9
+ readTokenOptimized,
10
+ runReaderCli
11
+ } from './reader.js';
12
+
13
+ describe('AST Logic Skeleton & Template Reader', () => {
14
+ const sampleControllerCode = `import { ref, computed } from 'vue';
15
+ import type { UserCardState, UserCardDescriptor } from './types';
16
+
17
+ interface ControllerOptions {
18
+ readonly initialState?: UserCardState;
19
+ readonly onAction?: () => void;
20
+ }
21
+
22
+ export const useUserCardController = (options: ControllerOptions = {}) => {
23
+ const state = ref<UserCardState>(
24
+ options.initialState || { status: 'idle' }
25
+ );
26
+
27
+ const isIdle = computed(() => state.value.status === 'idle');
28
+ const isPending = computed(() => state.value.status === 'loading');
29
+ const canProceed = computed(() => isIdle.value && !isPending.value);
30
+
31
+ const handleAction = () => {
32
+ if (!canProceed.value) return;
33
+ state.value = { status: 'active', activeId: 'item-1' };
34
+ options.onAction?.();
35
+ };
36
+
37
+ return { state, canProceed, handleAction };
38
+ };
39
+ `;
40
+
41
+ const sampleVueComponent = `<template>
42
+ <div class="m-card">
43
+ <a-button @click="handleAction">Proceed</a-button>
44
+ <m-modal-dialog v-on:close="handleClose" />
45
+ </div>
46
+ </template>
47
+
48
+ <script setup lang="ts">
49
+ import { ref, computed } from 'vue';
50
+ import { useUserCardController } from './controller';
51
+
52
+ const { state, canProceed, handleAction } = useUserCardController();
53
+ const isOpen = ref(false);
54
+
55
+ const handleClose = () => {
56
+ isOpen.value = false;
57
+ };
58
+ </script>
59
+ `;
60
+
61
+ it('generates logic skeleton preserving guard clauses, state, and mutations', () => {
62
+ const skeleton = generateAstLogicSkeleton(sampleControllerCode, 'useUserCardController.ts');
63
+ assert.ok(skeleton.includes('Logic Skeleton: useUserCardController.ts'));
64
+ assert.ok(skeleton.includes('ref'), 'Should retain ref');
65
+ assert.ok(skeleton.includes('canProceed'), 'Should retain computed canProceed');
66
+ assert.ok(skeleton.includes('if (!canProceed.value) return;'), 'Should retain guard clause');
67
+ assert.ok(skeleton.includes("state.value = { status: 'active'"), 'Should retain state mutation');
68
+ assert.ok(skeleton.includes('return { state, canProceed, handleAction }'), 'Should retain return');
69
+ assert.ok(!skeleton.includes('interface ControllerOptions'), 'Should strip TypeScript interfaces');
70
+ });
71
+
72
+ it('summarizes template elements and events', () => {
73
+ const summary = summarizeTemplate(sampleVueComponent, 'Card.vue');
74
+ assert.ok(summary);
75
+ assert.ok(summary.includes('<a-button>'));
76
+ assert.ok(summary.includes('<m-modal-dialog>'));
77
+ assert.ok(summary.includes('@click="handleAction"'));
78
+ });
79
+
80
+ it('extracts raw template markup', () => {
81
+ const tpl = extractTemplateContent(sampleVueComponent, 'Card.vue');
82
+ assert.ok(tpl.startsWith('<template>'));
83
+ assert.ok(tpl.includes('</template>'));
84
+ assert.ok(tpl.includes('<a-button'));
85
+ });
86
+
87
+ it('supports readTokenOptimized with logic mode', () => {
88
+ const tempFile = path.resolve(process.cwd(), 'scratch/temp-controller.ts');
89
+ fs.mkdirSync(path.dirname(tempFile), { recursive: true });
90
+ fs.writeFileSync(tempFile, sampleControllerCode, 'utf-8');
91
+
92
+ try {
93
+ const res = readTokenOptimized(tempFile, { logic: true });
94
+ assert.equal(res.mode, 'logic');
95
+ assert.ok(res.content.includes('if (!canProceed.value) return;'));
96
+ assert.ok(res.tokensEst < 200, `Expected token estimate < 200, got ${res.tokensEst}`);
97
+ } finally {
98
+ fs.unlinkSync(tempFile);
99
+ }
100
+ });
101
+
102
+ it('supports runReaderCli with -l and --logic flags', () => {
103
+ const tempFile = path.resolve(process.cwd(), 'scratch/temp-cli-controller.ts');
104
+ fs.mkdirSync(path.dirname(tempFile), { recursive: true });
105
+ fs.writeFileSync(tempFile, sampleControllerCode, 'utf-8');
106
+
107
+ try {
108
+ const res = runReaderCli([tempFile, '-l'], false);
109
+ assert.ok(res);
110
+ assert.equal(res.mode, 'logic');
111
+ assert.ok(res.content.includes('canProceed'));
112
+ } finally {
113
+ fs.unlinkSync(tempFile);
114
+ }
115
+ });
116
+
117
+ it('supports readTokenOptimized with template mode', () => {
118
+ const tempFile = path.resolve(process.cwd(), 'scratch/temp-comp.vue');
119
+ fs.mkdirSync(path.dirname(tempFile), { recursive: true });
120
+ fs.writeFileSync(tempFile, sampleVueComponent, 'utf-8');
121
+
122
+ try {
123
+ const res = readTokenOptimized(tempFile, { template: true });
124
+ assert.equal(res.mode, 'template');
125
+ assert.ok(res.content.includes('<a-button'));
126
+ } finally {
127
+ fs.unlinkSync(tempFile);
128
+ }
129
+ });
130
+
131
+ it('supports --outline --enrich: returns outline mode with enriched logic field populated', () => {
132
+ const tempFile = path.resolve(process.cwd(), 'scratch/temp-enrich.ts');
133
+ fs.mkdirSync(path.dirname(tempFile), { recursive: true });
134
+ fs.writeFileSync(tempFile, sampleControllerCode, 'utf-8');
135
+
136
+ try {
137
+ const res = readTokenOptimized(tempFile, { outline: true, enrich: true });
138
+ assert.equal(res.mode, 'outline', 'Mode stays outline when enrich is set');
139
+ assert.ok(res.enriched, 'enriched field should be populated');
140
+ assert.ok(res.enriched.includes('// --- Logic Skeleton ---'), 'enriched should contain section header');
141
+ assert.ok(res.tokensEnriched > 0, `tokensEnriched should be > 0, got ${res.tokensEnriched}`);
142
+ } finally {
143
+ fs.unlinkSync(tempFile);
144
+ }
145
+ });
146
+
147
+ it('--enrich without --outline is a no-op: enriched field is null', () => {
148
+ const tempFile = path.resolve(process.cwd(), 'scratch/temp-enrich-noop.ts');
149
+ fs.mkdirSync(path.dirname(tempFile), { recursive: true });
150
+ fs.writeFileSync(tempFile, sampleControllerCode, 'utf-8');
151
+
152
+ try {
153
+ const res = readTokenOptimized(tempFile, { enrich: true });
154
+ assert.equal(res.enriched ?? null, null, 'enriched should be null when no base mode triggers it');
155
+ assert.equal(res.tokensEnriched ?? 0, 0, 'tokensEnriched should be 0');
156
+ } finally {
157
+ fs.unlinkSync(tempFile);
158
+ }
159
+ });
160
+ });