@openpolicy/vite-auto-collect 0.0.18 → 0.0.19
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/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +251 -39
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;KAOY,kBAAA;;AAAZ;;;EAKC,MAAA;EAAA;;;EAIA,UAAA;EASC;;;AAyCF;;EA5CC,MAAA;EAEA,YAAA;IACC,cAAA;EAAA;AAAA;;;;;;;;;;;;;iBAyCc,WAAA,CAAY,OAAA,GAAS,kBAAA,GAA0B,MAAA"}
|
package/dist/index.js
CHANGED
|
@@ -4,65 +4,93 @@ import { parseSync } from "oxc-parser";
|
|
|
4
4
|
//#region src/analyse.ts
|
|
5
5
|
const SDK_SPECIFIER = "@openpolicy/sdk";
|
|
6
6
|
const COLLECTING_NAME = "collecting";
|
|
7
|
+
const THIRD_PARTY_NAME = "thirdParty";
|
|
7
8
|
/**
|
|
8
|
-
* Extract `collecting()` call metadata from a single source file.
|
|
9
|
+
* Extract `collecting()` and `thirdParty()` call metadata from a single source file.
|
|
9
10
|
*
|
|
10
|
-
* Returns
|
|
11
|
-
*
|
|
12
|
-
* that fail to parse — return
|
|
11
|
+
* Returns an `ExtractResult` with `dataCollected` (category → labels) and
|
|
12
|
+
* `thirdParties` (array of third-party entries). Files with no matching calls
|
|
13
|
+
* — or that fail to parse — return empty defaults.
|
|
13
14
|
*
|
|
14
15
|
* The analyser runs in two phases:
|
|
15
|
-
* 1. Collect local names bound to `collecting` imported from
|
|
16
|
-
* (handles renamed imports, skips type-only imports,
|
|
17
|
-
* imported from other modules).
|
|
16
|
+
* 1. Collect local names bound to `collecting` / `thirdParty` imported from
|
|
17
|
+
* `@openpolicy/sdk` (handles renamed imports, skips type-only imports,
|
|
18
|
+
* ignores look-alikes imported from other modules).
|
|
18
19
|
* 2. Walk the program body and inspect every `CallExpression` whose callee
|
|
19
20
|
* is one of those tracked local names.
|
|
20
21
|
*/
|
|
21
|
-
function
|
|
22
|
+
function extractFromFile(filename, code) {
|
|
23
|
+
const empty = {
|
|
24
|
+
dataCollected: {},
|
|
25
|
+
thirdParties: []
|
|
26
|
+
};
|
|
22
27
|
let result;
|
|
23
28
|
try {
|
|
24
29
|
result = parseSync(filename, code);
|
|
25
30
|
} catch {
|
|
26
31
|
console.warn(`[openpolicy-auto-collect] parse error in ${filename}`);
|
|
27
|
-
return
|
|
32
|
+
return empty;
|
|
28
33
|
}
|
|
29
34
|
if (result.errors.length > 0) {
|
|
30
35
|
if (result.errors.some((e) => e.severity === "Error")) {
|
|
31
36
|
console.warn(`[openpolicy-auto-collect] parse error in ${filename}`);
|
|
32
|
-
return
|
|
37
|
+
return empty;
|
|
33
38
|
}
|
|
34
39
|
}
|
|
35
40
|
const program = result.program;
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
const collectingNames = collectSdkBindings(program, COLLECTING_NAME);
|
|
42
|
+
const thirdPartyNames = collectSdkBindings(program, THIRD_PARTY_NAME);
|
|
43
|
+
if (collectingNames.size === 0 && thirdPartyNames.size === 0) return empty;
|
|
44
|
+
const dataCollected = {};
|
|
45
|
+
const thirdParties = [];
|
|
46
|
+
const seenThirdParties = /* @__PURE__ */ new Set();
|
|
39
47
|
walk(program, (node) => {
|
|
40
48
|
if (node.type !== "CallExpression") return;
|
|
41
49
|
const callee = node.callee;
|
|
42
50
|
if (!callee || callee.type !== "Identifier") return;
|
|
43
|
-
|
|
51
|
+
const calleeName = callee.name;
|
|
44
52
|
const args = node.arguments;
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
if (collectingNames.has(calleeName)) {
|
|
54
|
+
if (!args || args.length < 3) return;
|
|
55
|
+
const category = extractStringLiteral(args[0]);
|
|
56
|
+
if (category === null) return;
|
|
57
|
+
const labels = extractLabelKeys(args[2]);
|
|
58
|
+
if (labels === null) return;
|
|
59
|
+
const existing = dataCollected[category] ?? [];
|
|
60
|
+
const seen = new Set(existing);
|
|
61
|
+
for (const label of labels) if (!seen.has(label)) {
|
|
62
|
+
existing.push(label);
|
|
63
|
+
seen.add(label);
|
|
64
|
+
}
|
|
65
|
+
dataCollected[category] = existing;
|
|
66
|
+
} else if (thirdPartyNames.has(calleeName)) {
|
|
67
|
+
if (!args || args.length < 3) return;
|
|
68
|
+
const name = extractStringLiteral(args[0]);
|
|
69
|
+
if (name === null) return;
|
|
70
|
+
const purpose = extractStringLiteral(args[1]);
|
|
71
|
+
if (purpose === null) return;
|
|
72
|
+
const policyUrl = extractStringLiteral(args[2]);
|
|
73
|
+
if (policyUrl === null) return;
|
|
74
|
+
if (seenThirdParties.has(name)) return;
|
|
75
|
+
seenThirdParties.add(name);
|
|
76
|
+
thirdParties.push({
|
|
77
|
+
name,
|
|
78
|
+
purpose,
|
|
79
|
+
policyUrl
|
|
80
|
+
});
|
|
55
81
|
}
|
|
56
|
-
out[category] = existing;
|
|
57
82
|
});
|
|
58
|
-
return
|
|
83
|
+
return {
|
|
84
|
+
dataCollected,
|
|
85
|
+
thirdParties
|
|
86
|
+
};
|
|
59
87
|
}
|
|
60
88
|
/**
|
|
61
|
-
* Walk `ImportDeclaration` nodes and return the local names bound to
|
|
62
|
-
* `
|
|
63
|
-
* specifiers whose imported name
|
|
89
|
+
* Walk `ImportDeclaration` nodes and return the local names bound to the given
|
|
90
|
+
* `exportName` imported from `@openpolicy/sdk`. Skips type-only imports and
|
|
91
|
+
* specifiers whose imported name doesn't match.
|
|
64
92
|
*/
|
|
65
|
-
function
|
|
93
|
+
function collectSdkBindings(program, exportName) {
|
|
66
94
|
const names = /* @__PURE__ */ new Set();
|
|
67
95
|
const body = program.body;
|
|
68
96
|
if (!body) return names;
|
|
@@ -78,7 +106,7 @@ function collectSdkCollectingBindings(program) {
|
|
|
78
106
|
if (spec.importKind === "type") continue;
|
|
79
107
|
const imported = spec.imported;
|
|
80
108
|
if (!imported) continue;
|
|
81
|
-
if ((imported.type === "Identifier" ? imported.name : imported.type === "Literal" ? typeof imported.value === "string" ? imported.value : void 0 : void 0) !==
|
|
109
|
+
if ((imported.type === "Identifier" ? imported.name : imported.type === "Literal" ? typeof imported.value === "string" ? imported.value : void 0 : void 0) !== exportName) continue;
|
|
82
110
|
const local = spec.local;
|
|
83
111
|
if (!local || local.type !== "Identifier") continue;
|
|
84
112
|
names.add(local.name);
|
|
@@ -132,6 +160,140 @@ function walk(node, visit) {
|
|
|
132
160
|
}
|
|
133
161
|
}
|
|
134
162
|
//#endregion
|
|
163
|
+
//#region src/known-packages.ts
|
|
164
|
+
/**
|
|
165
|
+
* Registry of known npm packages mapped to their ThirdPartyEntry metadata.
|
|
166
|
+
* Multiple package names can point to the same service — deduplication by
|
|
167
|
+
* `ThirdPartyEntry.name` is handled at merge time in the caller.
|
|
168
|
+
*/
|
|
169
|
+
const KNOWN_PACKAGES = new Map([
|
|
170
|
+
["stripe", {
|
|
171
|
+
name: "Stripe",
|
|
172
|
+
purpose: "Payment processing",
|
|
173
|
+
policyUrl: "https://stripe.com/privacy"
|
|
174
|
+
}],
|
|
175
|
+
["@stripe/stripe-js", {
|
|
176
|
+
name: "Stripe",
|
|
177
|
+
purpose: "Payment processing",
|
|
178
|
+
policyUrl: "https://stripe.com/privacy"
|
|
179
|
+
}],
|
|
180
|
+
["braintree", {
|
|
181
|
+
name: "Braintree",
|
|
182
|
+
purpose: "Payment processing",
|
|
183
|
+
policyUrl: "https://www.braintreepayments.com/legal/braintree-privacy-policy"
|
|
184
|
+
}],
|
|
185
|
+
["@braintree/browser-drop-in", {
|
|
186
|
+
name: "Braintree",
|
|
187
|
+
purpose: "Payment processing",
|
|
188
|
+
policyUrl: "https://www.braintreepayments.com/legal/braintree-privacy-policy"
|
|
189
|
+
}],
|
|
190
|
+
["@sentry/browser", {
|
|
191
|
+
name: "Sentry",
|
|
192
|
+
purpose: "Error tracking",
|
|
193
|
+
policyUrl: "https://sentry.io/privacy/"
|
|
194
|
+
}],
|
|
195
|
+
["@sentry/node", {
|
|
196
|
+
name: "Sentry",
|
|
197
|
+
purpose: "Error tracking",
|
|
198
|
+
policyUrl: "https://sentry.io/privacy/"
|
|
199
|
+
}],
|
|
200
|
+
["@sentry/nextjs", {
|
|
201
|
+
name: "Sentry",
|
|
202
|
+
purpose: "Error tracking",
|
|
203
|
+
policyUrl: "https://sentry.io/privacy/"
|
|
204
|
+
}],
|
|
205
|
+
["@sentry/react", {
|
|
206
|
+
name: "Sentry",
|
|
207
|
+
purpose: "Error tracking",
|
|
208
|
+
policyUrl: "https://sentry.io/privacy/"
|
|
209
|
+
}],
|
|
210
|
+
["@sentry/vue", {
|
|
211
|
+
name: "Sentry",
|
|
212
|
+
purpose: "Error tracking",
|
|
213
|
+
policyUrl: "https://sentry.io/privacy/"
|
|
214
|
+
}],
|
|
215
|
+
["@datadog/browser-rum", {
|
|
216
|
+
name: "Datadog",
|
|
217
|
+
purpose: "Monitoring",
|
|
218
|
+
policyUrl: "https://www.datadoghq.com/legal/privacy/"
|
|
219
|
+
}],
|
|
220
|
+
["dd-trace", {
|
|
221
|
+
name: "Datadog",
|
|
222
|
+
purpose: "Monitoring",
|
|
223
|
+
policyUrl: "https://www.datadoghq.com/legal/privacy/"
|
|
224
|
+
}],
|
|
225
|
+
["posthog-js", {
|
|
226
|
+
name: "PostHog",
|
|
227
|
+
purpose: "Product analytics",
|
|
228
|
+
policyUrl: "https://posthog.com/privacy"
|
|
229
|
+
}],
|
|
230
|
+
["posthog-node", {
|
|
231
|
+
name: "PostHog",
|
|
232
|
+
purpose: "Product analytics",
|
|
233
|
+
policyUrl: "https://posthog.com/privacy"
|
|
234
|
+
}],
|
|
235
|
+
["mixpanel-browser", {
|
|
236
|
+
name: "Mixpanel",
|
|
237
|
+
purpose: "Product analytics",
|
|
238
|
+
policyUrl: "https://mixpanel.com/legal/privacy-policy/"
|
|
239
|
+
}],
|
|
240
|
+
["@segment/analytics-next", {
|
|
241
|
+
name: "Segment",
|
|
242
|
+
purpose: "Customer data platform",
|
|
243
|
+
policyUrl: "https://www.twilio.com/en-us/legal/privacy"
|
|
244
|
+
}],
|
|
245
|
+
["@amplitude/analytics-browser", {
|
|
246
|
+
name: "Amplitude",
|
|
247
|
+
purpose: "Product analytics",
|
|
248
|
+
policyUrl: "https://amplitude.com/privacy"
|
|
249
|
+
}],
|
|
250
|
+
["amplitude-js", {
|
|
251
|
+
name: "Amplitude",
|
|
252
|
+
purpose: "Product analytics",
|
|
253
|
+
policyUrl: "https://amplitude.com/privacy"
|
|
254
|
+
}],
|
|
255
|
+
["@vercel/analytics", {
|
|
256
|
+
name: "Vercel Analytics",
|
|
257
|
+
purpose: "Web analytics",
|
|
258
|
+
policyUrl: "https://vercel.com/legal/privacy-policy"
|
|
259
|
+
}],
|
|
260
|
+
["plausible-tracker", {
|
|
261
|
+
name: "Plausible",
|
|
262
|
+
purpose: "Web analytics",
|
|
263
|
+
policyUrl: "https://plausible.io/privacy"
|
|
264
|
+
}],
|
|
265
|
+
["logrocket", {
|
|
266
|
+
name: "LogRocket",
|
|
267
|
+
purpose: "Session recording",
|
|
268
|
+
policyUrl: "https://logrocket.com/privacy/"
|
|
269
|
+
}],
|
|
270
|
+
["@hotjar/browser", {
|
|
271
|
+
name: "Hotjar",
|
|
272
|
+
purpose: "Session recording",
|
|
273
|
+
policyUrl: "https://www.hotjar.com/legal/policies/privacy/"
|
|
274
|
+
}],
|
|
275
|
+
["resend", {
|
|
276
|
+
name: "Resend",
|
|
277
|
+
purpose: "Transactional email",
|
|
278
|
+
policyUrl: "https://resend.com/legal/privacy-policy"
|
|
279
|
+
}],
|
|
280
|
+
["@sendgrid/mail", {
|
|
281
|
+
name: "SendGrid",
|
|
282
|
+
purpose: "Transactional email",
|
|
283
|
+
policyUrl: "https://www.twilio.com/en-us/legal/privacy"
|
|
284
|
+
}],
|
|
285
|
+
["intercom-client", {
|
|
286
|
+
name: "Intercom",
|
|
287
|
+
purpose: "Customer messaging",
|
|
288
|
+
policyUrl: "https://www.intercom.com/legal/privacy"
|
|
289
|
+
}],
|
|
290
|
+
["@intercom/messenger-js-sdk", {
|
|
291
|
+
name: "Intercom",
|
|
292
|
+
purpose: "Customer messaging",
|
|
293
|
+
policyUrl: "https://www.intercom.com/legal/privacy"
|
|
294
|
+
}]
|
|
295
|
+
]);
|
|
296
|
+
//#endregion
|
|
135
297
|
//#region src/scan.ts
|
|
136
298
|
const DEFAULT_IGNORES = new Set([
|
|
137
299
|
"node_modules",
|
|
@@ -215,11 +377,46 @@ function autoCollect(options = {}) {
|
|
|
215
377
|
const srcDirOpt = options.srcDir ?? "src";
|
|
216
378
|
const extensions = options.extensions ?? [".ts", ".tsx"];
|
|
217
379
|
const ignore = options.ignore ?? [];
|
|
380
|
+
const usePackageJsonOpt = options.thirdParties?.usePackageJson ?? false;
|
|
381
|
+
let resolvedRoot;
|
|
218
382
|
let resolvedSrcDir;
|
|
219
|
-
let scanned = {
|
|
383
|
+
let scanned = {
|
|
384
|
+
dataCollected: {},
|
|
385
|
+
thirdParties: []
|
|
386
|
+
};
|
|
387
|
+
async function detectFromPackageJson(root) {
|
|
388
|
+
let raw;
|
|
389
|
+
try {
|
|
390
|
+
raw = await readFile(resolve(root, "package.json"), "utf8");
|
|
391
|
+
} catch {
|
|
392
|
+
return [];
|
|
393
|
+
}
|
|
394
|
+
let pkg;
|
|
395
|
+
try {
|
|
396
|
+
pkg = JSON.parse(raw);
|
|
397
|
+
} catch {
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
const allDeps = {
|
|
401
|
+
...pkg.dependencies,
|
|
402
|
+
...pkg.devDependencies
|
|
403
|
+
};
|
|
404
|
+
const entries = [];
|
|
405
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
406
|
+
for (const pkgName of Object.keys(allDeps)) {
|
|
407
|
+
const entry = KNOWN_PACKAGES.get(pkgName);
|
|
408
|
+
if (entry && !seenNames.has(entry.name)) {
|
|
409
|
+
seenNames.add(entry.name);
|
|
410
|
+
entries.push(entry);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return entries;
|
|
414
|
+
}
|
|
220
415
|
async function scanAndMerge() {
|
|
221
416
|
const files = await walkSources(resolvedSrcDir, extensions, ignore);
|
|
222
|
-
const
|
|
417
|
+
const mergedData = {};
|
|
418
|
+
const mergedParties = [];
|
|
419
|
+
const seenParties = /* @__PURE__ */ new Set();
|
|
223
420
|
for (const file of files) {
|
|
224
421
|
let code;
|
|
225
422
|
try {
|
|
@@ -227,18 +424,32 @@ function autoCollect(options = {}) {
|
|
|
227
424
|
} catch {
|
|
228
425
|
continue;
|
|
229
426
|
}
|
|
230
|
-
const extracted =
|
|
231
|
-
for (const [category, labels] of Object.entries(extracted)) {
|
|
232
|
-
const existing =
|
|
427
|
+
const extracted = extractFromFile(file, code);
|
|
428
|
+
for (const [category, labels] of Object.entries(extracted.dataCollected)) {
|
|
429
|
+
const existing = mergedData[category] ?? [];
|
|
233
430
|
const seen = new Set(existing);
|
|
234
431
|
for (const label of labels) if (!seen.has(label)) {
|
|
235
432
|
existing.push(label);
|
|
236
433
|
seen.add(label);
|
|
237
434
|
}
|
|
238
|
-
|
|
435
|
+
mergedData[category] = existing;
|
|
436
|
+
}
|
|
437
|
+
for (const entry of extracted.thirdParties) if (!seenParties.has(entry.name)) {
|
|
438
|
+
seenParties.add(entry.name);
|
|
439
|
+
mergedParties.push(entry);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (usePackageJsonOpt) {
|
|
443
|
+
const pkgEntries = await detectFromPackageJson(resolvedRoot);
|
|
444
|
+
for (const entry of pkgEntries) if (!seenParties.has(entry.name)) {
|
|
445
|
+
seenParties.add(entry.name);
|
|
446
|
+
mergedParties.push(entry);
|
|
239
447
|
}
|
|
240
448
|
}
|
|
241
|
-
return
|
|
449
|
+
return {
|
|
450
|
+
dataCollected: mergedData,
|
|
451
|
+
thirdParties: mergedParties
|
|
452
|
+
};
|
|
242
453
|
}
|
|
243
454
|
/**
|
|
244
455
|
* Returns true when `file` lives inside `resolvedSrcDir` and has one of
|
|
@@ -269,6 +480,7 @@ function autoCollect(options = {}) {
|
|
|
269
480
|
name: "openpolicy-auto-collect",
|
|
270
481
|
enforce: "pre",
|
|
271
482
|
configResolved(config) {
|
|
483
|
+
resolvedRoot = config.root;
|
|
272
484
|
resolvedSrcDir = resolve(config.root, srcDirOpt);
|
|
273
485
|
},
|
|
274
486
|
async buildStart() {
|
|
@@ -300,7 +512,7 @@ function autoCollect(options = {}) {
|
|
|
300
512
|
},
|
|
301
513
|
load(id) {
|
|
302
514
|
if (id !== RESOLVED_VIRTUAL_ID) return null;
|
|
303
|
-
return `export const dataCollected = ${JSON.stringify(scanned)};\n`;
|
|
515
|
+
return `export const dataCollected = ${JSON.stringify(scanned.dataCollected)};\nexport const thirdParties = ${JSON.stringify(scanned.thirdParties)};\n`;
|
|
304
516
|
}
|
|
305
517
|
};
|
|
306
518
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/analyse.ts","../src/scan.ts","../src/index.ts"],"sourcesContent":["import { parseSync } from \"oxc-parser\";\n\nconst SDK_SPECIFIER = \"@openpolicy/sdk\";\nconst COLLECTING_NAME = \"collecting\";\n\ntype AnyNode = { type: string; [key: string]: unknown };\n\n/**\n * Extract `collecting()` call metadata from a single source file.\n *\n * Returns a `Record<category, labels[]>` for every detected call whose\n * arguments match the analysable shape. Files with no matching calls — or\n * that fail to parse — return `{}`.\n *\n * The analyser runs in two phases:\n * 1. Collect local names bound to `collecting` imported from `@openpolicy/sdk`\n * (handles renamed imports, skips type-only imports, ignores look-alikes\n * imported from other modules).\n * 2. Walk the program body and inspect every `CallExpression` whose callee\n * is one of those tracked local names.\n */\nexport function extractCollecting(\n\tfilename: string,\n\tcode: string,\n): Record<string, string[]> {\n\tlet result: ReturnType<typeof parseSync>;\n\ttry {\n\t\tresult = parseSync(filename, code);\n\t} catch {\n\t\tconsole.warn(`[openpolicy-auto-collect] parse error in ${filename}`);\n\t\treturn {};\n\t}\n\n\tif (result.errors.length > 0) {\n\t\t// Hard parse failures only — oxc reports recoverable errors but still\n\t\t// produces a usable AST, so we keep going and let the walker decide.\n\t\tconst fatal = result.errors.some((e) => e.severity === (\"Error\" as never));\n\t\tif (fatal) {\n\t\t\tconsole.warn(`[openpolicy-auto-collect] parse error in ${filename}`);\n\t\t\treturn {};\n\t\t}\n\t}\n\n\tconst program = result.program as unknown as AnyNode;\n\tconst localNames = collectSdkCollectingBindings(program);\n\tif (localNames.size === 0) return {};\n\n\tconst out: Record<string, string[]> = {};\n\twalk(program, (node) => {\n\t\tif (node.type !== \"CallExpression\") return;\n\t\tconst callee = node.callee as AnyNode | undefined;\n\t\tif (!callee || callee.type !== \"Identifier\") return;\n\t\tif (!localNames.has(callee.name as string)) return;\n\n\t\tconst args = node.arguments as AnyNode[] | undefined;\n\t\tif (!args || args.length < 3) return;\n\n\t\tconst category = extractStringLiteral(args[0]);\n\t\tif (category === null) return;\n\n\t\tconst labels = extractLabelKeys(args[2]);\n\t\tif (labels === null) return;\n\n\t\tconst existing = out[category] ?? [];\n\t\tconst seen = new Set(existing);\n\t\tfor (const label of labels) {\n\t\t\tif (!seen.has(label)) {\n\t\t\t\texisting.push(label);\n\t\t\t\tseen.add(label);\n\t\t\t}\n\t\t}\n\t\tout[category] = existing;\n\t});\n\treturn out;\n}\n\n/**\n * Walk `ImportDeclaration` nodes and return the local names bound to\n * `collecting` imported from `@openpolicy/sdk`. Skips type-only imports and\n * specifiers whose imported name isn't literally `collecting`.\n */\nfunction collectSdkCollectingBindings(program: AnyNode): Set<string> {\n\tconst names = new Set<string>();\n\tconst body = program.body as AnyNode[] | undefined;\n\tif (!body) return names;\n\tfor (const node of body) {\n\t\tif (node.type !== \"ImportDeclaration\") continue;\n\t\tif ((node.importKind as string | undefined) === \"type\") continue;\n\t\tconst source = node.source as AnyNode | undefined;\n\t\tif (!source || source.value !== SDK_SPECIFIER) continue;\n\t\tconst specifiers = node.specifiers as AnyNode[] | undefined;\n\t\tif (!specifiers) continue;\n\t\tfor (const spec of specifiers) {\n\t\t\tif (spec.type !== \"ImportSpecifier\") continue;\n\t\t\tif ((spec.importKind as string | undefined) === \"type\") continue;\n\t\t\tconst imported = spec.imported as AnyNode | undefined;\n\t\t\tif (!imported) continue;\n\t\t\t// ESTree allows either an Identifier (name) or a string Literal (value)\n\t\t\t// for the imported binding (e.g. `import { \"foo\" as bar }`).\n\t\t\tconst importedName =\n\t\t\t\timported.type === \"Identifier\"\n\t\t\t\t\t? (imported.name as string | undefined)\n\t\t\t\t\t: imported.type === \"Literal\"\n\t\t\t\t\t\t? typeof imported.value === \"string\"\n\t\t\t\t\t\t\t? imported.value\n\t\t\t\t\t\t\t: undefined\n\t\t\t\t\t\t: undefined;\n\t\t\tif (importedName !== COLLECTING_NAME) continue;\n\t\t\tconst local = spec.local as AnyNode | undefined;\n\t\t\tif (!local || local.type !== \"Identifier\") continue;\n\t\t\tnames.add(local.name as string);\n\t\t}\n\t}\n\treturn names;\n}\n\n/**\n * If `node` is a string `Literal`, return its string value. Otherwise\n * return `null` so the caller silently skips the call.\n */\nfunction extractStringLiteral(node: AnyNode | undefined): string | null {\n\tif (!node) return null;\n\tif (node.type !== \"Literal\") return null;\n\tif (typeof node.value !== \"string\") return null;\n\treturn node.value;\n}\n\n/**\n * Extract the string values from a plain `{ fieldName: \"Human Label\" }`\n * object literal. Returns an array of label strings, deduped while\n * preserving insertion order. Returns `null` if the shape doesn't match.\n */\nfunction extractLabelKeys(node: AnyNode | undefined): string[] | null {\n\tif (!node || node.type !== \"ObjectExpression\") return null;\n\tconst properties = node.properties as AnyNode[] | undefined;\n\tif (!properties) return null;\n\n\tconst labels: string[] = [];\n\tconst seen = new Set<string>();\n\tfor (const prop of properties) {\n\t\tif (prop.type !== \"Property\") continue; // drop SpreadElement silently\n\t\tconst val = prop.value as AnyNode | undefined;\n\t\tif (!val || val.type !== \"Literal\" || typeof val.value !== \"string\")\n\t\t\tcontinue;\n\t\tif (seen.has(val.value)) continue;\n\t\tseen.add(val.value);\n\t\tlabels.push(val.value);\n\t}\n\treturn labels;\n}\n\n/**\n * Recursive AST walker. Visits every `AnyNode` (depth-first) reachable\n * through array / nested-object children and invokes `visit` on each.\n */\nfunction walk(node: AnyNode, visit: (node: AnyNode) => void): void {\n\tvisit(node);\n\tfor (const key of Object.keys(node)) {\n\t\tif (key === \"parent\") continue;\n\t\tconst value = node[key];\n\t\tif (Array.isArray(value)) {\n\t\t\tfor (const item of value) {\n\t\t\t\tif (item && typeof item === \"object\" && typeof item.type === \"string\") {\n\t\t\t\t\twalk(item as AnyNode, visit);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (\n\t\t\tvalue &&\n\t\t\ttypeof value === \"object\" &&\n\t\t\ttypeof (value as { type?: unknown }).type === \"string\"\n\t\t) {\n\t\t\twalk(value as AnyNode, visit);\n\t\t}\n\t}\n}\n","import type { Dirent } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { extname, join } from \"node:path\";\n\nconst DEFAULT_IGNORES: ReadonlySet<string> = new Set([\n\t\"node_modules\",\n\t\"dist\",\n\t\".git\",\n\t\".next\",\n\t\".output\",\n\t\".svelte-kit\",\n\t\".cache\",\n]);\n\n/**\n * Recursively walks `root`, returning absolute paths of every regular file\n * whose extension is in `extensions`. Directories whose basename appears in\n * the built-in ignore list (or the extra `ignore` argument) are skipped\n * entirely.\n *\n * Missing roots resolve to an empty array — the plugin must not throw if the\n * user's `srcDir` hasn't been created yet.\n */\nexport async function walkSources(\n\troot: string,\n\textensions: readonly string[],\n\tignore: readonly string[] = [],\n): Promise<string[]> {\n\tconst ignored = new Set<string>([...DEFAULT_IGNORES, ...ignore]);\n\tconst exts = new Set(extensions);\n\tconst results: string[] = [];\n\n\tasync function walk(dir: string): Promise<void> {\n\t\tlet entries: Dirent[];\n\t\ttry {\n\t\t\tentries = (await readdir(dir, { withFileTypes: true })) as Dirent[];\n\t\t} catch (err) {\n\t\t\tconst code = (err as NodeJS.ErrnoException).code;\n\t\t\tif (code === \"ENOENT\" || code === \"ENOTDIR\") return;\n\t\t\tthrow err;\n\t\t}\n\t\tfor (const entry of entries) {\n\t\t\tif (ignored.has(entry.name)) continue;\n\t\t\tconst full = join(dir, entry.name);\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\tawait walk(full);\n\t\t\t} else if (entry.isFile() && exts.has(extname(entry.name))) {\n\t\t\t\tresults.push(full);\n\t\t\t}\n\t\t}\n\t}\n\n\tawait walk(root);\n\tresults.sort();\n\treturn results;\n}\n","import { readFile } from \"node:fs/promises\";\nimport { relative, resolve } from \"node:path\";\nimport type { Plugin, ViteDevServer } from \"vite\";\nimport { extractCollecting } from \"./analyse\";\nimport { walkSources } from \"./scan\";\n\nexport type AutoCollectOptions = {\n\t/**\n\t * Directory walked for `collecting()` calls. Resolved relative to the\n\t * Vite project root. Defaults to `\"src\"`.\n\t */\n\tsrcDir?: string;\n\t/**\n\t * File extensions scanned. Defaults to `[\".ts\", \".tsx\"]`.\n\t */\n\textensions?: string[];\n\t/**\n\t * Extra directory names skipped during the walk. Appended to the built-in\n\t * defaults (`node_modules`, `dist`, `.git`, `.next`, `.output`,\n\t * `.svelte-kit`, `.cache`).\n\t */\n\tignore?: string[];\n};\n\n/**\n * Marker returned from `resolveId` so `load` can recognise a hit. The leading\n * NUL prefix is the Rollup/Vite convention for virtual IDs so other plugins\n * leave it alone.\n */\nconst RESOLVED_VIRTUAL_ID = \"\\0virtual:openpolicy/auto-collected\";\n\n/**\n * Matches any path that lives inside the `@openpolicy/sdk` package, whether\n * it's resolved via a workspace symlink (`.../packages/sdk/...`) or a\n * published `node_modules` install (`.../@openpolicy/sdk/...`). Used to scope\n * the `./auto-collected` relative-import interception to the SDK itself.\n */\nconst SDK_PATH_PATTERN = /[\\\\/](?:@openpolicy[\\\\/]sdk|packages[\\\\/]sdk)[\\\\/]/;\n\n/**\n * Matches the relative specifier the SDK uses for its own internal\n * `./auto-collected` import. Both the source form (`./auto-collected`) and\n * the published dist form (`./auto-collected.js`) need to be intercepted:\n * the former applies when consumers resolve the SDK via its workspace\n * source, the latter when resolving against `dist/` with the separate\n * `auto-collected.js` chunk.\n */\nconst AUTO_COLLECTED_SPECIFIER = /^\\.\\/auto-collected(?:\\.js)?$/;\n\n/**\n * Vite plugin that scans source files for `@openpolicy/sdk` `collecting()`\n * calls at the start of each build and inlines the discovered categories into\n * the SDK's `dataCollected` sentinel.\n *\n * Internally the plugin intercepts `@openpolicy/sdk`'s own relative import of\n * `./auto-collected` and redirects it to a virtual module whose body is a\n * literal `export const dataCollected = { ... }`. Because the replacement\n * becomes part of the consumer's own module graph, the scanned data survives\n * any downstream bundler boundary (e.g. nitro's SSR output), which a shared\n * module-level registry would not.\n */\nexport function autoCollect(options: AutoCollectOptions = {}): Plugin {\n\tconst srcDirOpt = options.srcDir ?? \"src\";\n\tconst extensions = options.extensions ?? [\".ts\", \".tsx\"];\n\tconst ignore = options.ignore ?? [];\n\tlet resolvedSrcDir: string;\n\tlet scanned: Record<string, string[]> = {};\n\n\tasync function scanAndMerge(): Promise<Record<string, string[]>> {\n\t\tconst files = await walkSources(resolvedSrcDir, extensions, ignore);\n\t\tconst merged: Record<string, string[]> = {};\n\t\tfor (const file of files) {\n\t\t\tlet code: string;\n\t\t\ttry {\n\t\t\t\tcode = await readFile(file, \"utf8\");\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst extracted = extractCollecting(file, code);\n\t\t\tfor (const [category, labels] of Object.entries(extracted)) {\n\t\t\t\tconst existing = merged[category] ?? [];\n\t\t\t\tconst seen = new Set(existing);\n\t\t\t\tfor (const label of labels) {\n\t\t\t\t\tif (!seen.has(label)) {\n\t\t\t\t\t\texisting.push(label);\n\t\t\t\t\t\tseen.add(label);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tmerged[category] = existing;\n\t\t\t}\n\t\t}\n\t\treturn merged;\n\t}\n\n\t/**\n\t * Returns true when `file` lives inside `resolvedSrcDir` and has one of\n\t * the tracked extensions. Used by the dev-server watcher to skip events\n\t * for unrelated files (configs, public assets, other packages, etc.).\n\t */\n\tfunction isTrackedSource(file: string): boolean {\n\t\tconst rel = relative(resolvedSrcDir, file);\n\t\tif (!rel || rel.startsWith(\"..\")) return false;\n\t\treturn extensions.some((ext) => file.endsWith(ext));\n\t}\n\n\t/**\n\t * Re-runs the scan and, if anything changed, invalidates the virtual\n\t * module and triggers a full page reload. A full reload is used because\n\t * `dataCollected` is spread into the policy config at module-evaluation\n\t * time and the result is captured by the React tree as a prop — there's\n\t * no clean way to hot-swap it in place.\n\t */\n\tasync function rescanAndRefresh(server: ViteDevServer): Promise<void> {\n\t\tconst next = await scanAndMerge();\n\t\tif (JSON.stringify(next) === JSON.stringify(scanned)) return;\n\t\tscanned = next;\n\t\tconst mod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_ID);\n\t\tif (mod) server.moduleGraph.invalidateModule(mod);\n\t\tserver.ws.send({ type: \"full-reload\" });\n\t}\n\n\treturn {\n\t\tname: \"openpolicy-auto-collect\",\n\t\tenforce: \"pre\",\n\t\tconfigResolved(config) {\n\t\t\tresolvedSrcDir = resolve(config.root, srcDirOpt);\n\t\t},\n\t\tasync buildStart() {\n\t\t\tscanned = await scanAndMerge();\n\t\t},\n\t\tconfigureServer(server) {\n\t\t\t// Make sure chokidar watches the whole src tree, not just files\n\t\t\t// already in the module graph. Without this, creating a brand-new\n\t\t\t// source file that nothing imports yet wouldn't fire a watcher\n\t\t\t// event — the very case we most need to re-scan on.\n\t\t\tserver.watcher.add(resolvedSrcDir);\n\n\t\t\tconst handler = async (file: string): Promise<void> => {\n\t\t\t\tif (!isTrackedSource(file)) return;\n\t\t\t\t// Surface errors via the logger but don't rethrow — an\n\t\t\t\t// unhandled rejection would crash the watcher process.\n\t\t\t\ttry {\n\t\t\t\t\tawait rescanAndRefresh(server);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tserver.config.logger.error(\n\t\t\t\t\t\t`[openpolicy-auto-collect] rescan failed: ${error}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tserver.watcher.on(\"change\", handler);\n\t\t\tserver.watcher.on(\"add\", handler);\n\t\t\tserver.watcher.on(\"unlink\", handler);\n\t\t},\n\t\tasync resolveId(source, importer, resolveOptions) {\n\t\t\tif (!importer || !AUTO_COLLECTED_SPECIFIER.test(source)) return null;\n\t\t\t// Defer to Vite's resolver first so we only redirect when the\n\t\t\t// relative specifier actually points inside @openpolicy/sdk.\n\t\t\tconst resolved = await this.resolve(source, importer, {\n\t\t\t\t...resolveOptions,\n\t\t\t\tskipSelf: true,\n\t\t\t});\n\t\t\tif (!resolved) return null;\n\t\t\tif (!SDK_PATH_PATTERN.test(resolved.id)) return null;\n\t\t\treturn RESOLVED_VIRTUAL_ID;\n\t\t},\n\t\tload(id) {\n\t\t\tif (id !== RESOLVED_VIRTUAL_ID) return null;\n\t\t\treturn `export const dataCollected = ${JSON.stringify(scanned)};\\n`;\n\t\t},\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/analyse.ts","../src/known-packages.ts","../src/scan.ts","../src/index.ts"],"sourcesContent":["import { parseSync } from \"oxc-parser\";\n\nconst SDK_SPECIFIER = \"@openpolicy/sdk\";\nconst COLLECTING_NAME = \"collecting\";\nconst THIRD_PARTY_NAME = \"thirdParty\";\n\ntype AnyNode = { type: string; [key: string]: unknown };\n\nexport type ThirdPartyEntry = {\n\tname: string;\n\tpurpose: string;\n\tpolicyUrl: string;\n};\n\nexport type ExtractResult = {\n\tdataCollected: Record<string, string[]>;\n\tthirdParties: ThirdPartyEntry[];\n};\n\n/**\n * Extract `collecting()` and `thirdParty()` call metadata from a single source file.\n *\n * Returns an `ExtractResult` with `dataCollected` (category → labels) and\n * `thirdParties` (array of third-party entries). Files with no matching calls\n * — or that fail to parse — return empty defaults.\n *\n * The analyser runs in two phases:\n * 1. Collect local names bound to `collecting` / `thirdParty` imported from\n * `@openpolicy/sdk` (handles renamed imports, skips type-only imports,\n * ignores look-alikes imported from other modules).\n * 2. Walk the program body and inspect every `CallExpression` whose callee\n * is one of those tracked local names.\n */\nexport function extractFromFile(filename: string, code: string): ExtractResult {\n\tconst empty: ExtractResult = { dataCollected: {}, thirdParties: [] };\n\tlet result: ReturnType<typeof parseSync>;\n\ttry {\n\t\tresult = parseSync(filename, code);\n\t} catch {\n\t\tconsole.warn(`[openpolicy-auto-collect] parse error in ${filename}`);\n\t\treturn empty;\n\t}\n\n\tif (result.errors.length > 0) {\n\t\t// Hard parse failures only — oxc reports recoverable errors but still\n\t\t// produces a usable AST, so we keep going and let the walker decide.\n\t\tconst fatal = result.errors.some((e) => e.severity === (\"Error\" as never));\n\t\tif (fatal) {\n\t\t\tconsole.warn(`[openpolicy-auto-collect] parse error in ${filename}`);\n\t\t\treturn empty;\n\t\t}\n\t}\n\n\tconst program = result.program as unknown as AnyNode;\n\tconst collectingNames = collectSdkBindings(program, COLLECTING_NAME);\n\tconst thirdPartyNames = collectSdkBindings(program, THIRD_PARTY_NAME);\n\tif (collectingNames.size === 0 && thirdPartyNames.size === 0) return empty;\n\n\tconst dataCollected: Record<string, string[]> = {};\n\tconst thirdParties: ThirdPartyEntry[] = [];\n\tconst seenThirdParties = new Set<string>();\n\n\twalk(program, (node) => {\n\t\tif (node.type !== \"CallExpression\") return;\n\t\tconst callee = node.callee as AnyNode | undefined;\n\t\tif (!callee || callee.type !== \"Identifier\") return;\n\t\tconst calleeName = callee.name as string;\n\t\tconst args = node.arguments as AnyNode[] | undefined;\n\n\t\tif (collectingNames.has(calleeName)) {\n\t\t\tif (!args || args.length < 3) return;\n\t\t\tconst category = extractStringLiteral(args[0]);\n\t\t\tif (category === null) return;\n\t\t\tconst labels = extractLabelKeys(args[2]);\n\t\t\tif (labels === null) return;\n\t\t\tconst existing = dataCollected[category] ?? [];\n\t\t\tconst seen = new Set(existing);\n\t\t\tfor (const label of labels) {\n\t\t\t\tif (!seen.has(label)) {\n\t\t\t\t\texisting.push(label);\n\t\t\t\t\tseen.add(label);\n\t\t\t\t}\n\t\t\t}\n\t\t\tdataCollected[category] = existing;\n\t\t} else if (thirdPartyNames.has(calleeName)) {\n\t\t\tif (!args || args.length < 3) return;\n\t\t\tconst name = extractStringLiteral(args[0]);\n\t\t\tif (name === null) return;\n\t\t\tconst purpose = extractStringLiteral(args[1]);\n\t\t\tif (purpose === null) return;\n\t\t\tconst policyUrl = extractStringLiteral(args[2]);\n\t\t\tif (policyUrl === null) return;\n\t\t\t// Deduplicate by name — first occurrence wins (files walked in sorted order)\n\t\t\tif (seenThirdParties.has(name)) return;\n\t\t\tseenThirdParties.add(name);\n\t\t\tthirdParties.push({ name, purpose, policyUrl });\n\t\t}\n\t});\n\n\treturn { dataCollected, thirdParties };\n}\n\n/**\n * Walk `ImportDeclaration` nodes and return the local names bound to the given\n * `exportName` imported from `@openpolicy/sdk`. Skips type-only imports and\n * specifiers whose imported name doesn't match.\n */\nfunction collectSdkBindings(program: AnyNode, exportName: string): Set<string> {\n\tconst names = new Set<string>();\n\tconst body = program.body as AnyNode[] | undefined;\n\tif (!body) return names;\n\tfor (const node of body) {\n\t\tif (node.type !== \"ImportDeclaration\") continue;\n\t\tif ((node.importKind as string | undefined) === \"type\") continue;\n\t\tconst source = node.source as AnyNode | undefined;\n\t\tif (!source || source.value !== SDK_SPECIFIER) continue;\n\t\tconst specifiers = node.specifiers as AnyNode[] | undefined;\n\t\tif (!specifiers) continue;\n\t\tfor (const spec of specifiers) {\n\t\t\tif (spec.type !== \"ImportSpecifier\") continue;\n\t\t\tif ((spec.importKind as string | undefined) === \"type\") continue;\n\t\t\tconst imported = spec.imported as AnyNode | undefined;\n\t\t\tif (!imported) continue;\n\t\t\t// ESTree allows either an Identifier (name) or a string Literal (value)\n\t\t\t// for the imported binding (e.g. `import { \"foo\" as bar }`).\n\t\t\tconst importedName =\n\t\t\t\timported.type === \"Identifier\"\n\t\t\t\t\t? (imported.name as string | undefined)\n\t\t\t\t\t: imported.type === \"Literal\"\n\t\t\t\t\t\t? typeof imported.value === \"string\"\n\t\t\t\t\t\t\t? imported.value\n\t\t\t\t\t\t\t: undefined\n\t\t\t\t\t\t: undefined;\n\t\t\tif (importedName !== exportName) continue;\n\t\t\tconst local = spec.local as AnyNode | undefined;\n\t\t\tif (!local || local.type !== \"Identifier\") continue;\n\t\t\tnames.add(local.name as string);\n\t\t}\n\t}\n\treturn names;\n}\n\n/**\n * If `node` is a string `Literal`, return its string value. Otherwise\n * return `null` so the caller silently skips the call.\n */\nfunction extractStringLiteral(node: AnyNode | undefined): string | null {\n\tif (!node) return null;\n\tif (node.type !== \"Literal\") return null;\n\tif (typeof node.value !== \"string\") return null;\n\treturn node.value;\n}\n\n/**\n * Extract the string values from a plain `{ fieldName: \"Human Label\" }`\n * object literal. Returns an array of label strings, deduped while\n * preserving insertion order. Returns `null` if the shape doesn't match.\n */\nfunction extractLabelKeys(node: AnyNode | undefined): string[] | null {\n\tif (!node || node.type !== \"ObjectExpression\") return null;\n\tconst properties = node.properties as AnyNode[] | undefined;\n\tif (!properties) return null;\n\n\tconst labels: string[] = [];\n\tconst seen = new Set<string>();\n\tfor (const prop of properties) {\n\t\tif (prop.type !== \"Property\") continue; // drop SpreadElement silently\n\t\tconst val = prop.value as AnyNode | undefined;\n\t\tif (!val || val.type !== \"Literal\" || typeof val.value !== \"string\")\n\t\t\tcontinue;\n\t\tif (seen.has(val.value)) continue;\n\t\tseen.add(val.value);\n\t\tlabels.push(val.value);\n\t}\n\treturn labels;\n}\n\n/**\n * Recursive AST walker. Visits every `AnyNode` (depth-first) reachable\n * through array / nested-object children and invokes `visit` on each.\n */\nfunction walk(node: AnyNode, visit: (node: AnyNode) => void): void {\n\tvisit(node);\n\tfor (const key of Object.keys(node)) {\n\t\tif (key === \"parent\") continue;\n\t\tconst value = node[key];\n\t\tif (Array.isArray(value)) {\n\t\t\tfor (const item of value) {\n\t\t\t\tif (item && typeof item === \"object\" && typeof item.type === \"string\") {\n\t\t\t\t\twalk(item as AnyNode, visit);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (\n\t\t\tvalue &&\n\t\t\ttypeof value === \"object\" &&\n\t\t\ttypeof (value as { type?: unknown }).type === \"string\"\n\t\t) {\n\t\t\twalk(value as AnyNode, visit);\n\t\t}\n\t}\n}\n","import type { ThirdPartyEntry } from \"./analyse\";\n\n/**\n * Registry of known npm packages mapped to their ThirdPartyEntry metadata.\n * Multiple package names can point to the same service — deduplication by\n * `ThirdPartyEntry.name` is handled at merge time in the caller.\n */\nexport const KNOWN_PACKAGES: ReadonlyMap<string, ThirdPartyEntry> = new Map([\n\t[\n\t\t\"stripe\",\n\t\t{\n\t\t\tname: \"Stripe\",\n\t\t\tpurpose: \"Payment processing\",\n\t\t\tpolicyUrl: \"https://stripe.com/privacy\",\n\t\t},\n\t],\n\t[\n\t\t\"@stripe/stripe-js\",\n\t\t{\n\t\t\tname: \"Stripe\",\n\t\t\tpurpose: \"Payment processing\",\n\t\t\tpolicyUrl: \"https://stripe.com/privacy\",\n\t\t},\n\t],\n\t[\n\t\t\"braintree\",\n\t\t{\n\t\t\tname: \"Braintree\",\n\t\t\tpurpose: \"Payment processing\",\n\t\t\tpolicyUrl:\n\t\t\t\t\"https://www.braintreepayments.com/legal/braintree-privacy-policy\",\n\t\t},\n\t],\n\t[\n\t\t\"@braintree/browser-drop-in\",\n\t\t{\n\t\t\tname: \"Braintree\",\n\t\t\tpurpose: \"Payment processing\",\n\t\t\tpolicyUrl:\n\t\t\t\t\"https://www.braintreepayments.com/legal/braintree-privacy-policy\",\n\t\t},\n\t],\n\t[\n\t\t\"@sentry/browser\",\n\t\t{\n\t\t\tname: \"Sentry\",\n\t\t\tpurpose: \"Error tracking\",\n\t\t\tpolicyUrl: \"https://sentry.io/privacy/\",\n\t\t},\n\t],\n\t[\n\t\t\"@sentry/node\",\n\t\t{\n\t\t\tname: \"Sentry\",\n\t\t\tpurpose: \"Error tracking\",\n\t\t\tpolicyUrl: \"https://sentry.io/privacy/\",\n\t\t},\n\t],\n\t[\n\t\t\"@sentry/nextjs\",\n\t\t{\n\t\t\tname: \"Sentry\",\n\t\t\tpurpose: \"Error tracking\",\n\t\t\tpolicyUrl: \"https://sentry.io/privacy/\",\n\t\t},\n\t],\n\t[\n\t\t\"@sentry/react\",\n\t\t{\n\t\t\tname: \"Sentry\",\n\t\t\tpurpose: \"Error tracking\",\n\t\t\tpolicyUrl: \"https://sentry.io/privacy/\",\n\t\t},\n\t],\n\t[\n\t\t\"@sentry/vue\",\n\t\t{\n\t\t\tname: \"Sentry\",\n\t\t\tpurpose: \"Error tracking\",\n\t\t\tpolicyUrl: \"https://sentry.io/privacy/\",\n\t\t},\n\t],\n\t[\n\t\t\"@datadog/browser-rum\",\n\t\t{\n\t\t\tname: \"Datadog\",\n\t\t\tpurpose: \"Monitoring\",\n\t\t\tpolicyUrl: \"https://www.datadoghq.com/legal/privacy/\",\n\t\t},\n\t],\n\t[\n\t\t\"dd-trace\",\n\t\t{\n\t\t\tname: \"Datadog\",\n\t\t\tpurpose: \"Monitoring\",\n\t\t\tpolicyUrl: \"https://www.datadoghq.com/legal/privacy/\",\n\t\t},\n\t],\n\t[\n\t\t\"posthog-js\",\n\t\t{\n\t\t\tname: \"PostHog\",\n\t\t\tpurpose: \"Product analytics\",\n\t\t\tpolicyUrl: \"https://posthog.com/privacy\",\n\t\t},\n\t],\n\t[\n\t\t\"posthog-node\",\n\t\t{\n\t\t\tname: \"PostHog\",\n\t\t\tpurpose: \"Product analytics\",\n\t\t\tpolicyUrl: \"https://posthog.com/privacy\",\n\t\t},\n\t],\n\t[\n\t\t\"mixpanel-browser\",\n\t\t{\n\t\t\tname: \"Mixpanel\",\n\t\t\tpurpose: \"Product analytics\",\n\t\t\tpolicyUrl: \"https://mixpanel.com/legal/privacy-policy/\",\n\t\t},\n\t],\n\t[\n\t\t\"@segment/analytics-next\",\n\t\t{\n\t\t\tname: \"Segment\",\n\t\t\tpurpose: \"Customer data platform\",\n\t\t\tpolicyUrl: \"https://www.twilio.com/en-us/legal/privacy\",\n\t\t},\n\t],\n\t[\n\t\t\"@amplitude/analytics-browser\",\n\t\t{\n\t\t\tname: \"Amplitude\",\n\t\t\tpurpose: \"Product analytics\",\n\t\t\tpolicyUrl: \"https://amplitude.com/privacy\",\n\t\t},\n\t],\n\t[\n\t\t\"amplitude-js\",\n\t\t{\n\t\t\tname: \"Amplitude\",\n\t\t\tpurpose: \"Product analytics\",\n\t\t\tpolicyUrl: \"https://amplitude.com/privacy\",\n\t\t},\n\t],\n\t[\n\t\t\"@vercel/analytics\",\n\t\t{\n\t\t\tname: \"Vercel Analytics\",\n\t\t\tpurpose: \"Web analytics\",\n\t\t\tpolicyUrl: \"https://vercel.com/legal/privacy-policy\",\n\t\t},\n\t],\n\t[\n\t\t\"plausible-tracker\",\n\t\t{\n\t\t\tname: \"Plausible\",\n\t\t\tpurpose: \"Web analytics\",\n\t\t\tpolicyUrl: \"https://plausible.io/privacy\",\n\t\t},\n\t],\n\t[\n\t\t\"logrocket\",\n\t\t{\n\t\t\tname: \"LogRocket\",\n\t\t\tpurpose: \"Session recording\",\n\t\t\tpolicyUrl: \"https://logrocket.com/privacy/\",\n\t\t},\n\t],\n\t[\n\t\t\"@hotjar/browser\",\n\t\t{\n\t\t\tname: \"Hotjar\",\n\t\t\tpurpose: \"Session recording\",\n\t\t\tpolicyUrl: \"https://www.hotjar.com/legal/policies/privacy/\",\n\t\t},\n\t],\n\t[\n\t\t\"resend\",\n\t\t{\n\t\t\tname: \"Resend\",\n\t\t\tpurpose: \"Transactional email\",\n\t\t\tpolicyUrl: \"https://resend.com/legal/privacy-policy\",\n\t\t},\n\t],\n\t[\n\t\t\"@sendgrid/mail\",\n\t\t{\n\t\t\tname: \"SendGrid\",\n\t\t\tpurpose: \"Transactional email\",\n\t\t\tpolicyUrl: \"https://www.twilio.com/en-us/legal/privacy\",\n\t\t},\n\t],\n\t[\n\t\t\"intercom-client\",\n\t\t{\n\t\t\tname: \"Intercom\",\n\t\t\tpurpose: \"Customer messaging\",\n\t\t\tpolicyUrl: \"https://www.intercom.com/legal/privacy\",\n\t\t},\n\t],\n\t[\n\t\t\"@intercom/messenger-js-sdk\",\n\t\t{\n\t\t\tname: \"Intercom\",\n\t\t\tpurpose: \"Customer messaging\",\n\t\t\tpolicyUrl: \"https://www.intercom.com/legal/privacy\",\n\t\t},\n\t],\n]);\n","import type { Dirent } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { extname, join } from \"node:path\";\n\nconst DEFAULT_IGNORES: ReadonlySet<string> = new Set([\n\t\"node_modules\",\n\t\"dist\",\n\t\".git\",\n\t\".next\",\n\t\".output\",\n\t\".svelte-kit\",\n\t\".cache\",\n]);\n\n/**\n * Recursively walks `root`, returning absolute paths of every regular file\n * whose extension is in `extensions`. Directories whose basename appears in\n * the built-in ignore list (or the extra `ignore` argument) are skipped\n * entirely.\n *\n * Missing roots resolve to an empty array — the plugin must not throw if the\n * user's `srcDir` hasn't been created yet.\n */\nexport async function walkSources(\n\troot: string,\n\textensions: readonly string[],\n\tignore: readonly string[] = [],\n): Promise<string[]> {\n\tconst ignored = new Set<string>([...DEFAULT_IGNORES, ...ignore]);\n\tconst exts = new Set(extensions);\n\tconst results: string[] = [];\n\n\tasync function walk(dir: string): Promise<void> {\n\t\tlet entries: Dirent[];\n\t\ttry {\n\t\t\tentries = (await readdir(dir, { withFileTypes: true })) as Dirent[];\n\t\t} catch (err) {\n\t\t\tconst code = (err as NodeJS.ErrnoException).code;\n\t\t\tif (code === \"ENOENT\" || code === \"ENOTDIR\") return;\n\t\t\tthrow err;\n\t\t}\n\t\tfor (const entry of entries) {\n\t\t\tif (ignored.has(entry.name)) continue;\n\t\t\tconst full = join(dir, entry.name);\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\tawait walk(full);\n\t\t\t} else if (entry.isFile() && exts.has(extname(entry.name))) {\n\t\t\t\tresults.push(full);\n\t\t\t}\n\t\t}\n\t}\n\n\tawait walk(root);\n\tresults.sort();\n\treturn results;\n}\n","import { readFile } from \"node:fs/promises\";\nimport { relative, resolve } from \"node:path\";\nimport type { Plugin, ViteDevServer } from \"vite\";\nimport { extractFromFile, type ThirdPartyEntry } from \"./analyse\";\nimport { KNOWN_PACKAGES } from \"./known-packages\";\nimport { walkSources } from \"./scan\";\n\nexport type AutoCollectOptions = {\n\t/**\n\t * Directory walked for `collecting()` calls. Resolved relative to the\n\t * Vite project root. Defaults to `\"src\"`.\n\t */\n\tsrcDir?: string;\n\t/**\n\t * File extensions scanned. Defaults to `[\".ts\", \".tsx\"]`.\n\t */\n\textensions?: string[];\n\t/**\n\t * Extra directory names skipped during the walk. Appended to the built-in\n\t * defaults (`node_modules`, `dist`, `.git`, `.next`, `.output`,\n\t * `.svelte-kit`, `.cache`).\n\t */\n\tignore?: string[];\n\n\tthirdParties?: {\n\t\tusePackageJson?: boolean;\n\t};\n};\n\n/**\n * Marker returned from `resolveId` so `load` can recognise a hit. The leading\n * NUL prefix is the Rollup/Vite convention for virtual IDs so other plugins\n * leave it alone.\n */\nconst RESOLVED_VIRTUAL_ID = \"\\0virtual:openpolicy/auto-collected\";\n\n/**\n * Matches any path that lives inside the `@openpolicy/sdk` package, whether\n * it's resolved via a workspace symlink (`.../packages/sdk/...`) or a\n * published `node_modules` install (`.../@openpolicy/sdk/...`). Used to scope\n * the `./auto-collected` relative-import interception to the SDK itself.\n */\nconst SDK_PATH_PATTERN = /[\\\\/](?:@openpolicy[\\\\/]sdk|packages[\\\\/]sdk)[\\\\/]/;\n\n/**\n * Matches the relative specifier the SDK uses for its own internal\n * `./auto-collected` import. Both the source form (`./auto-collected`) and\n * the published dist form (`./auto-collected.js`) need to be intercepted:\n * the former applies when consumers resolve the SDK via its workspace\n * source, the latter when resolving against `dist/` with the separate\n * `auto-collected.js` chunk.\n */\nconst AUTO_COLLECTED_SPECIFIER = /^\\.\\/auto-collected(?:\\.js)?$/;\n\n/**\n * Vite plugin that scans source files for `@openpolicy/sdk` `collecting()`\n * calls at the start of each build and inlines the discovered categories into\n * the SDK's `dataCollected` sentinel.\n *\n * Internally the plugin intercepts `@openpolicy/sdk`'s own relative import of\n * `./auto-collected` and redirects it to a virtual module whose body is a\n * literal `export const dataCollected = { ... }`. Because the replacement\n * becomes part of the consumer's own module graph, the scanned data survives\n * any downstream bundler boundary (e.g. nitro's SSR output), which a shared\n * module-level registry would not.\n */\nexport function autoCollect(options: AutoCollectOptions = {}): Plugin {\n\tconst srcDirOpt = options.srcDir ?? \"src\";\n\tconst extensions = options.extensions ?? [\".ts\", \".tsx\"];\n\tconst ignore = options.ignore ?? [];\n\tconst usePackageJsonOpt = options.thirdParties?.usePackageJson ?? false;\n\tlet resolvedRoot: string;\n\tlet resolvedSrcDir: string;\n\tlet scanned: {\n\t\tdataCollected: Record<string, string[]>;\n\t\tthirdParties: ThirdPartyEntry[];\n\t} = { dataCollected: {}, thirdParties: [] };\n\n\tasync function detectFromPackageJson(\n\t\troot: string,\n\t): Promise<ThirdPartyEntry[]> {\n\t\tlet raw: string;\n\t\ttry {\n\t\t\traw = await readFile(resolve(root, \"package.json\"), \"utf8\");\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t\tlet pkg: {\n\t\t\tdependencies?: Record<string, string>;\n\t\t\tdevDependencies?: Record<string, string>;\n\t\t};\n\t\ttry {\n\t\t\tpkg = JSON.parse(raw) as typeof pkg;\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t\tconst allDeps = {\n\t\t\t...pkg.dependencies,\n\t\t\t...pkg.devDependencies,\n\t\t};\n\t\tconst entries: ThirdPartyEntry[] = [];\n\t\tconst seenNames = new Set<string>();\n\t\tfor (const pkgName of Object.keys(allDeps)) {\n\t\t\tconst entry = KNOWN_PACKAGES.get(pkgName);\n\t\t\tif (entry && !seenNames.has(entry.name)) {\n\t\t\t\tseenNames.add(entry.name);\n\t\t\t\tentries.push(entry);\n\t\t\t}\n\t\t}\n\t\treturn entries;\n\t}\n\n\tasync function scanAndMerge(): Promise<typeof scanned> {\n\t\tconst files = await walkSources(resolvedSrcDir, extensions, ignore);\n\t\tconst mergedData: Record<string, string[]> = {};\n\t\tconst mergedParties: ThirdPartyEntry[] = [];\n\t\tconst seenParties = new Set<string>();\n\t\tfor (const file of files) {\n\t\t\tlet code: string;\n\t\t\ttry {\n\t\t\t\tcode = await readFile(file, \"utf8\");\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst extracted = extractFromFile(file, code);\n\t\t\tfor (const [category, labels] of Object.entries(\n\t\t\t\textracted.dataCollected,\n\t\t\t)) {\n\t\t\t\tconst existing = mergedData[category] ?? [];\n\t\t\t\tconst seen = new Set(existing);\n\t\t\t\tfor (const label of labels) {\n\t\t\t\t\tif (!seen.has(label)) {\n\t\t\t\t\t\texisting.push(label);\n\t\t\t\t\t\tseen.add(label);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tmergedData[category] = existing;\n\t\t\t}\n\t\t\tfor (const entry of extracted.thirdParties) {\n\t\t\t\tif (!seenParties.has(entry.name)) {\n\t\t\t\t\tseenParties.add(entry.name);\n\t\t\t\t\tmergedParties.push(entry);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (usePackageJsonOpt) {\n\t\t\tconst pkgEntries = await detectFromPackageJson(resolvedRoot);\n\t\t\tfor (const entry of pkgEntries) {\n\t\t\t\tif (!seenParties.has(entry.name)) {\n\t\t\t\t\tseenParties.add(entry.name);\n\t\t\t\t\tmergedParties.push(entry);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn { dataCollected: mergedData, thirdParties: mergedParties };\n\t}\n\n\t/**\n\t * Returns true when `file` lives inside `resolvedSrcDir` and has one of\n\t * the tracked extensions. Used by the dev-server watcher to skip events\n\t * for unrelated files (configs, public assets, other packages, etc.).\n\t */\n\tfunction isTrackedSource(file: string): boolean {\n\t\tconst rel = relative(resolvedSrcDir, file);\n\t\tif (!rel || rel.startsWith(\"..\")) return false;\n\t\treturn extensions.some((ext) => file.endsWith(ext));\n\t}\n\n\t/**\n\t * Re-runs the scan and, if anything changed, invalidates the virtual\n\t * module and triggers a full page reload. A full reload is used because\n\t * `dataCollected` is spread into the policy config at module-evaluation\n\t * time and the result is captured by the React tree as a prop — there's\n\t * no clean way to hot-swap it in place.\n\t */\n\tasync function rescanAndRefresh(server: ViteDevServer): Promise<void> {\n\t\tconst next = await scanAndMerge();\n\t\tif (JSON.stringify(next) === JSON.stringify(scanned)) return;\n\t\tscanned = next;\n\t\tconst mod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_ID);\n\t\tif (mod) server.moduleGraph.invalidateModule(mod);\n\t\tserver.ws.send({ type: \"full-reload\" });\n\t}\n\n\treturn {\n\t\tname: \"openpolicy-auto-collect\",\n\t\tenforce: \"pre\",\n\t\tconfigResolved(config) {\n\t\t\tresolvedRoot = config.root;\n\t\t\tresolvedSrcDir = resolve(config.root, srcDirOpt);\n\t\t},\n\t\tasync buildStart() {\n\t\t\tscanned = await scanAndMerge();\n\t\t},\n\t\tconfigureServer(server) {\n\t\t\t// Make sure chokidar watches the whole src tree, not just files\n\t\t\t// already in the module graph. Without this, creating a brand-new\n\t\t\t// source file that nothing imports yet wouldn't fire a watcher\n\t\t\t// event — the very case we most need to re-scan on.\n\t\t\tserver.watcher.add(resolvedSrcDir);\n\n\t\t\tconst handler = async (file: string): Promise<void> => {\n\t\t\t\tif (!isTrackedSource(file)) return;\n\t\t\t\t// Surface errors via the logger but don't rethrow — an\n\t\t\t\t// unhandled rejection would crash the watcher process.\n\t\t\t\ttry {\n\t\t\t\t\tawait rescanAndRefresh(server);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tserver.config.logger.error(\n\t\t\t\t\t\t`[openpolicy-auto-collect] rescan failed: ${error}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tserver.watcher.on(\"change\", handler);\n\t\t\tserver.watcher.on(\"add\", handler);\n\t\t\tserver.watcher.on(\"unlink\", handler);\n\t\t},\n\t\tasync resolveId(source, importer, resolveOptions) {\n\t\t\tif (!importer || !AUTO_COLLECTED_SPECIFIER.test(source)) return null;\n\t\t\t// Defer to Vite's resolver first so we only redirect when the\n\t\t\t// relative specifier actually points inside @openpolicy/sdk.\n\t\t\tconst resolved = await this.resolve(source, importer, {\n\t\t\t\t...resolveOptions,\n\t\t\t\tskipSelf: true,\n\t\t\t});\n\t\t\tif (!resolved) return null;\n\t\t\tif (!SDK_PATH_PATTERN.test(resolved.id)) return null;\n\t\t\treturn RESOLVED_VIRTUAL_ID;\n\t\t},\n\t\tload(id) {\n\t\t\tif (id !== RESOLVED_VIRTUAL_ID) return null;\n\t\t\treturn (\n\t\t\t\t`export const dataCollected = ${JSON.stringify(\n\t\t\t\t\tscanned.dataCollected,\n\t\t\t\t)};\\n` +\n\t\t\t\t`export const thirdParties = ${JSON.stringify(scanned.thirdParties)};\\n`\n\t\t\t);\n\t\t},\n\t};\n}\n"],"mappings":"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,CACC,gBACA;EACC,MAAM;EACN,SAAS;EACT,WAAW;EACX,CACD;CACD,CACC,oBACA;EACC,MAAM;EACN,SAAS;EACT,WAAW;EACX,CACD;CACD,CACC,2BACA;EACC,MAAM;EACN,SAAS;EACT,WAAW;EACX,CACD;CACD,CACC,gCACA;EACC,MAAM;EACN,SAAS;EACT,WAAW;EACX,CACD;CACD,CACC,gBACA;EACC,MAAM;EACN,SAAS;EACT,WAAW;EACX,CACD;CACD,CACC,qBACA;EACC,MAAM;EACN,SAAS;EACT,WAAW;EACX,CACD;CACD,CACC,qBACA;EACC,MAAM;EACN,SAAS;EACT,WAAW;EACX,CACD;CACD,CACC,aACA;EACC,MAAM;EACN,SAAS;EACT,WAAW;EACX,CACD;CACD,CACC,mBACA;EACC,MAAM;EACN,SAAS;EACT,WAAW;EACX,CACD;CACD,CACC,UACA;EACC,MAAM;EACN,SAAS;EACT,WAAW;EACX,CACD;CACD,CACC,kBACA;EACC,MAAM;EACN,SAAS;EACT,WAAW;EACX,CACD;CACD,CACC,mBACA;EACC,MAAM;EACN,SAAS;EACT,WAAW;EACX,CACD;CACD,CACC,8BACA;EACC,MAAM;EACN,SAAS;EACT,WAAW;EACX,CACD;CACD,CAAC;;;AC9MF,MAAM,kBAAuC,IAAI,IAAI;CACpD;CACA;CACA;CACA;CACA;CACA;CACA;CACA,CAAC;;;;;;;;;;AAWF,eAAsB,YACrB,MACA,YACA,SAA4B,EAAE,EACV;CACpB,MAAM,UAAU,IAAI,IAAY,CAAC,GAAG,iBAAiB,GAAG,OAAO,CAAC;CAChE,MAAM,OAAO,IAAI,IAAI,WAAW;CAChC,MAAM,UAAoB,EAAE;CAE5B,eAAe,KAAK,KAA4B;EAC/C,IAAI;AACJ,MAAI;AACH,aAAW,MAAM,QAAQ,KAAK,EAAE,eAAe,MAAM,CAAC;WAC9C,KAAK;GACb,MAAM,OAAQ,IAA8B;AAC5C,OAAI,SAAS,YAAY,SAAS,UAAW;AAC7C,SAAM;;AAEP,OAAK,MAAM,SAAS,SAAS;AAC5B,OAAI,QAAQ,IAAI,MAAM,KAAK,CAAE;GAC7B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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpolicy/vite-auto-collect",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Vite plugin that scans source files for @openpolicy/sdk collecting() calls and populates autoCollected() at build time",
|
|
6
6
|
"license": "GPL-3.0-only",
|