ccqa-tools 1.37.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.
Files changed (38) hide show
  1. package/README.md +177 -0
  2. package/dist/coverage/collector.cjs +229 -0
  3. package/dist/coverage/collector.d.cts +183 -0
  4. package/dist/coverage/collector.d.ts +183 -0
  5. package/dist/coverage/collector.js +225 -0
  6. package/dist/coverage/core.cjs +176 -0
  7. package/dist/coverage/core.d.cts +157 -0
  8. package/dist/coverage/core.d.ts +157 -0
  9. package/dist/coverage/core.js +163 -0
  10. package/dist/coverage/middleware.cjs +161 -0
  11. package/dist/coverage/middleware.d.cts +23 -0
  12. package/dist/coverage/middleware.d.ts +23 -0
  13. package/dist/coverage/middleware.js +158 -0
  14. package/dist/coverage/next-loader.cjs +158 -0
  15. package/dist/coverage/next-loader.d.cts +18 -0
  16. package/dist/coverage/next-loader.d.ts +19 -0
  17. package/dist/coverage/next-loader.js +158 -0
  18. package/dist/coverage/next.cjs +101 -0
  19. package/dist/coverage/next.d.cts +33 -0
  20. package/dist/coverage/next.d.ts +33 -0
  21. package/dist/coverage/next.js +100 -0
  22. package/dist/coverage/register.cjs +741 -0
  23. package/dist/coverage/register.d.cts +1 -0
  24. package/dist/coverage/register.d.ts +1 -0
  25. package/dist/coverage/register.js +716 -0
  26. package/dist/coverage/slack.cjs +228 -0
  27. package/dist/coverage/slack.d.cts +47 -0
  28. package/dist/coverage/slack.d.ts +47 -0
  29. package/dist/coverage/slack.js +225 -0
  30. package/dist/coverage/temporal-workflow.cjs +154 -0
  31. package/dist/coverage/temporal-workflow.d.cts +28 -0
  32. package/dist/coverage/temporal-workflow.d.ts +28 -0
  33. package/dist/coverage/temporal-workflow.js +153 -0
  34. package/dist/coverage/temporal.cjs +253 -0
  35. package/dist/coverage/temporal.d.cts +40 -0
  36. package/dist/coverage/temporal.d.ts +40 -0
  37. package/dist/coverage/temporal.js +250 -0
  38. package/package.json +95 -0
