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 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. Generate AGENTS.md (run once)
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
- Leave `carto watch` running in a background terminal. Every file save updates AGENTS.md automatically.
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` | Watch files, update AGENTS.md on every save |
155
- | `carto sync` | One-time refresh, no watcher |
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, when you add Carto to a project
161
- - `watch` — every work session, leave it running
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 and JS/TS today. Want Go, Ruby, Django, Rails? Open an issue — or read [CONTRIBUTING.md](CONTRIBUTING.md) to add it yourself.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carto-md",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
4
4
  "description": "The context layer for AI-native development.",
5
5
  "bin": {
6
6
  "carto": "src/cli/index.js"
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
 
@@ -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 results = detectAllFromPythonDeps(f);
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 results = detectAllFromPythonDeps(f);
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 results = detectAllFromPackageJson(f);
39
- for (const r of results) jsDetections.add(r);
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
+ };
@@ -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