@dereekb/dbx-cli 13.15.0 → 13.17.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.
- package/eslint/index.cjs.default.js +1 -0
- package/eslint/index.cjs.js +1050 -0
- package/eslint/index.cjs.mjs +2 -0
- package/eslint/index.d.ts +1 -0
- package/eslint/index.esm.js +1046 -0
- package/eslint/package.json +25 -0
- package/eslint/rollup.alias-internal.config.d.ts +11 -0
- package/eslint/src/index.d.ts +1 -0
- package/eslint/src/lib/index.d.ts +2 -0
- package/eslint/src/lib/plugin.d.ts +22 -0
- package/eslint/src/lib/valid-dbx-route-model-tags.rule.d.ts +59 -0
- package/firebase-api-manifest/main.js +318 -228
- package/firebase-api-manifest/package.json +3 -3
- package/generate-firestore-indexes/main.js +37 -24
- package/generate-firestore-indexes/package.json +2 -2
- package/generate-mcp-manifest/main.js +57 -39
- package/generate-mcp-manifest/package.json +3 -3
- package/generate-route-manifest/main.js +1137 -0
- package/generate-route-manifest/package.json +10 -0
- package/index.cjs.js +4847 -1953
- package/index.esm.js +4827 -1954
- package/lint-cache/package.json +2 -2
- package/manifest-extract/index.cjs.js +175 -240
- package/manifest-extract/index.esm.js +174 -239
- package/manifest-extract/package.json +9 -4
- package/package.json +16 -6
- package/src/lib/index.d.ts +2 -0
- package/src/lib/manifest/types.d.ts +53 -0
- package/src/lib/mcp-scan/manifest/package-root.d.ts +17 -0
- package/src/lib/mcp-scan/manifest/tokens-schema.d.ts +5 -4
- package/src/lib/mcp-scan/scan/extract-models/assemble.d.ts +17 -0
- package/src/lib/route/component-resolve.d.ts +48 -0
- package/src/lib/route/index.d.ts +18 -0
- package/src/lib/route/route-build-tree.d.ts +31 -0
- package/src/lib/route/route-extract.d.ts +46 -0
- package/src/lib/route/route-load-tree.d.ts +17 -0
- package/src/lib/route/route-manifest.d.ts +132 -0
- package/src/lib/route/route-model-tag.d.ts +89 -0
- package/src/lib/route/route-models-extract.d.ts +22 -0
- package/src/lib/route/route-resolve-sources.d.ts +39 -0
- package/src/lib/route/route-types.d.ts +136 -0
- package/src/lib/route/url-match.d.ts +116 -0
- package/src/lib/scan-helpers/firestore-model-extract-utils.d.ts +43 -0
- package/test/index.cjs.js +1 -1
- package/test/index.esm.js +1 -1
- package/test/package.json +9 -9
|
@@ -0,0 +1,1046 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the JSDoc Block comment immediately preceding `anchor`, or `null` when
|
|
3
|
+
* the anchor has no JSDoc leader. Used by the `@dbx<Family>` companion-tag rules
|
|
4
|
+
* to locate the tagged declaration's documentation.
|
|
5
|
+
*
|
|
6
|
+
* @param sourceCode - The ESLint `SourceCode` object.
|
|
7
|
+
* @param anchor - The statement-level node ESLint attaches leading comments to.
|
|
8
|
+
* @returns The JSDoc block comment, or null when none is present.
|
|
9
|
+
*/ function leadingJsdocFor(sourceCode, anchor) {
|
|
10
|
+
var comments = sourceCode.getCommentsBefore(anchor) || [];
|
|
11
|
+
var result = null;
|
|
12
|
+
var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
|
|
13
|
+
try {
|
|
14
|
+
for(var _iterator = comments[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
|
|
15
|
+
var comment = _step.value;
|
|
16
|
+
if (comment.type === 'Block' && typeof comment.value === 'string' && comment.value.startsWith('*')) {
|
|
17
|
+
result = comment;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
} catch (err) {
|
|
21
|
+
_didIteratorError = true;
|
|
22
|
+
_iteratorError = err;
|
|
23
|
+
} finally{
|
|
24
|
+
try {
|
|
25
|
+
if (!_iteratorNormalCompletion && _iterator.return != null) {
|
|
26
|
+
_iterator.return();
|
|
27
|
+
}
|
|
28
|
+
} finally{
|
|
29
|
+
if (_didIteratorError) {
|
|
30
|
+
throw _iteratorError;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Default maximum positional parameters before the warn-level rule suggests a config object.
|
|
39
|
+
* Triggers a warning when a function has more than 2 positional parameters (i.e. 3+ args).
|
|
40
|
+
*/ var DEFAULT_MAX_PARAMS_WARN = 2;
|
|
41
|
+
/**
|
|
42
|
+
* Default maximum positional parameters before the hard-error rule rejects the signature.
|
|
43
|
+
* Triggers an error when a function has more than 4 positional parameters (i.e. 5+ args).
|
|
44
|
+
*/ var DEFAULT_MAX_PARAMS_HARD = 4;
|
|
45
|
+
/**
|
|
46
|
+
* Default JSDoc tag that opts a function out of this rule.
|
|
47
|
+
*/ var DEFAULT_ALLOW_JSDOC_TAG = '@dbxAllowMultiParams';
|
|
48
|
+
/**
|
|
49
|
+
* Returns a human-readable display name for the function-like node, or `<anonymous>`.
|
|
50
|
+
*
|
|
51
|
+
* @param node - The function-like AST node.
|
|
52
|
+
* @returns The identifier string used in diagnostic messages.
|
|
53
|
+
*/ function getFunctionDisplayName(node) {
|
|
54
|
+
var _node_id;
|
|
55
|
+
var name = '<anonymous>';
|
|
56
|
+
if (((_node_id = node.id) === null || _node_id === void 0 ? void 0 : _node_id.type) === 'Identifier') {
|
|
57
|
+
name = node.id.name;
|
|
58
|
+
} else if (node.parent) {
|
|
59
|
+
var _parent_id, _parent_key, _parent_key1, _parent_left;
|
|
60
|
+
var parent = node.parent;
|
|
61
|
+
if (parent.type === 'VariableDeclarator' && ((_parent_id = parent.id) === null || _parent_id === void 0 ? void 0 : _parent_id.type) === 'Identifier') {
|
|
62
|
+
name = parent.id.name;
|
|
63
|
+
} else if (parent.type === 'Property' && ((_parent_key = parent.key) === null || _parent_key === void 0 ? void 0 : _parent_key.type) === 'Identifier') {
|
|
64
|
+
name = parent.key.name;
|
|
65
|
+
} else if (parent.type === 'MethodDefinition' && ((_parent_key1 = parent.key) === null || _parent_key1 === void 0 ? void 0 : _parent_key1.type) === 'Identifier') {
|
|
66
|
+
name = parent.key.name;
|
|
67
|
+
} else if (parent.type === 'AssignmentExpression' && ((_parent_left = parent.left) === null || _parent_left === void 0 ? void 0 : _parent_left.type) === 'Identifier') {
|
|
68
|
+
name = parent.left.name;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return name;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Returns true if a parameter has any decorators (NestJS handler/Inject pattern).
|
|
75
|
+
*
|
|
76
|
+
* @param param - The parameter AST node.
|
|
77
|
+
* @returns True when the parameter carries at least one decorator.
|
|
78
|
+
*/ function paramHasDecorator(param) {
|
|
79
|
+
var _param_decorators;
|
|
80
|
+
var decorators = (_param_decorators = param.decorators) !== null && _param_decorators !== void 0 ? _param_decorators : [];
|
|
81
|
+
return Array.isArray(decorators) && decorators.length > 0;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Returns true when the function is the constructor of a class.
|
|
85
|
+
*
|
|
86
|
+
* @param node - The function-like AST node.
|
|
87
|
+
* @returns True if `node` is the `constructor` body of a class.
|
|
88
|
+
*/ function isConstructor(node) {
|
|
89
|
+
var _node_parent;
|
|
90
|
+
return ((_node_parent = node.parent) === null || _node_parent === void 0 ? void 0 : _node_parent.type) === 'MethodDefinition' && node.parent.kind === 'constructor';
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Returns true if any leading JSDoc block above `anchor` contains the allow tag.
|
|
94
|
+
*
|
|
95
|
+
* @param sourceCode - The ESLint `SourceCode` instance.
|
|
96
|
+
* @param anchor - The AST node whose leading comments are scanned.
|
|
97
|
+
* @param allowTag - The JSDoc tag string that opts the function out.
|
|
98
|
+
* @returns True when a JSDoc with the allow tag is present.
|
|
99
|
+
*/ function hasAllowJsdoc(sourceCode, anchor, allowTag) {
|
|
100
|
+
var comments = sourceCode.getCommentsBefore(anchor) || [];
|
|
101
|
+
var allow = false;
|
|
102
|
+
var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
|
|
103
|
+
try {
|
|
104
|
+
for(var _iterator = comments[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
|
|
105
|
+
var comment = _step.value;
|
|
106
|
+
if (comment.type === 'Block' && comment.value.startsWith('*') && comment.value.includes(allowTag)) {
|
|
107
|
+
allow = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
_didIteratorError = true;
|
|
112
|
+
_iteratorError = err;
|
|
113
|
+
} finally{
|
|
114
|
+
try {
|
|
115
|
+
if (!_iteratorNormalCompletion && _iterator.return != null) {
|
|
116
|
+
_iterator.return();
|
|
117
|
+
}
|
|
118
|
+
} finally{
|
|
119
|
+
if (_didIteratorError) {
|
|
120
|
+
throw _iteratorError;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return allow;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Builds a prefer-config-object-style rule with a configurable default `maxParams` threshold.
|
|
128
|
+
* Class constructors and decorated parameters (e.g. NestJS `@Inject`) are exempted. Functions can
|
|
129
|
+
* opt out via a leading JSDoc block carrying the configured allow tag (default `@dbxAllowMultiParams`).
|
|
130
|
+
*
|
|
131
|
+
* @param config - Default threshold and rule description.
|
|
132
|
+
* @returns A complete ESLint rule definition that emits `tooManyParams` reports.
|
|
133
|
+
*/ function createPreferConfigObjectRule(config) {
|
|
134
|
+
return {
|
|
135
|
+
meta: {
|
|
136
|
+
type: 'suggestion',
|
|
137
|
+
docs: {
|
|
138
|
+
description: config.description,
|
|
139
|
+
recommended: true
|
|
140
|
+
},
|
|
141
|
+
messages: {
|
|
142
|
+
tooManyParams: "Function '{{name}}' takes {{count}} positional parameters; use a single config object instead (see dbx__note__typescript-programming → Prefer Single Config Object)."
|
|
143
|
+
},
|
|
144
|
+
schema: [
|
|
145
|
+
{
|
|
146
|
+
type: 'object',
|
|
147
|
+
properties: {
|
|
148
|
+
maxParams: {
|
|
149
|
+
type: 'number',
|
|
150
|
+
minimum: 0,
|
|
151
|
+
description: 'Maximum number of positional parameters before the rule fires.'
|
|
152
|
+
},
|
|
153
|
+
allowJsdocTag: {
|
|
154
|
+
type: 'string',
|
|
155
|
+
description: 'JSDoc tag that opts a function out of this rule.'
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
additionalProperties: false
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
},
|
|
162
|
+
create: function create(context) {
|
|
163
|
+
var _context_options_, _options_maxParams, _options_allowJsdocTag;
|
|
164
|
+
var options = (_context_options_ = context.options[0]) !== null && _context_options_ !== void 0 ? _context_options_ : {};
|
|
165
|
+
var maxParams = (_options_maxParams = options.maxParams) !== null && _options_maxParams !== void 0 ? _options_maxParams : config.defaultMaxParams;
|
|
166
|
+
var allowTag = (_options_allowJsdocTag = options.allowJsdocTag) !== null && _options_allowJsdocTag !== void 0 ? _options_allowJsdocTag : DEFAULT_ALLOW_JSDOC_TAG;
|
|
167
|
+
var sourceCode = context.sourceCode;
|
|
168
|
+
function checkFunction(node) {
|
|
169
|
+
if (!isConstructor(node)) {
|
|
170
|
+
var _node_params;
|
|
171
|
+
var params = (_node_params = node.params) !== null && _node_params !== void 0 ? _node_params : [];
|
|
172
|
+
// Decorated parameters indicate framework-driven signatures (NestJS handlers, Angular DI inside
|
|
173
|
+
// constructors which we already skip — but standalone decorated functions exist too).
|
|
174
|
+
if (!params.some(paramHasDecorator) && params.length > maxParams) {
|
|
175
|
+
var _node_parent, _node_parent_parent;
|
|
176
|
+
// Anchor for JSDoc lookup: prefer the enclosing export statement, then a VariableDeclaration
|
|
177
|
+
// (for `const fn = () => ...`), otherwise the function node itself.
|
|
178
|
+
var anchor = node;
|
|
179
|
+
if (((_node_parent = node.parent) === null || _node_parent === void 0 ? void 0 : _node_parent.type) === 'VariableDeclarator' && ((_node_parent_parent = node.parent.parent) === null || _node_parent_parent === void 0 ? void 0 : _node_parent_parent.type) === 'VariableDeclaration') {
|
|
180
|
+
anchor = node.parent.parent;
|
|
181
|
+
}
|
|
182
|
+
if (anchor.parent && (anchor.parent.type === 'ExportNamedDeclaration' || anchor.parent.type === 'ExportDefaultDeclaration')) {
|
|
183
|
+
anchor = anchor.parent;
|
|
184
|
+
}
|
|
185
|
+
if (!hasAllowJsdoc(sourceCode, anchor, allowTag)) {
|
|
186
|
+
var _node_id;
|
|
187
|
+
var name = getFunctionDisplayName(node);
|
|
188
|
+
context.report({
|
|
189
|
+
node: (_node_id = node.id) !== null && _node_id !== void 0 ? _node_id : node,
|
|
190
|
+
messageId: 'tooManyParams',
|
|
191
|
+
data: {
|
|
192
|
+
name: name,
|
|
193
|
+
count: String(params.length)
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
FunctionDeclaration: checkFunction,
|
|
202
|
+
FunctionExpression: checkFunction,
|
|
203
|
+
ArrowFunctionExpression: checkFunction
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* ESLint rule recommending a single config object when a function takes more than two positional
|
|
210
|
+
* parameters (default `maxParams: 2`, i.e. fires at 3+ args). Intended to be configured at the
|
|
211
|
+
* `warn` severity. Pair with `prefer-config-object-hard` for a stricter cap.
|
|
212
|
+
*
|
|
213
|
+
* @see `dbx__note__typescript-programming` → Prefer Single Config Object
|
|
214
|
+
*/ createPreferConfigObjectRule({
|
|
215
|
+
defaultMaxParams: DEFAULT_MAX_PARAMS_WARN,
|
|
216
|
+
description: 'Prefer a single config object when a function takes more than two positional parameters.'
|
|
217
|
+
});
|
|
218
|
+
/**
|
|
219
|
+
* Hard-stop variant of `prefer-config-object`. Fires when a function takes more than four positional
|
|
220
|
+
* parameters (default `maxParams: 4`, i.e. fires at 5+ args). Intended to be configured at the
|
|
221
|
+
* `error` severity so genuinely unwieldy signatures break the build even when the softer warn-level
|
|
222
|
+
* rule is disabled or downgraded.
|
|
223
|
+
*
|
|
224
|
+
* @see `dbx__note__typescript-programming` → Prefer Single Config Object
|
|
225
|
+
*/ createPreferConfigObjectRule({
|
|
226
|
+
defaultMaxParams: DEFAULT_MAX_PARAMS_HARD,
|
|
227
|
+
description: 'Reject function signatures with more than four positional parameters; require a single config object.'
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
function _array_like_to_array$1(arr, len) {
|
|
231
|
+
if (len == null || len > arr.length) len = arr.length;
|
|
232
|
+
for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i];
|
|
233
|
+
return arr2;
|
|
234
|
+
}
|
|
235
|
+
function _array_with_holes$1(arr) {
|
|
236
|
+
if (Array.isArray(arr)) return arr;
|
|
237
|
+
}
|
|
238
|
+
function _iterable_to_array_limit$1(arr, i) {
|
|
239
|
+
var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"];
|
|
240
|
+
if (_i == null) return;
|
|
241
|
+
var _arr = [];
|
|
242
|
+
var _n = true;
|
|
243
|
+
var _d = false;
|
|
244
|
+
var _s, _e;
|
|
245
|
+
try {
|
|
246
|
+
for(_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true){
|
|
247
|
+
_arr.push(_s.value);
|
|
248
|
+
if (i && _arr.length === i) break;
|
|
249
|
+
}
|
|
250
|
+
} catch (err) {
|
|
251
|
+
_d = true;
|
|
252
|
+
_e = err;
|
|
253
|
+
} finally{
|
|
254
|
+
try {
|
|
255
|
+
if (!_n && _i["return"] != null) _i["return"]();
|
|
256
|
+
} finally{
|
|
257
|
+
if (_d) throw _e;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return _arr;
|
|
261
|
+
}
|
|
262
|
+
function _non_iterable_rest$1() {
|
|
263
|
+
throw new TypeError("Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
|
|
264
|
+
}
|
|
265
|
+
function _sliced_to_array$1(arr, i) {
|
|
266
|
+
return _array_with_holes$1(arr) || _iterable_to_array_limit$1(arr, i) || _unsupported_iterable_to_array$1(arr, i) || _non_iterable_rest$1();
|
|
267
|
+
}
|
|
268
|
+
function _unsupported_iterable_to_array$1(o, minLen) {
|
|
269
|
+
if (!o) return;
|
|
270
|
+
if (typeof o === "string") return _array_like_to_array$1(o, minLen);
|
|
271
|
+
var n = Object.prototype.toString.call(o).slice(8, -1);
|
|
272
|
+
if (n === "Object" && o.constructor) n = o.constructor.name;
|
|
273
|
+
if (n === "Map" || n === "Set") return Array.from(n);
|
|
274
|
+
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _array_like_to_array$1(o, minLen);
|
|
275
|
+
}
|
|
276
|
+
var TAG_LINE_REGEX = /^@([A-Za-z_]\w*)\s*(.*)$/;
|
|
277
|
+
var TYPE_ANNOTATION_REGEX = /^\{([^}]*)\}\s*(.*)$/;
|
|
278
|
+
var PARAM_NAME_REGEX = /^([A-Za-z_$][A-Za-z0-9_$.[\]]*)\s*(.*)$/;
|
|
279
|
+
var LINE_PREFIX_REGEX = /^(\s*\*?\s?)(.*)$/;
|
|
280
|
+
/**
|
|
281
|
+
* Strips the leading whitespace + `*` + optional space prefix from a JSDoc body line and reports the length stripped.
|
|
282
|
+
*
|
|
283
|
+
* @param raw - The raw line as it appears in `comment.value`.
|
|
284
|
+
* @returns A `{ text, prefixLength }` pair.
|
|
285
|
+
*
|
|
286
|
+
* @example
|
|
287
|
+
* ```ts
|
|
288
|
+
* stripPrefix(' * @param x - desc'); // { text: '@param x - desc', prefixLength: 3 }
|
|
289
|
+
* stripPrefix(' *'); // { text: '', prefixLength: 2 }
|
|
290
|
+
* stripPrefix('* hello'); // { text: 'hello', prefixLength: 2 }
|
|
291
|
+
* ```
|
|
292
|
+
*/ function stripPrefix(raw) {
|
|
293
|
+
var match = LINE_PREFIX_REGEX.exec(raw);
|
|
294
|
+
var result = {
|
|
295
|
+
text: raw,
|
|
296
|
+
prefixLength: 0
|
|
297
|
+
};
|
|
298
|
+
if (match) {
|
|
299
|
+
result.text = match[2];
|
|
300
|
+
result.prefixLength = match[1].length;
|
|
301
|
+
}
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Splits the raw comment value into per-line views with prefix/offset metadata.
|
|
306
|
+
*
|
|
307
|
+
* @param commentValue - The `value` of an ESLint Block comment.
|
|
308
|
+
* @returns Array of parsed line records in source order.
|
|
309
|
+
*/ function buildParsedLines(commentValue) {
|
|
310
|
+
var rawLines = commentValue.split('\n');
|
|
311
|
+
var runningOffset = 0;
|
|
312
|
+
return rawLines.map(function(raw, index) {
|
|
313
|
+
var _stripPrefix = stripPrefix(raw), stripped = _stripPrefix.text, prefixLength = _stripPrefix.prefixLength;
|
|
314
|
+
var text = stripped.trimEnd();
|
|
315
|
+
var blank = text.length === 0;
|
|
316
|
+
var valueOffsetStart = runningOffset;
|
|
317
|
+
var textOffsetStart = runningOffset + prefixLength;
|
|
318
|
+
runningOffset += raw.length + 1; // +1 for the consumed `\n` (overshoots on last line, harmless)
|
|
319
|
+
return {
|
|
320
|
+
raw: raw,
|
|
321
|
+
text: text,
|
|
322
|
+
blank: blank,
|
|
323
|
+
index: index,
|
|
324
|
+
valueOffsetStart: valueOffsetStart,
|
|
325
|
+
textOffsetStart: textOffsetStart
|
|
326
|
+
};
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Computes, per line, whether it sits inside a fenced code block (a ```` ``` ```` block, typically
|
|
331
|
+
* an `@example` body) and therefore must not be treated as a tag boundary. Fence delimiter lines
|
|
332
|
+
* themselves are flagged too. Without this, `@`-prefixed lines inside a fence — decorators like
|
|
333
|
+
* `@Global()` / `@Module()`, or JSDoc snippets — would be mis-parsed as standalone JSDoc tags.
|
|
334
|
+
*
|
|
335
|
+
* @param lines - Parsed lines in source order.
|
|
336
|
+
* @returns Boolean mask where `true` marks a line that must not start a tag.
|
|
337
|
+
*/ function computeFenceMask(lines) {
|
|
338
|
+
var mask = lines.map(function() {
|
|
339
|
+
return false;
|
|
340
|
+
});
|
|
341
|
+
var fenceOpen = false;
|
|
342
|
+
var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
|
|
343
|
+
try {
|
|
344
|
+
for(var _iterator = lines.entries()[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
|
|
345
|
+
var _step_value = _sliced_to_array$1(_step.value, 2), i = _step_value[0], line = _step_value[1];
|
|
346
|
+
var isDelimiter = line.text.trimStart().startsWith('```');
|
|
347
|
+
if (isDelimiter) {
|
|
348
|
+
mask[i] = true;
|
|
349
|
+
fenceOpen = !fenceOpen;
|
|
350
|
+
} else {
|
|
351
|
+
mask[i] = fenceOpen;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
} catch (err) {
|
|
355
|
+
_didIteratorError = true;
|
|
356
|
+
_iteratorError = err;
|
|
357
|
+
} finally{
|
|
358
|
+
try {
|
|
359
|
+
if (!_iteratorNormalCompletion && _iterator.return != null) {
|
|
360
|
+
_iterator.return();
|
|
361
|
+
}
|
|
362
|
+
} finally{
|
|
363
|
+
if (_didIteratorError) {
|
|
364
|
+
throw _iteratorError;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return mask;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Returns true when the line at `index` opens a JSDoc tag and is not masked out by a code fence.
|
|
372
|
+
*
|
|
373
|
+
* @param lines - Parsed lines in source order.
|
|
374
|
+
* @param fenceMask - Mask from {@link computeFenceMask} marking fenced lines.
|
|
375
|
+
* @param index - Line index to test.
|
|
376
|
+
* @returns True when the line begins a tag that should be treated as a tag boundary.
|
|
377
|
+
*/ function isTagStart(lines, fenceMask, index) {
|
|
378
|
+
return !fenceMask[index] && TAG_LINE_REGEX.test(lines[index].text);
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Returns the index of the first line that begins with a JSDoc `@tag`, or `-1` when none exists.
|
|
382
|
+
*
|
|
383
|
+
* @param lines - Parsed lines in source order.
|
|
384
|
+
* @param fenceMask - Mask from {@link computeFenceMask} marking fenced lines.
|
|
385
|
+
* @returns Zero-based line index of the first tag, or `-1` when no tag is present.
|
|
386
|
+
*/ function findFirstTagIndex(lines, fenceMask) {
|
|
387
|
+
var firstTagIndex = -1;
|
|
388
|
+
for(var i = 0; i < lines.length; i += 1){
|
|
389
|
+
if (isTagStart(lines, fenceMask, i)) {
|
|
390
|
+
firstTagIndex = i;
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return firstTagIndex;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Trims leading and trailing blank lines from a contiguous run of description lines.
|
|
398
|
+
*
|
|
399
|
+
* @param descriptionLines - Description-section lines before any tag.
|
|
400
|
+
* @returns Sub-array with surrounding blank lines stripped.
|
|
401
|
+
*/ function trimBlankBoundaries(descriptionLines) {
|
|
402
|
+
var descStart = 0;
|
|
403
|
+
var descEnd = descriptionLines.length;
|
|
404
|
+
while(descStart < descEnd && descriptionLines[descStart].blank)descStart += 1;
|
|
405
|
+
while(descEnd > descStart && descriptionLines[descEnd - 1].blank)descEnd -= 1;
|
|
406
|
+
return descriptionLines.slice(descStart, descEnd);
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Splits the trimmed description lines into paragraphs separated by blank-line runs.
|
|
410
|
+
*
|
|
411
|
+
* @param trimmedDescription - Description lines with surrounding blank lines removed.
|
|
412
|
+
* @returns Paragraph strings joined by `\n`.
|
|
413
|
+
*/ function buildDescriptionParagraphs(trimmedDescription) {
|
|
414
|
+
var descriptionParagraphs = [];
|
|
415
|
+
var paragraphBuffer = [];
|
|
416
|
+
var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
|
|
417
|
+
try {
|
|
418
|
+
for(var _iterator = trimmedDescription[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
|
|
419
|
+
var line = _step.value;
|
|
420
|
+
if (line.blank) {
|
|
421
|
+
if (paragraphBuffer.length > 0) {
|
|
422
|
+
descriptionParagraphs.push(paragraphBuffer.join('\n'));
|
|
423
|
+
paragraphBuffer = [];
|
|
424
|
+
}
|
|
425
|
+
} else {
|
|
426
|
+
paragraphBuffer.push(line.text);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
} catch (err) {
|
|
430
|
+
_didIteratorError = true;
|
|
431
|
+
_iteratorError = err;
|
|
432
|
+
} finally{
|
|
433
|
+
try {
|
|
434
|
+
if (!_iteratorNormalCompletion && _iterator.return != null) {
|
|
435
|
+
_iterator.return();
|
|
436
|
+
}
|
|
437
|
+
} finally{
|
|
438
|
+
if (_didIteratorError) {
|
|
439
|
+
throw _iteratorError;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (paragraphBuffer.length > 0) {
|
|
444
|
+
descriptionParagraphs.push(paragraphBuffer.join('\n'));
|
|
445
|
+
}
|
|
446
|
+
return descriptionParagraphs;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Pulls an optional `{Type}` annotation off the front of a tag remainder.
|
|
450
|
+
*
|
|
451
|
+
* @param remainder - The tag-line text after the `@tagName` prefix.
|
|
452
|
+
* @returns The annotation (or `undefined`) plus the remaining text.
|
|
453
|
+
*/ function extractTypeAnnotation(remainder) {
|
|
454
|
+
var type;
|
|
455
|
+
var rest = remainder;
|
|
456
|
+
var typeMatch = TYPE_ANNOTATION_REGEX.exec(remainder);
|
|
457
|
+
if (typeMatch) {
|
|
458
|
+
type = typeMatch[1];
|
|
459
|
+
rest = typeMatch[2];
|
|
460
|
+
}
|
|
461
|
+
return {
|
|
462
|
+
type: type,
|
|
463
|
+
rest: rest
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Pulls an optional parameter name off the front of a `@param` tag remainder.
|
|
468
|
+
*
|
|
469
|
+
* @param tagName - Tag name (only `'param'` extracts a name; other tags pass through).
|
|
470
|
+
* @param remainder - The tag-line text after the optional `{Type}` annotation.
|
|
471
|
+
* @returns The parameter name (or `undefined`) plus the remaining text.
|
|
472
|
+
*/ function extractParamName(tagName, remainder) {
|
|
473
|
+
var name;
|
|
474
|
+
var rest = remainder;
|
|
475
|
+
if (tagName === 'param') {
|
|
476
|
+
var nameMatch = PARAM_NAME_REGEX.exec(remainder);
|
|
477
|
+
if (nameMatch) {
|
|
478
|
+
name = nameMatch[1];
|
|
479
|
+
rest = nameMatch[2];
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
name: name,
|
|
484
|
+
rest: rest
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Collects the tag line at `startIndex` plus every following non-tag continuation line.
|
|
489
|
+
*
|
|
490
|
+
* @param lines - All parsed lines in the comment.
|
|
491
|
+
* @param fenceMask - Mask from {@link computeFenceMask} marking fenced lines.
|
|
492
|
+
* @param startIndex - Index of the `@tag` opening line.
|
|
493
|
+
* @returns The collected tag lines and the index of the next unconsumed line.
|
|
494
|
+
*/ function collectTagLines(lines, fenceMask, startIndex) {
|
|
495
|
+
var tagLines = [
|
|
496
|
+
lines[startIndex]
|
|
497
|
+
];
|
|
498
|
+
var j = startIndex + 1;
|
|
499
|
+
while(j < lines.length){
|
|
500
|
+
if (isTagStart(lines, fenceMask, j)) break;
|
|
501
|
+
tagLines.push(lines[j]);
|
|
502
|
+
j += 1;
|
|
503
|
+
}
|
|
504
|
+
return {
|
|
505
|
+
tagLines: tagLines,
|
|
506
|
+
nextIndex: j
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Joins the on-line remainder and continuation-line text into a tag description, dropping trailing
|
|
511
|
+
* blank lines while preserving interior blanks.
|
|
512
|
+
*
|
|
513
|
+
* @param remainder - The on-line remainder after stripping `@tagName {Type} name`.
|
|
514
|
+
* @param tagLines - All lines that belong to the tag (including the header line at index 0).
|
|
515
|
+
* @returns Description text joined by `\n`.
|
|
516
|
+
*/ function buildTagDescription(remainder, tagLines) {
|
|
517
|
+
var descriptionParts = [];
|
|
518
|
+
if (remainder.length > 0) descriptionParts.push(remainder);
|
|
519
|
+
for(var k = 1; k < tagLines.length; k += 1){
|
|
520
|
+
descriptionParts.push(tagLines[k].text);
|
|
521
|
+
}
|
|
522
|
+
while(descriptionParts.length > 0 && descriptionParts.at(-1).trim().length === 0){
|
|
523
|
+
descriptionParts.pop();
|
|
524
|
+
}
|
|
525
|
+
return descriptionParts.join('\n');
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Builds a single parsed-tag record starting at `startIndex` in the line array.
|
|
529
|
+
*
|
|
530
|
+
* @param lines - All parsed lines in the comment.
|
|
531
|
+
* @param fenceMask - Mask from {@link computeFenceMask} marking fenced lines.
|
|
532
|
+
* @param startIndex - Index of the `@tag` opening line.
|
|
533
|
+
* @returns The parsed tag and the next unconsumed line index.
|
|
534
|
+
*/ function parseTagAt(lines, fenceMask, startIndex) {
|
|
535
|
+
var line = lines[startIndex];
|
|
536
|
+
var match = TAG_LINE_REGEX.exec(line.text);
|
|
537
|
+
var tagName = match[1];
|
|
538
|
+
var _extractTypeAnnotation = extractTypeAnnotation(match[2]), type = _extractTypeAnnotation.type, afterType = _extractTypeAnnotation.rest;
|
|
539
|
+
var _extractParamName = extractParamName(tagName, afterType), name = _extractParamName.name, afterName = _extractParamName.rest;
|
|
540
|
+
var _collectTagLines = collectTagLines(lines, fenceMask, startIndex), tagLines = _collectTagLines.tagLines, nextIndex = _collectTagLines.nextIndex;
|
|
541
|
+
var description = buildTagDescription(afterName, tagLines);
|
|
542
|
+
return {
|
|
543
|
+
tag: {
|
|
544
|
+
tag: tagName,
|
|
545
|
+
name: name,
|
|
546
|
+
type: type,
|
|
547
|
+
description: description,
|
|
548
|
+
lines: tagLines,
|
|
549
|
+
startLineIndex: startIndex,
|
|
550
|
+
endLineIndex: nextIndex - 1
|
|
551
|
+
},
|
|
552
|
+
nextIndex: nextIndex
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Parses every `@tag` block starting from `firstTagIndex` to the end of the line array.
|
|
557
|
+
*
|
|
558
|
+
* @param lines - All parsed lines in the comment.
|
|
559
|
+
* @param fenceMask - Mask from {@link computeFenceMask} marking fenced lines.
|
|
560
|
+
* @param firstTagIndex - Index where tag parsing should begin (`-1` skips entirely).
|
|
561
|
+
* @returns All parsed tags in source order.
|
|
562
|
+
*/ function parseTags(lines, fenceMask, firstTagIndex) {
|
|
563
|
+
var tags = [];
|
|
564
|
+
if (firstTagIndex !== -1) {
|
|
565
|
+
var i = firstTagIndex;
|
|
566
|
+
while(i < lines.length){
|
|
567
|
+
if (!isTagStart(lines, fenceMask, i)) {
|
|
568
|
+
i += 1;
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
var _parseTagAt = parseTagAt(lines, fenceMask, i), tag = _parseTagAt.tag, nextIndex = _parseTagAt.nextIndex;
|
|
572
|
+
tags.push(tag);
|
|
573
|
+
i = nextIndex;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return tags;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Parses the value of an ESLint Block comment that represents a JSDoc into a structured form.
|
|
580
|
+
*
|
|
581
|
+
* @param commentValue - The `value` of an ESLint Block comment (text between `/*` and `*\/`, including the leading `*`).
|
|
582
|
+
* @returns A structured view of the JSDoc with description, paragraphs, and tags.
|
|
583
|
+
*
|
|
584
|
+
* @example
|
|
585
|
+
* ```ts
|
|
586
|
+
* const parsed = parseJsdocComment('*\n * Hello.\n *\n * @param x - The value.\n ');
|
|
587
|
+
* // parsed.description === 'Hello.'
|
|
588
|
+
* // parsed.tags[0].tag === 'param'
|
|
589
|
+
* // parsed.tags[0].name === 'x'
|
|
590
|
+
* // parsed.tags[0].description === 'The value.'
|
|
591
|
+
* ```
|
|
592
|
+
*/ function parseJsdocComment(commentValue) {
|
|
593
|
+
var singleLine = !commentValue.includes('\n');
|
|
594
|
+
var lines = buildParsedLines(commentValue);
|
|
595
|
+
var fenceMask = computeFenceMask(lines);
|
|
596
|
+
var firstTagIndex = findFirstTagIndex(lines, fenceMask);
|
|
597
|
+
var descriptionLines = firstTagIndex === -1 ? lines.slice() : lines.slice(0, firstTagIndex);
|
|
598
|
+
var trimmedDescription = trimBlankBoundaries(descriptionLines);
|
|
599
|
+
var description = trimmedDescription.map(function(l) {
|
|
600
|
+
return l.text;
|
|
601
|
+
}).join('\n');
|
|
602
|
+
var descriptionParagraphs = buildDescriptionParagraphs(trimmedDescription);
|
|
603
|
+
var tags = parseTags(lines, fenceMask, firstTagIndex);
|
|
604
|
+
return {
|
|
605
|
+
lines: lines,
|
|
606
|
+
descriptionLines: descriptionLines,
|
|
607
|
+
description: description,
|
|
608
|
+
descriptionParagraphs: descriptionParagraphs,
|
|
609
|
+
tags: tags,
|
|
610
|
+
singleLine: singleLine
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Returns the source-text offset of an offset-within-comment-value, given a Block comment node.
|
|
616
|
+
*
|
|
617
|
+
* @param commentNode - The ESLint Block comment AST node.
|
|
618
|
+
* @param valueOffset - The character offset within `comment.value`.
|
|
619
|
+
* @returns The character offset in the source file.
|
|
620
|
+
*/ function commentValueToSourceOffset(commentNode, valueOffset) {
|
|
621
|
+
return commentNode.range[0] + 2 + valueOffset;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Translates a JSDoc-line violation into an ESLint `context.report()` call by computing the
|
|
625
|
+
* source range of the offending line and attaching the supplied message + optional fixer.
|
|
626
|
+
*
|
|
627
|
+
* @param input - Reporting context (comment node, parsed JSDoc, source code, line index, message id, optional data + fixer, report sink).
|
|
628
|
+
*
|
|
629
|
+
* @example
|
|
630
|
+
* ```ts
|
|
631
|
+
* reportOnJsdocLine({ commentNode, parsed, sourceCode, lineIndex: tag.startLineIndex, messageId: 'unknown', report: context.report });
|
|
632
|
+
* ```
|
|
633
|
+
*/ function reportOnJsdocLine(input) {
|
|
634
|
+
var _ref, _ref1;
|
|
635
|
+
var _line_text;
|
|
636
|
+
var commentNode = input.commentNode, parsed = input.parsed, sourceCode = input.sourceCode, lineIndex = input.lineIndex, messageId = input.messageId, data = input.data, report = input.report, fix = input.fix;
|
|
637
|
+
var line = parsed.lines[lineIndex];
|
|
638
|
+
var startInValue = (_ref = line === null || line === void 0 ? void 0 : line.textOffsetStart) !== null && _ref !== void 0 ? _ref : 0;
|
|
639
|
+
var endInValue = startInValue + ((_ref1 = line === null || line === void 0 ? void 0 : (_line_text = line.text) === null || _line_text === void 0 ? void 0 : _line_text.length) !== null && _ref1 !== void 0 ? _ref1 : 0);
|
|
640
|
+
var start = commentValueToSourceOffset(commentNode, startInValue);
|
|
641
|
+
var end = commentValueToSourceOffset(commentNode, endInValue);
|
|
642
|
+
report({
|
|
643
|
+
loc: {
|
|
644
|
+
type: 'SourceLocation',
|
|
645
|
+
start: sourceCode.getLocFromIndex(start),
|
|
646
|
+
end: sourceCode.getLocFromIndex(end)
|
|
647
|
+
},
|
|
648
|
+
messageId: messageId,
|
|
649
|
+
data: data,
|
|
650
|
+
fix: fix
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Pure parser for the `@dbxRouteModel` / `@dbxRouteModelList` JSDoc tag grammar
|
|
656
|
+
* that annotates which Firestore models a route renders.
|
|
657
|
+
*
|
|
658
|
+
* Grammar:
|
|
659
|
+
*
|
|
660
|
+
* ```
|
|
661
|
+
* @dbxRouteModel <modelType> <keyTemplate> [- <description>]
|
|
662
|
+
* @dbxRouteModelList <modelType> [- <description>]
|
|
663
|
+
* ```
|
|
664
|
+
*
|
|
665
|
+
* `keyTemplate` shapes:
|
|
666
|
+
* - `:param` / `{authUid}` — single placeholder → `kind: 'id'` (the value is
|
|
667
|
+
* promoted to `<collectionName>/<id>` at runtime via the model identity).
|
|
668
|
+
* - `gb/:id/gbe/{authUid}` — alternating literal / placeholder segments (even
|
|
669
|
+
* count) → `kind: 'key'` (a full FirestoreModelKey for a subcollection model).
|
|
670
|
+
* - (absent, list tag) → `kind: 'list'`.
|
|
671
|
+
*
|
|
672
|
+
* This module is deliberately runtime-dependency-free (no ts-morph): the same
|
|
673
|
+
* grammar is reused by the build-time manifest builder, the dev MCP route tools,
|
|
674
|
+
* and the `@dereekb/dbx-cli/eslint` rule so they can never disagree about what a
|
|
675
|
+
* valid tag is. The ts-morph consumer (`extractComponentRouteModelTags`) lives in
|
|
676
|
+
* `./route-models-extract.ts` and re-exports these symbols for existing importers.
|
|
677
|
+
*/ /**
|
|
678
|
+
* Whether a route-model entry resolves to a promoted id, a full key, or a
|
|
679
|
+
* keyless list.
|
|
680
|
+
*/ function _array_like_to_array(arr, len) {
|
|
681
|
+
if (len == null || len > arr.length) len = arr.length;
|
|
682
|
+
for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i];
|
|
683
|
+
return arr2;
|
|
684
|
+
}
|
|
685
|
+
function _array_with_holes(arr) {
|
|
686
|
+
if (Array.isArray(arr)) return arr;
|
|
687
|
+
}
|
|
688
|
+
function _iterable_to_array_limit(arr, i) {
|
|
689
|
+
var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"];
|
|
690
|
+
if (_i == null) return;
|
|
691
|
+
var _arr = [];
|
|
692
|
+
var _n = true;
|
|
693
|
+
var _d = false;
|
|
694
|
+
var _s, _e;
|
|
695
|
+
try {
|
|
696
|
+
for(_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true){
|
|
697
|
+
_arr.push(_s.value);
|
|
698
|
+
if (i && _arr.length === i) break;
|
|
699
|
+
}
|
|
700
|
+
} catch (err) {
|
|
701
|
+
_d = true;
|
|
702
|
+
_e = err;
|
|
703
|
+
} finally{
|
|
704
|
+
try {
|
|
705
|
+
if (!_n && _i["return"] != null) _i["return"]();
|
|
706
|
+
} finally{
|
|
707
|
+
if (_d) throw _e;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return _arr;
|
|
711
|
+
}
|
|
712
|
+
function _non_iterable_rest() {
|
|
713
|
+
throw new TypeError("Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
|
|
714
|
+
}
|
|
715
|
+
function _sliced_to_array(arr, i) {
|
|
716
|
+
return _array_with_holes(arr) || _iterable_to_array_limit(arr, i) || _unsupported_iterable_to_array(arr, i) || _non_iterable_rest();
|
|
717
|
+
}
|
|
718
|
+
function _unsupported_iterable_to_array(o, minLen) {
|
|
719
|
+
if (!o) return;
|
|
720
|
+
if (typeof o === "string") return _array_like_to_array(o, minLen);
|
|
721
|
+
var n = Object.prototype.toString.call(o).slice(8, -1);
|
|
722
|
+
if (n === "Object" && o.constructor) n = o.constructor.name;
|
|
723
|
+
if (n === "Map" || n === "Set") return Array.from(n);
|
|
724
|
+
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _array_like_to_array(o, minLen);
|
|
725
|
+
}
|
|
726
|
+
var MODEL_TYPE_RE = RegExp("^[a-zA-Z][a-zA-Z0-9]*$", "u");
|
|
727
|
+
var LITERAL_SEGMENT_RE = RegExp("^[a-zA-Z0-9][a-zA-Z0-9_-]*$", "u");
|
|
728
|
+
var AUTH_UID_PLACEHOLDER = '{authUid}';
|
|
729
|
+
/**
|
|
730
|
+
* The bare `@dbxRouteModel` tag name (without the leading `@`).
|
|
731
|
+
*/ var ROUTE_MODEL_TAG = 'dbxRouteModel';
|
|
732
|
+
/**
|
|
733
|
+
* The `@dbxRouteModelList` tag name (without the leading `@`).
|
|
734
|
+
*/ var ROUTE_MODEL_LIST_TAG = 'dbxRouteModelList';
|
|
735
|
+
/**
|
|
736
|
+
* Parses one `@dbxRouteModel` / `@dbxRouteModelList` tag into a structured
|
|
737
|
+
* model, or returns a malformed-tag message. The description (everything after
|
|
738
|
+
* the first ` - `) is split off first; the remaining head is tokenized.
|
|
739
|
+
*
|
|
740
|
+
* @param tag - The raw tag name + comment text.
|
|
741
|
+
* @returns The parsed model on success, else a malformed-tag message.
|
|
742
|
+
*
|
|
743
|
+
* @example
|
|
744
|
+
* ```ts
|
|
745
|
+
* parseRouteModelTag({ name: 'dbxRouteModel', text: 'profile :uid - The profile' });
|
|
746
|
+
* // => { ok: true, model: { modelType: 'profile', kind: 'id', keyTemplate: ':uid', description: 'The profile', routeParams: ['uid'] } }
|
|
747
|
+
* ```
|
|
748
|
+
*/ function parseRouteModelTag(tag) {
|
|
749
|
+
var dashIdx = tag.text.indexOf(' - ');
|
|
750
|
+
var head = (dashIdx >= 0 ? tag.text.slice(0, dashIdx) : tag.text).trim();
|
|
751
|
+
var description = dashIdx >= 0 ? tag.text.slice(dashIdx + 3).trim() : undefined;
|
|
752
|
+
var tokens = head.split(RegExp("\\s+", "u")).filter(function(t) {
|
|
753
|
+
return t.length > 0;
|
|
754
|
+
});
|
|
755
|
+
var result;
|
|
756
|
+
if (tag.name === ROUTE_MODEL_LIST_TAG) {
|
|
757
|
+
result = parseListTag(tokens, description);
|
|
758
|
+
} else if (tag.name === ROUTE_MODEL_TAG) {
|
|
759
|
+
result = parseModelTag(tokens, description);
|
|
760
|
+
} else {
|
|
761
|
+
result = {
|
|
762
|
+
ok: false,
|
|
763
|
+
message: "Unknown route-model tag `@".concat(tag.name, "`. Expected `@").concat(ROUTE_MODEL_TAG, "` or `@").concat(ROUTE_MODEL_LIST_TAG, "`.")
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
return result;
|
|
767
|
+
}
|
|
768
|
+
function parseListTag(tokens, description) {
|
|
769
|
+
var result;
|
|
770
|
+
if (tokens.length !== 1) {
|
|
771
|
+
result = {
|
|
772
|
+
ok: false,
|
|
773
|
+
message: "`@".concat(ROUTE_MODEL_LIST_TAG, "` expects a single `<modelType>` token; got ").concat(tokens.length, ".")
|
|
774
|
+
};
|
|
775
|
+
} else if (MODEL_TYPE_RE.test(tokens[0])) {
|
|
776
|
+
result = {
|
|
777
|
+
ok: true,
|
|
778
|
+
model: {
|
|
779
|
+
modelType: tokens[0],
|
|
780
|
+
kind: 'list',
|
|
781
|
+
description: description,
|
|
782
|
+
routeParams: []
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
} else {
|
|
786
|
+
result = {
|
|
787
|
+
ok: false,
|
|
788
|
+
message: "`@".concat(ROUTE_MODEL_LIST_TAG, "` model type `").concat(tokens[0], "` is not a valid identifier.")
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
return result;
|
|
792
|
+
}
|
|
793
|
+
function parseModelTag(tokens, description) {
|
|
794
|
+
var result;
|
|
795
|
+
if (tokens.length !== 2) {
|
|
796
|
+
result = {
|
|
797
|
+
ok: false,
|
|
798
|
+
message: "`@".concat(ROUTE_MODEL_TAG, "` expects `<modelType> <keyTemplate>`; got ").concat(tokens.length, " token(s).")
|
|
799
|
+
};
|
|
800
|
+
} else if (MODEL_TYPE_RE.test(tokens[0])) {
|
|
801
|
+
var parsedKey = parseKeyTemplate(tokens[1]);
|
|
802
|
+
if (parsedKey.ok) {
|
|
803
|
+
result = {
|
|
804
|
+
ok: true,
|
|
805
|
+
model: {
|
|
806
|
+
modelType: tokens[0],
|
|
807
|
+
kind: parsedKey.kind,
|
|
808
|
+
keyTemplate: tokens[1],
|
|
809
|
+
description: description,
|
|
810
|
+
routeParams: parsedKey.routeParams
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
} else {
|
|
814
|
+
result = {
|
|
815
|
+
ok: false,
|
|
816
|
+
message: parsedKey.message
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
} else {
|
|
820
|
+
result = {
|
|
821
|
+
ok: false,
|
|
822
|
+
message: "`@".concat(ROUTE_MODEL_TAG, "` model type `").concat(tokens[0], "` is not a valid identifier.")
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
return result;
|
|
826
|
+
}
|
|
827
|
+
function parseKeyTemplate(keyTemplate) {
|
|
828
|
+
var segments = keyTemplate.split('/');
|
|
829
|
+
var result;
|
|
830
|
+
if (segments.length === 1) {
|
|
831
|
+
result = parseSingleSegmentKey(segments[0], keyTemplate);
|
|
832
|
+
} else if (segments.length % 2 === 0) {
|
|
833
|
+
result = parseAlternatingKey(segments, keyTemplate);
|
|
834
|
+
} else {
|
|
835
|
+
result = {
|
|
836
|
+
ok: false,
|
|
837
|
+
message: "Key template `".concat(keyTemplate, "` must be a single placeholder or an even number of literal/placeholder segments.")
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
return result;
|
|
841
|
+
}
|
|
842
|
+
function parseSingleSegmentKey(segment, keyTemplate) {
|
|
843
|
+
var placeholder = placeholderParam(segment);
|
|
844
|
+
var result;
|
|
845
|
+
if (placeholder === undefined) {
|
|
846
|
+
result = {
|
|
847
|
+
ok: false,
|
|
848
|
+
message: "Single-segment key template `".concat(keyTemplate, "` must be a placeholder (`:param` or `").concat(AUTH_UID_PLACEHOLDER, "`).")
|
|
849
|
+
};
|
|
850
|
+
} else {
|
|
851
|
+
result = {
|
|
852
|
+
ok: true,
|
|
853
|
+
kind: 'id',
|
|
854
|
+
routeParams: placeholder.routeParam === undefined ? [] : [
|
|
855
|
+
placeholder.routeParam
|
|
856
|
+
]
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
return result;
|
|
860
|
+
}
|
|
861
|
+
function parseAlternatingKey(segments, keyTemplate) {
|
|
862
|
+
var routeParams = [];
|
|
863
|
+
var message;
|
|
864
|
+
var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
|
|
865
|
+
try {
|
|
866
|
+
for(var _iterator = segments.entries()[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
|
|
867
|
+
var _step_value = _sliced_to_array(_step.value, 2), i = _step_value[0], segment = _step_value[1];
|
|
868
|
+
if (i % 2 === 0) {
|
|
869
|
+
if (!LITERAL_SEGMENT_RE.test(segment)) {
|
|
870
|
+
message = "Key template `".concat(keyTemplate, "` segment `").concat(segment, "` must be a literal collection name.");
|
|
871
|
+
break;
|
|
872
|
+
}
|
|
873
|
+
} else {
|
|
874
|
+
var placeholder = placeholderParam(segment);
|
|
875
|
+
if (placeholder === undefined) {
|
|
876
|
+
message = "Key template `".concat(keyTemplate, "` segment `").concat(segment, "` must be a placeholder (`:param` or `").concat(AUTH_UID_PLACEHOLDER, "`).");
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
if (placeholder.routeParam !== undefined) {
|
|
880
|
+
routeParams.push(placeholder.routeParam);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
} catch (err) {
|
|
885
|
+
_didIteratorError = true;
|
|
886
|
+
_iteratorError = err;
|
|
887
|
+
} finally{
|
|
888
|
+
try {
|
|
889
|
+
if (!_iteratorNormalCompletion && _iterator.return != null) {
|
|
890
|
+
_iterator.return();
|
|
891
|
+
}
|
|
892
|
+
} finally{
|
|
893
|
+
if (_didIteratorError) {
|
|
894
|
+
throw _iteratorError;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return message === undefined ? {
|
|
899
|
+
ok: true,
|
|
900
|
+
kind: 'key',
|
|
901
|
+
routeParams: routeParams
|
|
902
|
+
} : {
|
|
903
|
+
ok: false,
|
|
904
|
+
message: message
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
// MARK: Segment helpers
|
|
908
|
+
/**
|
|
909
|
+
* Classifies a key-template segment as a placeholder, returning the referenced
|
|
910
|
+
* route param name (for `:name`) or `undefined` for `{authUid}`. Non-placeholder
|
|
911
|
+
* segments return `undefined` for the whole result.
|
|
912
|
+
*
|
|
913
|
+
* @param segment - The single key-template segment to classify.
|
|
914
|
+
* @returns The placeholder descriptor, or `undefined` when the segment is a literal.
|
|
915
|
+
*/ function placeholderParam(segment) {
|
|
916
|
+
var result;
|
|
917
|
+
if (segment.startsWith(':') && segment.length > 1) {
|
|
918
|
+
result = {
|
|
919
|
+
routeParam: segment.slice(1)
|
|
920
|
+
};
|
|
921
|
+
} else if (segment === AUTH_UID_PLACEHOLDER) {
|
|
922
|
+
result = {
|
|
923
|
+
routeParam: undefined
|
|
924
|
+
};
|
|
925
|
+
} else {
|
|
926
|
+
result = undefined;
|
|
927
|
+
}
|
|
928
|
+
return result;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* ESLint rule enforcing that `@dbxRouteModel` / `@dbxRouteModelList` tags parse
|
|
933
|
+
* cleanly through the canonical route-model grammar. Reuses
|
|
934
|
+
* {@link parseRouteModelTag} from `@dereekb/dbx-cli` so a tag that lints clean is
|
|
935
|
+
* a tag the build-time manifest builder will accept.
|
|
936
|
+
*
|
|
937
|
+
* Checks (delegated to the grammar parser):
|
|
938
|
+
*
|
|
939
|
+
* - `@dbxRouteModel <modelType> <keyTemplate>` has exactly two tokens with a valid
|
|
940
|
+
* model-type identifier and a parseable key template (`:param` / `{authUid}` /
|
|
941
|
+
* even-segment `gb/:id/...` form).
|
|
942
|
+
* - `@dbxRouteModelList <modelType>` has a single valid model-type token.
|
|
943
|
+
* - A `@dbxRouteModel*` tag with an unknown name is flagged.
|
|
944
|
+
*
|
|
945
|
+
* Report-only — no auto-fix, since the corrected value needs judgment.
|
|
946
|
+
*/ var DBX_CLI_VALID_DBX_ROUTE_MODEL_TAGS_RULE = {
|
|
947
|
+
meta: {
|
|
948
|
+
type: 'problem',
|
|
949
|
+
docs: {
|
|
950
|
+
description: 'Validate `@dbxRouteModel` / `@dbxRouteModelList` JSDoc tag grammar against the canonical route-model parser.',
|
|
951
|
+
recommended: true
|
|
952
|
+
},
|
|
953
|
+
messages: {
|
|
954
|
+
malformedRouteModelTag: 'Malformed `@dbxRouteModel*` tag: {{reason}}'
|
|
955
|
+
},
|
|
956
|
+
schema: []
|
|
957
|
+
},
|
|
958
|
+
create: function create(context) {
|
|
959
|
+
var sourceCode = context.sourceCode;
|
|
960
|
+
var checkedComments = new WeakSet();
|
|
961
|
+
function checkJsdoc(commentNode) {
|
|
962
|
+
if (checkedComments.has(commentNode)) {
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
checkedComments.add(commentNode);
|
|
966
|
+
var parsed = parseJsdocComment(commentNode.value);
|
|
967
|
+
var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
|
|
968
|
+
try {
|
|
969
|
+
for(var _iterator = parsed.tags[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
|
|
970
|
+
var tag = _step.value;
|
|
971
|
+
if (!tag.tag.startsWith(ROUTE_MODEL_TAG)) {
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
var raw = {
|
|
975
|
+
name: tag.tag,
|
|
976
|
+
text: tag.description.trim()
|
|
977
|
+
};
|
|
978
|
+
var result = parseRouteModelTag(raw);
|
|
979
|
+
if (!result.ok) {
|
|
980
|
+
reportOnJsdocLine({
|
|
981
|
+
commentNode: commentNode,
|
|
982
|
+
parsed: parsed,
|
|
983
|
+
sourceCode: sourceCode,
|
|
984
|
+
lineIndex: tag.startLineIndex,
|
|
985
|
+
messageId: 'malformedRouteModelTag',
|
|
986
|
+
data: {
|
|
987
|
+
reason: result.message
|
|
988
|
+
},
|
|
989
|
+
report: context.report
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
} catch (err) {
|
|
994
|
+
_didIteratorError = true;
|
|
995
|
+
_iteratorError = err;
|
|
996
|
+
} finally{
|
|
997
|
+
try {
|
|
998
|
+
if (!_iteratorNormalCompletion && _iterator.return != null) {
|
|
999
|
+
_iterator.return();
|
|
1000
|
+
}
|
|
1001
|
+
} finally{
|
|
1002
|
+
if (_didIteratorError) {
|
|
1003
|
+
throw _iteratorError;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
function visit(node, anchorParentTypes) {
|
|
1009
|
+
var anchor = node.parent && anchorParentTypes.includes(node.parent.type) ? node.parent : node;
|
|
1010
|
+
var jsdoc = leadingJsdocFor(sourceCode, anchor);
|
|
1011
|
+
if (jsdoc) {
|
|
1012
|
+
checkJsdoc(jsdoc);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
var exportAnchors = [
|
|
1016
|
+
'ExportNamedDeclaration',
|
|
1017
|
+
'ExportDefaultDeclaration'
|
|
1018
|
+
];
|
|
1019
|
+
return {
|
|
1020
|
+
ClassDeclaration: function ClassDeclaration(node) {
|
|
1021
|
+
return visit(node, exportAnchors);
|
|
1022
|
+
},
|
|
1023
|
+
VariableDeclaration: function VariableDeclaration(node) {
|
|
1024
|
+
return visit(node, exportAnchors);
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* ESLint plugin for `@dereekb/dbx-cli` rules.
|
|
1032
|
+
*
|
|
1033
|
+
* Register as a plugin in your flat ESLint config, then enable individual rules
|
|
1034
|
+
* under the chosen plugin prefix (e.g. 'dereekb-dbx-cli/valid-dbx-route-model-tags').
|
|
1035
|
+
*/ var DBX_CLI_ESLINT_PLUGIN = {
|
|
1036
|
+
rules: {
|
|
1037
|
+
'valid-dbx-route-model-tags': DBX_CLI_VALID_DBX_ROUTE_MODEL_TAGS_RULE
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
/**
|
|
1041
|
+
* camelCase alias of {@link DBX_CLI_ESLINT_PLUGIN} matching the conventional ESLint plugin export name.
|
|
1042
|
+
*
|
|
1043
|
+
* @dbxAllowConstantName
|
|
1044
|
+
*/ var dbxCliESLintPlugin = DBX_CLI_ESLINT_PLUGIN;
|
|
1045
|
+
|
|
1046
|
+
export { DBX_CLI_ESLINT_PLUGIN, DBX_CLI_VALID_DBX_ROUTE_MODEL_TAGS_RULE, dbxCliESLintPlugin };
|