@@ -0,0 +1,158 @@
1
+ //#region src/coverage/core.ts
2
+ const RUNTIME_KEY = Symbol.for("ccqa.coverage.runtime");
3
+ function globals() {
4
+ return globalThis;
5
+ }
6
+ /**
7
+ * Memoized so the hot path skips the `globalThis` read. `installRuntime`
8
+ * primes it and never replaces an installed runtime, so once set it never
9
+ * goes stale — but it must stay undefined (and keep re-reading `globalThis`)
10
+ * until then, or a module that finished loading before `register` installs
11
+ * the runtime would be stuck uninstrumented for the rest of the process.
12
+ */
13
+ let cachedRuntime;
14
+ function runtime() {
15
+ if (cachedRuntime === void 0) cachedRuntime = globals()[RUNTIME_KEY];
16
+ return cachedRuntime;
17
+ }
18
+ /** The spec the current async context belongs to, if any. */
19
+ function currentSpecId() {
20
+ return runtime()?.als.getStore()?.specId;
21
+ }
22
+ /**
23
+ * Opens `specId`'s bucket and runs `fn` inside its context. Every entry point —
24
+ * HTTP, Temporal activity, manual — funnels through here.
25
+ *
26
+ * The bucket outlives `fn`: work a request schedules and does not await still
27
+ * belongs to the spec that caused it.
28
+ */
29
+ function runInSpec(specId, fn) {
30
+ const rt = runtime();
31
+ if (rt === void 0) return fn();
32
+ return rt.als.run({
33
+ specId,
34
+ files: openBucket(rt, specId)
35
+ }, fn);
36
+ }
37
+ /** Returns `specId`'s file set, creating it — and arming the gate — if new. */
38
+ function openBucket(runtime, specId) {
39
+ let files = runtime.buckets.get(specId);
40
+ if (files === void 0) {
41
+ files = /* @__PURE__ */ new Set();
42
+ runtime.buckets.set(specId, files);
43
+ armGate(runtime);
44
+ }
45
+ return files;
46
+ }
47
+ function armGate(runtime) {
48
+ runtime.active = runtime.buckets.size + runtime.actors.size;
49
+ }
50
+ //#endregion
51
+ //#region src/coverage/wire.ts
52
+ /**
53
+ * The names a spec id travels under. Every carrier holds the same value,
54
+ * `<runId>.<specId>`, so a hop between them is a copy and never a translation.
55
+ *
56
+ * Like `core.ts` this file imports nothing: the Temporal workflow sandbox reads
57
+ * it too.
58
+ */
59
+ /** Set on the browser by ccqa at spec start, scoped to the target origin. */
60
+ const COOKIE_NAME = "__ccqa_coverage";
61
+ /** OTel baggage key, for the hop from the first service to downstream ones. */
62
+ const BAGGAGE_KEY = "ccqa.coverage";
63
+ const SPEC_ID = /^[A-Za-z0-9._\-/]{1,200}$/;
64
+ /**
65
+ * Accepts a carrier value only if it looks like an id we wrote.
66
+ *
67
+ * The cookie is client-controlled, so this is the first of two gates: the
68
+ * second is the hub refusing runs it never started.
69
+ */
70
+ function parseSpecId(raw) {
71
+ if (!raw) return void 0;
72
+ const value = raw.trim();
73
+ if (!SPEC_ID.test(value)) return void 0;
74
+ if (value === "1" || value === "true") return void 0;
75
+ return value;
76
+ }
77
+ function readCookie(header) {
78
+ return readKeyed(header, COOKIE_NAME, ";");
79
+ }
80
+ /** Pulls our key out of a `baggage` header (W3C: `k=v;props,k2=v2`). */
81
+ function readBaggage(header) {
82
+ return readKeyed(header, BAGGAGE_KEY, ",", ";");
83
+ }
84
+ /**
85
+ * Both carriers are `key=value` lists; they differ only in what separates the
86
+ * entries, and baggage allowing properties after each value.
87
+ *
88
+ * One function because the decode-and-validate step is the part that matters,
89
+ * and two copies of it would be free to drift into accepting different things.
90
+ */
91
+ function readKeyed(header, key, between, propertiesAfter) {
92
+ if (!header) return void 0;
93
+ if (header.indexOf(key) < 0) return void 0;
94
+ for (const raw of header.split(between)) {
95
+ const entry = propertiesAfter === void 0 ? raw : raw.split(propertiesAfter)[0] ?? "";
96
+ const eq = entry.indexOf("=");
97
+ if (eq < 0) continue;
98
+ if (entry.slice(0, eq).trim() !== key) continue;
99
+ try {
100
+ return parseSpecId(decodeURIComponent(entry.slice(eq + 1).trim()));
101
+ } catch {
102
+ return;
103
+ }
104
+ }
105
+ }
106
+ /** Adds our key to an existing `baggage` header value, replacing any old one. */
107
+ function writeBaggage(existing, specId) {
108
+ const kept = (existing ?? "").split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0 && !entry.startsWith(`ccqa.coverage=`));
109
+ kept.push(`${BAGGAGE_KEY}=${encodeURIComponent(specId)}`);
110
+ return kept.join(",");
111
+ }
112
+ //#endregion
113
+ //#region src/coverage/middleware.ts
114
+ /**
115
+ * Entry points for servers `ccqa-tools/coverage/register` cannot wrap on its own:
116
+ * anything that does not receive its requests from `node:http`.
117
+ *
118
+ * A framework running on plain Node needs none of this — the register hook
119
+ * already opened the context before the framework saw the request.
120
+ */
121
+ /** connect / express / fastify-compat middleware. */
122
+ function coverageMiddleware() {
123
+ return function coverage(request, _response, next) {
124
+ const specId = specIdFromNodeHeaders(request.headers);
125
+ if (specId === void 0) {
126
+ next();
127
+ return;
128
+ }
129
+ runInSpec(specId, next);
130
+ };
131
+ }
132
+ /** Wraps a `Request` -> `Response` handler (Hono, Next.js route handlers, workerd). */
133
+ function withCoverage(handler) {
134
+ return function covered(request, ...rest) {
135
+ const specId = readCookie(request.headers.get("cookie")) ?? readBaggage(request.headers.get("baggage"));
136
+ if (specId === void 0) return handler(request, ...rest);
137
+ return runInSpec(specId, () => handler(request, ...rest));
138
+ };
139
+ }
140
+ /**
141
+ * Converts the cookie into a `baggage` header so downstream services — which
142
+ * never see the browser's cookie jar — inherit the attribution. Call it where
143
+ * the first service fans out, or in an edge middleware that forwards to one.
144
+ */
145
+ function forwardHeaders(incoming, outgoing) {
146
+ const headers = outgoing ?? new Headers();
147
+ const specId = currentSpecId() ?? readCookie(incoming.get("cookie")) ?? readBaggage(incoming.get("baggage"));
148
+ if (specId !== void 0) headers.set("baggage", writeBaggage(headers.get("baggage") ?? incoming.get("baggage"), specId));
149
+ return headers;
150
+ }
151
+ function specIdFromNodeHeaders(headers) {
152
+ return readCookie(single(headers.cookie)) ?? readBaggage(single(headers.baggage));
153
+ }
154
+ function single(value) {
155
+ return Array.isArray(value) ? value[0] : value;
156
+ }
157
+ //#endregion
158
+ export { coverageMiddleware, forwardHeaders, withCoverage };
@@ -0,0 +1,158 @@
1
+ let node_path = require("node:path");
2
+ let acorn = require("acorn");
3
+ //#region src/coverage/instrument/select.ts
4
+ /** Path relative to the project root, in posix form so ids match across hosts. */
5
+ function fileIdFor(filename, root) {
6
+ const rel = (0, node_path.relative)((0, node_path.resolve)(root), filename);
7
+ if (rel.startsWith("..") || rel === "") return void 0;
8
+ return rel.split(node_path.sep).join("/");
9
+ }
10
+ //#endregion
11
+ //#region src/coverage/instrument/transform.ts
12
+ /**
13
+ * Rewrites a module so that entering it, or entering one of its functions,
14
+ * calls `globalThis.__ccqaCoverage`.
15
+ *
16
+ * Two properties drive the whole shape:
17
+ *
18
+ * - **Insertions only, never a newline.** Line numbers survive untouched, so
19
+ * the source map the application already ships keeps pointing at the right
20
+ * lines and stack traces stay readable. A codegen round-trip would have
21
+ * forced us to produce and merge maps of our own.
22
+ * - **File granularity.** The record is "this file ran", so there is no need to
23
+ * track statements or branches, and the whole class of line/branch
24
+ * normalisation bugs that follow V8-to-istanbul conversion never appears.
25
+ */
26
+ const DEFAULT_MAX_DEPTH = 2;
27
+ function transform(code, options) {
28
+ const program = parseProgram(code);
29
+ if (program === void 0) return void 0;
30
+ const local = `__ccqa_${hash(options.fileId)}`;
31
+ const literal = JSON.stringify(options.fileId);
32
+ const enter = `${local}&&${local}(${literal});`;
33
+ const points = [];
34
+ collect(program, options.maxDepth ?? DEFAULT_MAX_DEPTH, points);
35
+ if (code.length === 0) return void 0;
36
+ const prologueAt = afterDirectives(code, program);
37
+ const edits = points.filter((offset) => offset > prologueAt).map((offset) => ({
38
+ offset,
39
+ text: enter
40
+ }));
41
+ edits.push({
42
+ offset: prologueAt,
43
+ text: `var ${local}=globalThis.__ccqaCoverage;${local}&&${local}(${literal},true);`
44
+ });
45
+ edits.sort((a, b) => a.offset - b.offset);
46
+ const parts = [];
47
+ let last = 0;
48
+ for (const edit of edits) {
49
+ parts.push(code.slice(last, edit.offset), edit.text);
50
+ last = edit.offset;
51
+ }
52
+ parts.push(code.slice(last));
53
+ return parts.join("");
54
+ }
55
+ function parseProgram(code) {
56
+ for (const sourceType of ["module", "script"]) try {
57
+ return (0, acorn.parse)(code, {
58
+ ecmaVersion: "latest",
59
+ sourceType,
60
+ allowHashBang: true,
61
+ allowAwaitOutsideFunction: true,
62
+ allowReturnOutsideFunction: sourceType === "script"
63
+ });
64
+ } catch {}
65
+ }
66
+ /**
67
+ * A directive prologue only counts while it is still the first thing in its
68
+ * scope. Inserting ahead of `"use strict"` demotes it to an ordinary string
69
+ * expression, and the code it governed silently starts running sloppy — the
70
+ * instrumentation would be changing the behaviour it is supposed to observe.
71
+ * Applies to a function body as much as to the module.
72
+ */
73
+ function afterDirectives(code, program) {
74
+ let offset = program.start;
75
+ if (code.startsWith("#!")) {
76
+ const newline = code.indexOf("\n");
77
+ offset = newline < 0 ? code.length : newline + 1;
78
+ }
79
+ return skipDirectives(program.body, offset);
80
+ }
81
+ function skipDirectives(statements, from) {
82
+ let offset = from;
83
+ for (const statement of statements) {
84
+ if (statement.type !== "ExpressionStatement") break;
85
+ const expression = statement.expression;
86
+ if (expression.type !== "Literal" || typeof expression.value !== "string") break;
87
+ offset = statement.end;
88
+ }
89
+ return offset;
90
+ }
91
+ const FUNCTION_TYPES = new Set([
92
+ "FunctionDeclaration",
93
+ "FunctionExpression",
94
+ "ArrowFunctionExpression"
95
+ ]);
96
+ function collect(root, maxDepth, points) {
97
+ walk(root, 0, false);
98
+ function walk(node, depth, inClass) {
99
+ const isFunction = FUNCTION_TYPES.has(node.type);
100
+ const nextDepth = isFunction ? depth + 1 : depth;
101
+ if (isFunction) {
102
+ const wanted = inClass || nextDepth <= maxDepth;
103
+ const body = node.body;
104
+ if (wanted && body && body.type === "BlockStatement") {
105
+ const statements = body.body;
106
+ points.push(skipDirectives(statements, body.start + 1));
107
+ }
108
+ }
109
+ for (const key in node) {
110
+ if (key === "type" || key === "start" || key === "end" || key === "loc" || key === "range") continue;
111
+ const value = node[key];
112
+ if (Array.isArray(value)) {
113
+ for (const item of value) if (isNode(item)) walk(item, nextDepth, childInClass(node, key, inClass));
114
+ } else if (isNode(value)) walk(value, nextDepth, childInClass(node, key, inClass));
115
+ }
116
+ }
117
+ }
118
+ /** True while walking the value of a class member, false again inside its body. */
119
+ function childInClass(parent, key, inherited) {
120
+ if (parent.type === "MethodDefinition" || parent.type === "PropertyDefinition") return key === "value";
121
+ if (FUNCTION_TYPES.has(parent.type)) return false;
122
+ return inherited;
123
+ }
124
+ function isNode(value) {
125
+ return typeof value === "object" && value !== null && typeof value.type === "string";
126
+ }
127
+ /** Short, collision-resistant suffix so bundlers can hoist several modules into one scope. */
128
+ function hash(value) {
129
+ let h = 2166136261;
130
+ for (let i = 0; i < value.length; i++) {
131
+ h ^= value.charCodeAt(i);
132
+ h = Math.imul(h, 16777619);
133
+ }
134
+ return (h >>> 0).toString(36);
135
+ }
136
+ //#endregion
137
+ //#region src/coverage/next/loader.ts
138
+ /**
139
+ * webpack loader form of the instrumenter, for code that reaches the runtime as
140
+ * part of a bundle and so is invisible to the load hooks.
141
+ *
142
+ * It is registered with `enforce: "post"` so it sees JavaScript: webpack runs
143
+ * post loaders last, after the framework's own TypeScript/JSX transform, which
144
+ * keeps this file free of a TypeScript parser.
145
+ */
146
+ function ccqaCoverageLoader(source) {
147
+ const root = this.getOptions?.().root ?? process.cwd();
148
+ const fileId = fileIdFor(this.resourcePath, root);
149
+ if (fileId === void 0) return source;
150
+ const instrumented = transform(source, { fileId });
151
+ if (instrumented === void 0) {
152
+ this.emitWarning?.(/* @__PURE__ */ new Error(`ccqa-tools could not parse ${fileId}; left uninstrumented`));
153
+ return source;
154
+ }
155
+ return instrumented;
156
+ }
157
+ //#endregion
158
+ module.exports = ccqaCoverageLoader;
@@ -0,0 +1,18 @@
1
+ //#region src/coverage/next/loader.d.ts
2
+ /**
3
+ * webpack loader form of the instrumenter, for code that reaches the runtime as
4
+ * part of a bundle and so is invisible to the load hooks.
5
+ *
6
+ * It is registered with `enforce: "post"` so it sees JavaScript: webpack runs
7
+ * post loaders last, after the framework's own TypeScript/JSX transform, which
8
+ * keeps this file free of a TypeScript parser.
9
+ */
10
+ interface LoaderContext {
11
+ resourcePath: string;
12
+ getOptions?: () => {
13
+ root?: string;
14
+ };
15
+ emitWarning?: (warning: Error) => void;
16
+ }
17
+ declare function ccqaCoverageLoader(this: LoaderContext, source: string): string;
18
+ export = ccqaCoverageLoader;
@@ -0,0 +1,19 @@
1
+ //#region src/coverage/next/loader.d.ts
2
+ /**
3
+ * webpack loader form of the instrumenter, for code that reaches the runtime as
4
+ * part of a bundle and so is invisible to the load hooks.
5
+ *
6
+ * It is registered with `enforce: "post"` so it sees JavaScript: webpack runs
7
+ * post loaders last, after the framework's own TypeScript/JSX transform, which
8
+ * keeps this file free of a TypeScript parser.
9
+ */
10
+ interface LoaderContext {
11
+ resourcePath: string;
12
+ getOptions?: () => {
13
+ root?: string;
14
+ };
15
+ emitWarning?: (warning: Error) => void;
16
+ }
17
+ declare function ccqaCoverageLoader(this: LoaderContext, source: string): string;
18
+ //#endregion
19
+ export { ccqaCoverageLoader as default };
@@ -0,0 +1,158 @@
1
+ import { relative, resolve, sep } from "node:path";
2
+ import { parse } from "acorn";
3
+ //#region src/coverage/instrument/select.ts
4
+ /** Path relative to the project root, in posix form so ids match across hosts. */
5
+ function fileIdFor(filename, root) {
6
+ const rel = relative(resolve(root), filename);
7
+ if (rel.startsWith("..") || rel === "") return void 0;
8
+ return rel.split(sep).join("/");
9
+ }
10
+ //#endregion
11
+ //#region src/coverage/instrument/transform.ts
12
+ /**
13
+ * Rewrites a module so that entering it, or entering one of its functions,
14
+ * calls `globalThis.__ccqaCoverage`.
15
+ *
16
+ * Two properties drive the whole shape:
17
+ *
18
+ * - **Insertions only, never a newline.** Line numbers survive untouched, so
19
+ * the source map the application already ships keeps pointing at the right
20
+ * lines and stack traces stay readable. A codegen round-trip would have
21
+ * forced us to produce and merge maps of our own.
22
+ * - **File granularity.** The record is "this file ran", so there is no need to
23
+ * track statements or branches, and the whole class of line/branch
24
+ * normalisation bugs that follow V8-to-istanbul conversion never appears.
25
+ */
26
+ const DEFAULT_MAX_DEPTH = 2;
27
+ function transform(code, options) {
28
+ const program = parseProgram(code);
29
+ if (program === void 0) return void 0;
30
+ const local = `__ccqa_${hash(options.fileId)}`;
31
+ const literal = JSON.stringify(options.fileId);
32
+ const enter = `${local}&&${local}(${literal});`;
33
+ const points = [];
34
+ collect(program, options.maxDepth ?? DEFAULT_MAX_DEPTH, points);
35
+ if (code.length === 0) return void 0;
36
+ const prologueAt = afterDirectives(code, program);
37
+ const edits = points.filter((offset) => offset > prologueAt).map((offset) => ({
38
+ offset,
39
+ text: enter
40
+ }));
41
+ edits.push({
42
+ offset: prologueAt,
43
+ text: `var ${local}=globalThis.__ccqaCoverage;${local}&&${local}(${literal},true);`
44
+ });
45
+ edits.sort((a, b) => a.offset - b.offset);
46
+ const parts = [];
47
+ let last = 0;
48
+ for (const edit of edits) {
49
+ parts.push(code.slice(last, edit.offset), edit.text);
50
+ last = edit.offset;
51
+ }
52
+ parts.push(code.slice(last));
53
+ return parts.join("");
54
+ }
55
+ function parseProgram(code) {
56
+ for (const sourceType of ["module", "script"]) try {
57
+ return parse(code, {
58
+ ecmaVersion: "latest",
59
+ sourceType,
60
+ allowHashBang: true,
61
+ allowAwaitOutsideFunction: true,
62
+ allowReturnOutsideFunction: sourceType === "script"
63
+ });
64
+ } catch {}
65
+ }
66
+ /**
67
+ * A directive prologue only counts while it is still the first thing in its
68
+ * scope. Inserting ahead of `"use strict"` demotes it to an ordinary string
69
+ * expression, and the code it governed silently starts running sloppy — the
70
+ * instrumentation would be changing the behaviour it is supposed to observe.
71
+ * Applies to a function body as much as to the module.
72
+ */
73
+ function afterDirectives(code, program) {
74
+ let offset = program.start;
75
+ if (code.startsWith("#!")) {
76
+ const newline = code.indexOf("\n");
77
+ offset = newline < 0 ? code.length : newline + 1;
78
+ }
79
+ return skipDirectives(program.body, offset);
80
+ }
81
+ function skipDirectives(statements, from) {
82
+ let offset = from;
83
+ for (const statement of statements) {
84
+ if (statement.type !== "ExpressionStatement") break;
85
+ const expression = statement.expression;
86
+ if (expression.type !== "Literal" || typeof expression.value !== "string") break;
87
+ offset = statement.end;
88
+ }
89
+ return offset;
90
+ }
91
+ const FUNCTION_TYPES = new Set([
92
+ "FunctionDeclaration",
93
+ "FunctionExpression",
94
+ "ArrowFunctionExpression"
95
+ ]);
96
+ function collect(root, maxDepth, points) {
97
+ walk(root, 0, false);
98
+ function walk(node, depth, inClass) {
99
+ const isFunction = FUNCTION_TYPES.has(node.type);
100
+ const nextDepth = isFunction ? depth + 1 : depth;
101
+ if (isFunction) {
102
+ const wanted = inClass || nextDepth <= maxDepth;
103
+ const body = node.body;
104
+ if (wanted && body && body.type === "BlockStatement") {
105
+ const statements = body.body;
106
+ points.push(skipDirectives(statements, body.start + 1));
107
+ }
108
+ }
109
+ for (const key in node) {
110
+ if (key === "type" || key === "start" || key === "end" || key === "loc" || key === "range") continue;
111
+ const value = node[key];
112
+ if (Array.isArray(value)) {
113
+ for (const item of value) if (isNode(item)) walk(item, nextDepth, childInClass(node, key, inClass));
114
+ } else if (isNode(value)) walk(value, nextDepth, childInClass(node, key, inClass));
115
+ }
116
+ }
117
+ }
118
+ /** True while walking the value of a class member, false again inside its body. */
119
+ function childInClass(parent, key, inherited) {
120
+ if (parent.type === "MethodDefinition" || parent.type === "PropertyDefinition") return key === "value";
121
+ if (FUNCTION_TYPES.has(parent.type)) return false;
122
+ return inherited;
123
+ }
124
+ function isNode(value) {
125
+ return typeof value === "object" && value !== null && typeof value.type === "string";
126
+ }
127
+ /** Short, collision-resistant suffix so bundlers can hoist several modules into one scope. */
128
+ function hash(value) {
129
+ let h = 2166136261;
130
+ for (let i = 0; i < value.length; i++) {
131
+ h ^= value.charCodeAt(i);
132
+ h = Math.imul(h, 16777619);
133
+ }
134
+ return (h >>> 0).toString(36);
135
+ }
136
+ //#endregion
137
+ //#region src/coverage/next/loader.ts
138
+ /**
139
+ * webpack loader form of the instrumenter, for code that reaches the runtime as
140
+ * part of a bundle and so is invisible to the load hooks.
141
+ *
142
+ * It is registered with `enforce: "post"` so it sees JavaScript: webpack runs
143
+ * post loaders last, after the framework's own TypeScript/JSX transform, which
144
+ * keeps this file free of a TypeScript parser.
145
+ */
146
+ function ccqaCoverageLoader(source) {
147
+ const root = this.getOptions?.().root ?? process.cwd();
148
+ const fileId = fileIdFor(this.resourcePath, root);
149
+ if (fileId === void 0) return source;
150
+ const instrumented = transform(source, { fileId });
151
+ if (instrumented === void 0) {
152
+ this.emitWarning?.(/* @__PURE__ */ new Error(`ccqa-tools could not parse ${fileId}; left uninstrumented`));
153
+ return source;
154
+ }
155
+ return instrumented;
156
+ }
157
+ //#endregion
158
+ export { ccqaCoverageLoader as default };
@@ -0,0 +1,101 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let node_module = require("node:module");
3
+ let node_path = require("node:path");
4
+ //#region src/coverage/wire.ts
5
+ /**
6
+ * Enables the instrumentation. Unset means the register hook is never loaded
7
+ * and the application pays nothing at all.
8
+ *
9
+ * `1` / `true` turns it on and leaves attribution to the incoming carrier.
10
+ * Any other value is itself a `<runId>.<specId>` and becomes the ambient spec
11
+ * for the process — the only way to attribute an entry point that has no
12
+ * inbound request to read, such as a worker started per spec.
13
+ */
14
+ const ENV_NAME = "CCQA_COVERAGE";
15
+ const SPEC_ID = /^[A-Za-z0-9._\-/]{1,200}$/;
16
+ /**
17
+ * Accepts a carrier value only if it looks like an id we wrote.
18
+ *
19
+ * The cookie is client-controlled, so this is the first of two gates: the
20
+ * second is the hub refusing runs it never started.
21
+ */
22
+ function parseSpecId(raw) {
23
+ if (!raw) return void 0;
24
+ const value = raw.trim();
25
+ if (!SPEC_ID.test(value)) return void 0;
26
+ if (value === "1" || value === "true") return void 0;
27
+ return value;
28
+ }
29
+ //#endregion
30
+ //#region src/coverage/runtime-env.ts
31
+ function readConfig(env = process.env) {
32
+ const raw = env[ENV_NAME];
33
+ const include = (env["CCQA_COVERAGE_INCLUDE"] ?? "src").split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
34
+ return {
35
+ enabled: raw !== void 0 && raw !== "" && raw !== "0" && raw !== "false",
36
+ ambientSpecId: parseSpecId(raw),
37
+ root: env["CCQA_COVERAGE_ROOT"] ?? process.cwd(),
38
+ include,
39
+ debug: env["CCQA_COVERAGE_DEBUG"] === "1" || env["CCQA_COVERAGE_DEBUG"] === "true"
40
+ };
41
+ }
42
+ /**
43
+ * Diagnostics go to stderr and nowhere else. A `--import` preload is inherited
44
+ * by every child node process, and writing to stdout corrupts whatever the host
45
+ * was parsing there — enough to make a framework's own toolchain fail to start.
46
+ */
47
+ function debugLog(config, message) {
48
+ if (!config.debug) return;
49
+ process.stderr.write(`[ccqa-tools] ${message}\n`);
50
+ }
51
+ //#endregion
52
+ //#region src/coverage/next/index.ts
53
+ /**
54
+ * Next.js integration.
55
+ *
56
+ * Next bundles its server code, so the load hooks in
57
+ * `ccqa-tools/coverage/register` never see the application's own modules — only the
58
+ * bundle. The instrumentation therefore has to happen at build time, while the
59
+ * context that attributes it still comes from the register hook wrapping
60
+ * `node:http`. Both halves are required:
61
+ *
62
+ * // next.config.ts
63
+ * export default withCoverage({ ...yourConfig })
64
+ *
65
+ * NODE_OPTIONS='--import ccqa-tools/coverage/register' CCQA_COVERAGE=1 next start
66
+ *
67
+ * Only server bundles are instrumented. Instrumenting the client would ship
68
+ * `__ccqaCoverage` calls to browsers, where the front-end side of ccqa's
69
+ * coverage already reads V8's own counters and needs nothing injected.
70
+ */
71
+ const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
72
+ /** Wraps a Next config, preserving any `webpack` hook it already has. */
73
+ function withCoverage(config, options = {}) {
74
+ if (!(options.enabled ?? process.env["CCQA_COVERAGE"] !== void 0)) return config;
75
+ const root = (0, node_path.resolve)(options.root ?? readConfig().root);
76
+ const include = (options.include ?? ["src"]).map((dir) => (0, node_path.resolve)(root, dir));
77
+ const previous = config.webpack;
78
+ return {
79
+ ...config,
80
+ webpack(webpackConfig, context) {
81
+ const next = previous ? previous(webpackConfig, context) : webpackConfig;
82
+ if (!context.isServer) return next;
83
+ next.module ??= {};
84
+ next.module.rules ??= [];
85
+ next.module.rules.push({
86
+ enforce: "post",
87
+ test: /\.(?:[cm]?js|jsx|tsx?)$/,
88
+ include,
89
+ exclude: /[\\/]node_modules[\\/]/,
90
+ use: [{
91
+ loader: require$1.resolve("./next-loader.cjs"),
92
+ options: { root }
93
+ }]
94
+ });
95
+ debugLog(readConfig(), `instrumenting server bundles under ${include.join(", ")}`);
96
+ return next;
97
+ }
98
+ };
99
+ }
100
+ //#endregion
101
+ exports.withCoverage = withCoverage;
@@ -0,0 +1,33 @@
1
+ //#region src/coverage/next/index.d.ts
2
+ /**
3
+ * Next.js integration.
4
+ *
5
+ * Next bundles its server code, so the load hooks in
6
+ * `ccqa-tools/coverage/register` never see the application's own modules — only the
7
+ * bundle. The instrumentation therefore has to happen at build time, while the
8
+ * context that attributes it still comes from the register hook wrapping
9
+ * `node:http`. Both halves are required:
10
+ *
11
+ * // next.config.ts
12
+ * export default withCoverage({ ...yourConfig })
13
+ *
14
+ * NODE_OPTIONS='--import ccqa-tools/coverage/register' CCQA_COVERAGE=1 next start
15
+ *
16
+ * Only server bundles are instrumented. Instrumenting the client would ship
17
+ * `__ccqaCoverage` calls to browsers, where the front-end side of ccqa's
18
+ * coverage already reads V8's own counters and needs nothing injected.
19
+ */
20
+ interface CoverageNextOptions {
21
+ /** Project root that file ids are relative to. Defaults to `process.cwd()`. */
22
+ root?: string;
23
+ /** Directories, relative to the root, to instrument. Defaults to `["src"]`. */
24
+ include?: string[];
25
+ /** Set false to build without instrumentation while keeping the config in place. */
26
+ enabled?: boolean;
27
+ }
28
+ /** Wraps a Next config, preserving any `webpack` hook it already has. */
29
+ declare function withCoverage<T extends {
30
+ webpack?: unknown;
31
+ }>(config: T, options?: CoverageNextOptions): T;
32
+ //#endregion
33
+ export { CoverageNextOptions, withCoverage };
@@ -0,0 +1,33 @@
1
+ //#region src/coverage/next/index.d.ts
2
+ /**
3
+ * Next.js integration.
4
+ *
5
+ * Next bundles its server code, so the load hooks in
6
+ * `ccqa-tools/coverage/register` never see the application's own modules — only the
7
+ * bundle. The instrumentation therefore has to happen at build time, while the
8
+ * context that attributes it still comes from the register hook wrapping
9
+ * `node:http`. Both halves are required:
10
+ *
11
+ * // next.config.ts
12
+ * export default withCoverage({ ...yourConfig })
13
+ *
14
+ * NODE_OPTIONS='--import ccqa-tools/coverage/register' CCQA_COVERAGE=1 next start
15
+ *
16
+ * Only server bundles are instrumented. Instrumenting the client would ship
17
+ * `__ccqaCoverage` calls to browsers, where the front-end side of ccqa's
18
+ * coverage already reads V8's own counters and needs nothing injected.
19
+ */
20
+ interface CoverageNextOptions {
21
+ /** Project root that file ids are relative to. Defaults to `process.cwd()`. */
22
+ root?: string;
23
+ /** Directories, relative to the root, to instrument. Defaults to `["src"]`. */
24
+ include?: string[];
25
+ /** Set false to build without instrumentation while keeping the config in place. */
26
+ enabled?: boolean;
27
+ }
28
+ /** Wraps a Next config, preserving any `webpack` hook it already has. */
29
+ declare function withCoverage<T extends {
30
+ webpack?: unknown;
31
+ }>(config: T, options?: CoverageNextOptions): T;
32
+ //#endregion
33
+ export { CoverageNextOptions, withCoverage };