carto-md 1.0.17 → 1.0.19
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/README.md +13 -12
- package/package.json +1 -1
- package/src/cli/init.js +2 -0
- package/src/detector/files.js +15 -0
- package/src/detector/framework.js +58 -13
- package/src/extractors/imports.js +32 -0
- package/src/extractors/languages/r.js +255 -0
- package/src/extractors/routes.js +1 -0
package/README.md
CHANGED
|
@@ -135,14 +135,13 @@ npx carto-md init
|
|
|
135
135
|
# 1. Go to your project
|
|
136
136
|
cd your-project
|
|
137
137
|
|
|
138
|
-
# 2.
|
|
138
|
+
# 2. Run once — like git init
|
|
139
139
|
carto init
|
|
140
|
-
|
|
141
|
-
# 3. Keep it live while you work
|
|
142
|
-
carto watch
|
|
143
140
|
```
|
|
144
141
|
|
|
145
|
-
|
|
142
|
+
That's it. Carto installs a git hook. Every `git commit` syncs AGENTS.md automatically — no watching, no manual runs, nothing to remember.
|
|
143
|
+
|
|
144
|
+
Want live updates on every file save too? Run `carto watch` in a background terminal.
|
|
146
145
|
|
|
147
146
|
---
|
|
148
147
|
|
|
@@ -150,16 +149,17 @@ Leave `carto watch` running in a background terminal. Every file save updates AG
|
|
|
150
149
|
|
|
151
150
|
| Command | What it does |
|
|
152
151
|
|---------|-------------|
|
|
153
|
-
| `carto init` | Detect stack, generate AGENTS.md, install git hook |
|
|
154
|
-
| `carto watch` |
|
|
155
|
-
| `carto sync` | One-time refresh
|
|
152
|
+
| `carto init` | Detect stack, generate AGENTS.md, install git hook — auto-syncs on every commit |
|
|
153
|
+
| `carto watch` | Live updates on every file save — optional, for between commits |
|
|
154
|
+
| `carto sync` | One-time manual refresh |
|
|
156
155
|
| `carto impact <file>` | Show blast radius before touching a file |
|
|
156
|
+
| `carto remove` | Remove AGENTS.md and .carto/ from this project |
|
|
157
157
|
| `carto --version` | Show version |
|
|
158
158
|
|
|
159
159
|
**When to use each:**
|
|
160
|
-
- `init` — once
|
|
161
|
-
- `watch` —
|
|
162
|
-
- `sync` — skipped watch and need a fresh snapshot
|
|
160
|
+
- `init` — once per project, sets everything up
|
|
161
|
+
- `watch` — optional, if you want updates between commits
|
|
162
|
+
- `sync` — if you skipped watch and need a fresh snapshot
|
|
163
163
|
- `impact` — before editing anything critical
|
|
164
164
|
|
|
165
165
|
---
|
|
@@ -171,6 +171,7 @@ Leave `carto watch` running in a background terminal. Every file save updates AG
|
|
|
171
171
|
| Python | FastAPI, Pydantic |
|
|
172
172
|
| JavaScript | Express, Next.js |
|
|
173
173
|
| TypeScript | Express, Next.js, Prisma |
|
|
174
|
+
| R | Plumber, Shiny |
|
|
174
175
|
| HTML | fetch() calls |
|
|
175
176
|
|
|
176
177
|
More languages via community — open an issue or see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
@@ -248,7 +249,7 @@ Carto never writes secrets into AGENTS.md. `.cartoignore` blocks `.env` files, s
|
|
|
248
249
|
|
|
249
250
|
## Contributing
|
|
250
251
|
|
|
251
|
-
Python
|
|
252
|
+
Python, JS/TS, and R today. Want Go, Ruby, Django, Rails? Open an issue — or read [CONTRIBUTING.md](CONTRIBUTING.md) to add it yourself.
|
|
252
253
|
|
|
253
254
|
---
|
|
254
255
|
|
package/package.json
CHANGED
package/src/cli/init.js
CHANGED
|
@@ -17,11 +17,13 @@ async function run(projectRoot) {
|
|
|
17
17
|
// Count files for reporting
|
|
18
18
|
const pyCount = files.routeFiles.filter(f => f.endsWith('.py')).length;
|
|
19
19
|
const jsCount = files.routeFiles.filter(f => /\.(js|ts|jsx|tsx)$/.test(f)).length;
|
|
20
|
+
const rCount = files.routeFiles.filter(f => /\.[rR]$/.test(f)).length;
|
|
20
21
|
const htmlCount = files.frontendFiles.length;
|
|
21
22
|
|
|
22
23
|
const parts = [];
|
|
23
24
|
if (pyCount > 0) parts.push(`${pyCount} Python files`);
|
|
24
25
|
if (jsCount > 0) parts.push(`${jsCount} JS/TS files`);
|
|
26
|
+
if (rCount > 0) parts.push(`${rCount} R files`);
|
|
25
27
|
if (htmlCount > 0) parts.push(`${htmlCount} HTML files`);
|
|
26
28
|
console.log(`[CARTO] Found ${parts.join(', ') || '0 files'}`);
|
|
27
29
|
|
package/src/detector/files.js
CHANGED
|
@@ -9,6 +9,7 @@ const BASE_UTILITY_BUDGET = 20;
|
|
|
9
9
|
const PYTHON_IGNORE = new Set(['__pycache__', '.venv', 'venv', 'migrations', 'node_modules', '.git', '.carto']);
|
|
10
10
|
const JS_IGNORE = new Set(['node_modules', '.git', 'dist', 'build', '.carto', '.next', '.turbo', 'coverage', 'out', '.cache', 'generated', '__generated__', 'storybook-static', 'public', 'static']);
|
|
11
11
|
const HTML_IGNORE = new Set(['node_modules', '.git', '.carto']);
|
|
12
|
+
const R_IGNORE = new Set(['.Rhistory', '.RData', 'packrat', 'renv', 'node_modules', '.git', '__pycache__', '.carto']);
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* discoverFiles(projectRoot, framework, isIgnored, secondaryFramework) → { routeFiles, modelFiles, frontendFiles }
|
|
@@ -60,6 +61,20 @@ function discoverForFramework(projectRoot, framework, ignoreFn) {
|
|
|
60
61
|
return smartSelect(jsFiles, htmlFiles, projectRoot);
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
if (['plumber', 'shiny', 'r-generic'].includes(framework)) {
|
|
65
|
+
const rFiles = findFilesRecursive(projectRoot, ['.r'], R_IGNORE, ignoreFn)
|
|
66
|
+
.filter(f => {
|
|
67
|
+
const lbase = path.basename(f).toLowerCase();
|
|
68
|
+
return !lbase.startsWith('test_') && !lbase.startsWith('test-') && !lbase.endsWith('_test.r');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (rFiles.length <= MAX_FILES_TOTAL) {
|
|
72
|
+
return { routeFiles: rFiles, modelFiles: rFiles, frontendFiles: [] };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return smartSelect(rFiles, [], projectRoot);
|
|
76
|
+
}
|
|
77
|
+
|
|
63
78
|
// Unknown framework
|
|
64
79
|
const allCode = findFilesRecursive(projectRoot, ['.py', '.js', '.ts'], new Set(['node_modules', '.git', '__pycache__', '.venv', 'venv', '.carto']), ignoreFn);
|
|
65
80
|
const htmlFiles = findFilesRecursive(projectRoot, ['.html'], HTML_IGNORE, ignoreFn);
|
|
@@ -6,6 +6,7 @@ const IGNORE_DIRS = new Set(['node_modules', '.git', '__pycache__', '.venv', 've
|
|
|
6
6
|
// Priority order: lower index = higher priority
|
|
7
7
|
const PYTHON_PRIORITY = ['fastapi', 'django', 'flask', 'python-generic'];
|
|
8
8
|
const JS_PRIORITY = ['nextjs', 'express', 'react', 'node-generic'];
|
|
9
|
+
const R_PRIORITY = ['plumber', 'shiny', 'r-generic'];
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* detectFramework(projectRoot) → { framework, language, confidence, secondaryFramework?, secondaryLanguage? }
|
|
@@ -16,36 +17,37 @@ const JS_PRIORITY = ['nextjs', 'express', 'react', 'node-generic'];
|
|
|
16
17
|
* (primary = highest priority overall, secondary = the other language).
|
|
17
18
|
*/
|
|
18
19
|
function detectFramework(projectRoot) {
|
|
19
|
-
const candidates = findFile(projectRoot, ['requirements.txt', 'package.json', 'pyproject.toml'], 3);
|
|
20
|
+
const candidates = findFile(projectRoot, ['requirements.txt', 'package.json', 'pyproject.toml', 'DESCRIPTION'], 3);
|
|
20
21
|
|
|
21
22
|
const pythonDetections = new Set();
|
|
22
23
|
const jsDetections = new Set();
|
|
24
|
+
const rDetections = new Set();
|
|
23
25
|
|
|
24
|
-
// Check requirements.txt
|
|
25
26
|
for (const f of candidates.filter(f => path.basename(f) === 'requirements.txt')) {
|
|
26
|
-
const
|
|
27
|
-
for (const r of results) pythonDetections.add(r);
|
|
27
|
+
for (const r of detectAllFromPythonDeps(f)) pythonDetections.add(r);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
// Check pyproject.toml
|
|
31
30
|
for (const f of candidates.filter(f => path.basename(f) === 'pyproject.toml')) {
|
|
32
|
-
const
|
|
33
|
-
for (const r of results) pythonDetections.add(r);
|
|
31
|
+
for (const r of detectAllFromPythonDeps(f)) pythonDetections.add(r);
|
|
34
32
|
}
|
|
35
33
|
|
|
36
|
-
// Check package.json
|
|
37
34
|
for (const f of candidates.filter(f => path.basename(f) === 'package.json')) {
|
|
38
|
-
const
|
|
39
|
-
|
|
35
|
+
for (const r of detectAllFromPackageJson(f)) jsDetections.add(r);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const f of candidates.filter(f => path.basename(f) === 'DESCRIPTION')) {
|
|
39
|
+
for (const r of detectAllFromRDescription(f)) rDetections.add(r);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (rDetections.size === 0) {
|
|
43
|
+
for (const r of detectAllFromRFiles(projectRoot)) rDetections.add(r);
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
// Pick best Python framework by priority
|
|
43
46
|
const bestPython = PYTHON_PRIORITY.find(fw => pythonDetections.has(fw)) || null;
|
|
44
|
-
// Pick best JS framework by priority
|
|
45
47
|
const bestJS = JS_PRIORITY.find(fw => jsDetections.has(fw)) || null;
|
|
48
|
+
const bestR = R_PRIORITY.find(fw => rDetections.has(fw)) || null;
|
|
46
49
|
|
|
47
50
|
if (bestPython && bestJS) {
|
|
48
|
-
// Both detected — Python is primary (higher priority in the global list)
|
|
49
51
|
return {
|
|
50
52
|
framework: bestPython,
|
|
51
53
|
language: 'python',
|
|
@@ -63,6 +65,10 @@ function detectFramework(projectRoot) {
|
|
|
63
65
|
return { framework: bestJS, language: 'javascript', confidence: 'high' };
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
if (bestR) {
|
|
69
|
+
return { framework: bestR, language: 'r', confidence: 'high' };
|
|
70
|
+
}
|
|
71
|
+
|
|
66
72
|
return { framework: 'unknown', language: 'unknown', confidence: 'none' };
|
|
67
73
|
}
|
|
68
74
|
|
|
@@ -135,4 +141,43 @@ function detectAllFromPackageJson(filePath) {
|
|
|
135
141
|
return detected;
|
|
136
142
|
}
|
|
137
143
|
|
|
144
|
+
function detectAllFromRDescription(filePath) {
|
|
145
|
+
const detected = [];
|
|
146
|
+
let content;
|
|
147
|
+
try {
|
|
148
|
+
content = fs.readFileSync(filePath, 'utf-8').toLowerCase();
|
|
149
|
+
} catch {
|
|
150
|
+
return detected;
|
|
151
|
+
}
|
|
152
|
+
if (content.includes('plumber')) detected.push('plumber');
|
|
153
|
+
if (content.includes('shiny')) detected.push('shiny');
|
|
154
|
+
if (!detected.length) detected.push('r-generic');
|
|
155
|
+
return detected;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function detectAllFromRFiles(projectRoot) {
|
|
159
|
+
const detected = [];
|
|
160
|
+
let files;
|
|
161
|
+
try {
|
|
162
|
+
files = fs.readdirSync(projectRoot).filter(f => f.endsWith('.R') || f.endsWith('.r'));
|
|
163
|
+
} catch {
|
|
164
|
+
return detected;
|
|
165
|
+
}
|
|
166
|
+
if (!files.length) return detected;
|
|
167
|
+
|
|
168
|
+
for (const file of files.slice(0, 5)) {
|
|
169
|
+
let content;
|
|
170
|
+
try {
|
|
171
|
+
content = fs.readFileSync(path.join(projectRoot, file), 'utf-8').toLowerCase();
|
|
172
|
+
} catch {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (/library\s*\(\s*["']?plumber["']?\s*\)/.test(content)) detected.push('plumber');
|
|
176
|
+
if (/library\s*\(\s*["']?shiny["']?\s*\)/.test(content)) detected.push('shiny');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!detected.length) detected.push('r-generic');
|
|
180
|
+
return detected;
|
|
181
|
+
}
|
|
182
|
+
|
|
138
183
|
module.exports = { detectFramework };
|
|
@@ -20,6 +20,10 @@ const fs = require('fs');
|
|
|
20
20
|
* from app.module import X (local package — resolved if file exists)
|
|
21
21
|
* import .module (relative)
|
|
22
22
|
*
|
|
23
|
+
* R patterns:
|
|
24
|
+
* library(pkg) / require(pkg) (package name recorded as-is)
|
|
25
|
+
* source("./file.R") (resolved if file exists)
|
|
26
|
+
*
|
|
23
27
|
* Only includes paths that resolve to actual files in the project.
|
|
24
28
|
* Skips: node_modules, non-code files, anything that doesn't resolve.
|
|
25
29
|
*/
|
|
@@ -33,6 +37,8 @@ function extractImports(content, filePath, projectRoot) {
|
|
|
33
37
|
rawImports = extractJSImports(content);
|
|
34
38
|
} else if (ext === '.py') {
|
|
35
39
|
rawImports = extractPythonImports(content, filePath, projectRoot);
|
|
40
|
+
} else if (ext === '.r') {
|
|
41
|
+
return extractRImports(content, filePath, projectRoot);
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
// Resolve and deduplicate
|
|
@@ -72,6 +78,32 @@ function extractJSImports(content) {
|
|
|
72
78
|
return imports;
|
|
73
79
|
}
|
|
74
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Extract imports from R content.
|
|
83
|
+
* library(pkg) / require(pkg) → package name recorded directly.
|
|
84
|
+
* source("./file.R") → resolved relative path if the file exists.
|
|
85
|
+
*/
|
|
86
|
+
function extractRImports(content, filePath, projectRoot) {
|
|
87
|
+
const results = new Set();
|
|
88
|
+
const fileDir = path.dirname(filePath);
|
|
89
|
+
|
|
90
|
+
const pkgRe = /(?:library|require)\s*\(\s*["']?(\w[\w.]+)["']?\s*\)/g;
|
|
91
|
+
let m;
|
|
92
|
+
while ((m = pkgRe.exec(content)) !== null) {
|
|
93
|
+
results.add(m[1]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const sourceRe = /source\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
97
|
+
while ((m = sourceRe.exec(content)) !== null) {
|
|
98
|
+
const abs = path.resolve(fileDir, m[1]);
|
|
99
|
+
if (fs.existsSync(abs)) {
|
|
100
|
+
results.add(path.relative(projectRoot, abs));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return [...results].sort();
|
|
105
|
+
}
|
|
106
|
+
|
|
75
107
|
/**
|
|
76
108
|
* Extract import paths from Python content. Relative imports only.
|
|
77
109
|
*/
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
function pathToIdentifier(routePath) {
|
|
2
|
+
const segments = routePath.split('/').filter(Boolean);
|
|
3
|
+
const last = segments[segments.length - 1] || 'handler';
|
|
4
|
+
return last.replace(/[^a-zA-Z0-9_]/g, '_').replace(/^(\d)/, '_$1') || 'handler';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function descToIdentifier(desc) {
|
|
8
|
+
const words = desc.trim().split(/\s+/);
|
|
9
|
+
if (words.length === 1 && /^[a-zA-Z_]\w*$/.test(words[0])) return words[0];
|
|
10
|
+
return words
|
|
11
|
+
.map((w, i) => i === 0 ? w.toLowerCase() : w[0].toUpperCase() + w.slice(1).toLowerCase())
|
|
12
|
+
.join('')
|
|
13
|
+
.replace(/[^a-zA-Z0-9_]/g, '')
|
|
14
|
+
.replace(/^(\d)/, '_$1') || null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function extractRoutes(content) {
|
|
18
|
+
const routes = [];
|
|
19
|
+
const lines = content.split('\n');
|
|
20
|
+
const routePattern = /^#\*\s+@(get|post|put|delete|patch|options|head)\s+(\S+)/i;
|
|
21
|
+
const descPattern = /^#\*\s+(?!@)(.+)/;
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < lines.length; i++) {
|
|
24
|
+
const routeMatch = lines[i].match(routePattern);
|
|
25
|
+
if (!routeMatch) continue;
|
|
26
|
+
|
|
27
|
+
const method = routeMatch[1].toUpperCase();
|
|
28
|
+
const routePath = routeMatch[2].trim();
|
|
29
|
+
|
|
30
|
+
let functionName = pathToIdentifier(routePath);
|
|
31
|
+
for (let j = i - 1; j >= Math.max(0, i - 10); j--) {
|
|
32
|
+
const line = lines[j].trim();
|
|
33
|
+
if (!line.startsWith('#')) break;
|
|
34
|
+
const descMatch = line.match(descPattern);
|
|
35
|
+
if (descMatch) {
|
|
36
|
+
const derived = descToIdentifier(descMatch[1]);
|
|
37
|
+
if (derived) functionName = derived;
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
routes.push({ method, path: routePath, functionName });
|
|
43
|
+
}
|
|
44
|
+
return routes;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function extractFunctions(content) {
|
|
48
|
+
const functions = [];
|
|
49
|
+
const lines = content.split('\n');
|
|
50
|
+
|
|
51
|
+
for (let i = 0; i < lines.length; i++) {
|
|
52
|
+
const line = lines[i];
|
|
53
|
+
if (!/^\w+\s*<-\s*function\s*\(/.test(line)) continue;
|
|
54
|
+
|
|
55
|
+
const nameMatch = line.match(/^(\w+)\s*<-\s*function\s*\(/);
|
|
56
|
+
if (!nameMatch) continue;
|
|
57
|
+
|
|
58
|
+
const name = nameMatch[1];
|
|
59
|
+
if (name.startsWith('.')) continue;
|
|
60
|
+
|
|
61
|
+
let combined = line;
|
|
62
|
+
let depth = 0;
|
|
63
|
+
for (const ch of line) {
|
|
64
|
+
if (ch === '(') depth++;
|
|
65
|
+
else if (ch === ')') depth--;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let safety = 0;
|
|
69
|
+
while (depth > 0 && safety < 15 && i + 1 < lines.length) {
|
|
70
|
+
i++;
|
|
71
|
+
safety++;
|
|
72
|
+
const next = lines[i].trim();
|
|
73
|
+
combined += ' ' + next;
|
|
74
|
+
for (const ch of next) {
|
|
75
|
+
if (ch === '(') depth++;
|
|
76
|
+
else if (ch === ')') depth--;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const paramMatch = combined.match(/function\s*\(([^)]*)\)/);
|
|
81
|
+
if (!paramMatch) continue;
|
|
82
|
+
|
|
83
|
+
const params = paramMatch[1]
|
|
84
|
+
.split(',')
|
|
85
|
+
.map(p => p.split('=')[0].trim())
|
|
86
|
+
.filter(p => p.length > 0)
|
|
87
|
+
.join(', ');
|
|
88
|
+
|
|
89
|
+
functions.push({ name, params: params || '—', returnType: '—' });
|
|
90
|
+
}
|
|
91
|
+
return functions;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function collapseParens(content) {
|
|
95
|
+
const lines = content.split('\n');
|
|
96
|
+
const result = [];
|
|
97
|
+
let i = 0;
|
|
98
|
+
while (i < lines.length) {
|
|
99
|
+
let depth = 0;
|
|
100
|
+
for (const ch of lines[i]) {
|
|
101
|
+
if (ch === '(') depth++;
|
|
102
|
+
else if (ch === ')') depth--;
|
|
103
|
+
}
|
|
104
|
+
if (depth <= 0) {
|
|
105
|
+
result.push(lines[i]);
|
|
106
|
+
i++;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
let combined = lines[i];
|
|
110
|
+
let safety = 0;
|
|
111
|
+
while (depth > 0 && safety < 20 && i + 1 < lines.length) {
|
|
112
|
+
i++;
|
|
113
|
+
safety++;
|
|
114
|
+
const next = lines[i].trim();
|
|
115
|
+
combined += ' ' + next;
|
|
116
|
+
for (const ch of next) {
|
|
117
|
+
if (ch === '(') depth++;
|
|
118
|
+
else if (ch === ')') depth--;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
result.push(combined);
|
|
122
|
+
i++;
|
|
123
|
+
}
|
|
124
|
+
return result.join('\n');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function findBalancedEnd(str, openPos) {
|
|
128
|
+
let depth = 1;
|
|
129
|
+
let pos = openPos + 1;
|
|
130
|
+
while (depth > 0 && pos < str.length) {
|
|
131
|
+
if (str[pos] === '(') depth++;
|
|
132
|
+
else if (str[pos] === ')') depth--;
|
|
133
|
+
pos++;
|
|
134
|
+
}
|
|
135
|
+
return pos - 1;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function extractModels(content) {
|
|
139
|
+
const models = [];
|
|
140
|
+
const collapsed = collapseParens(content);
|
|
141
|
+
|
|
142
|
+
const setClassRe = /setClass\s*\(\s*["'](\w+)["']/g;
|
|
143
|
+
let m;
|
|
144
|
+
while ((m = setClassRe.exec(collapsed)) !== null) {
|
|
145
|
+
const className = m[1];
|
|
146
|
+
const slotsIdx = collapsed.indexOf('slots', m.index);
|
|
147
|
+
if (slotsIdx === -1 || slotsIdx > m.index + 300) continue;
|
|
148
|
+
const listIdx = collapsed.indexOf('list(', slotsIdx);
|
|
149
|
+
if (listIdx === -1) continue;
|
|
150
|
+
const openPos = listIdx + 4;
|
|
151
|
+
const closePos = findBalancedEnd(collapsed, openPos);
|
|
152
|
+
const slotsContent = collapsed.slice(openPos + 1, closePos);
|
|
153
|
+
const fields = [];
|
|
154
|
+
const slotRe = /(\w+)\s*=\s*["']([^"']+)["']/g;
|
|
155
|
+
let sm;
|
|
156
|
+
while ((sm = slotRe.exec(slotsContent)) !== null) {
|
|
157
|
+
fields.push({ name: sm[1], type: sm[2] });
|
|
158
|
+
}
|
|
159
|
+
models.push({ className, fields });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const dfRe = /^(\w+)\s*<-\s*data\.frame\s*\(/gm;
|
|
163
|
+
while ((m = dfRe.exec(collapsed)) !== null) {
|
|
164
|
+
const className = m[1];
|
|
165
|
+
const openPos = m.index + m[0].length - 1;
|
|
166
|
+
const closePos = findBalancedEnd(collapsed, openPos);
|
|
167
|
+
const innerContent = collapsed.slice(openPos + 1, closePos);
|
|
168
|
+
const fields = [];
|
|
169
|
+
const colRe = /\b(\w+)\s*=\s*(\w+)\s*\(/g;
|
|
170
|
+
let cm;
|
|
171
|
+
while ((cm = colRe.exec(innerContent)) !== null) {
|
|
172
|
+
fields.push({ name: cm[1], type: cm[2] });
|
|
173
|
+
}
|
|
174
|
+
if (fields.length > 0) {
|
|
175
|
+
models.push({ className, fields });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const r6Re = /^(\w+)\s*<-\s*(?:R6::)?R6Class\s*\(\s*["'](\w+)["']/gm;
|
|
180
|
+
while ((m = r6Re.exec(collapsed)) !== null) {
|
|
181
|
+
const className = m[2];
|
|
182
|
+
const publicIdx = collapsed.indexOf('public', m.index);
|
|
183
|
+
if (publicIdx === -1 || publicIdx > m.index + 600) continue;
|
|
184
|
+
const listIdx = collapsed.indexOf('list(', publicIdx);
|
|
185
|
+
if (listIdx === -1) continue;
|
|
186
|
+
const openPos = listIdx + 4;
|
|
187
|
+
const closePos = findBalancedEnd(collapsed, openPos);
|
|
188
|
+
const publicContent = collapsed.slice(openPos + 1, closePos);
|
|
189
|
+
const fields = [];
|
|
190
|
+
const fieldRe = /\b(\w+)\s*=\s*(?!function\s*\()(\w+|["'][^"']*["'])/g;
|
|
191
|
+
let fm;
|
|
192
|
+
while ((fm = fieldRe.exec(publicContent)) !== null) {
|
|
193
|
+
const name = fm[1];
|
|
194
|
+
const rawVal = fm[2].trim();
|
|
195
|
+
let type = 'any';
|
|
196
|
+
if (/^["']/.test(rawVal)) type = 'character';
|
|
197
|
+
else if (/^\d/.test(rawVal)) type = 'numeric';
|
|
198
|
+
else if (rawVal === 'TRUE' || rawVal === 'FALSE') type = 'logical';
|
|
199
|
+
fields.push({ name, type });
|
|
200
|
+
}
|
|
201
|
+
models.push({ className, fields });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const s7Re = /^(\w+)\s*<-\s*(?:S7::)?new_class\s*\(\s*(?:name\s*=\s*)?["'](\w+)["']/gm;
|
|
205
|
+
while ((m = s7Re.exec(collapsed)) !== null) {
|
|
206
|
+
const className = m[2];
|
|
207
|
+
const propsIdx = collapsed.indexOf('properties', m.index);
|
|
208
|
+
if (propsIdx === -1 || propsIdx > m.index + 400) continue;
|
|
209
|
+
const listIdx = collapsed.indexOf('list(', propsIdx);
|
|
210
|
+
if (listIdx === -1) continue;
|
|
211
|
+
const openPos = listIdx + 4;
|
|
212
|
+
const closePos = findBalancedEnd(collapsed, openPos);
|
|
213
|
+
const propsContent = collapsed.slice(openPos + 1, closePos);
|
|
214
|
+
const fields = [];
|
|
215
|
+
const propRe = /(\w+)\s*=\s*(?:S7::)?class_(\w+)/g;
|
|
216
|
+
let pm;
|
|
217
|
+
while ((pm = propRe.exec(propsContent)) !== null) {
|
|
218
|
+
fields.push({ name: pm[1], type: pm[2] });
|
|
219
|
+
}
|
|
220
|
+
models.push({ className, fields });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return models;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function extractEnvVars(content) {
|
|
227
|
+
const vars = new Set();
|
|
228
|
+
const re = /Sys\.getenv\s*\(\s*["']([A-Z_][A-Z0-9_]*)["']\s*\)/g;
|
|
229
|
+
let m;
|
|
230
|
+
while ((m = re.exec(content)) !== null) {
|
|
231
|
+
vars.add(m[1]);
|
|
232
|
+
}
|
|
233
|
+
return [...vars].sort();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports = {
|
|
237
|
+
name: 'r',
|
|
238
|
+
extensions: ['.r', '.R'],
|
|
239
|
+
extract(content, filename) {
|
|
240
|
+
try {
|
|
241
|
+
return {
|
|
242
|
+
routes: extractRoutes(content),
|
|
243
|
+
models: extractModels(content),
|
|
244
|
+
functions: extractFunctions(content),
|
|
245
|
+
envVars: extractEnvVars(content),
|
|
246
|
+
dbTables: [],
|
|
247
|
+
fetches: [],
|
|
248
|
+
storageKeys: [],
|
|
249
|
+
};
|
|
250
|
+
} catch (err) {
|
|
251
|
+
console.warn(`[CARTO] r plugin error on ${filename}: ${err.message}`);
|
|
252
|
+
return { routes: [], models: [], functions: [], envVars: [], dbTables: [], fetches: [], storageKeys: [] };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
};
|
package/src/extractors/routes.js
CHANGED
|
@@ -43,6 +43,7 @@ function extractRoutes(content) {
|
|
|
43
43
|
const lines = collapsed.split('\n');
|
|
44
44
|
|
|
45
45
|
for (let i = 0; i < lines.length; i++) {
|
|
46
|
+
if (/^\s*#/.test(lines[i])) { decoratorPattern.lastIndex = 0; continue; }
|
|
46
47
|
const match = decoratorPattern.exec(lines[i]);
|
|
47
48
|
if (match) {
|
|
48
49
|
// Look ahead up to 5 lines for the function definition
|