carto-md 1.0.0

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,80 @@
1
+ /**
2
+ * Prisma schema plugin — handles .prisma files.
3
+ * Pure regex — Prisma schema is simple enough.
4
+ */
5
+ module.exports = {
6
+ name: 'prisma',
7
+ extensions: ['.prisma'],
8
+ extract(content, filename) {
9
+ try {
10
+ const models = extractPrismaModels(content);
11
+ const dbTables = extractPrismaDBTables(content);
12
+ return {
13
+ routes: [],
14
+ models: models,
15
+ functions: [],
16
+ envVars: [],
17
+ dbTables: dbTables,
18
+ fetches: [],
19
+ storageKeys: [],
20
+ };
21
+ } catch (err) {
22
+ console.warn(`[CARTO] prisma plugin error on ${filename}: ${err.message}`);
23
+ return { routes: [], models: [], functions: [], envVars: [], dbTables: [], fetches: [], storageKeys: [] };
24
+ }
25
+ }
26
+ };
27
+
28
+ function extractPrismaModels(content) {
29
+ const models = [];
30
+ const modelPattern = /^model\s+(\w+)\s*\{([^}]+)\}/gm;
31
+
32
+ let match;
33
+ while ((match = modelPattern.exec(content)) !== null) {
34
+ const className = match[1];
35
+ const body = match[2];
36
+ const fields = [];
37
+
38
+ const lines = body.split('\n');
39
+ for (const line of lines) {
40
+ const trimmed = line.trim();
41
+ // Skip empty lines, comments, and @@directives
42
+ if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('@@')) continue;
43
+
44
+ const fieldMatch = trimmed.match(/^(\w+)\s+(\w+[\[\]?]*)/);
45
+ if (fieldMatch) {
46
+ fields.push({ name: fieldMatch[1], type: fieldMatch[2] });
47
+ }
48
+ }
49
+
50
+ models.push({ className, fields });
51
+ }
52
+
53
+ return models;
54
+ }
55
+
56
+ function extractPrismaDBTables(content) {
57
+ const tables = [];
58
+ const modelPattern = /^model\s+(\w+)\s*\{([^}]+)\}/gm;
59
+
60
+ let match;
61
+ while ((match = modelPattern.exec(content)) !== null) {
62
+ const modelName = match[1];
63
+ const body = match[2];
64
+
65
+ // Check for @@map('table_name')
66
+ const mapMatch = body.match(/@@map\s*\(\s*['"]([^'"]+)['"]\s*\)/);
67
+ const tableName = mapMatch ? mapMatch[1] : toSnakeCase(modelName);
68
+
69
+ tables.push({ tableName, modelName });
70
+ }
71
+
72
+ return tables;
73
+ }
74
+
75
+ function toSnakeCase(str) {
76
+ return str
77
+ .replace(/([A-Z])/g, '_$1')
78
+ .toLowerCase()
79
+ .replace(/^_/, '');
80
+ }
@@ -0,0 +1,26 @@
1
+ const { extractRoutes } = require('../routes');
2
+ const { extractModels } = require('../models');
3
+ const { extractFunctions } = require('../functions');
4
+ const { extractEnvVars } = require('../envvars');
5
+ const { extractDBTables } = require('../dbtables');
6
+
7
+ module.exports = {
8
+ name: 'python',
9
+ extensions: ['.py'],
10
+ extract(content, filename) {
11
+ try {
12
+ return {
13
+ routes: extractRoutes(content),
14
+ models: extractModels(content),
15
+ functions: extractFunctions(content, filename),
16
+ envVars: extractEnvVars(content),
17
+ dbTables: extractDBTables(content),
18
+ fetches: [],
19
+ storageKeys: [],
20
+ };
21
+ } catch (err) {
22
+ console.warn(`[CARTO] python plugin error on ${filename}: ${err.message}`);
23
+ return { routes: [], models: [], functions: [], envVars: [], dbTables: [], fetches: [], storageKeys: [] };
24
+ }
25
+ }
26
+ };
@@ -0,0 +1,235 @@
1
+ const parser = require('@babel/parser');
2
+ const jsPlugin = require('./javascript');
3
+
4
+ const TS_PARSE_OPTIONS = {
5
+ sourceType: 'module',
6
+ allowImportExportEverywhere: true,
7
+ allowReturnOutsideFunction: true,
8
+ plugins: ['typescript', 'jsx', 'decorators-legacy', 'classProperties', 'optionalChaining', 'nullishCoalescingOperator']
9
+ };
10
+
11
+ module.exports = {
12
+ name: 'typescript',
13
+ extensions: ['.ts', '.tsx'],
14
+ extract(content, filename) {
15
+ let ast;
16
+ try {
17
+ ast = parser.parse(content, TS_PARSE_OPTIONS);
18
+ } catch (err) {
19
+ console.warn(`[CARTO] TS parse failed on ${filename}: ${err.message} — skipping`);
20
+ return { routes: [], models: [], functions: [], envVars: [], dbTables: [], fetches: [], storageKeys: [] };
21
+ }
22
+
23
+ return {
24
+ routes: jsPlugin._extractExpressRoutes(ast, filename),
25
+ models: extractTSInterfaces(ast),
26
+ functions: extractTSFunctions(ast),
27
+ envVars: jsPlugin._extractProcessEnv(ast),
28
+ dbTables: [],
29
+ fetches: jsPlugin._extractJSFetches(ast),
30
+ storageKeys: [],
31
+ };
32
+ }
33
+ };
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // AST traversal helper
37
+ // ---------------------------------------------------------------------------
38
+
39
+ function walk(node, visitor) {
40
+ if (!node || typeof node !== 'object') return;
41
+ if (Array.isArray(node)) {
42
+ for (const child of node) walk(child, visitor);
43
+ return;
44
+ }
45
+ if (node.type) {
46
+ visitor(node);
47
+ }
48
+ for (const key of Object.keys(node)) {
49
+ if (key === 'leadingComments' || key === 'trailingComments' || key === 'innerComments') continue;
50
+ const child = node[key];
51
+ if (child && typeof child === 'object') {
52
+ walk(child, visitor);
53
+ }
54
+ }
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // TS function extraction (with return types)
59
+ // ---------------------------------------------------------------------------
60
+
61
+ function extractTSFunctions(ast) {
62
+ const functions = [];
63
+
64
+ if (!ast.program || !ast.program.body) return functions;
65
+
66
+ for (const node of ast.program.body) {
67
+ extractFuncFromNode(node, functions);
68
+ }
69
+
70
+ return functions;
71
+ }
72
+
73
+ function extractFuncFromNode(node, functions) {
74
+ // FunctionDeclaration
75
+ if (node.type === 'FunctionDeclaration' && node.id) {
76
+ const name = node.id.name;
77
+ if (shouldSkip(name, node.params)) return;
78
+ functions.push({
79
+ name,
80
+ params: extractTSParams(node.params),
81
+ returnType: extractReturnType(node)
82
+ });
83
+ }
84
+
85
+ // VariableDeclaration
86
+ if (node.type === 'VariableDeclaration') {
87
+ for (const decl of node.declarations) {
88
+ if (!decl.id || !decl.id.name || !decl.init) continue;
89
+ if (decl.init.type === 'ArrowFunctionExpression' || decl.init.type === 'FunctionExpression') {
90
+ const name = decl.id.name;
91
+ if (shouldSkip(name, decl.init.params)) continue;
92
+ functions.push({
93
+ name,
94
+ params: extractTSParams(decl.init.params),
95
+ returnType: extractReturnType(decl.init)
96
+ });
97
+ }
98
+ }
99
+ }
100
+
101
+ // ExportDefaultDeclaration
102
+ if (node.type === 'ExportDefaultDeclaration' && node.declaration) {
103
+ extractFuncFromNode(node.declaration, functions);
104
+ }
105
+
106
+ // ExportNamedDeclaration
107
+ if (node.type === 'ExportNamedDeclaration' && node.declaration) {
108
+ extractFuncFromNode(node.declaration, functions);
109
+ }
110
+ }
111
+
112
+ function shouldSkip(name, params) {
113
+ if (name.startsWith('_') && (!params || params.length === 0)) return true;
114
+ return false;
115
+ }
116
+
117
+ function extractTSParams(params) {
118
+ if (!params || params.length === 0) return '\u2014';
119
+ const names = [];
120
+ for (const p of params) {
121
+ if (p.type === 'Identifier') {
122
+ names.push(p.name);
123
+ } else if (p.type === 'AssignmentPattern' && p.left && p.left.type === 'Identifier') {
124
+ names.push(p.left.name);
125
+ } else if (p.type === 'RestElement' && p.argument && p.argument.type === 'Identifier') {
126
+ continue; // skip ...args
127
+ } else if (p.type === 'ObjectPattern') {
128
+ names.push('{...}');
129
+ } else if (p.type === 'ArrayPattern') {
130
+ names.push('[...]');
131
+ }
132
+ }
133
+ return names.length > 0 ? names.join(', ') : '\u2014';
134
+ }
135
+
136
+ function extractReturnType(funcNode) {
137
+ // Check for TSTypeAnnotation on the function's returnType
138
+ if (funcNode.returnType && funcNode.returnType.typeAnnotation) {
139
+ return typeToString(funcNode.returnType.typeAnnotation);
140
+ }
141
+ return '\u2014';
142
+ }
143
+
144
+ function typeToString(typeNode) {
145
+ if (!typeNode) return '\u2014';
146
+
147
+ switch (typeNode.type) {
148
+ case 'TSStringKeyword': return 'string';
149
+ case 'TSNumberKeyword': return 'number';
150
+ case 'TSBooleanKeyword': return 'boolean';
151
+ case 'TSVoidKeyword': return 'void';
152
+ case 'TSAnyKeyword': return 'any';
153
+ case 'TSNullKeyword': return 'null';
154
+ case 'TSUndefinedKeyword': return 'undefined';
155
+ case 'TSNeverKeyword': return 'never';
156
+ case 'TSObjectKeyword': return 'object';
157
+ case 'TSUnknownKeyword': return 'unknown';
158
+ case 'TSTypeReference':
159
+ if (typeNode.typeName && typeNode.typeName.name) {
160
+ const generics = typeNode.typeParameters
161
+ ? `<${typeNode.typeParameters.params.map(typeToString).join(', ')}>`
162
+ : '';
163
+ return typeNode.typeName.name + generics;
164
+ }
165
+ return '\u2014';
166
+ case 'TSArrayType':
167
+ return typeToString(typeNode.elementType) + '[]';
168
+ case 'TSUnionType':
169
+ return typeNode.types.map(typeToString).join(' | ');
170
+ case 'TSIntersectionType':
171
+ return typeNode.types.map(typeToString).join(' & ');
172
+ case 'TSTypeLiteral':
173
+ return 'object';
174
+ case 'TSFunctionType':
175
+ return 'Function';
176
+ default:
177
+ return '\u2014';
178
+ }
179
+ }
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // TS interface/type extraction → same shape as Pydantic models
183
+ // ---------------------------------------------------------------------------
184
+
185
+ function extractTSInterfaces(ast) {
186
+ const models = [];
187
+
188
+ if (!ast.program || !ast.program.body) return models;
189
+
190
+ for (const node of ast.program.body) {
191
+ let target = node;
192
+ // Unwrap exports
193
+ if (node.type === 'ExportNamedDeclaration' && node.declaration) {
194
+ target = node.declaration;
195
+ }
196
+
197
+ // TSInterfaceDeclaration
198
+ if (target.type === 'TSInterfaceDeclaration' && target.id) {
199
+ const className = target.id.name;
200
+ const fields = [];
201
+ if (target.body && target.body.body) {
202
+ for (const member of target.body.body) {
203
+ if (member.type === 'TSPropertySignature' && member.key) {
204
+ const name = member.key.name || member.key.value;
205
+ const type = member.typeAnnotation
206
+ ? typeToString(member.typeAnnotation.typeAnnotation)
207
+ : '\u2014';
208
+ if (name) fields.push({ name, type });
209
+ }
210
+ }
211
+ }
212
+ models.push({ className, fields });
213
+ }
214
+
215
+ // TSTypeAliasDeclaration with TSTypeLiteral
216
+ if (target.type === 'TSTypeAliasDeclaration' && target.id && target.typeAnnotation) {
217
+ if (target.typeAnnotation.type === 'TSTypeLiteral') {
218
+ const className = target.id.name;
219
+ const fields = [];
220
+ for (const member of target.typeAnnotation.members) {
221
+ if (member.type === 'TSPropertySignature' && member.key) {
222
+ const name = member.key.name || member.key.value;
223
+ const type = member.typeAnnotation
224
+ ? typeToString(member.typeAnnotation.typeAnnotation)
225
+ : '\u2014';
226
+ if (name) fields.push({ name, type });
227
+ }
228
+ }
229
+ models.push({ className, fields });
230
+ }
231
+ }
232
+ }
233
+
234
+ return models;
235
+ }
@@ -0,0 +1,67 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * loadLanguagePlugins() → Array<plugin>
6
+ *
7
+ * Auto-discovers all .js files in src/extractors/languages/
8
+ * Validates each has: name (string), extensions (array), extract (function)
9
+ * Logs a warning and skips any plugin that fails validation or throws on require()
10
+ */
11
+ function loadLanguagePlugins() {
12
+ const pluginsDir = path.join(__dirname, 'languages');
13
+ const plugins = [];
14
+
15
+ let files;
16
+ try {
17
+ files = fs.readdirSync(pluginsDir).filter(f => f.endsWith('.js'));
18
+ } catch (err) {
19
+ console.warn(`[CARTO] Warning: Could not read plugins directory: ${err.message}`);
20
+ return plugins;
21
+ }
22
+
23
+ for (const file of files) {
24
+ const fullPath = path.join(pluginsDir, file);
25
+ try {
26
+ const plugin = require(fullPath);
27
+
28
+ // Validate plugin shape
29
+ if (typeof plugin.name !== 'string') {
30
+ console.warn(`[CARTO] Warning: Plugin ${file} missing 'name' (string) — skipping`);
31
+ continue;
32
+ }
33
+ if (!Array.isArray(plugin.extensions)) {
34
+ console.warn(`[CARTO] Warning: Plugin ${file} missing 'extensions' (array) — skipping`);
35
+ continue;
36
+ }
37
+ if (typeof plugin.extract !== 'function') {
38
+ console.warn(`[CARTO] Warning: Plugin ${file} missing 'extract' (function) — skipping`);
39
+ continue;
40
+ }
41
+
42
+ plugins.push(plugin);
43
+ } catch (err) {
44
+ console.warn(`[CARTO] Warning: Failed to load plugin ${file}: ${err.message} — skipping`);
45
+ }
46
+ }
47
+
48
+ return plugins;
49
+ }
50
+
51
+ /**
52
+ * getPluginForFile(plugins, filePath) → plugin | null
53
+ *
54
+ * Returns the first plugin whose extensions array includes the file's extension.
55
+ * Returns null if no plugin handles this file type.
56
+ */
57
+ function getPluginForFile(plugins, filePath) {
58
+ const ext = path.extname(filePath).toLowerCase();
59
+ for (const plugin of plugins) {
60
+ if (plugin.extensions.includes(ext)) {
61
+ return plugin;
62
+ }
63
+ }
64
+ return null;
65
+ }
66
+
67
+ module.exports = { loadLanguagePlugins, getPluginForFile };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Extracts Pydantic model class definitions — class name + fields with types.
3
+ * Skips method definitions (def ...) to avoid false positives.
4
+ * Strips inline # comments from field types.
5
+ */
6
+ function extractModels(content) {
7
+ const models = [];
8
+ const classPattern = /^class\s+(\w+)\s*\(.*BaseModel.*\)\s*:/gm;
9
+ const fieldPattern = /^\s{4}(\w+)\s*:\s*(.+?)(?:\s*=.*)?$/;
10
+
11
+ let classMatch;
12
+ while ((classMatch = classPattern.exec(content)) !== null) {
13
+ const className = classMatch[1];
14
+ const classStart = classMatch.index + classMatch[0].length;
15
+ const fields = [];
16
+
17
+ const remaining = content.substring(classStart);
18
+ const bodyLines = remaining.split('\n');
19
+
20
+ for (const line of bodyLines) {
21
+ // Stop at next top-level definition
22
+ if (/^class\s/.test(line) || (/^\S/.test(line) && line.trim() !== '')) {
23
+ break;
24
+ }
25
+ // Skip method definitions — prevents false-positive on `def validate_...(self):`
26
+ if (/^\s{4}def\s/.test(line)) {
27
+ continue;
28
+ }
29
+ const fieldMatch = line.match(fieldPattern);
30
+ if (fieldMatch) {
31
+ fields.push({ name: fieldMatch[1], type: fieldMatch[2].replace(/#.*$/, '').trim() });
32
+ }
33
+ }
34
+
35
+ models.push({ className, fields });
36
+ }
37
+ return models;
38
+ }
39
+
40
+ module.exports = { extractModels };
@@ -0,0 +1,66 @@
1
+ const fs = require('fs');
2
+
3
+ /**
4
+ * Joins multiline decorator expressions into single lines.
5
+ * Scans for lines starting with @ and, if parentheses are unbalanced,
6
+ * appends subsequent lines until they balance.
7
+ */
8
+ function collapseMultilineDecorators(content) {
9
+ const lines = content.split('\n');
10
+ const result = [];
11
+
12
+ for (let i = 0; i < lines.length; i++) {
13
+ if (/^\s*@/.test(lines[i])) {
14
+ let combined = lines[i];
15
+ let openParens = (combined.match(/\(/g) || []).length;
16
+ let closeParens = (combined.match(/\)/g) || []).length;
17
+
18
+ while (openParens > closeParens && i + 1 < lines.length) {
19
+ i++;
20
+ combined += ' ' + lines[i].trim();
21
+ openParens = (combined.match(/\(/g) || []).length;
22
+ closeParens = (combined.match(/\)/g) || []).length;
23
+ }
24
+ result.push(combined);
25
+ } else {
26
+ result.push(lines[i]);
27
+ }
28
+ }
29
+ return result.join('\n');
30
+ }
31
+
32
+ /**
33
+ * Extracts HTTP route definitions from FastAPI Python files.
34
+ * Handles @app.get/post/put/delete/patch and @router.get/post/put/delete/patch,
35
+ * including multiline decorators.
36
+ */
37
+ function extractRoutes(content) {
38
+ const routes = [];
39
+ const decoratorPattern = /@(?:app|router)\.(get|post|put|delete|patch)\s*\(\s*["']([^"']+)["']/gi;
40
+ const funcPattern = /(?:async\s+)?def\s+(\w+)/;
41
+
42
+ const collapsed = collapseMultilineDecorators(content);
43
+ const lines = collapsed.split('\n');
44
+
45
+ for (let i = 0; i < lines.length; i++) {
46
+ const match = decoratorPattern.exec(lines[i]);
47
+ if (match) {
48
+ // Look ahead up to 5 lines for the function definition
49
+ for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
50
+ const funcMatch = lines[j].match(funcPattern);
51
+ if (funcMatch) {
52
+ routes.push({
53
+ method: match[1].toUpperCase(),
54
+ path: match[2],
55
+ functionName: funcMatch[1]
56
+ });
57
+ break;
58
+ }
59
+ }
60
+ }
61
+ decoratorPattern.lastIndex = 0;
62
+ }
63
+ return routes;
64
+ }
65
+
66
+ module.exports = { extractRoutes, collapseMultilineDecorators };
@@ -0,0 +1,66 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const DEFAULT_IGNORE_PATTERNS = [
5
+ '.env',
6
+ '.env.*',
7
+ '*secret*',
8
+ '*SECRET*',
9
+ '*password*',
10
+ '*PASSWORD*',
11
+ '*credential*',
12
+ '*CREDENTIAL*',
13
+ '*private_key*',
14
+ '*PRIVATE_KEY*',
15
+ '*.pem',
16
+ '*.key'
17
+ ];
18
+
19
+ /**
20
+ * parseCartoIgnore(projectRoot) → isIgnored(filePath) → boolean
21
+ *
22
+ * Reads .cartoignore from the project root (if it exists) and merges with defaults.
23
+ * Returns a function that checks if a file path matches any ignore pattern.
24
+ */
25
+ function parseCartoIgnore(projectRoot) {
26
+ let userPatterns = [];
27
+
28
+ const ignoreFile = path.join(projectRoot, '.cartoignore');
29
+ try {
30
+ const content = fs.readFileSync(ignoreFile, 'utf-8');
31
+ userPatterns = content
32
+ .split('\n')
33
+ .map(line => line.trim())
34
+ .filter(line => line && !line.startsWith('#'));
35
+ } catch {
36
+ // No .cartoignore file — that's fine, use defaults only
37
+ }
38
+
39
+ const allPatterns = [...DEFAULT_IGNORE_PATTERNS, ...userPatterns];
40
+
41
+ return function isIgnored(filePath) {
42
+ const basename = path.basename(filePath);
43
+ const relativePath = filePath; // can be absolute or relative
44
+
45
+ for (const pattern of allPatterns) {
46
+ if (matchPattern(basename, pattern) || matchPattern(relativePath, pattern)) {
47
+ return true;
48
+ }
49
+ }
50
+ return false;
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Simple glob matching supporting * as wildcard.
56
+ */
57
+ function matchPattern(str, pattern) {
58
+ // Escape regex special chars except *
59
+ const regexStr = pattern
60
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
61
+ .replace(/\*/g, '.*');
62
+ const regex = new RegExp(`^${regexStr}$`, 'i');
63
+ return regex.test(str);
64
+ }
65
+
66
+ module.exports = { parseCartoIgnore, DEFAULT_IGNORE_PATTERNS };