@dogsbay/literal 0.2.0-beta.135 → 0.2.0-beta.136
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 +6 -2
- package/dist/index.js +113 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,8 +12,12 @@ const { value, unresolved } = evaluateModule(source); // the module's default ex
|
|
|
12
12
|
It understands object, array, string, number, boolean and null literals; template strings
|
|
13
13
|
with no `${}`; spreads; module-level `const` references; zero-argument helpers that return a
|
|
14
14
|
literal; wrapper calls such as `defineConfig({…})`; member access and literal indexes
|
|
15
|
-
(`releases[0]["x"]`); `"a" + "b"` (strings only); `
|
|
16
|
-
|
|
15
|
+
(`releases[0]["x"]`); `"a" + "b"` (strings only); `.length` and a fixed list of pure string
|
|
16
|
+
and list methods with literal arguments (`slice`, `substring`, `split`, `join`, `replace`,
|
|
17
|
+
`replaceAll`, `trim`, `toLowerCase`, `toUpperCase`, `startsWith`, `endsWith`, `includes`,
|
|
18
|
+
`indexOf`, `at`, `padStart`, `padEnd`, `repeat`: `version.split('.').slice(0, 2).join('.')`);
|
|
19
|
+
`module.exports =`; and TypeScript type noise. A method is computed from that list, never by
|
|
20
|
+
calling what a name finds. Anything else becomes `UNRESOLVED` and is listed in `unresolved`: never a plausible
|
|
17
21
|
wrong value, never a silent omission.
|
|
18
22
|
|
|
19
23
|
Other modules (`require("./x")`, `import x from "./x"`) are read only through a resolver you
|
package/dist/index.js
CHANGED
|
@@ -251,6 +251,68 @@ const CLOSERS = new Set(["}", "]", ")"]);
|
|
|
251
251
|
const MAX_VALUES = 100_000;
|
|
252
252
|
/** The longest string `+` may build: `const b = a + a` doubles, thirty times is a gigabyte. */
|
|
253
253
|
const MAX_STRING = 1024 * 1024;
|
|
254
|
+
const isInt = (v) => typeof v === "number" && Number.isInteger(v);
|
|
255
|
+
const isStr = (v) => typeof v === "string";
|
|
256
|
+
const optInt = (v) => v === undefined || isInt(v);
|
|
257
|
+
/** A count a method may repeat or pad to: bounded before anything is built. */
|
|
258
|
+
const smallInt = (v) => isInt(v) && v >= 0 && v <= MAX_STRING;
|
|
259
|
+
/** A method's result that would be larger than MAX_STRING / MAX_VALUES: refused before it is built. */
|
|
260
|
+
const TOO_LARGE = Symbol("too large");
|
|
261
|
+
/** How often `a` occurs in `s` (an empty `a`: before every character and at the end). */
|
|
262
|
+
const occurrences = (s, a) => {
|
|
263
|
+
if (a === "")
|
|
264
|
+
return s.length + 1;
|
|
265
|
+
let n = 0;
|
|
266
|
+
for (let i = s.indexOf(a); i !== -1; i = s.indexOf(a, i + a.length))
|
|
267
|
+
n++;
|
|
268
|
+
return n;
|
|
269
|
+
};
|
|
270
|
+
/** The longest one replacement by `b` can be: each `$` pattern may insert the match or the whole string. */
|
|
271
|
+
const replacementBound = (s, a, b) => b.length + occurrences(b, "$") * Math.max(s.length, a.length);
|
|
272
|
+
const within = (length, build) => (length > MAX_STRING ? TOO_LARGE : build());
|
|
273
|
+
/**
|
|
274
|
+
* The string and list methods a config may call (Tigera's `releases[0].title.slice(0, 5)`,
|
|
275
|
+
* `version.split('.').slice(0, 2).join('.')`): pure, with literal arguments of the types each
|
|
276
|
+
* takes, else undefined (unresolved, reported). Nothing here calls a function a name refers to,
|
|
277
|
+
* and nothing larger than the limits is built: its size is worked out first (TOO_LARGE).
|
|
278
|
+
*/
|
|
279
|
+
const METHODS = {
|
|
280
|
+
slice: (v, [a, b]) => (optInt(a) && optInt(b) ? v.slice(a, b) : undefined),
|
|
281
|
+
// `null` is a value a list may hold; past either end is undefined, which is no value here
|
|
282
|
+
at: (v, [a]) => (isInt(a) ? v.at(a) : undefined),
|
|
283
|
+
includes: (v, [a]) => (typeof v === "string" ? (isStr(a) ? v.includes(a) : undefined) : v.includes(a)),
|
|
284
|
+
indexOf: (v, [a]) => (typeof v === "string" ? (isStr(a) ? v.indexOf(a) : undefined) : v.indexOf(a)),
|
|
285
|
+
// Strings
|
|
286
|
+
substring: (v, [a, b]) => (typeof v === "string" && optInt(a) && optInt(b) ? v.substring(a, b) : undefined),
|
|
287
|
+
split: (v, [sep, limit]) => {
|
|
288
|
+
if (typeof v !== "string" || !isStr(sep) || !optInt(limit))
|
|
289
|
+
return undefined;
|
|
290
|
+
const pieces = Math.min(sep === "" ? v.length : occurrences(v, sep) + 1, limit === undefined || limit < 0 ? Infinity : limit);
|
|
291
|
+
return pieces > MAX_VALUES ? TOO_LARGE : v.split(sep, limit);
|
|
292
|
+
},
|
|
293
|
+
toLowerCase: (v, a) => (typeof v === "string" && !a.length ? v.toLowerCase() : undefined),
|
|
294
|
+
toUpperCase: (v, a) => (typeof v === "string" && !a.length ? v.toUpperCase() : undefined),
|
|
295
|
+
trim: (v, a) => (typeof v === "string" && !a.length ? v.trim() : undefined),
|
|
296
|
+
trimStart: (v, a) => (typeof v === "string" && !a.length ? v.trimStart() : undefined),
|
|
297
|
+
trimEnd: (v, a) => (typeof v === "string" && !a.length ? v.trimEnd() : undefined),
|
|
298
|
+
startsWith: (v, [a]) => (typeof v === "string" && isStr(a) ? v.startsWith(a) : undefined),
|
|
299
|
+
endsWith: (v, [a]) => (typeof v === "string" && isStr(a) ? v.endsWith(a) : undefined),
|
|
300
|
+
// A string pattern only (a regex literal is not a value here); `$` patterns as JavaScript has them
|
|
301
|
+
replace: (v, [a, b]) => typeof v === "string" && isStr(a) && isStr(b) ? within(v.length + replacementBound(v, a, b), () => v.replace(a, b)) : undefined,
|
|
302
|
+
replaceAll: (v, [a, b]) => typeof v === "string" && isStr(a) && isStr(b)
|
|
303
|
+
? within(v.length + occurrences(v, a) * replacementBound(v, a, b), () => v.replaceAll(a, b))
|
|
304
|
+
: undefined,
|
|
305
|
+
padStart: (v, [n, f]) => (typeof v === "string" && smallInt(n) && (f === undefined || isStr(f)) ? v.padStart(n, f) : undefined),
|
|
306
|
+
padEnd: (v, [n, f]) => (typeof v === "string" && smallInt(n) && (f === undefined || isStr(f)) ? v.padEnd(n, f) : undefined),
|
|
307
|
+
repeat: (v, [n]) => (typeof v === "string" && smallInt(n) && v.length * n <= MAX_STRING ? v.repeat(n) : undefined),
|
|
308
|
+
// Lists
|
|
309
|
+
join: (v, [sep]) => {
|
|
310
|
+
if (!Array.isArray(v) || !(sep === undefined || isStr(sep)) || !v.every((x) => isStr(x) || typeof x === "number"))
|
|
311
|
+
return undefined;
|
|
312
|
+
const length = v.reduce((n, x) => n + String(x).length, 0) + Math.max(0, v.length - 1) * (sep === undefined ? 1 : sep.length);
|
|
313
|
+
return within(length, () => v.join(sep));
|
|
314
|
+
},
|
|
315
|
+
};
|
|
254
316
|
/** How deeply values may nest: past this a parser or a consumer walking it runs out of stack. */
|
|
255
317
|
const MAX_DEPTH = 500;
|
|
256
318
|
/**
|
|
@@ -939,6 +1001,44 @@ class Evaluator {
|
|
|
939
1001
|
size += this.sizeOf(v);
|
|
940
1002
|
return this.postfix(this.bounded(out, size, i), k + 1);
|
|
941
1003
|
}
|
|
1004
|
+
/** `value.name(args)` at the `(` at `i`: the arguments read as literals, the method from METHODS. */
|
|
1005
|
+
methodCall(value, name, i) {
|
|
1006
|
+
const args = [];
|
|
1007
|
+
let k = i + 1;
|
|
1008
|
+
while (k < this.toks.length && !this.is(k, ")")) {
|
|
1009
|
+
const arg = this.parseValue(k);
|
|
1010
|
+
args.push(arg.value);
|
|
1011
|
+
k = arg.next;
|
|
1012
|
+
if (this.is(k, ","))
|
|
1013
|
+
k++;
|
|
1014
|
+
else if (!this.is(k, ")"))
|
|
1015
|
+
break;
|
|
1016
|
+
}
|
|
1017
|
+
if (!this.is(k, ")")) {
|
|
1018
|
+
this.report(i, `a call to \`.${name}\` this reader could not read`);
|
|
1019
|
+
return { value: UNRESOLVED, next: this.skipBalanced(i) };
|
|
1020
|
+
}
|
|
1021
|
+
const next = k + 1;
|
|
1022
|
+
if (args.includes(UNRESOLVED))
|
|
1023
|
+
return { value: UNRESOLVED, next };
|
|
1024
|
+
const result = METHODS[name](value, args);
|
|
1025
|
+
if (result === undefined) {
|
|
1026
|
+
this.report(i, `\`.${name}(…)\` with arguments this reader does not take, or no value`);
|
|
1027
|
+
return { value: UNRESOLVED, next };
|
|
1028
|
+
}
|
|
1029
|
+
if (result === TOO_LARGE || (typeof result === "string" && result.length > MAX_STRING)) {
|
|
1030
|
+
this.report(i, `\`.${name}(…)\`: a value larger than ${MAX_STRING} characters or ${MAX_VALUES} entries`);
|
|
1031
|
+
return { value: UNRESOLVED, next };
|
|
1032
|
+
}
|
|
1033
|
+
// A list costs what it expands to, like any list built here: copies of a large one add up
|
|
1034
|
+
if (Array.isArray(result)) {
|
|
1035
|
+
let size = 1;
|
|
1036
|
+
for (const x of result)
|
|
1037
|
+
size += this.sizeOf(x);
|
|
1038
|
+
return { value: this.bounded(result, size, i), next };
|
|
1039
|
+
}
|
|
1040
|
+
return { value: result, next };
|
|
1041
|
+
}
|
|
942
1042
|
/**
|
|
943
1043
|
* Postfix operators after a value: TS `as`/`satisfies`, and member access
|
|
944
1044
|
* into a value we already have.
|
|
@@ -981,6 +1081,19 @@ class Evaluator {
|
|
|
981
1081
|
}
|
|
982
1082
|
if ((this.is(k, ".") || this.is(k, "?.")) && this.toks[k + 1]?.kind === "ident") {
|
|
983
1083
|
const prop = this.toks[k + 1].text;
|
|
1084
|
+
// A string or list method with literal arguments (`.slice(1)`, `.split('.').slice(0, 2).join('.')`):
|
|
1085
|
+
// computed here from a fixed list of pure methods, never by calling what a name finds
|
|
1086
|
+
if ((typeof v === "string" || Array.isArray(v)) && this.is(k + 2, "(") && Object.hasOwn(METHODS, prop)) {
|
|
1087
|
+
const call = this.methodCall(v, prop, k + 2);
|
|
1088
|
+
v = call.value;
|
|
1089
|
+
k = call.next;
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
if ((typeof v === "string" || Array.isArray(v)) && prop === "length" && !this.is(k + 2, "(")) {
|
|
1093
|
+
v = v.length;
|
|
1094
|
+
k += 2;
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
984
1097
|
const obj = v && typeof v === "object" && !Array.isArray(v)
|
|
985
1098
|
? v
|
|
986
1099
|
: undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dogsbay/literal",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.136",
|
|
4
4
|
"description": "Read a JavaScript/TypeScript config module as data without running it: object and array literals, spreads, module-level constants, zero-argument helpers and wrapper calls; anything else is reported, never guessed. No Node, no dependencies",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|