@dudousxd/nestjs-catalog 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,419 @@
1
+ "use strict";
2
+ /**
3
+ * Which of the two shapes a JavaScript or TypeScript transform is written in,
4
+ * and the one rule that tells them apart.
5
+ *
6
+ * ## The two shapes
7
+ *
8
+ * A transform used to be — and, for everything already stored, still is — a
9
+ * **bare body**: the text between the braces of a function the harness supplies.
10
+ *
11
+ * ```js
12
+ * return records.map((r) => ({ mgmtCd: r["Mgmt Cd"] }));
13
+ * ```
14
+ *
15
+ * That shape works, and its problem is not syntax. The harness supplied
16
+ * `(records, context)` **positionally**, so the set of things a transform can be
17
+ * given was fixed by the day the second parameter was added: a third one changes
18
+ * the meaning of every signature ever written, and there is no version of
19
+ * "records, context, andNowAlsoThis" that does not make the previous shape a
20
+ * subset by luck rather than by design. So the supported shape is now a real
21
+ * function over **one object**:
22
+ *
23
+ * ```js
24
+ * export default function transform({ records, context }) {
25
+ * return records.map((r) => ({ mgmtCd: r["Mgmt Cd"] }));
26
+ * }
27
+ * ```
28
+ *
29
+ * A field can be added to that object without touching a single stored
30
+ * transform, which is the entire argument for it.
31
+ *
32
+ * ## The rule
33
+ *
34
+ * **A top-level `export` keyword, and nothing else.** Code that has one is a
35
+ * module; code that has none is a body.
36
+ *
37
+ * That is a discriminator rather than a heuristic, and the reason is worth being
38
+ * exact about: `export` is a *syntax error* inside a function body. Every
39
+ * transform stored today runs as a function body today, so no stored transform
40
+ * can contain a top-level `export` — not "probably does not", cannot. Backward
41
+ * compatibility here is a property of the language, not of how good the guess is.
42
+ *
43
+ * ## What the rule deliberately does not look at
44
+ *
45
+ * Not the word `function`. Not a function *declaration* named `transform`
46
+ * either, which is the tempting second rule and is the one that would break
47
+ * real code:
48
+ *
49
+ * ```js
50
+ * function transform(r) { return { mgmtCd: r["Mgmt Cd"] }; }
51
+ * return records.map(transform);
52
+ * ```
53
+ *
54
+ * That is a bare body which declares a local helper it happens to have named
55
+ * `transform`. A detector that called it the new shape would call the helper
56
+ * with `{records, context}` — one object where a record was expected — and store
57
+ * 100,000 rows of `undefined` without erroring once. Silent wrong data is the
58
+ * worst failure available here, so the detector does not offer an opinion about
59
+ * names at all.
60
+ *
61
+ * ## The scan, and its one known limit
62
+ *
63
+ * Strings, template literals (including `${}` nesting), regular-expression
64
+ * literals and both kinds of comment are skipped, so `// export default` and
65
+ * `"export"` are not exports. The keyword must then appear at **statement
66
+ * position** — start of input, or after `;`, `}`, or a newline — at brace,
67
+ * paren and bracket depth zero.
68
+ *
69
+ * Regular-expression literals are found by the usual rule (a `/` is a regex
70
+ * unless what precedes it could end a value), which is the one place a scanner
71
+ * without a full parser can be wrong. Its consequence is bounded on purpose: a
72
+ * misread makes this return `false` for a module, the code is run as a body, and
73
+ * the author gets `SyntaxError: Unexpected token 'export'` — which
74
+ * {@link transformShapeHint} turns into a sentence naming this exact rule. A
75
+ * wrong answer here produces a reported error, never a silently different run.
76
+ */
77
+ Object.defineProperty(exports, "__esModule", { value: true });
78
+ exports.transformDeclaresModule = transformDeclaresModule;
79
+ exports.transformShape = transformShape;
80
+ exports.transformShapeHint = transformShapeHint;
81
+ /** Characters that may make up an identifier, for the boundary check. */
82
+ function isIdentifierChar(char) {
83
+ return /[\p{ID_Continue}$]/u.test(char);
84
+ }
85
+ /**
86
+ * Words after which a `/` is a regular expression even though the character
87
+ * before it is an identifier character.
88
+ *
89
+ * `return /a'b/` is the case that matters: without this the apostrophe would
90
+ * open a string literal that never closes, and everything after it — including
91
+ * a real `export` — would be skipped as string contents. That is precisely the
92
+ * scanner's documented failure mode, so the cheap fix for its likeliest cause is
93
+ * worth the fifteen words.
94
+ */
95
+ const REGEX_PRECEDING_KEYWORDS = new Set([
96
+ 'await',
97
+ 'case',
98
+ 'delete',
99
+ 'do',
100
+ 'else',
101
+ 'in',
102
+ 'instanceof',
103
+ 'new',
104
+ 'of',
105
+ 'return',
106
+ 'throw',
107
+ 'typeof',
108
+ 'void',
109
+ 'yield',
110
+ ]);
111
+ /**
112
+ * Whether a `/` at this point starts a regular expression rather than a
113
+ * division.
114
+ *
115
+ * The conventional rule: a regex may only appear where a *value* may appear, so
116
+ * anything that could end a value — an identifier, a number, a closing bracket
117
+ * or paren — means division. `}` is treated as ending a value too, which is the
118
+ * common convention and wrong only for `if (x) {} /re/.test(y)`, a statement
119
+ * nobody writes.
120
+ */
121
+ function startsRegex(code, at, lastSignificant) {
122
+ if (lastSignificant === '')
123
+ return true;
124
+ if (lastSignificant === ')' || lastSignificant === ']' || lastSignificant === '}')
125
+ return false;
126
+ if (!isIdentifierChar(lastSignificant))
127
+ return true;
128
+ // Back over the whitespace between the word and the slash, then over the word
129
+ // itself: `return /x/` puts a space where `code[at - 1]` is.
130
+ let end = at;
131
+ while (end > 0 && /\s/.test(code[end - 1]))
132
+ end -= 1;
133
+ let start = end;
134
+ while (start > 0 && isIdentifierChar(code[start - 1]))
135
+ start -= 1;
136
+ return REGEX_PRECEDING_KEYWORDS.has(code.slice(start, end));
137
+ }
138
+ /**
139
+ * The scan itself, as a cursor over the source.
140
+ *
141
+ * A class rather than one long loop because the loop *was* one long loop, and it
142
+ * scored 126 on a complexity budget of 15 — which in this case the linter was
143
+ * right about. Every branch below is one lexical thing the scanner has to be
144
+ * able to walk past without losing its place, and naming them separately is what
145
+ * makes it possible to read whether the list is complete.
146
+ *
147
+ * State is deliberately minimal: where we are, what the last meaningful
148
+ * character was, whether a newline has happened since, and three depths plus a
149
+ * stack of open template literals. Nothing here builds a tree, because nothing
150
+ * here needs to answer any question except one.
151
+ */
152
+ class ModuleScanner {
153
+ code;
154
+ i = 0;
155
+ lastSignificant = '';
156
+ newlineSince = false;
157
+ braces = 0;
158
+ parens = 0;
159
+ brackets = 0;
160
+ /**
161
+ * Brace depths at which template literals opened, innermost last. A `}` ends
162
+ * an interpolation when the depth has come back to the one recorded for it.
163
+ */
164
+ templates = [];
165
+ constructor(code) {
166
+ this.code = code;
167
+ }
168
+ /** Whether a top-level `export` appears anywhere in the source. */
169
+ findsExport() {
170
+ while (this.i < this.code.length) {
171
+ if (this.step())
172
+ return true;
173
+ this.i += 1;
174
+ }
175
+ return false;
176
+ }
177
+ step() {
178
+ const char = this.code[this.i];
179
+ if (this.skipTrivia(char))
180
+ return false;
181
+ if (this.skipLiteral(char))
182
+ return false;
183
+ if (this.closesInterpolation(char))
184
+ return false;
185
+ this.adjustDepth(char);
186
+ if (this.isExportHere(char))
187
+ return true;
188
+ this.lastSignificant = char;
189
+ this.newlineSince = false;
190
+ return false;
191
+ }
192
+ /**
193
+ * Whitespace and comments.
194
+ *
195
+ * A comment leaves {@link lastSignificant} alone on purpose: a comment between
196
+ * a `;` and an `export` does not move the `export` off the start of a
197
+ * statement, and a rule that thought it did would fail on the most ordinary
198
+ * thing anybody writes above a function.
199
+ */
200
+ skipTrivia(char) {
201
+ if (char === '\n') {
202
+ this.newlineSince = true;
203
+ return true;
204
+ }
205
+ if (char === ' ' || char === '\t' || char === '\r')
206
+ return true;
207
+ return this.skipComment(char);
208
+ }
209
+ skipComment(char) {
210
+ if (char !== '/')
211
+ return false;
212
+ const next = this.code[this.i + 1];
213
+ if (next === '/') {
214
+ while (this.i < this.code.length && this.code[this.i] !== '\n')
215
+ this.i += 1;
216
+ this.newlineSince = true;
217
+ return true;
218
+ }
219
+ if (next !== '*')
220
+ return false;
221
+ const end = this.code.indexOf('*/', this.i + 2);
222
+ const chunk = end === -1 ? this.code.slice(this.i) : this.code.slice(this.i, end + 2);
223
+ // A block comment spanning lines ends the line for the newline rule, and an
224
+ // unterminated one swallows the rest of the file.
225
+ if (chunk.includes('\n'))
226
+ this.newlineSince = true;
227
+ this.i = end === -1 ? this.code.length : end + 1;
228
+ return true;
229
+ }
230
+ /** Strings, template literals and regular expressions — walked past whole. */
231
+ skipLiteral(char) {
232
+ if (char === '"' || char === "'") {
233
+ this.skipQuoted(char);
234
+ return this.consumedValue(char);
235
+ }
236
+ if (char === '`') {
237
+ this.openTemplate();
238
+ return this.consumedValue('`');
239
+ }
240
+ if (char === '/' && startsRegex(this.code, this.i, this.lastSignificant)) {
241
+ this.skipRegex();
242
+ return this.consumedValue('/');
243
+ }
244
+ return false;
245
+ }
246
+ /** Record that something which can end a value has just been walked past. */
247
+ consumedValue(ending) {
248
+ this.lastSignificant = ending;
249
+ this.newlineSince = false;
250
+ return true;
251
+ }
252
+ skipQuoted(quote) {
253
+ this.i += 1;
254
+ while (this.i < this.code.length && this.code[this.i] !== quote) {
255
+ if (this.code[this.i] === '\\')
256
+ this.i += 1;
257
+ this.i += 1;
258
+ }
259
+ }
260
+ skipRegex() {
261
+ this.i += 1;
262
+ let inClass = false;
263
+ while (this.i < this.code.length) {
264
+ const char = this.code[this.i];
265
+ if (char === '\\')
266
+ this.i += 1;
267
+ else if (char === '[')
268
+ inClass = true;
269
+ else if (char === ']')
270
+ inClass = false;
271
+ // A newline ends it either way: an unterminated regex is a syntax error,
272
+ // and running to the end of the file on one would hide everything after.
273
+ else if (char === '\n' || (char === '/' && !inClass))
274
+ break;
275
+ this.i += 1;
276
+ }
277
+ }
278
+ /**
279
+ * Walk a template literal to its closing backtick, or to the `${` that hands
280
+ * control back to the code scanner.
281
+ */
282
+ openTemplate() {
283
+ this.templates.push(this.braces);
284
+ this.i += 1;
285
+ while (this.i < this.code.length) {
286
+ const char = this.code[this.i];
287
+ if (char === '\\') {
288
+ this.i += 1;
289
+ }
290
+ else if (char === '`') {
291
+ this.templates.pop();
292
+ return;
293
+ }
294
+ else if (char === '$' && this.code[this.i + 1] === '{') {
295
+ this.i += 1;
296
+ return;
297
+ }
298
+ this.i += 1;
299
+ }
300
+ }
301
+ /**
302
+ * A `}` that ends an interpolation rather than a block: keep reading the
303
+ * template literal it was inside.
304
+ */
305
+ closesInterpolation(char) {
306
+ if (char !== '}')
307
+ return false;
308
+ if (this.templates.length === 0)
309
+ return false;
310
+ if (this.templates[this.templates.length - 1] !== this.braces)
311
+ return false;
312
+ this.templates.pop();
313
+ this.i += 1;
314
+ this.resumeTemplate();
315
+ return true;
316
+ }
317
+ resumeTemplate() {
318
+ while (this.i < this.code.length) {
319
+ const char = this.code[this.i];
320
+ if (char === '\\') {
321
+ this.i += 1;
322
+ }
323
+ else if (char === '`') {
324
+ this.consumedValue('`');
325
+ return;
326
+ }
327
+ else if (char === '$' && this.code[this.i + 1] === '{') {
328
+ this.templates.push(this.braces);
329
+ this.i += 1;
330
+ this.newlineSince = false;
331
+ return;
332
+ }
333
+ this.i += 1;
334
+ }
335
+ }
336
+ adjustDepth(char) {
337
+ if (char === '{')
338
+ this.braces += 1;
339
+ else if (char === '}')
340
+ this.braces -= 1;
341
+ else if (char === '[')
342
+ this.brackets += 1;
343
+ else if (char === ']')
344
+ this.brackets -= 1;
345
+ else if (char === '(')
346
+ this.parens += 1;
347
+ else if (char === ')')
348
+ this.parens -= 1;
349
+ }
350
+ /**
351
+ * The whole question, in one place: the keyword `export`, whole, at the start
352
+ * of a statement, at the outermost level of the source.
353
+ */
354
+ isExportHere(char) {
355
+ if (char !== 'e')
356
+ return false;
357
+ if (this.braces !== 0 || this.parens !== 0 || this.brackets !== 0)
358
+ return false;
359
+ if (this.templates.length > 0)
360
+ return false;
361
+ if (!this.atStatementStart())
362
+ return false;
363
+ if (!this.code.startsWith('export', this.i))
364
+ return false;
365
+ return !isIdentifierChar(this.code[this.i + 6] ?? '');
366
+ }
367
+ /**
368
+ * Start of input, after a `;` or a `}`, or on a new line — the last because
369
+ * JavaScript does not require the semicolon and plenty of people do not write
370
+ * one.
371
+ */
372
+ atStatementStart() {
373
+ return (this.lastSignificant === '' ||
374
+ this.lastSignificant === ';' ||
375
+ this.lastSignificant === '}' ||
376
+ this.newlineSince);
377
+ }
378
+ }
379
+ /**
380
+ * Does this code declare an ES module — and so ask to be called as a function
381
+ * over one object?
382
+ *
383
+ * See the module docblock for the rule and why it is the rule. Python is not
384
+ * asked this question: its harness writes the `def` itself, so a Python
385
+ * transform never states a signature and never had the problem this detector
386
+ * exists to solve.
387
+ */
388
+ function transformDeclaresModule(code) {
389
+ return new ModuleScanner(code).findsExport();
390
+ }
391
+ /** {@link transformDeclaresModule}, as the shape it names. */
392
+ function transformShape(code) {
393
+ return transformDeclaresModule(code) ? 'module' : 'body';
394
+ }
395
+ /**
396
+ * The sentence to add when a body-shaped run died on the one syntax error that
397
+ * means the detector and the author disagreed.
398
+ *
399
+ * The rule above is a scan, not a parser, and its documented limit is that a
400
+ * regular-expression literal read as a division can hide a real `export`. The
401
+ * author then sees `Unexpected token 'export'` from code they believe is a
402
+ * perfectly good module, and has no way to know that a *rule they have never
403
+ * read* is what decided otherwise. Naming the rule in the error is the whole
404
+ * difference between a two-minute fix and an afternoon.
405
+ *
406
+ * Deliberately not a fallback re-run in the other shape. Running code twice
407
+ * because the first attempt failed is guessing with extra steps: the second
408
+ * attempt would report a different error for the same text, and neither error
409
+ * would be trustworthy.
410
+ */
411
+ function transformShapeHint(shape, stderr) {
412
+ if (shape !== 'body')
413
+ return '';
414
+ if (!/Unexpected token '?export/.test(stderr))
415
+ return '';
416
+ return ('\nThis ran as a bare function body, because the catalog looks for an `export` keyword at ' +
417
+ 'the start of a statement, outside any brackets, string or comment — and found none. If ' +
418
+ 'this was meant to be a module, move the `export` to the start of its own line.');
419
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dudousxd/nestjs-catalog",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "description": "A metadata registry for NestJS: object types, properties and relations, derived from your ORM and enriched with decorators.",
5
5
  "license": "MIT",
6
6
  "author": "Davide Carvalho",