trimcode 0.0.0-stage → 0.1.1

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/src/parse.js ADDED
@@ -0,0 +1,64 @@
1
+ import { parse } from 'acorn';
2
+
3
+ /** Thrown when the source cannot be parsed. `line` is 1-based, `column` is 1-based. */
4
+ export class ParseError extends Error {
5
+ constructor(message, { line, column } = {}) {
6
+ super(message);
7
+ this.name = 'ParseError';
8
+ this.line = line;
9
+ this.column = column;
10
+ }
11
+ }
12
+
13
+ const BASE_OPTIONS = {
14
+ ecmaVersion: 'latest',
15
+ locations: true,
16
+ allowHashBang: true,
17
+ // CommonJS files may `return` at the top level, and scripts run through
18
+ // wrappers may `await` there.
19
+ allowReturnOutsideFunction: true,
20
+ allowAwaitOutsideFunction: true,
21
+ };
22
+
23
+ function toParseError(err) {
24
+ const line = err?.loc?.line;
25
+ const column = typeof err?.loc?.column === 'number' ? err.loc.column + 1 : undefined;
26
+ const message = String(err?.message ?? err).replace(/\s*\(\d+:\d+\)$/, '');
27
+ return new ParseError(message, { line, column });
28
+ }
29
+
30
+ /**
31
+ * Parse JavaScript source.
32
+ *
33
+ * With `sourceType: 'auto'` the code is parsed as an ES module first and, if
34
+ * that fails, as a sloppy-mode script (old CommonJS code can use `with`,
35
+ * octal literals and so on). When both fail, the module error is reported.
36
+ */
37
+ export function parseSource(code, sourceType = 'auto') {
38
+ const comments = [];
39
+ const attempt = (type) => {
40
+ comments.length = 0;
41
+ return parse(code, { ...BASE_OPTIONS, sourceType: type, onComment: comments });
42
+ };
43
+
44
+ try {
45
+ if (sourceType === 'module' || sourceType === 'script') {
46
+ return { ast: attempt(sourceType), comments };
47
+ }
48
+ if (sourceType !== 'auto') {
49
+ throw new TypeError(`Unknown sourceType "${sourceType}" (expected "auto", "module" or "script")`);
50
+ }
51
+ try {
52
+ return { ast: attempt('module'), comments };
53
+ } catch (moduleError) {
54
+ try {
55
+ return { ast: attempt('script'), comments };
56
+ } catch {
57
+ throw moduleError;
58
+ }
59
+ }
60
+ } catch (err) {
61
+ if (err instanceof TypeError) throw err;
62
+ throw toParseError(err);
63
+ }
64
+ }
package/src/util.js ADDED
@@ -0,0 +1,119 @@
1
+ /** Name of a non-dynamic property key, or null when it cannot be known statically. */
2
+ export function propKey(key, computed = false) {
3
+ if (!key) return null;
4
+ if (key.type === 'PrivateIdentifier') return `#${key.name}`;
5
+ if (!computed && key.type === 'Identifier') return key.name;
6
+ if (key.type === 'Literal' && (typeof key.value === 'string' || typeof key.value === 'number')) {
7
+ return String(key.value);
8
+ }
9
+ return null;
10
+ }
11
+
12
+ /** Property name of a MemberExpression (`a.b`, `a['b']`, `this.#b`), or null. */
13
+ export function propName(member) {
14
+ if (member?.type !== 'MemberExpression') return null;
15
+ return propKey(member.property, member.computed);
16
+ }
17
+
18
+ /**
19
+ * Dotted path for member chains rooted at an identifier, `this` or `super`:
20
+ * `module.exports.foo` -> "module.exports.foo". Returns null for anything else.
21
+ */
22
+ export function memberPath(node) {
23
+ if (!node) return null;
24
+ switch (node.type) {
25
+ case 'Identifier':
26
+ return node.name;
27
+ case 'ThisExpression':
28
+ return 'this';
29
+ case 'Super':
30
+ return 'super';
31
+ case 'MemberExpression': {
32
+ const prop = propName(node);
33
+ const base = memberPath(node.object);
34
+ return prop !== null && base ? `${base}.${prop}` : null;
35
+ }
36
+ default:
37
+ return null;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Printable name for a call target. Chains through calls are shown with "()":
43
+ * `db.query(x).then(y)` -> "db.query().then". Returns null if it cannot be named.
44
+ */
45
+ export function calleeName(callee) {
46
+ const direct = memberPath(callee);
47
+ if (direct) return direct;
48
+ if (callee?.type === 'MemberExpression') {
49
+ const prop = propName(callee);
50
+ let object = callee.object;
51
+ if (object.type === 'ChainExpression') object = object.expression;
52
+ if (prop !== null && object.type === 'CallExpression') {
53
+ const inner = calleeName(object.callee);
54
+ if (inner) return `${inner}().${prop}`;
55
+ }
56
+ }
57
+ return null;
58
+ }
59
+
60
+ /** String value of a string literal or an expression-free template literal, else null. */
61
+ export function stringArg(node) {
62
+ if (!node) return null;
63
+ if (node.type === 'Literal' && typeof node.value === 'string') return node.value;
64
+ if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
65
+ return node.quasis[0].value.cooked ?? null;
66
+ }
67
+ return null;
68
+ }
69
+
70
+ /** Source text of a node on one line, shortened to `max` characters. */
71
+ export function snippet(code, node, max = 60) {
72
+ const text = code.slice(node.start, node.end).replace(/\s+/g, ' ').trim();
73
+ return text.length > max ? `${text.slice(0, max - 1)}…` : text;
74
+ }
75
+
76
+ /** All identifiers bound by a destructuring pattern. */
77
+ export function patternNames(pattern) {
78
+ if (!pattern) return [];
79
+ switch (pattern.type) {
80
+ case 'Identifier':
81
+ return [pattern.name];
82
+ case 'ObjectPattern':
83
+ return pattern.properties.flatMap((p) =>
84
+ p.type === 'RestElement' ? patternNames(p.argument) : patternNames(p.value),
85
+ );
86
+ case 'ArrayPattern':
87
+ return pattern.elements.flatMap((el) => patternNames(el));
88
+ case 'AssignmentPattern':
89
+ return patternNames(pattern.left);
90
+ case 'RestElement':
91
+ return patternNames(pattern.argument);
92
+ default:
93
+ return [];
94
+ }
95
+ }
96
+
97
+ /** True for `require(...)` calls (any argument). */
98
+ export function isRequireCall(node) {
99
+ return (
100
+ node?.type === 'CallExpression' &&
101
+ node.callee.type === 'Identifier' &&
102
+ node.callee.name === 'require' &&
103
+ node.arguments.length > 0
104
+ );
105
+ }
106
+
107
+ /** First sentence of a JSDoc block's description, or undefined if it has none. */
108
+ export function jsdocSummary(raw) {
109
+ const lines = raw.split('\n').map((l) => l.replace(/^\s*\*+ ?/, '').trim());
110
+ const description = [];
111
+ for (const line of lines) {
112
+ if (line.startsWith('@')) break;
113
+ description.push(line);
114
+ }
115
+ const text = description.join(' ').replace(/\s+/g, ' ').trim();
116
+ if (!text) return undefined;
117
+ const first = /^(.+?[.!?])(\s|$)/.exec(text)?.[1] ?? text;
118
+ return first.length > 160 ? `${first.slice(0, 159)}…` : first;
119
+ }
package/src/walk.js ADDED
@@ -0,0 +1,32 @@
1
+ const SKIP = new Set(['type', 'start', 'end', 'loc', 'range']);
2
+
3
+ /**
4
+ * Depth-first AST walk with enter/leave hooks.
5
+ *
6
+ * Both hooks receive `(node, ancestors)` where `ancestors` is the chain of
7
+ * parents, nearest parent last. The array is reused between calls, so copy it
8
+ * if you need to keep it.
9
+ */
10
+ export function walk(root, { enter, leave } = {}) {
11
+ const ancestors = [];
12
+
13
+ function visit(node) {
14
+ if (enter) enter(node, ancestors);
15
+ ancestors.push(node);
16
+ for (const key in node) {
17
+ if (SKIP.has(key)) continue;
18
+ const value = node[key];
19
+ if (Array.isArray(value)) {
20
+ for (const child of value) {
21
+ if (child && typeof child.type === 'string') visit(child);
22
+ }
23
+ } else if (value && typeof value.type === 'string') {
24
+ visit(value);
25
+ }
26
+ }
27
+ ancestors.pop();
28
+ if (leave) leave(node, ancestors);
29
+ }
30
+
31
+ visit(root);
32
+ }