trimcode 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 trimcode contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,128 @@
1
- # Temporary Holding Version
1
+ # trimcode
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Turn a Node.js source file into a simple, structured JSON summary: what it imports, what it exports, which functions and classes it defines, which HTTP routes it declares, what it does when it loads, and which environment variables it reads.
4
+
5
+ It parses the file into an AST and reads it. It never runs your code, never touches the network, and has a single dependency ([acorn](https://github.com/acornjs/acorn)). It works on CommonJS and ES modules, and does not need JSDoc comments.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install trimcode # as a library
11
+ npx trimcode src/server.js # or just run it
12
+ ```
13
+
14
+ Requires Node.js 18.3 or newer.
15
+
16
+ ## Command line
17
+
18
+ ```sh
19
+ trimcode <file...> [options]
20
+ cat file.js | trimcode
21
+
22
+ -o, --out <path> write the JSON to a file instead of stdout
23
+ -c, --compact print JSON on a single line
24
+ --source-type <type> auto (default), module or script
25
+ -h, --help
26
+ -v, --version
27
+ ```
28
+
29
+ One file prints one JSON object; several files print an array. A file that cannot be read or parsed is reported on stderr as `file:line:column: message`, the other files are still printed, and the exit code is `1`.
30
+
31
+ ## API
32
+
33
+ ```js
34
+ import { analyze, analyzeFile } from 'trimcode';
35
+
36
+ const result = analyze(sourceCode, { filename: 'server.js' });
37
+ const same = await analyzeFile('server.js');
38
+ ```
39
+
40
+ `analyze` throws a `ParseError` (with 1-based `line` and `column`) if the source is not valid JavaScript. TypeScript definitions are included.
41
+
42
+ ## Example
43
+
44
+ Given [`examples/server.js`](examples/server.js), a small Express app, `trimcode examples/server.js` prints (trimmed here):
45
+
46
+ ```json
47
+ {
48
+ "file": "examples/server.js",
49
+ "moduleType": "commonjs",
50
+ "summary": "CommonJS module, 41 lines. Depends on 2 npm packages, 1 Node built-in and 1 local module. Defines 4 functions. Exports 1 name. Declares 2 HTTP routes. When loaded it loads environment variables from a file, touches the filesystem and starts a server. Reads 2 environment variables.",
51
+ "dependencies": { "builtin": ["fs"], "packages": ["express", "dotenv"], "local": ["./db"] },
52
+ "exports": [{ "name": "getUser", "kind": "function", "style": "commonjs", "line": 41 }],
53
+ "functions": [
54
+ {
55
+ "name": "getUser",
56
+ "kind": "function",
57
+ "async": true,
58
+ "params": ["id"],
59
+ "exported": true,
60
+ "doc": "Looks a user up and strips private fields.",
61
+ "complexity": 2,
62
+ "calls": ["findUser", "publicView"],
63
+ "localCalls": ["publicView"]
64
+ },
65
+ {
66
+ "name": "GET /users/:id",
67
+ "kind": "route-handler",
68
+ "calls": ["res.json", "getUser"],
69
+ "localCalls": ["getUser"]
70
+ }
71
+ ],
72
+ "routes": [
73
+ { "method": "GET", "path": "/users/:id", "handler": "GET /users/:id", "middleware": ["requireAuth"], "line": 28 },
74
+ { "method": "POST", "path": "/users", "handler": "POST /users", "line": 32 }
75
+ ],
76
+ "sideEffects": [
77
+ { "type": "env-load", "detail": "dotenv.config", "line": 4 },
78
+ { "type": "filesystem", "detail": "fs.mkdirSync", "line": 37 },
79
+ { "type": "server-listen", "detail": "PORT", "line": 39 }
80
+ ],
81
+ "env": [
82
+ { "name": "PORT", "line": 7, "hasDefault": true },
83
+ { "name": "LOG_DIR", "line": 8, "hasDefault": false }
84
+ ]
85
+ }
86
+ ```
87
+
88
+ ## Output reference
89
+
90
+ | Field | What it holds |
91
+ | --- | --- |
92
+ | `schemaVersion` | Version of the output shape. It only changes on breaking changes. |
93
+ | `file`, `moduleType` | The filename you gave, and `esm` or `commonjs`. |
94
+ | `summary` | One short paragraph in plain language, built from the fields below. |
95
+ | `stats` | Line, import, export, function, class and route counts. |
96
+ | `dependencies` | Unique `builtin`, `packages` and `local` modules. |
97
+ | `imports` | `import`, `require()` and `import()`: source, kind, binding, named imports, line. |
98
+ | `exports` | Named and default exports for both module systems, including re-exports. |
99
+ | `functions` | Named functions, arrows, methods and route handlers, with params, async/generator flags, JSDoc summary, complexity, nesting, `calls` and `localCalls`. |
100
+ | `classes` | Name, `extends`, methods (static, getters, async). |
101
+ | `routes` | Express/Fastify-style routes: method, path, handler, middleware. |
102
+ | `sideEffects` | What happens when the file loads (see below). |
103
+ | `env` | `process.env` variables read, and whether a fallback is supplied. |
104
+
105
+ Side-effect types: `server-listen`, `server-create`, `connection`, `env-load`, `process-handler`, `process-exit`, `timer`, `filesystem`, `child-process`, `console-output`, `global-mutation`, `top-level-await`.
106
+
107
+ ## What to know before relying on it
108
+
109
+ This is static analysis, so some answers are best-effort:
110
+
111
+ - **Routes are heuristic.** A call like `x.get('/path', handler)` counts when `x` looks like a router (`app`, `router`, `fastify`, `userRouter`, ...) or a handler is an inline function. Known HTTP clients (`axios`, `got`, `supertest`, ...) are excluded. A router with an unusual name and named handlers can be missed.
112
+ - **Side effects are load-time only.** Code at the top level and inside an immediately-invoked function is reported. Code inside a function that is called later is not, so `main()` hiding a `listen()` will not show up.
113
+ - **The call graph is by name.** `localCalls` links calls to functions defined in the same file by name (and `this.method()` inside a class). It does not follow calls across files or through variables.
114
+ - **Complexity is approximate.** It counts branches (`if`, loops, `case`, `catch`, `?:`, `&&`, `||`, `??`) and attributes anonymous callbacks to the named function that contains them.
115
+ - **Export kinds for imported names are generic.** `export { Foo }` where `Foo` was imported from another file is reported as `value`.
116
+ - **Plain JavaScript only.** TypeScript, JSX and Flow syntax are reported as parse errors.
117
+ - **Dynamic imports** whose specifier is not a string literal are listed with `"kind": "dynamic"` and `"source": null`.
118
+
119
+ ## Development
120
+
121
+ ```sh
122
+ npm install
123
+ npm test # node:test, no extra dependencies
124
+ ```
125
+
126
+ ## License
127
+
128
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import { readFile, writeFile } from 'node:fs/promises';
4
+ import { parseArgs } from 'node:util';
5
+ import { analyze, ParseError } from '../src/index.js';
6
+
7
+ const USAGE = `trimcode - summarize a Node.js source file as structured JSON
8
+
9
+ Usage:
10
+ trimcode <file...> [options]
11
+ cat file.js | trimcode [options]
12
+
13
+ Options:
14
+ -o, --out <path> Write the JSON to a file instead of stdout
15
+ -c, --compact Print JSON on a single line
16
+ --source-type <type> auto (default), module or script
17
+ -h, --help Show this help
18
+ -v, --version Show the version
19
+
20
+ One file prints one JSON object; several files print an array.
21
+ Files that cannot be read or parsed are reported on stderr and the exit code is 1.
22
+ `;
23
+
24
+ function fail(message, code = 2) {
25
+ process.stderr.write(`trimcode: ${message}\n`);
26
+ process.exit(code);
27
+ }
28
+
29
+ let args;
30
+ try {
31
+ args = parseArgs({
32
+ allowPositionals: true,
33
+ options: {
34
+ out: { type: 'string', short: 'o' },
35
+ compact: { type: 'boolean', short: 'c' },
36
+ 'source-type': { type: 'string' },
37
+ help: { type: 'boolean', short: 'h' },
38
+ version: { type: 'boolean', short: 'v' },
39
+ },
40
+ });
41
+ } catch (err) {
42
+ fail(`${err.message}\n\n${USAGE}`);
43
+ }
44
+
45
+ const { values, positionals } = args;
46
+
47
+ if (values.help) {
48
+ process.stdout.write(USAGE);
49
+ process.exit(0);
50
+ }
51
+ if (values.version) {
52
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
53
+ process.stdout.write(`${pkg.version}\n`);
54
+ process.exit(0);
55
+ }
56
+
57
+ const sourceType = values['source-type'] ?? 'auto';
58
+ if (!['auto', 'module', 'script'].includes(sourceType)) {
59
+ fail(`--source-type must be "auto", "module" or "script" (got "${sourceType}")`);
60
+ }
61
+
62
+ async function readStdin() {
63
+ const chunks = [];
64
+ for await (const chunk of process.stdin) chunks.push(chunk);
65
+ return Buffer.concat(chunks).toString('utf8');
66
+ }
67
+
68
+ const inputs = positionals.length ? positionals : [];
69
+ if (inputs.length === 0) {
70
+ if (process.stdin.isTTY) {
71
+ process.stderr.write(USAGE);
72
+ process.exit(2);
73
+ }
74
+ inputs.push('-');
75
+ }
76
+
77
+ const results = [];
78
+ let failed = false;
79
+
80
+ for (const input of inputs) {
81
+ try {
82
+ const code = input === '-' ? await readStdin() : await readFile(input, 'utf8');
83
+ results.push(analyze(code, { filename: input === '-' ? null : input, sourceType }));
84
+ } catch (err) {
85
+ failed = true;
86
+ const name = input === '-' ? '<stdin>' : input;
87
+ if (err instanceof ParseError) {
88
+ const where = err.line ? `:${err.line}${err.column ? `:${err.column}` : ''}` : '';
89
+ process.stderr.write(`${name}${where}: ${err.message}\n`);
90
+ } else {
91
+ process.stderr.write(`${name}: ${err.message}\n`);
92
+ }
93
+ }
94
+ }
95
+
96
+ if (results.length > 0) {
97
+ const output = inputs.length === 1 ? results[0] : results;
98
+ const json = `${JSON.stringify(output, null, values.compact ? 0 : 2)}\n`;
99
+ if (values.out) await writeFile(values.out, json);
100
+ else process.stdout.write(json);
101
+ }
102
+
103
+ process.exitCode = failed ? 1 : 0;
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
2
  "name": "trimcode",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Turn a Node.js source file into a simple, structured JSON summary: imports, exports, functions, classes, HTTP routes, side effects and env vars.",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "types": "./src/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.d.ts",
11
+ "default": "./src/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "bin": {
16
+ "trimcode": "bin/cli.js"
17
+ },
18
+ "files": [
19
+ "src",
20
+ "bin",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "scripts": {
25
+ "test": "node --test",
26
+ "prepublishOnly": "npm test"
27
+ },
28
+ "engines": {
29
+ "node": ">=18.3"
30
+ },
31
+ "dependencies": {
32
+ "acorn": "^8.18.0"
33
+ },
34
+ "keywords": [
35
+ "ast",
36
+ "static-analysis",
37
+ "code-summary",
38
+ "code-structure",
39
+ "javascript",
40
+ "nodejs",
41
+ "json",
42
+ "cli",
43
+ "express",
44
+ "documentation"
45
+ ],
46
+ "license": "MIT"
47
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,141 @@
1
+ export declare const SCHEMA_VERSION: 1;
2
+
3
+ export interface AnalyzeOptions {
4
+ /** Stored in the result as `file`. */
5
+ filename?: string;
6
+ /** `auto` tries an ES module first, then a sloppy-mode script. Default: `auto`. */
7
+ sourceType?: 'auto' | 'module' | 'script';
8
+ }
9
+
10
+ export interface ImportEntry {
11
+ /** The specifier as written, or `null` when it is computed at runtime. */
12
+ source: string | null;
13
+ kind: 'builtin' | 'package' | 'local' | 'dynamic';
14
+ /** npm package name (`@scope/name` or `name`) when `kind` is `package`. */
15
+ package?: string;
16
+ style: 'import' | 'require' | 'dynamic-import';
17
+ /** Local name bound to the whole module (default or namespace import, `const x = require()`). */
18
+ binding?: string;
19
+ /** Named imports / destructured names, as exported by the module. */
20
+ names?: string[];
21
+ line: number;
22
+ }
23
+
24
+ export interface ExportEntry {
25
+ name: string;
26
+ kind: 'function' | 'class' | 'value' | 're-export';
27
+ style: 'commonjs' | 'esm';
28
+ /** Module re-exported from, for `export ... from`. */
29
+ from?: string;
30
+ /** Local binding when it differs from the exported name. */
31
+ local?: string;
32
+ line: number;
33
+ }
34
+
35
+ export interface FunctionEntry {
36
+ /** `name`, `Class.method`, `object.key`, or `METHOD /path` for route handlers. */
37
+ name: string;
38
+ kind: 'function' | 'arrow' | 'method' | 'constructor' | 'getter' | 'setter' | 'route-handler';
39
+ async: boolean;
40
+ generator: boolean;
41
+ static?: true;
42
+ params: string[];
43
+ line: number;
44
+ endLine: number;
45
+ lines: number;
46
+ exported: boolean;
47
+ /** First sentence of the JSDoc description, if there is one. */
48
+ doc?: string;
49
+ /** Approximate cyclomatic complexity (1 + branches). */
50
+ complexity: number;
51
+ /** Deepest nesting of control-flow statements. */
52
+ maxNesting: number;
53
+ /** Unique call targets, excluding `require` and `console.*`. */
54
+ calls: string[];
55
+ /** The subset of `calls` that resolve to functions defined in this file. */
56
+ localCalls: string[];
57
+ }
58
+
59
+ export interface ClassEntry {
60
+ name: string;
61
+ extends: string | null;
62
+ line: number;
63
+ endLine: number;
64
+ methods: Array<{
65
+ name: string;
66
+ kind: 'function' | 'arrow' | 'method' | 'constructor' | 'getter' | 'setter';
67
+ static: boolean;
68
+ async: boolean;
69
+ }>;
70
+ exported: boolean;
71
+ }
72
+
73
+ export interface RouteEntry {
74
+ method: string;
75
+ path: string;
76
+ handler: string;
77
+ middleware?: string[];
78
+ line: number;
79
+ }
80
+
81
+ export type SideEffectType =
82
+ | 'server-listen'
83
+ | 'server-create'
84
+ | 'connection'
85
+ | 'env-load'
86
+ | 'process-handler'
87
+ | 'process-exit'
88
+ | 'timer'
89
+ | 'filesystem'
90
+ | 'child-process'
91
+ | 'console-output'
92
+ | 'global-mutation'
93
+ | 'top-level-await';
94
+
95
+ export interface SideEffectEntry {
96
+ type: SideEffectType;
97
+ detail?: string;
98
+ line: number;
99
+ }
100
+
101
+ export interface EnvEntry {
102
+ name: string;
103
+ line: number;
104
+ /** True if some use supplies a fallback (`||`, `??` or a destructuring default). */
105
+ hasDefault: boolean;
106
+ }
107
+
108
+ export interface AnalysisResult {
109
+ schemaVersion: 1;
110
+ file: string | null;
111
+ moduleType: 'esm' | 'commonjs';
112
+ stats: {
113
+ lines: number;
114
+ imports: number;
115
+ exports: number;
116
+ functions: number;
117
+ classes: number;
118
+ routes: number;
119
+ };
120
+ /** A short, plain-language description assembled from the fields below. */
121
+ summary: string;
122
+ dependencies: { builtin: string[]; packages: string[]; local: string[] };
123
+ imports: ImportEntry[];
124
+ exports: ExportEntry[];
125
+ functions: FunctionEntry[];
126
+ classes: ClassEntry[];
127
+ routes: RouteEntry[];
128
+ sideEffects: SideEffectEntry[];
129
+ env: EnvEntry[];
130
+ }
131
+
132
+ export declare class ParseError extends Error {
133
+ name: 'ParseError';
134
+ /** 1-based. */
135
+ line?: number;
136
+ /** 1-based. */
137
+ column?: number;
138
+ }
139
+
140
+ export declare function analyze(code: string, options?: AnalyzeOptions): AnalysisResult;
141
+ export declare function analyzeFile(path: string, options?: Omit<AnalyzeOptions, 'filename'>): Promise<AnalysisResult>;