@glasstrace/sdk 0.0.1 → 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,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Main Street Integrations LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # @glasstrace/sdk
2
+
3
+ Server-side debugging SDK for AI coding agents. Captures traces,
4
+ errors, and runtime context from your Node.js application and delivers
5
+ them to coding agents through an MCP server and live dashboard.
6
+
7
+ > **Status: Pre-release** -- not yet published to npm.
8
+
9
+ See the [monorepo README](../../README.md) for the planned API.
10
+
11
+ ## License
12
+
13
+ [MIT](./LICENSE)
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/adapters/drizzle.ts
21
+ var drizzle_exports = {};
22
+ __export(drizzle_exports, {
23
+ GlasstraceDrizzleLogger: () => GlasstraceDrizzleLogger
24
+ });
25
+ module.exports = __toCommonJS(drizzle_exports);
26
+ var import_api = require("@opentelemetry/api");
27
+ function extractOperation(query) {
28
+ const trimmed = query.trimStart().toUpperCase();
29
+ if (trimmed.startsWith("SELECT")) return "SELECT";
30
+ if (trimmed.startsWith("INSERT")) return "INSERT";
31
+ if (trimmed.startsWith("UPDATE")) return "UPDATE";
32
+ if (trimmed.startsWith("DELETE")) return "DELETE";
33
+ return "unknown";
34
+ }
35
+ function extractTable(query) {
36
+ const fromMatch = /\bFROM\s+["'`]?(\w+)["'`]?/i.exec(query);
37
+ if (fromMatch) return fromMatch[1];
38
+ const insertMatch = /\bINSERT\s+INTO\s+["'`]?(\w+)["'`]?/i.exec(query);
39
+ if (insertMatch) return insertMatch[1];
40
+ const updateMatch = /\bUPDATE\s+["'`]?(\w+)["'`]?/i.exec(query);
41
+ if (updateMatch) return updateMatch[1];
42
+ return void 0;
43
+ }
44
+ var GlasstraceDrizzleLogger = class {
45
+ tracer;
46
+ captureParams;
47
+ constructor(options) {
48
+ this.tracer = import_api.trace.getTracer("glasstrace-drizzle");
49
+ this.captureParams = options?.captureParams ?? false;
50
+ }
51
+ /**
52
+ * Called by Drizzle ORM for each query execution.
53
+ * Creates an OTel span with query metadata.
54
+ */
55
+ logQuery(query, params) {
56
+ const operation = extractOperation(query);
57
+ const spanName = operation === "unknown" ? "drizzle.query" : `drizzle.${operation}`;
58
+ const span = this.tracer.startSpan(spanName, {
59
+ kind: import_api.SpanKind.CLIENT,
60
+ attributes: {
61
+ "db.system": "drizzle",
62
+ "db.statement": query,
63
+ "db.operation": operation,
64
+ "glasstrace.orm.provider": "drizzle"
65
+ }
66
+ });
67
+ const table = extractTable(query);
68
+ if (table !== void 0) {
69
+ span.setAttribute("db.sql.table", table);
70
+ }
71
+ if (this.captureParams) {
72
+ try {
73
+ span.setAttribute("db.sql.params", JSON.stringify(params));
74
+ } catch {
75
+ span.setAttribute("db.sql.params", "[serialization_error]");
76
+ }
77
+ } else {
78
+ span.setAttribute("db.sql.params", "[REDACTED]");
79
+ }
80
+ span.end();
81
+ }
82
+ };
83
+ // Annotate the CommonJS export names for ESM import in node:
84
+ 0 && (module.exports = {
85
+ GlasstraceDrizzleLogger
86
+ });
87
+ //# sourceMappingURL=drizzle.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/drizzle.ts"],"sourcesContent":["import { trace, SpanKind, type Tracer } from \"@opentelemetry/api\";\n\n/**\n * Options for the Glasstrace Drizzle logger.\n */\nexport interface GlasstraceDrizzleLoggerOptions {\n /** Whether to capture query parameters. Defaults to false (safe default). */\n captureParams?: boolean;\n}\n\n/**\n * Extracts the SQL operation (SELECT, INSERT, UPDATE, DELETE) from a query.\n * Returns 'unknown' if the operation cannot be determined.\n */\nfunction extractOperation(query: string): string {\n const trimmed = query.trimStart().toUpperCase();\n if (trimmed.startsWith(\"SELECT\")) return \"SELECT\";\n if (trimmed.startsWith(\"INSERT\")) return \"INSERT\";\n if (trimmed.startsWith(\"UPDATE\")) return \"UPDATE\";\n if (trimmed.startsWith(\"DELETE\")) return \"DELETE\";\n return \"unknown\";\n}\n\n/**\n * Extracts the table name from a SQL query using best-effort regex.\n * Returns undefined if the table cannot be determined.\n */\nfunction extractTable(query: string): string | undefined {\n // FROM table_name (SELECT, DELETE)\n const fromMatch = /\\bFROM\\s+[\"'`]?(\\w+)[\"'`]?/i.exec(query);\n if (fromMatch) return fromMatch[1];\n\n // INSERT INTO table_name\n const insertMatch = /\\bINSERT\\s+INTO\\s+[\"'`]?(\\w+)[\"'`]?/i.exec(query);\n if (insertMatch) return insertMatch[1];\n\n // UPDATE table_name\n const updateMatch = /\\bUPDATE\\s+[\"'`]?(\\w+)[\"'`]?/i.exec(query);\n if (updateMatch) return updateMatch[1];\n\n return undefined;\n}\n\n/**\n * Implements Drizzle's Logger interface to create OTel spans for Drizzle queries.\n *\n * Exported via `@glasstrace/sdk/drizzle` subpath to avoid bundling Drizzle\n * for Prisma-only users.\n *\n * When OTel is not initialized, tracer.startSpan() returns a no-op span\n * and the logger still executes without errors.\n */\nexport class GlasstraceDrizzleLogger {\n private readonly tracer: Tracer;\n private readonly captureParams: boolean;\n\n constructor(options?: GlasstraceDrizzleLoggerOptions) {\n this.tracer = trace.getTracer(\"glasstrace-drizzle\");\n this.captureParams = options?.captureParams ?? false;\n }\n\n /**\n * Called by Drizzle ORM for each query execution.\n * Creates an OTel span with query metadata.\n */\n logQuery(query: string, params: unknown[]): void {\n const operation = extractOperation(query);\n const spanName =\n operation === \"unknown\" ? \"drizzle.query\" : `drizzle.${operation}`;\n\n const span = this.tracer.startSpan(spanName, {\n kind: SpanKind.CLIENT,\n attributes: {\n \"db.system\": \"drizzle\",\n \"db.statement\": query,\n \"db.operation\": operation,\n \"glasstrace.orm.provider\": \"drizzle\",\n },\n });\n\n // Table extraction\n const table = extractTable(query);\n if (table !== undefined) {\n span.setAttribute(\"db.sql.table\", table);\n }\n\n // Param handling\n if (this.captureParams) {\n try {\n span.setAttribute(\"db.sql.params\", JSON.stringify(params));\n } catch {\n span.setAttribute(\"db.sql.params\", \"[serialization_error]\");\n }\n } else {\n span.setAttribute(\"db.sql.params\", \"[REDACTED]\");\n }\n\n span.end();\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAA6C;AAc7C,SAAS,iBAAiB,OAAuB;AAC/C,QAAM,UAAU,MAAM,UAAU,EAAE,YAAY;AAC9C,MAAI,QAAQ,WAAW,QAAQ,EAAG,QAAO;AACzC,MAAI,QAAQ,WAAW,QAAQ,EAAG,QAAO;AACzC,MAAI,QAAQ,WAAW,QAAQ,EAAG,QAAO;AACzC,MAAI,QAAQ,WAAW,QAAQ,EAAG,QAAO;AACzC,SAAO;AACT;AAMA,SAAS,aAAa,OAAmC;AAEvD,QAAM,YAAY,8BAA8B,KAAK,KAAK;AAC1D,MAAI,UAAW,QAAO,UAAU,CAAC;AAGjC,QAAM,cAAc,uCAAuC,KAAK,KAAK;AACrE,MAAI,YAAa,QAAO,YAAY,CAAC;AAGrC,QAAM,cAAc,gCAAgC,KAAK,KAAK;AAC9D,MAAI,YAAa,QAAO,YAAY,CAAC;AAErC,SAAO;AACT;AAWO,IAAM,0BAAN,MAA8B;AAAA,EAClB;AAAA,EACA;AAAA,EAEjB,YAAY,SAA0C;AACpD,SAAK,SAAS,iBAAM,UAAU,oBAAoB;AAClD,SAAK,gBAAgB,SAAS,iBAAiB;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,SAAS,OAAe,QAAyB;AAC/C,UAAM,YAAY,iBAAiB,KAAK;AACxC,UAAM,WACJ,cAAc,YAAY,kBAAkB,WAAW,SAAS;AAElE,UAAM,OAAO,KAAK,OAAO,UAAU,UAAU;AAAA,MAC3C,MAAM,oBAAS;AAAA,MACf,YAAY;AAAA,QACV,aAAa;AAAA,QACb,gBAAgB;AAAA,QAChB,gBAAgB;AAAA,QAChB,2BAA2B;AAAA,MAC7B;AAAA,IACF,CAAC;AAGD,UAAM,QAAQ,aAAa,KAAK;AAChC,QAAI,UAAU,QAAW;AACvB,WAAK,aAAa,gBAAgB,KAAK;AAAA,IACzC;AAGA,QAAI,KAAK,eAAe;AACtB,UAAI;AACF,aAAK,aAAa,iBAAiB,KAAK,UAAU,MAAM,CAAC;AAAA,MAC3D,QAAQ;AACN,aAAK,aAAa,iBAAiB,uBAAuB;AAAA,MAC5D;AAAA,IACF,OAAO;AACL,WAAK,aAAa,iBAAiB,YAAY;AAAA,IACjD;AAEA,SAAK,IAAI;AAAA,EACX;AACF;","names":[]}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Options for the Glasstrace Drizzle logger.
3
+ */
4
+ interface GlasstraceDrizzleLoggerOptions {
5
+ /** Whether to capture query parameters. Defaults to false (safe default). */
6
+ captureParams?: boolean;
7
+ }
8
+ /**
9
+ * Implements Drizzle's Logger interface to create OTel spans for Drizzle queries.
10
+ *
11
+ * Exported via `@glasstrace/sdk/drizzle` subpath to avoid bundling Drizzle
12
+ * for Prisma-only users.
13
+ *
14
+ * When OTel is not initialized, tracer.startSpan() returns a no-op span
15
+ * and the logger still executes without errors.
16
+ */
17
+ declare class GlasstraceDrizzleLogger {
18
+ private readonly tracer;
19
+ private readonly captureParams;
20
+ constructor(options?: GlasstraceDrizzleLoggerOptions);
21
+ /**
22
+ * Called by Drizzle ORM for each query execution.
23
+ * Creates an OTel span with query metadata.
24
+ */
25
+ logQuery(query: string, params: unknown[]): void;
26
+ }
27
+
28
+ export { GlasstraceDrizzleLogger, type GlasstraceDrizzleLoggerOptions };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Options for the Glasstrace Drizzle logger.
3
+ */
4
+ interface GlasstraceDrizzleLoggerOptions {
5
+ /** Whether to capture query parameters. Defaults to false (safe default). */
6
+ captureParams?: boolean;
7
+ }
8
+ /**
9
+ * Implements Drizzle's Logger interface to create OTel spans for Drizzle queries.
10
+ *
11
+ * Exported via `@glasstrace/sdk/drizzle` subpath to avoid bundling Drizzle
12
+ * for Prisma-only users.
13
+ *
14
+ * When OTel is not initialized, tracer.startSpan() returns a no-op span
15
+ * and the logger still executes without errors.
16
+ */
17
+ declare class GlasstraceDrizzleLogger {
18
+ private readonly tracer;
19
+ private readonly captureParams;
20
+ constructor(options?: GlasstraceDrizzleLoggerOptions);
21
+ /**
22
+ * Called by Drizzle ORM for each query execution.
23
+ * Creates an OTel span with query metadata.
24
+ */
25
+ logQuery(query: string, params: unknown[]): void;
26
+ }
27
+
28
+ export { GlasstraceDrizzleLogger, type GlasstraceDrizzleLoggerOptions };
@@ -0,0 +1,62 @@
1
+ // src/adapters/drizzle.ts
2
+ import { trace, SpanKind } from "@opentelemetry/api";
3
+ function extractOperation(query) {
4
+ const trimmed = query.trimStart().toUpperCase();
5
+ if (trimmed.startsWith("SELECT")) return "SELECT";
6
+ if (trimmed.startsWith("INSERT")) return "INSERT";
7
+ if (trimmed.startsWith("UPDATE")) return "UPDATE";
8
+ if (trimmed.startsWith("DELETE")) return "DELETE";
9
+ return "unknown";
10
+ }
11
+ function extractTable(query) {
12
+ const fromMatch = /\bFROM\s+["'`]?(\w+)["'`]?/i.exec(query);
13
+ if (fromMatch) return fromMatch[1];
14
+ const insertMatch = /\bINSERT\s+INTO\s+["'`]?(\w+)["'`]?/i.exec(query);
15
+ if (insertMatch) return insertMatch[1];
16
+ const updateMatch = /\bUPDATE\s+["'`]?(\w+)["'`]?/i.exec(query);
17
+ if (updateMatch) return updateMatch[1];
18
+ return void 0;
19
+ }
20
+ var GlasstraceDrizzleLogger = class {
21
+ tracer;
22
+ captureParams;
23
+ constructor(options) {
24
+ this.tracer = trace.getTracer("glasstrace-drizzle");
25
+ this.captureParams = options?.captureParams ?? false;
26
+ }
27
+ /**
28
+ * Called by Drizzle ORM for each query execution.
29
+ * Creates an OTel span with query metadata.
30
+ */
31
+ logQuery(query, params) {
32
+ const operation = extractOperation(query);
33
+ const spanName = operation === "unknown" ? "drizzle.query" : `drizzle.${operation}`;
34
+ const span = this.tracer.startSpan(spanName, {
35
+ kind: SpanKind.CLIENT,
36
+ attributes: {
37
+ "db.system": "drizzle",
38
+ "db.statement": query,
39
+ "db.operation": operation,
40
+ "glasstrace.orm.provider": "drizzle"
41
+ }
42
+ });
43
+ const table = extractTable(query);
44
+ if (table !== void 0) {
45
+ span.setAttribute("db.sql.table", table);
46
+ }
47
+ if (this.captureParams) {
48
+ try {
49
+ span.setAttribute("db.sql.params", JSON.stringify(params));
50
+ } catch {
51
+ span.setAttribute("db.sql.params", "[serialization_error]");
52
+ }
53
+ } else {
54
+ span.setAttribute("db.sql.params", "[REDACTED]");
55
+ }
56
+ span.end();
57
+ }
58
+ };
59
+ export {
60
+ GlasstraceDrizzleLogger
61
+ };
62
+ //# sourceMappingURL=drizzle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/drizzle.ts"],"sourcesContent":["import { trace, SpanKind, type Tracer } from \"@opentelemetry/api\";\n\n/**\n * Options for the Glasstrace Drizzle logger.\n */\nexport interface GlasstraceDrizzleLoggerOptions {\n /** Whether to capture query parameters. Defaults to false (safe default). */\n captureParams?: boolean;\n}\n\n/**\n * Extracts the SQL operation (SELECT, INSERT, UPDATE, DELETE) from a query.\n * Returns 'unknown' if the operation cannot be determined.\n */\nfunction extractOperation(query: string): string {\n const trimmed = query.trimStart().toUpperCase();\n if (trimmed.startsWith(\"SELECT\")) return \"SELECT\";\n if (trimmed.startsWith(\"INSERT\")) return \"INSERT\";\n if (trimmed.startsWith(\"UPDATE\")) return \"UPDATE\";\n if (trimmed.startsWith(\"DELETE\")) return \"DELETE\";\n return \"unknown\";\n}\n\n/**\n * Extracts the table name from a SQL query using best-effort regex.\n * Returns undefined if the table cannot be determined.\n */\nfunction extractTable(query: string): string | undefined {\n // FROM table_name (SELECT, DELETE)\n const fromMatch = /\\bFROM\\s+[\"'`]?(\\w+)[\"'`]?/i.exec(query);\n if (fromMatch) return fromMatch[1];\n\n // INSERT INTO table_name\n const insertMatch = /\\bINSERT\\s+INTO\\s+[\"'`]?(\\w+)[\"'`]?/i.exec(query);\n if (insertMatch) return insertMatch[1];\n\n // UPDATE table_name\n const updateMatch = /\\bUPDATE\\s+[\"'`]?(\\w+)[\"'`]?/i.exec(query);\n if (updateMatch) return updateMatch[1];\n\n return undefined;\n}\n\n/**\n * Implements Drizzle's Logger interface to create OTel spans for Drizzle queries.\n *\n * Exported via `@glasstrace/sdk/drizzle` subpath to avoid bundling Drizzle\n * for Prisma-only users.\n *\n * When OTel is not initialized, tracer.startSpan() returns a no-op span\n * and the logger still executes without errors.\n */\nexport class GlasstraceDrizzleLogger {\n private readonly tracer: Tracer;\n private readonly captureParams: boolean;\n\n constructor(options?: GlasstraceDrizzleLoggerOptions) {\n this.tracer = trace.getTracer(\"glasstrace-drizzle\");\n this.captureParams = options?.captureParams ?? false;\n }\n\n /**\n * Called by Drizzle ORM for each query execution.\n * Creates an OTel span with query metadata.\n */\n logQuery(query: string, params: unknown[]): void {\n const operation = extractOperation(query);\n const spanName =\n operation === \"unknown\" ? \"drizzle.query\" : `drizzle.${operation}`;\n\n const span = this.tracer.startSpan(spanName, {\n kind: SpanKind.CLIENT,\n attributes: {\n \"db.system\": \"drizzle\",\n \"db.statement\": query,\n \"db.operation\": operation,\n \"glasstrace.orm.provider\": \"drizzle\",\n },\n });\n\n // Table extraction\n const table = extractTable(query);\n if (table !== undefined) {\n span.setAttribute(\"db.sql.table\", table);\n }\n\n // Param handling\n if (this.captureParams) {\n try {\n span.setAttribute(\"db.sql.params\", JSON.stringify(params));\n } catch {\n span.setAttribute(\"db.sql.params\", \"[serialization_error]\");\n }\n } else {\n span.setAttribute(\"db.sql.params\", \"[REDACTED]\");\n }\n\n span.end();\n }\n}\n"],"mappings":";AAAA,SAAS,OAAO,gBAA6B;AAc7C,SAAS,iBAAiB,OAAuB;AAC/C,QAAM,UAAU,MAAM,UAAU,EAAE,YAAY;AAC9C,MAAI,QAAQ,WAAW,QAAQ,EAAG,QAAO;AACzC,MAAI,QAAQ,WAAW,QAAQ,EAAG,QAAO;AACzC,MAAI,QAAQ,WAAW,QAAQ,EAAG,QAAO;AACzC,MAAI,QAAQ,WAAW,QAAQ,EAAG,QAAO;AACzC,SAAO;AACT;AAMA,SAAS,aAAa,OAAmC;AAEvD,QAAM,YAAY,8BAA8B,KAAK,KAAK;AAC1D,MAAI,UAAW,QAAO,UAAU,CAAC;AAGjC,QAAM,cAAc,uCAAuC,KAAK,KAAK;AACrE,MAAI,YAAa,QAAO,YAAY,CAAC;AAGrC,QAAM,cAAc,gCAAgC,KAAK,KAAK;AAC9D,MAAI,YAAa,QAAO,YAAY,CAAC;AAErC,SAAO;AACT;AAWO,IAAM,0BAAN,MAA8B;AAAA,EAClB;AAAA,EACA;AAAA,EAEjB,YAAY,SAA0C;AACpD,SAAK,SAAS,MAAM,UAAU,oBAAoB;AAClD,SAAK,gBAAgB,SAAS,iBAAiB;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,SAAS,OAAe,QAAyB;AAC/C,UAAM,YAAY,iBAAiB,KAAK;AACxC,UAAM,WACJ,cAAc,YAAY,kBAAkB,WAAW,SAAS;AAElE,UAAM,OAAO,KAAK,OAAO,UAAU,UAAU;AAAA,MAC3C,MAAM,SAAS;AAAA,MACf,YAAY;AAAA,QACV,aAAa;AAAA,QACb,gBAAgB;AAAA,QAChB,gBAAgB;AAAA,QAChB,2BAA2B;AAAA,MAC7B;AAAA,IACF,CAAC;AAGD,UAAM,QAAQ,aAAa,KAAK;AAChC,QAAI,UAAU,QAAW;AACvB,WAAK,aAAa,gBAAgB,KAAK;AAAA,IACzC;AAGA,QAAI,KAAK,eAAe;AACtB,UAAI;AACF,aAAK,aAAa,iBAAiB,KAAK,UAAU,MAAM,CAAC;AAAA,MAC3D,QAAQ;AACN,aAAK,aAAa,iBAAiB,uBAAuB;AAAA,MAC5D;AAAA,IACF,OAAO;AACL,WAAK,aAAa,iBAAiB,YAAY;AAAA,IACjD;AAEA,SAAK,IAAI;AAAA,EACX;AACF;","names":[]}
@@ -0,0 +1,169 @@
1
+ // src/import-graph.ts
2
+ import * as fs from "fs/promises";
3
+ import * as fsSync from "fs";
4
+ import * as path from "path";
5
+ import * as crypto from "crypto";
6
+ import { createBuildHash } from "@glasstrace/protocol";
7
+ var MAX_TEST_FILES = 5e3;
8
+ var EXCLUDED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".next", ".git", "dist", ".turbo"]);
9
+ var DEFAULT_TEST_PATTERNS = [
10
+ /\.test\.tsx?$/,
11
+ /\.spec\.tsx?$/
12
+ ];
13
+ function globToRegExp(glob) {
14
+ const DOUBLE_STAR_PLACEHOLDER = "\0DSTAR\0";
15
+ const regexStr = glob.replace(/\*\*\//g, DOUBLE_STAR_PLACEHOLDER).replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]+").replace(new RegExp(DOUBLE_STAR_PLACEHOLDER.replace(/\0/g, "\\0"), "g"), "(?:.+/)?");
16
+ return new RegExp("^" + regexStr + "$");
17
+ }
18
+ function loadCustomTestPatterns(projectRoot) {
19
+ const configNames = [
20
+ "vitest.config.ts",
21
+ "vitest.config.js",
22
+ "vitest.config.mts",
23
+ "vitest.config.mjs",
24
+ "vite.config.ts",
25
+ "vite.config.js",
26
+ "vite.config.mts",
27
+ "vite.config.mjs",
28
+ "jest.config.ts",
29
+ "jest.config.js",
30
+ "jest.config.mts",
31
+ "jest.config.mjs"
32
+ ];
33
+ for (const name of configNames) {
34
+ const configPath = path.join(projectRoot, name);
35
+ let content;
36
+ try {
37
+ content = fsSync.readFileSync(configPath, "utf-8");
38
+ } catch {
39
+ continue;
40
+ }
41
+ try {
42
+ const isJest = name.startsWith("jest.");
43
+ let includeMatch = null;
44
+ if (isJest) {
45
+ includeMatch = /testMatch\s*:\s*\[([^\]]*)\]/s.exec(content);
46
+ } else {
47
+ const testBlockMatch = /\btest\s*[:{]\s*/s.exec(content);
48
+ if (testBlockMatch) {
49
+ const afterTest = content.slice(testBlockMatch.index, testBlockMatch.index + 500);
50
+ includeMatch = /include\s*:\s*\[([^\]]*)\]/s.exec(afterTest);
51
+ }
52
+ }
53
+ if (!includeMatch) {
54
+ continue;
55
+ }
56
+ const arrayContent = includeMatch[1];
57
+ const stringRegex = /['"]([^'"]+)['"]/g;
58
+ const patterns = [];
59
+ let match;
60
+ match = stringRegex.exec(arrayContent);
61
+ while (match !== null) {
62
+ patterns.push(globToRegExp(match[1]));
63
+ match = stringRegex.exec(arrayContent);
64
+ }
65
+ if (patterns.length > 0) {
66
+ return patterns;
67
+ }
68
+ } catch {
69
+ continue;
70
+ }
71
+ }
72
+ return [];
73
+ }
74
+ async function discoverTestFiles(projectRoot) {
75
+ const customPatterns = loadCustomTestPatterns(projectRoot);
76
+ const testPatterns = [...DEFAULT_TEST_PATTERNS, ...customPatterns];
77
+ const results = [];
78
+ try {
79
+ await walkForTests(projectRoot, projectRoot, results, testPatterns);
80
+ } catch {
81
+ return [];
82
+ }
83
+ return results.slice(0, MAX_TEST_FILES);
84
+ }
85
+ async function walkForTests(baseDir, currentDir, results, testPatterns) {
86
+ if (results.length >= MAX_TEST_FILES) {
87
+ return;
88
+ }
89
+ let entries;
90
+ try {
91
+ entries = await fs.readdir(currentDir, { withFileTypes: true });
92
+ } catch {
93
+ return;
94
+ }
95
+ for (const entry of entries) {
96
+ if (results.length >= MAX_TEST_FILES) {
97
+ return;
98
+ }
99
+ const fullPath = path.join(currentDir, entry.name);
100
+ if (entry.isDirectory()) {
101
+ if (EXCLUDED_DIRS.has(entry.name)) {
102
+ continue;
103
+ }
104
+ await walkForTests(baseDir, fullPath, results, testPatterns);
105
+ } else if (entry.isFile()) {
106
+ const relativePath = path.relative(baseDir, fullPath).replace(/\\/g, "/");
107
+ const isTestFile = testPatterns.some((p) => p.test(entry.name) || p.test(relativePath)) || relativePath.includes("__tests__");
108
+ if (isTestFile && (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx"))) {
109
+ results.push(relativePath);
110
+ }
111
+ }
112
+ }
113
+ }
114
+ function extractImports(fileContent) {
115
+ const seen = /* @__PURE__ */ new Set();
116
+ const imports = [];
117
+ const addUnique = (importPath) => {
118
+ if (!seen.has(importPath)) {
119
+ seen.add(importPath);
120
+ imports.push(importPath);
121
+ }
122
+ };
123
+ const esImportRegex = /import\s+(?:(?:[\w*{}\s,]+)\s+from\s+)?['"]([^'"]+)['"]/g;
124
+ let match;
125
+ match = esImportRegex.exec(fileContent);
126
+ while (match !== null) {
127
+ addUnique(match[1]);
128
+ match = esImportRegex.exec(fileContent);
129
+ }
130
+ const requireRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
131
+ match = requireRegex.exec(fileContent);
132
+ while (match !== null) {
133
+ addUnique(match[1]);
134
+ match = requireRegex.exec(fileContent);
135
+ }
136
+ const dynamicImportRegex = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
137
+ match = dynamicImportRegex.exec(fileContent);
138
+ while (match !== null) {
139
+ addUnique(match[1]);
140
+ match = dynamicImportRegex.exec(fileContent);
141
+ }
142
+ return imports;
143
+ }
144
+ async function buildImportGraph(projectRoot) {
145
+ const testFiles = await discoverTestFiles(projectRoot);
146
+ const graph = {};
147
+ for (const testFile of testFiles) {
148
+ const fullPath = path.join(projectRoot, testFile);
149
+ try {
150
+ const content = await fs.readFile(fullPath, "utf-8");
151
+ const imports = extractImports(content);
152
+ graph[testFile] = imports;
153
+ } catch {
154
+ continue;
155
+ }
156
+ }
157
+ const sortedKeys = Object.keys(graph).sort();
158
+ const serialized = sortedKeys.map((key) => `${key}:${JSON.stringify(graph[key])}`).join("\n");
159
+ const hashHex = crypto.createHash("sha256").update(serialized).digest("hex");
160
+ const buildHash = createBuildHash(hashHex);
161
+ return { buildHash, graph };
162
+ }
163
+
164
+ export {
165
+ discoverTestFiles,
166
+ extractImports,
167
+ buildImportGraph
168
+ };
169
+ //# sourceMappingURL=chunk-BKMITIEZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/import-graph.ts"],"sourcesContent":["import * as fs from \"node:fs/promises\";\nimport * as fsSync from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as crypto from \"node:crypto\";\nimport { createBuildHash, type ImportGraphPayload } from \"@glasstrace/protocol\";\n\n/** Maximum number of test files to process to prevent runaway in large projects */\nconst MAX_TEST_FILES = 5000;\n\n/** Directories to exclude from test file discovery */\nconst EXCLUDED_DIRS = new Set([\"node_modules\", \".next\", \".git\", \"dist\", \".turbo\"]);\n\n/** Conventional test file patterns */\nconst DEFAULT_TEST_PATTERNS = [\n /\\.test\\.tsx?$/,\n /\\.spec\\.tsx?$/,\n];\n\n/**\n * Converts a glob pattern (e.g. \"e2e/**\\/*.ts\") to an anchored RegExp.\n * Uses a placeholder to avoid `*` replacement corrupting the `**\\/` output.\n *\n * @param glob - A file glob pattern such as \"src/**\\/*.test.ts\".\n * @returns A RegExp that matches paths against the glob from start to end.\n */\nfunction globToRegExp(glob: string): RegExp {\n const DOUBLE_STAR_PLACEHOLDER = \"\\0DSTAR\\0\";\n const regexStr = glob\n .replace(/\\*\\*\\//g, DOUBLE_STAR_PLACEHOLDER) // protect **/ first\n .replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\") // escape all regex metacharacters (except *)\n .replace(/\\*/g, \"[^/]+\")\n .replace(new RegExp(DOUBLE_STAR_PLACEHOLDER.replace(/\\0/g, \"\\\\0\"), \"g\"), \"(?:.+/)?\");\n return new RegExp(\"^\" + regexStr + \"$\");\n}\n\n/**\n * Attempts to read include patterns from vitest.config.*, vite.config.*,\n * or jest.config.* files. Returns additional RegExp patterns extracted\n * from the config, or an empty array if no config is found or parsing fails.\n * This is best-effort — it reads the config as text and extracts patterns\n * via regex, without evaluating the JS.\n *\n * For Vitest/Vite configs, looks for `test.include` arrays.\n * For Jest configs, looks for `testMatch` arrays.\n * Does not support `testRegex` (string-based Jest pattern) — that is\n * left as future work.\n */\nfunction loadCustomTestPatterns(projectRoot: string): RegExp[] {\n const configNames = [\n \"vitest.config.ts\",\n \"vitest.config.js\",\n \"vitest.config.mts\",\n \"vitest.config.mjs\",\n \"vite.config.ts\",\n \"vite.config.js\",\n \"vite.config.mts\",\n \"vite.config.mjs\",\n \"jest.config.ts\",\n \"jest.config.js\",\n \"jest.config.mts\",\n \"jest.config.mjs\",\n ];\n\n for (const name of configNames) {\n const configPath = path.join(projectRoot, name);\n let content: string;\n try {\n content = fsSync.readFileSync(configPath, \"utf-8\");\n } catch {\n // Config file does not exist at this path — try next candidate\n continue;\n }\n\n try {\n const isJest = name.startsWith(\"jest.\");\n let includeMatch: RegExpExecArray | null = null;\n\n if (isJest) {\n // Jest: look for testMatch: [...]\n includeMatch = /testMatch\\s*:\\s*\\[([^\\]]*)\\]/s.exec(content);\n } else {\n // Vitest/Vite: look for `test` block's `include` to avoid\n // matching `coverage.include` or other unrelated arrays.\n // Strategy: find `test` property, then look for `include` within\n // the next ~500 chars (heuristic to stay within the test block).\n const testBlockMatch = /\\btest\\s*[:{]\\s*/s.exec(content);\n if (testBlockMatch) {\n const afterTest = content.slice(testBlockMatch.index, testBlockMatch.index + 500);\n includeMatch = /include\\s*:\\s*\\[([^\\]]*)\\]/s.exec(afterTest);\n }\n }\n\n if (!includeMatch) {\n continue;\n }\n\n const arrayContent = includeMatch[1];\n const stringRegex = /['\"]([^'\"]+)['\"]/g;\n const patterns: RegExp[] = [];\n let match: RegExpExecArray | null;\n match = stringRegex.exec(arrayContent);\n while (match !== null) {\n patterns.push(globToRegExp(match[1]));\n match = stringRegex.exec(arrayContent);\n }\n\n if (patterns.length > 0) {\n return patterns;\n }\n } catch {\n // Regex-based config parsing failed — fall through to next config file\n continue;\n }\n }\n\n return [];\n}\n\n/**\n * Discovers test files by scanning the project directory for conventional\n * test file patterns. Also reads vitest/jest config files for custom include\n * patterns and merges them with the defaults. Excludes node_modules/ and .next/.\n *\n * @param projectRoot - Absolute path to the project root directory.\n * @returns Relative POSIX paths from projectRoot, capped at {@link MAX_TEST_FILES}.\n */\nexport async function discoverTestFiles(\n projectRoot: string,\n): Promise<string[]> {\n const customPatterns = loadCustomTestPatterns(projectRoot);\n const testPatterns = [...DEFAULT_TEST_PATTERNS, ...customPatterns];\n const results: string[] = [];\n\n try {\n await walkForTests(projectRoot, projectRoot, results, testPatterns);\n } catch {\n // Project root directory does not exist or is unreadable — return empty\n return [];\n }\n\n return results.slice(0, MAX_TEST_FILES);\n}\n\n/** Recursively walks directories, collecting test file paths into `results`. */\nasync function walkForTests(\n baseDir: string,\n currentDir: string,\n results: string[],\n testPatterns: RegExp[],\n): Promise<void> {\n if (results.length >= MAX_TEST_FILES) {\n return;\n }\n\n let entries: import(\"node:fs\").Dirent[];\n try {\n entries = await fs.readdir(currentDir, { withFileTypes: true });\n } catch {\n // Directory is unreadable (permissions, broken symlink) — skip subtree\n return;\n }\n\n for (const entry of entries) {\n if (results.length >= MAX_TEST_FILES) {\n return;\n }\n\n const fullPath = path.join(currentDir, entry.name);\n\n if (entry.isDirectory()) {\n if (EXCLUDED_DIRS.has(entry.name)) {\n continue;\n }\n await walkForTests(baseDir, fullPath, results, testPatterns);\n } else if (entry.isFile()) {\n const relativePath = path.relative(baseDir, fullPath).replace(/\\\\/g, \"/\");\n\n // Check if it matches test patterns or is in __tests__\n const isTestFile =\n testPatterns.some((p) => p.test(entry.name) || p.test(relativePath)) ||\n relativePath.includes(\"__tests__\");\n\n if (isTestFile && (entry.name.endsWith(\".ts\") || entry.name.endsWith(\".tsx\"))) {\n results.push(relativePath);\n }\n }\n }\n}\n\n/**\n * Extracts import paths from file content using regex.\n * Handles ES module imports, CommonJS requires, and dynamic imports.\n *\n * @param fileContent - The full text content of a TypeScript/JavaScript file.\n * @returns An array of import path strings as written in the source (e.g. \"./foo\", \"react\").\n */\nexport function extractImports(fileContent: string): string[] {\n const seen = new Set<string>();\n const imports: string[] = [];\n\n /** Adds a path to the result if not already present. */\n const addUnique = (importPath: string): void => {\n if (!seen.has(importPath)) {\n seen.add(importPath);\n imports.push(importPath);\n }\n };\n\n // ES module: import ... from 'path' or import 'path'\n const esImportRegex = /import\\s+(?:(?:[\\w*{}\\s,]+)\\s+from\\s+)?['\"]([^'\"]+)['\"]/g;\n let match: RegExpExecArray | null;\n\n match = esImportRegex.exec(fileContent);\n while (match !== null) {\n addUnique(match[1]);\n match = esImportRegex.exec(fileContent);\n }\n\n // CommonJS: require('path')\n const requireRegex = /require\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)/g;\n match = requireRegex.exec(fileContent);\n while (match !== null) {\n addUnique(match[1]);\n match = requireRegex.exec(fileContent);\n }\n\n // Dynamic import: import('path')\n const dynamicImportRegex = /import\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)/g;\n match = dynamicImportRegex.exec(fileContent);\n while (match !== null) {\n addUnique(match[1]);\n match = dynamicImportRegex.exec(fileContent);\n }\n\n return imports;\n}\n\n/**\n * Builds an import graph mapping test file paths to their imported module paths.\n *\n * Discovers test files, reads each, extracts imports, and builds a graph.\n * Computes a deterministic buildHash from the serialized graph content.\n * Individual file read failures are silently skipped.\n *\n * @param projectRoot - Absolute path to the project root directory.\n * @returns An {@link ImportGraphPayload} containing the graph and a deterministic buildHash.\n */\nexport async function buildImportGraph(\n projectRoot: string,\n): Promise<ImportGraphPayload> {\n const testFiles = await discoverTestFiles(projectRoot);\n const graph: Record<string, string[]> = {};\n\n for (const testFile of testFiles) {\n const fullPath = path.join(projectRoot, testFile);\n try {\n const content = await fs.readFile(fullPath, \"utf-8\");\n const imports = extractImports(content);\n graph[testFile] = imports;\n } catch {\n // File is unreadable (permissions, deleted between discovery and read) — skip\n continue;\n }\n }\n\n // Compute deterministic build hash from graph content\n const sortedKeys = Object.keys(graph).sort();\n const serialized = sortedKeys\n .map((key) => `${key}:${JSON.stringify(graph[key])}`)\n .join(\"\\n\");\n const hashHex = crypto\n .createHash(\"sha256\")\n .update(serialized)\n .digest(\"hex\");\n const buildHash = createBuildHash(hashHex);\n\n return { buildHash, graph };\n}\n"],"mappings":"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","names":[]}