@ttt-productions/repo-guards 0.1.26 → 0.1.28

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.
@@ -13,4 +13,9 @@ export interface FirestoreRuntimeInUiFacts {
13
13
  }
14
14
  export declare function firestoreRuntimeImports(file: FileView): Hit[];
15
15
  export declare const firestoreRuntimeInUiGuard: GenericGuardDefinition<FirestoreRuntimeInUiFacts>;
16
+ export interface FirestoreClientWritesFacts {
17
+ sanctioned: readonly Reviewed[];
18
+ }
19
+ export declare function firestoreClientWriteHits(file: FileView): Hit[];
20
+ export declare const firestoreClientWritesGuard: GenericGuardDefinition<FirestoreClientWritesFacts>;
16
21
  //# sourceMappingURL=client-guards.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client-guards.d.ts","sourceRoot":"","sources":["../../../src/modules/firestore/client-guards.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAEzE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAqB,KAAK,GAAG,EAAE,MAAM,wBAAwB,CAAC;AASrE,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,QAAQ,GAAG,GAAG,EAAE,CA4B1D;AAED,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,SAAS,QAAQ,EAAE,CAAC;CAChC;AAED,eAAO,MAAM,qBAAqB,EAAE,sBAAsB,CAAC,qBAAqB,CAwC/E,CAAC;AAEF,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7B,UAAU,EAAE,SAAS,QAAQ,EAAE,CAAC;CACjC;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,QAAQ,GAAG,GAAG,EAAE,CAoB7D;AAED,eAAO,MAAM,yBAAyB,EAAE,sBAAsB,CAAC,yBAAyB,CA+CvF,CAAC"}
1
+ {"version":3,"file":"client-guards.d.ts","sourceRoot":"","sources":["../../../src/modules/firestore/client-guards.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAEzE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAqB,KAAK,GAAG,EAAE,MAAM,wBAAwB,CAAC;AASrE,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,QAAQ,GAAG,GAAG,EAAE,CA4B1D;AAED,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,SAAS,QAAQ,EAAE,CAAC;CAChC;AAED,eAAO,MAAM,qBAAqB,EAAE,sBAAsB,CAAC,qBAAqB,CAwC/E,CAAC;AAEF,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7B,UAAU,EAAE,SAAS,QAAQ,EAAE,CAAC;CACjC;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,QAAQ,GAAG,GAAG,EAAE,CAoB7D;AAED,eAAO,MAAM,yBAAyB,EAAE,sBAAsB,CAAC,yBAAyB,CA+CvF,CAAC;AAWF,MAAM,WAAW,0BAA0B;IACzC,UAAU,EAAE,SAAS,QAAQ,EAAE,CAAC;CACjC;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,QAAQ,GAAG,GAAG,EAAE,CAwB9D;AAED,eAAO,MAAM,0BAA0B,EAAE,sBAAsB,CAAC,0BAA0B,CAoCzF,CAAC"}
@@ -153,4 +153,73 @@ export const firestoreRuntimeInUiGuard = {
153
153
  ];
154
154
  },
155
155
  };
156
+ const CLIENT_WRITES = new Set([
157
+ 'setDoc',
158
+ 'updateDoc',
159
+ 'deleteDoc',
160
+ 'addDoc',
161
+ 'writeBatch',
162
+ 'runTransaction',
163
+ ]);
164
+ export function firestoreClientWriteHits(file) {
165
+ const source = file.sourceFile;
166
+ const hits = [];
167
+ const namespaces = new Set();
168
+ for (const site of moduleImports(source, ['import', 'export'])) {
169
+ if (!FIRESTORE_SDK.test(site.spec) || site.typeOnly)
170
+ continue;
171
+ if (site.namespace)
172
+ namespaces.add(site.namespace);
173
+ for (const name of site.named)
174
+ if (!name.typeOnly && CLIENT_WRITES.has(name.imported))
175
+ hits.push({
176
+ index: site.node.getStart(source),
177
+ detail: `${name.imported} from '${site.spec}'`,
178
+ });
179
+ }
180
+ forEachNode(source, (node) => {
181
+ if (ts.isPropertyAccessExpression(node) &&
182
+ ts.isIdentifier(node.expression) &&
183
+ namespaces.has(node.expression.text) &&
184
+ CLIENT_WRITES.has(node.name.text))
185
+ hits.push({ index: node.getStart(source), detail: node.getText(source) });
186
+ });
187
+ return hits;
188
+ }
189
+ export const firestoreClientWritesGuard = {
190
+ id: 'firestoreClientWrites',
191
+ absentWhen: 'the app stores nothing in Firestore',
192
+ factProblems(facts) {
193
+ const problems = [];
194
+ reviewedProblems(facts?.sanctioned, 'sanctioned', problems);
195
+ return problems;
196
+ },
197
+ build(facts) {
198
+ return [
199
+ reviewedAllowlist({
200
+ title: 'the client never writes Firestore',
201
+ description: 'Every write goes through a callable that checks it on the server; a client SDK write skips that check and ' +
202
+ 'every rule the callable enforces. Flagged: a runtime import of a Firestore write function (' +
203
+ `${[...CLIENT_WRITES].join(', ')}) from the Firestore SDK, or a call to one through a namespace import, in ` +
204
+ "the app's client source. Fix: call the callable that owns the write. A sanctioned site is a reviewed entry " +
205
+ 'with its why; one that no longer writes is stale.',
206
+ scope: APP_SOURCE,
207
+ match: firestoreClientWriteHits,
208
+ allowlist: facts.sanctioned,
209
+ samples: {
210
+ flags: [
211
+ "import { doc, setDoc } from 'firebase/firestore';",
212
+ "import { writeBatch as batchOf } from 'firebase/firestore';",
213
+ "import * as fs from 'firebase/firestore';\nawait fs.updateDoc(ref, patch);",
214
+ ],
215
+ passes: [
216
+ "import { doc, getDoc, onSnapshot } from 'firebase/firestore';",
217
+ "import { type WriteBatch } from 'firebase/firestore';",
218
+ 'await callable({ id });',
219
+ ],
220
+ },
221
+ }),
222
+ ];
223
+ },
224
+ };
156
225
  //# sourceMappingURL=client-guards.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"client-guards.js","sourceRoot":"","sources":["../../../src/modules/firestore/client-guards.ts"],"names":[],"mappings":"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","sourcesContent":["import ts from 'typescript';\nimport type { GenericGuardDefinition } from '../../node/suite/config.js';\nimport type { Scope } from '../../node/reader.js';\nimport type { Reviewed } from '../../node/layout.js';\nimport type { FileView } from '../../reader/view.js';\nimport { reviewedAllowlist, type Hit } from '../../shapes/shapes.js';\nimport { forEachNode } from '../../support/ast.js';\nimport { namesProblem, reviewedProblems } from '../../support/facts.js';\nimport { moduleImports } from '../../support/imports.js';\nimport { isUnder } from '../../support/text.js';\n\nconst FIRESTORE_SDK = /^firebase\\/firestore(?:\\/|$)/;\nconst APP_SOURCE: Scope = { roots: ['app'], kinds: ['code'], tests: false };\n\nexport function liveSubscriptionHits(file: FileView): Hit[] {\n const source = file.sourceFile;\n const snapshotNames = new Set(['onSnapshot']);\n const namespaces = new Set<string>();\n for (const site of moduleImports(source, ['import'])) {\n if (!site.topLevel || !FIRESTORE_SDK.test(site.spec)) continue;\n if (site.namespace) namespaces.add(site.namespace);\n for (const name of site.named)\n if (name.imported === 'onSnapshot') snapshotNames.add(name.local);\n }\n const hits: Hit[] = [];\n const at = (node: ts.Node, detail: string) => hits.push({ index: node.getStart(source), detail });\n forEachNode(source, (node) => {\n if (ts.isPropertyAssignment(node) && ts.isObjectLiteralExpression(node.parent)) {\n const key =\n ts.isIdentifier(node.name) || ts.isStringLiteral(node.name) ? node.name.text : null;\n if (key === 'subscribe' && node.initializer.kind !== ts.SyntaxKind.FalseKeyword)\n at(node, `subscribe: ${node.initializer.getText(source)}`);\n } else if (ts.isShorthandPropertyAssignment(node) && node.name.text === 'subscribe') {\n at(node, '{ subscribe }');\n } else if (ts.isCallExpression(node)) {\n const callee = node.expression;\n if (ts.isIdentifier(callee) && snapshotNames.has(callee.text)) at(node, `${callee.text}(…)`);\n else if (ts.isPropertyAccessExpression(callee) && callee.name.text === 'onSnapshot')\n at(node, `${callee.getText(source)}(…)`);\n }\n });\n return hits;\n}\n\nexport interface LiveSubscriptionFacts {\n allowlist: readonly Reviewed[];\n}\n\nexport const liveSubscriptionGuard: GenericGuardDefinition<LiveSubscriptionFacts> = {\n id: 'firestoreLiveSubscriptions',\n absentWhen: 'the app stores nothing in Firestore',\n factProblems(facts) {\n const problems: string[] = [];\n reviewedProblems(facts?.allowlist, 'allowlist', problems);\n return problems;\n },\n build(facts) {\n return [\n reviewedAllowlist({\n title: 'every live Firestore subscription is on the reviewed list',\n description:\n 'A live read bills a document read on every change for as long as it is mounted, so it is a reviewed exception: ' +\n 'a file that declares live mode — a subscribe option set to anything but a literal false, the { subscribe } ' +\n \"shorthand, or an onSnapshot call under any name the file bound the Firestore SDK's to — is on the reviewed \" +\n 'list with why it needs real-time delivery. Fix: read once and keep it fresh through the cache, or add the ' +\n 'reviewed entry. An entry whose file no longer subscribes is stale.',\n scope: APP_SOURCE,\n match: liveSubscriptionHits,\n allowlist: facts.allowlist,\n samples: {\n flags: [\n 'useFirestoreDoc({ docPath, queryKey, subscribe: true });',\n 'useBatchFirestoreDocs({ ids, subscribe: isLive });',\n 'useFirestoreCollection({ collectionPath, subscribe, enabled });',\n \"import { doc, onSnapshot as listen } from 'firebase/firestore';\\nconst stop = listen(doc(db, p), render);\",\n \"import * as fs from 'firebase/firestore';\\nconst stop = fs.onSnapshot(ref, handle);\",\n ],\n passes: [\n \"useBatchFirestoreDocs({ db, collectionPath, ids, transport: 'get' });\",\n 'useFirestoreDoc({ docPath, queryKey, subscribe: false });',\n 'const unsubscribe = cache.subscribe((event) => handle(event));',\n 'return useSyncExternalStore(subscribe, readValue, getServerValue);',\n '// subscribe: true is a reviewed exception\\nconst x = 1;',\n ],\n },\n }),\n ];\n },\n};\n\nexport interface FirestoreRuntimeInUiFacts {\n uiFolders: readonly string[];\n sanctioned: readonly Reviewed[];\n}\n\nexport function firestoreRuntimeImports(file: FileView): Hit[] {\n const source = file.sourceFile;\n const hits: Hit[] = [];\n for (const site of moduleImports(source, ['import', 'export', 'dynamic'])) {\n if (!FIRESTORE_SDK.test(site.spec)) continue;\n const at = site.node.getStart(source);\n if (site.kind === 'import') {\n const typeOnly =\n !site.sideEffect &&\n (site.typeOnly ||\n (!site.defaultName &&\n !site.namespace &&\n site.named.length > 0 &&\n site.named.every((n) => n.typeOnly)));\n if (!typeOnly) hits.push({ index: at, detail: `import from '${site.spec}'` });\n } else if (site.kind === 'export') {\n if (!site.typeOnly) hits.push({ index: at, detail: `re-export from '${site.spec}'` });\n } else hits.push({ index: at, detail: `import('${site.spec}')` });\n }\n return hits;\n}\n\nexport const firestoreRuntimeInUiGuard: GenericGuardDefinition<FirestoreRuntimeInUiFacts> = {\n id: 'firestoreRuntimeInUi',\n absentWhen: 'the app stores nothing in Firestore',\n factProblems(facts) {\n const problems: string[] = [];\n namesProblem(facts?.uiFolders, 'uiFolders', problems);\n reviewedProblems(facts?.sanctioned, 'sanctioned', problems);\n return problems;\n },\n build(facts) {\n const inUi = (path: string) => facts.uiFolders.some((folder) => isUnder(path, folder));\n return [\n reviewedAllowlist({\n title: 'no component or route imports the Firestore runtime',\n description:\n 'A component or route that reaches into Firestore bypasses the cache, the query keys, and every server check ' +\n 'at once. Flagged: a runtime import, re-export, or dynamic import of the Firestore SDK (a type-only import ' +\n 'passes) in the UI folders. Fix: read through a hook, write through a callable. A sanctioned root provider is ' +\n 'a reviewed entry with its why; one that no longer imports the runtime is stale.',\n scope: APP_SOURCE,\n match: (file) => (inUi(file.path) ? firestoreRuntimeImports(file) : []),\n allowlist: facts.sanctioned,\n samples: {\n flags: [\n {\n path: `${facts.uiFolders[0]}/probe.tsx`,\n text: \"import { doc, getDoc } from 'firebase/firestore';\",\n },\n {\n path: `${facts.uiFolders[0]}/probe.tsx`,\n text: 'import {\\n updateDoc,\\n} from \"firebase/firestore\";',\n },\n ],\n passes: [\n {\n path: `${facts.uiFolders[0]}/typed.tsx`,\n text: \"import type { Timestamp } from 'firebase/firestore';\",\n },\n {\n path: `${facts.uiFolders[0]}/typed.tsx`,\n text: \"import { type Timestamp } from 'firebase/firestore';\",\n },\n ],\n },\n }),\n ];\n },\n};\n"]}
1
+ {"version":3,"file":"client-guards.js","sourceRoot":"","sources":["../../../src/modules/firestore/client-guards.ts"],"names":[],"mappings":"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","sourcesContent":["import ts from 'typescript';\nimport type { GenericGuardDefinition } from '../../node/suite/config.js';\nimport type { Scope } from '../../node/reader.js';\nimport type { Reviewed } from '../../node/layout.js';\nimport type { FileView } from '../../reader/view.js';\nimport { reviewedAllowlist, type Hit } from '../../shapes/shapes.js';\nimport { forEachNode } from '../../support/ast.js';\nimport { namesProblem, reviewedProblems } from '../../support/facts.js';\nimport { moduleImports } from '../../support/imports.js';\nimport { isUnder } from '../../support/text.js';\n\nconst FIRESTORE_SDK = /^firebase\\/firestore(?:\\/|$)/;\nconst APP_SOURCE: Scope = { roots: ['app'], kinds: ['code'], tests: false };\n\nexport function liveSubscriptionHits(file: FileView): Hit[] {\n const source = file.sourceFile;\n const snapshotNames = new Set(['onSnapshot']);\n const namespaces = new Set<string>();\n for (const site of moduleImports(source, ['import'])) {\n if (!site.topLevel || !FIRESTORE_SDK.test(site.spec)) continue;\n if (site.namespace) namespaces.add(site.namespace);\n for (const name of site.named)\n if (name.imported === 'onSnapshot') snapshotNames.add(name.local);\n }\n const hits: Hit[] = [];\n const at = (node: ts.Node, detail: string) => hits.push({ index: node.getStart(source), detail });\n forEachNode(source, (node) => {\n if (ts.isPropertyAssignment(node) && ts.isObjectLiteralExpression(node.parent)) {\n const key =\n ts.isIdentifier(node.name) || ts.isStringLiteral(node.name) ? node.name.text : null;\n if (key === 'subscribe' && node.initializer.kind !== ts.SyntaxKind.FalseKeyword)\n at(node, `subscribe: ${node.initializer.getText(source)}`);\n } else if (ts.isShorthandPropertyAssignment(node) && node.name.text === 'subscribe') {\n at(node, '{ subscribe }');\n } else if (ts.isCallExpression(node)) {\n const callee = node.expression;\n if (ts.isIdentifier(callee) && snapshotNames.has(callee.text)) at(node, `${callee.text}(…)`);\n else if (ts.isPropertyAccessExpression(callee) && callee.name.text === 'onSnapshot')\n at(node, `${callee.getText(source)}(…)`);\n }\n });\n return hits;\n}\n\nexport interface LiveSubscriptionFacts {\n allowlist: readonly Reviewed[];\n}\n\nexport const liveSubscriptionGuard: GenericGuardDefinition<LiveSubscriptionFacts> = {\n id: 'firestoreLiveSubscriptions',\n absentWhen: 'the app stores nothing in Firestore',\n factProblems(facts) {\n const problems: string[] = [];\n reviewedProblems(facts?.allowlist, 'allowlist', problems);\n return problems;\n },\n build(facts) {\n return [\n reviewedAllowlist({\n title: 'every live Firestore subscription is on the reviewed list',\n description:\n 'A live read bills a document read on every change for as long as it is mounted, so it is a reviewed exception: ' +\n 'a file that declares live mode — a subscribe option set to anything but a literal false, the { subscribe } ' +\n \"shorthand, or an onSnapshot call under any name the file bound the Firestore SDK's to — is on the reviewed \" +\n 'list with why it needs real-time delivery. Fix: read once and keep it fresh through the cache, or add the ' +\n 'reviewed entry. An entry whose file no longer subscribes is stale.',\n scope: APP_SOURCE,\n match: liveSubscriptionHits,\n allowlist: facts.allowlist,\n samples: {\n flags: [\n 'useFirestoreDoc({ docPath, queryKey, subscribe: true });',\n 'useBatchFirestoreDocs({ ids, subscribe: isLive });',\n 'useFirestoreCollection({ collectionPath, subscribe, enabled });',\n \"import { doc, onSnapshot as listen } from 'firebase/firestore';\\nconst stop = listen(doc(db, p), render);\",\n \"import * as fs from 'firebase/firestore';\\nconst stop = fs.onSnapshot(ref, handle);\",\n ],\n passes: [\n \"useBatchFirestoreDocs({ db, collectionPath, ids, transport: 'get' });\",\n 'useFirestoreDoc({ docPath, queryKey, subscribe: false });',\n 'const unsubscribe = cache.subscribe((event) => handle(event));',\n 'return useSyncExternalStore(subscribe, readValue, getServerValue);',\n '// subscribe: true is a reviewed exception\\nconst x = 1;',\n ],\n },\n }),\n ];\n },\n};\n\nexport interface FirestoreRuntimeInUiFacts {\n uiFolders: readonly string[];\n sanctioned: readonly Reviewed[];\n}\n\nexport function firestoreRuntimeImports(file: FileView): Hit[] {\n const source = file.sourceFile;\n const hits: Hit[] = [];\n for (const site of moduleImports(source, ['import', 'export', 'dynamic'])) {\n if (!FIRESTORE_SDK.test(site.spec)) continue;\n const at = site.node.getStart(source);\n if (site.kind === 'import') {\n const typeOnly =\n !site.sideEffect &&\n (site.typeOnly ||\n (!site.defaultName &&\n !site.namespace &&\n site.named.length > 0 &&\n site.named.every((n) => n.typeOnly)));\n if (!typeOnly) hits.push({ index: at, detail: `import from '${site.spec}'` });\n } else if (site.kind === 'export') {\n if (!site.typeOnly) hits.push({ index: at, detail: `re-export from '${site.spec}'` });\n } else hits.push({ index: at, detail: `import('${site.spec}')` });\n }\n return hits;\n}\n\nexport const firestoreRuntimeInUiGuard: GenericGuardDefinition<FirestoreRuntimeInUiFacts> = {\n id: 'firestoreRuntimeInUi',\n absentWhen: 'the app stores nothing in Firestore',\n factProblems(facts) {\n const problems: string[] = [];\n namesProblem(facts?.uiFolders, 'uiFolders', problems);\n reviewedProblems(facts?.sanctioned, 'sanctioned', problems);\n return problems;\n },\n build(facts) {\n const inUi = (path: string) => facts.uiFolders.some((folder) => isUnder(path, folder));\n return [\n reviewedAllowlist({\n title: 'no component or route imports the Firestore runtime',\n description:\n 'A component or route that reaches into Firestore bypasses the cache, the query keys, and every server check ' +\n 'at once. Flagged: a runtime import, re-export, or dynamic import of the Firestore SDK (a type-only import ' +\n 'passes) in the UI folders. Fix: read through a hook, write through a callable. A sanctioned root provider is ' +\n 'a reviewed entry with its why; one that no longer imports the runtime is stale.',\n scope: APP_SOURCE,\n match: (file) => (inUi(file.path) ? firestoreRuntimeImports(file) : []),\n allowlist: facts.sanctioned,\n samples: {\n flags: [\n {\n path: `${facts.uiFolders[0]}/probe.tsx`,\n text: \"import { doc, getDoc } from 'firebase/firestore';\",\n },\n {\n path: `${facts.uiFolders[0]}/probe.tsx`,\n text: 'import {\\n updateDoc,\\n} from \"firebase/firestore\";',\n },\n ],\n passes: [\n {\n path: `${facts.uiFolders[0]}/typed.tsx`,\n text: \"import type { Timestamp } from 'firebase/firestore';\",\n },\n {\n path: `${facts.uiFolders[0]}/typed.tsx`,\n text: \"import { type Timestamp } from 'firebase/firestore';\",\n },\n ],\n },\n }),\n ];\n },\n};\n\nconst CLIENT_WRITES = new Set([\n 'setDoc',\n 'updateDoc',\n 'deleteDoc',\n 'addDoc',\n 'writeBatch',\n 'runTransaction',\n]);\n\nexport interface FirestoreClientWritesFacts {\n sanctioned: readonly Reviewed[];\n}\n\nexport function firestoreClientWriteHits(file: FileView): Hit[] {\n const source = file.sourceFile;\n const hits: Hit[] = [];\n const namespaces = new Set<string>();\n for (const site of moduleImports(source, ['import', 'export'])) {\n if (!FIRESTORE_SDK.test(site.spec) || site.typeOnly) continue;\n if (site.namespace) namespaces.add(site.namespace);\n for (const name of site.named)\n if (!name.typeOnly && CLIENT_WRITES.has(name.imported))\n hits.push({\n index: site.node.getStart(source),\n detail: `${name.imported} from '${site.spec}'`,\n });\n }\n forEachNode(source, (node) => {\n if (\n ts.isPropertyAccessExpression(node) &&\n ts.isIdentifier(node.expression) &&\n namespaces.has(node.expression.text) &&\n CLIENT_WRITES.has(node.name.text)\n )\n hits.push({ index: node.getStart(source), detail: node.getText(source) });\n });\n return hits;\n}\n\nexport const firestoreClientWritesGuard: GenericGuardDefinition<FirestoreClientWritesFacts> = {\n id: 'firestoreClientWrites',\n absentWhen: 'the app stores nothing in Firestore',\n factProblems(facts) {\n const problems: string[] = [];\n reviewedProblems(facts?.sanctioned, 'sanctioned', problems);\n return problems;\n },\n build(facts) {\n return [\n reviewedAllowlist({\n title: 'the client never writes Firestore',\n description:\n 'Every write goes through a callable that checks it on the server; a client SDK write skips that check and ' +\n 'every rule the callable enforces. Flagged: a runtime import of a Firestore write function (' +\n `${[...CLIENT_WRITES].join(', ')}) from the Firestore SDK, or a call to one through a namespace import, in ` +\n \"the app's client source. Fix: call the callable that owns the write. A sanctioned site is a reviewed entry \" +\n 'with its why; one that no longer writes is stale.',\n scope: APP_SOURCE,\n match: firestoreClientWriteHits,\n allowlist: facts.sanctioned,\n samples: {\n flags: [\n \"import { doc, setDoc } from 'firebase/firestore';\",\n \"import { writeBatch as batchOf } from 'firebase/firestore';\",\n \"import * as fs from 'firebase/firestore';\\nawait fs.updateDoc(ref, patch);\",\n ],\n passes: [\n \"import { doc, getDoc, onSnapshot } from 'firebase/firestore';\",\n \"import { type WriteBatch } from 'firebase/firestore';\",\n 'await callable({ id });',\n ],\n },\n }),\n ];\n },\n};\n"]}
@@ -1,5 +1,5 @@
1
1
  import type { Absent, GenericGuardDefinition } from '../../node/suite/config.js';
2
- import { type FirestoreRuntimeInUiFacts, type LiveSubscriptionFacts } from './client-guards.js';
2
+ import { type FirestoreClientWritesFacts, type FirestoreRuntimeInUiFacts, type LiveSubscriptionFacts } from './client-guards.js';
3
3
  import { type TestPathFacts, type WriteGuardFacts } from './guard.js';
4
4
  import { type CollectionGroupIndexFacts } from './index-guard.js';
5
5
  import { type PathAssemblyFacts, type RetiredSegmentFacts } from './path-guards.js';
@@ -21,6 +21,7 @@ export type FirestoreGuardFacts = {
21
21
  firestoreCounterDecrements: NoFacts | Absent;
22
22
  firestoreLiveSubscriptions: LiveSubscriptionFacts | Absent;
23
23
  firestoreRuntimeInUi: FirestoreRuntimeInUiFacts | Absent;
24
+ firestoreClientWrites: FirestoreClientWritesFacts | Absent;
24
25
  firestoreRandomUuidSites: RandomUuidFacts | Absent;
25
26
  firestoreTimestampNow: NoFacts | Absent;
26
27
  rulesClaimNames: RulesClaimFacts;
@@ -1 +1 @@
1
- {"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../../../src/modules/firestore/guards.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACjF,OAAO,EAGL,KAAK,yBAAyB,EAC9B,KAAK,qBAAqB,EAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAA6B,KAAK,aAAa,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AACjG,OAAO,EAA6B,KAAK,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AAC7F,OAAO,EAIL,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACzB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC/E,OAAO,EAAoB,KAAK,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAClF,OAAO,EAKL,KAAK,eAAe,EACrB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAGL,KAAK,0BAA0B,EAC/B,KAAK,wBAAwB,EAC9B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C,MAAM,MAAM,mBAAmB,GAAG;IAChC,eAAe,EAAE,eAAe,GAAG,MAAM,CAAC;IAC1C,kBAAkB,EAAE,aAAa,GAAG,MAAM,CAAC;IAC3C,wBAAwB,EAAE,OAAO,GAAG,MAAM,CAAC;IAC3C,qBAAqB,EAAE,iBAAiB,GAAG,MAAM,CAAC;IAClD,wBAAwB,EAAE,mBAAmB,GAAG,MAAM,CAAC;IACvD,+BAA+B,EAAE,yBAAyB,GAAG,MAAM,CAAC;IACpE,oBAAoB,EAAE,wBAAwB,GAAG,MAAM,CAAC;IACxD,qBAAqB,EAAE,0BAA0B,GAAG,MAAM,CAAC;IAC3D,yBAAyB,EAAE,OAAO,GAAG,MAAM,CAAC;IAC5C,0BAA0B,EAAE,OAAO,GAAG,MAAM,CAAC;IAC7C,0BAA0B,EAAE,qBAAqB,GAAG,MAAM,CAAC;IAC3D,oBAAoB,EAAE,yBAAyB,GAAG,MAAM,CAAC;IACzD,wBAAwB,EAAE,eAAe,GAAG,MAAM,CAAC;IACnD,qBAAqB,EAAE,OAAO,GAAG,MAAM,CAAC;IACxC,eAAe,EAAE,eAAe,CAAC;IACjC,eAAe,EAAE,gBAAgB,CAAC;CACnC,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,aAAa,CAAC,sBAAsB,CAAC,KAAK,CAAC,CAiBxB,CAAC"}
1
+ {"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../../../src/modules/firestore/guards.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACjF,OAAO,EAIL,KAAK,0BAA0B,EAC/B,KAAK,yBAAyB,EAC9B,KAAK,qBAAqB,EAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAA6B,KAAK,aAAa,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AACjG,OAAO,EAA6B,KAAK,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AAC7F,OAAO,EAIL,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACzB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC/E,OAAO,EAAoB,KAAK,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAClF,OAAO,EAKL,KAAK,eAAe,EACrB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAGL,KAAK,0BAA0B,EAC/B,KAAK,wBAAwB,EAC9B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C,MAAM,MAAM,mBAAmB,GAAG;IAChC,eAAe,EAAE,eAAe,GAAG,MAAM,CAAC;IAC1C,kBAAkB,EAAE,aAAa,GAAG,MAAM,CAAC;IAC3C,wBAAwB,EAAE,OAAO,GAAG,MAAM,CAAC;IAC3C,qBAAqB,EAAE,iBAAiB,GAAG,MAAM,CAAC;IAClD,wBAAwB,EAAE,mBAAmB,GAAG,MAAM,CAAC;IACvD,+BAA+B,EAAE,yBAAyB,GAAG,MAAM,CAAC;IACpE,oBAAoB,EAAE,wBAAwB,GAAG,MAAM,CAAC;IACxD,qBAAqB,EAAE,0BAA0B,GAAG,MAAM,CAAC;IAC3D,yBAAyB,EAAE,OAAO,GAAG,MAAM,CAAC;IAC5C,0BAA0B,EAAE,OAAO,GAAG,MAAM,CAAC;IAC7C,0BAA0B,EAAE,qBAAqB,GAAG,MAAM,CAAC;IAC3D,oBAAoB,EAAE,yBAAyB,GAAG,MAAM,CAAC;IACzD,qBAAqB,EAAE,0BAA0B,GAAG,MAAM,CAAC;IAC3D,wBAAwB,EAAE,eAAe,GAAG,MAAM,CAAC;IACnD,qBAAqB,EAAE,OAAO,GAAG,MAAM,CAAC;IACxC,eAAe,EAAE,eAAe,CAAC;IACjC,eAAe,EAAE,gBAAgB,CAAC;CACnC,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,aAAa,CAAC,sBAAsB,CAAC,KAAK,CAAC,CAkBxB,CAAC"}
@@ -1,4 +1,4 @@
1
- import { firestoreRuntimeInUiGuard, liveSubscriptionGuard, } from './client-guards.js';
1
+ import { firestoreClientWritesGuard, firestoreRuntimeInUiGuard, liveSubscriptionGuard, } from './client-guards.js';
2
2
  import { testPathGuard, writeGuard } from './guard.js';
3
3
  import { collectionGroupIndexGuard } from './index-guard.js';
4
4
  import { literalSegmentGuard, pathAssemblyGuard, retiredSegmentGuard, } from './path-guards.js';
@@ -19,6 +19,7 @@ export const FIRESTORE_GUARDS = Object.freeze([
19
19
  counterDecrementGuard,
20
20
  liveSubscriptionGuard,
21
21
  firestoreRuntimeInUiGuard,
22
+ firestoreClientWritesGuard,
22
23
  randomUuidGuard,
23
24
  timestampNowGuard,
24
25
  rulesClaimGuard,
@@ -1 +1 @@
1
- {"version":3,"file":"guards.js","sourceRoot":"","sources":["../../../src/modules/firestore/guards.ts"],"names":[],"mappings":"AACA,OAAO,EACL,yBAAyB,EACzB,qBAAqB,GAGtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,aAAa,EAAE,UAAU,EAA4C,MAAM,YAAY,CAAC;AACjG,OAAO,EAAE,yBAAyB,EAAkC,MAAM,kBAAkB,CAAC;AAC7F,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,mBAAmB,GAGpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,eAAe,EAAwB,MAAM,wBAAwB,CAAC;AAC/E,OAAO,EAAE,gBAAgB,EAAyB,MAAM,yBAAyB,CAAC;AAClF,OAAO,EACL,qBAAqB,EACrB,eAAe,EACf,iBAAiB,EACjB,oBAAoB,GAErB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,0BAA0B,EAC1B,wBAAwB,GAGzB,MAAM,qBAAqB,CAAC;AAsB7B,MAAM,CAAC,MAAM,gBAAgB,GAAiD,MAAM,CAAC,MAAM,CAAC;IAC1F,UAAU;IACV,aAAa;IACb,mBAAmB;IACnB,iBAAiB;IACjB,mBAAmB;IACnB,yBAAyB;IACzB,wBAAwB;IACxB,0BAA0B;IAC1B,oBAAoB;IACpB,qBAAqB;IACrB,qBAAqB;IACrB,yBAAyB;IACzB,eAAe;IACf,iBAAiB;IACjB,eAAe;IACf,gBAAgB;CAC+B,CAAC,CAAC","sourcesContent":["import type { Absent, GenericGuardDefinition } from '../../node/suite/config.js';\nimport {\n firestoreRuntimeInUiGuard,\n liveSubscriptionGuard,\n type FirestoreRuntimeInUiFacts,\n type LiveSubscriptionFacts,\n} from './client-guards.js';\nimport { testPathGuard, writeGuard, type TestPathFacts, type WriteGuardFacts } from './guard.js';\nimport { collectionGroupIndexGuard, type CollectionGroupIndexFacts } from './index-guard.js';\nimport {\n literalSegmentGuard,\n pathAssemblyGuard,\n retiredSegmentGuard,\n type PathAssemblyFacts,\n type RetiredSegmentFacts,\n} from './path-guards.js';\nimport { rulesClaimGuard, type RulesClaimFacts } from './rules-claim-guard.js';\nimport { rulesMirrorGuard, type RulesMirrorFacts } from './rules-mirror-guard.js';\nimport {\n counterDecrementGuard,\n randomUuidGuard,\n timestampNowGuard,\n transactionReadGuard,\n type RandomUuidFacts,\n} from './server-guards.js';\nimport {\n storageObjectMetadataGuard,\n storageUploadOriginGuard,\n type StorageObjectMetadataFacts,\n type StorageUploadOriginFacts,\n} from './storage-guards.js';\nimport type { NoFacts } from './support.js';\n\nexport type FirestoreGuardFacts = {\n firestoreWrites: WriteGuardFacts | Absent;\n firestoreTestPaths: TestPathFacts | Absent;\n firestoreLiteralSegments: NoFacts | Absent;\n firestorePathAssembly: PathAssemblyFacts | Absent;\n firestoreRetiredSegments: RetiredSegmentFacts | Absent;\n firestoreCollectionGroupIndexes: CollectionGroupIndexFacts | Absent;\n storageUploadOrigins: StorageUploadOriginFacts | Absent;\n storageObjectMetadata: StorageObjectMetadataFacts | Absent;\n firestoreTransactionReads: NoFacts | Absent;\n firestoreCounterDecrements: NoFacts | Absent;\n firestoreLiveSubscriptions: LiveSubscriptionFacts | Absent;\n firestoreRuntimeInUi: FirestoreRuntimeInUiFacts | Absent;\n firestoreRandomUuidSites: RandomUuidFacts | Absent;\n firestoreTimestampNow: NoFacts | Absent;\n rulesClaimNames: RulesClaimFacts;\n rulesMirrorCore: RulesMirrorFacts;\n};\n\nexport const FIRESTORE_GUARDS: ReadonlyArray<GenericGuardDefinition<never>> = Object.freeze([\n writeGuard,\n testPathGuard,\n literalSegmentGuard,\n pathAssemblyGuard,\n retiredSegmentGuard,\n collectionGroupIndexGuard,\n storageUploadOriginGuard,\n storageObjectMetadataGuard,\n transactionReadGuard,\n counterDecrementGuard,\n liveSubscriptionGuard,\n firestoreRuntimeInUiGuard,\n randomUuidGuard,\n timestampNowGuard,\n rulesClaimGuard,\n rulesMirrorGuard,\n] as ReadonlyArray<GenericGuardDefinition<never>>);\n"]}
1
+ {"version":3,"file":"guards.js","sourceRoot":"","sources":["../../../src/modules/firestore/guards.ts"],"names":[],"mappings":"AACA,OAAO,EACL,0BAA0B,EAC1B,yBAAyB,EACzB,qBAAqB,GAItB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,aAAa,EAAE,UAAU,EAA4C,MAAM,YAAY,CAAC;AACjG,OAAO,EAAE,yBAAyB,EAAkC,MAAM,kBAAkB,CAAC;AAC7F,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,mBAAmB,GAGpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,eAAe,EAAwB,MAAM,wBAAwB,CAAC;AAC/E,OAAO,EAAE,gBAAgB,EAAyB,MAAM,yBAAyB,CAAC;AAClF,OAAO,EACL,qBAAqB,EACrB,eAAe,EACf,iBAAiB,EACjB,oBAAoB,GAErB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,0BAA0B,EAC1B,wBAAwB,GAGzB,MAAM,qBAAqB,CAAC;AAuB7B,MAAM,CAAC,MAAM,gBAAgB,GAAiD,MAAM,CAAC,MAAM,CAAC;IAC1F,UAAU;IACV,aAAa;IACb,mBAAmB;IACnB,iBAAiB;IACjB,mBAAmB;IACnB,yBAAyB;IACzB,wBAAwB;IACxB,0BAA0B;IAC1B,oBAAoB;IACpB,qBAAqB;IACrB,qBAAqB;IACrB,yBAAyB;IACzB,0BAA0B;IAC1B,eAAe;IACf,iBAAiB;IACjB,eAAe;IACf,gBAAgB;CAC+B,CAAC,CAAC","sourcesContent":["import type { Absent, GenericGuardDefinition } from '../../node/suite/config.js';\nimport {\n firestoreClientWritesGuard,\n firestoreRuntimeInUiGuard,\n liveSubscriptionGuard,\n type FirestoreClientWritesFacts,\n type FirestoreRuntimeInUiFacts,\n type LiveSubscriptionFacts,\n} from './client-guards.js';\nimport { testPathGuard, writeGuard, type TestPathFacts, type WriteGuardFacts } from './guard.js';\nimport { collectionGroupIndexGuard, type CollectionGroupIndexFacts } from './index-guard.js';\nimport {\n literalSegmentGuard,\n pathAssemblyGuard,\n retiredSegmentGuard,\n type PathAssemblyFacts,\n type RetiredSegmentFacts,\n} from './path-guards.js';\nimport { rulesClaimGuard, type RulesClaimFacts } from './rules-claim-guard.js';\nimport { rulesMirrorGuard, type RulesMirrorFacts } from './rules-mirror-guard.js';\nimport {\n counterDecrementGuard,\n randomUuidGuard,\n timestampNowGuard,\n transactionReadGuard,\n type RandomUuidFacts,\n} from './server-guards.js';\nimport {\n storageObjectMetadataGuard,\n storageUploadOriginGuard,\n type StorageObjectMetadataFacts,\n type StorageUploadOriginFacts,\n} from './storage-guards.js';\nimport type { NoFacts } from './support.js';\n\nexport type FirestoreGuardFacts = {\n firestoreWrites: WriteGuardFacts | Absent;\n firestoreTestPaths: TestPathFacts | Absent;\n firestoreLiteralSegments: NoFacts | Absent;\n firestorePathAssembly: PathAssemblyFacts | Absent;\n firestoreRetiredSegments: RetiredSegmentFacts | Absent;\n firestoreCollectionGroupIndexes: CollectionGroupIndexFacts | Absent;\n storageUploadOrigins: StorageUploadOriginFacts | Absent;\n storageObjectMetadata: StorageObjectMetadataFacts | Absent;\n firestoreTransactionReads: NoFacts | Absent;\n firestoreCounterDecrements: NoFacts | Absent;\n firestoreLiveSubscriptions: LiveSubscriptionFacts | Absent;\n firestoreRuntimeInUi: FirestoreRuntimeInUiFacts | Absent;\n firestoreClientWrites: FirestoreClientWritesFacts | Absent;\n firestoreRandomUuidSites: RandomUuidFacts | Absent;\n firestoreTimestampNow: NoFacts | Absent;\n rulesClaimNames: RulesClaimFacts;\n rulesMirrorCore: RulesMirrorFacts;\n};\n\nexport const FIRESTORE_GUARDS: ReadonlyArray<GenericGuardDefinition<never>> = Object.freeze([\n writeGuard,\n testPathGuard,\n literalSegmentGuard,\n pathAssemblyGuard,\n retiredSegmentGuard,\n collectionGroupIndexGuard,\n storageUploadOriginGuard,\n storageObjectMetadataGuard,\n transactionReadGuard,\n counterDecrementGuard,\n liveSubscriptionGuard,\n firestoreRuntimeInUiGuard,\n firestoreClientWritesGuard,\n randomUuidGuard,\n timestampNowGuard,\n rulesClaimGuard,\n rulesMirrorGuard,\n] as ReadonlyArray<GenericGuardDefinition<never>>);\n"]}
@@ -47,12 +47,11 @@ export interface PartialMockFacts {
47
47
  module: string;
48
48
  why: string;
49
49
  }>;
50
+ passThroughHelpers: ReadonlyArray<{
51
+ owner: string;
52
+ names: readonly string[];
53
+ }>;
50
54
  sharedFakes?: Pick<SharedFakeFacts, 'importAliases' | 'fakes'>;
51
55
  }
52
- export declare function moduleMockCalls(code: string, module: string): Array<{
53
- call: string;
54
- index: number;
55
- }>;
56
- export declare function spreadsImportOriginal(call: string): boolean;
57
56
  export declare const partialMockGuard: GenericGuardDefinition<PartialMockFacts>;
58
57
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/modules/frontend-actions/client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAEzE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAErD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAErD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,GAAG,EACT,MAAM,wBAAwB,CAAC;AAwBhC,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,SAAS,QAAQ,EAAE,CAAC;CAClC;AAED,eAAO,MAAM,oBAAoB,EAAE,sBAAsB,CAAC,oBAAoB,CAuD7E,CAAC;AAEF,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,SAAS,cAAc,EAAE,CAAC;CACtC;AAED,eAAO,MAAM,mBAAmB,EAAE,sBAAsB,CAAC,mBAAmB,CAkC3E,CAAC;AAEF,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,MAAM,CAAC;IACvB,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,mBAAmB,EAAE,sBAAsB,CAAC,mBAAmB,CAqC3E,CAAC;AAEF,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,aAAa,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,KAAK,CAAA;KAAE,CAAC,CAAC;IAChG,iBAAiB,EAAE,SAAS,cAAc,EAAE,CAAC;CAC9C;AAKD,wBAAgB,eAAe,CAC7B,IAAI,EAAE,QAAQ,EACd,QAAQ,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,GAChD,GAAG,EAAE,CAeP;AAED,eAAO,MAAM,kBAAkB,EAAE,sBAAsB,CAAC,kBAAkB,CAgFzE,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAKpD,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,QAAQ,GAAG,GAAG,EAAE,CA0B/D;AAED,eAAO,MAAM,eAAe,EAAE,sBAAsB,CAAC,eAAe,CAiCnE,CAAC;AAEF,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;CACf;AAED,eAAO,MAAM,oBAAoB,EAAE,sBAAsB,CAAC,oBAAoB,CAsC7E,CAAC;AAEF,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,eAAO,MAAM,gBAAgB,EAAE,sBAAsB,CAAC,gBAAgB,CAoCrE,CAAC;AAEF,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,aAAa,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7D,WAAW,CAAC,EAAE,IAAI,CAAC,eAAe,EAAE,eAAe,GAAG,OAAO,CAAC,CAAC;CAChE;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAcxC;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAI3D;AA2CD,eAAO,MAAM,gBAAgB,EAAE,sBAAsB,CAAC,gBAAgB,CA+ErE,CAAC"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/modules/frontend-actions/client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAEzE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAErD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAErD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,GAAG,EACT,MAAM,wBAAwB,CAAC;AAkChC,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,SAAS,QAAQ,EAAE,CAAC;CAClC;AAED,eAAO,MAAM,oBAAoB,EAAE,sBAAsB,CAAC,oBAAoB,CAuD7E,CAAC;AAEF,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,SAAS,cAAc,EAAE,CAAC;CACtC;AAED,eAAO,MAAM,mBAAmB,EAAE,sBAAsB,CAAC,mBAAmB,CAkC3E,CAAC;AAEF,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,MAAM,CAAC;IACvB,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,mBAAmB,EAAE,sBAAsB,CAAC,mBAAmB,CAqC3E,CAAC;AAEF,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,aAAa,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,KAAK,CAAA;KAAE,CAAC,CAAC;IAChG,iBAAiB,EAAE,SAAS,cAAc,EAAE,CAAC;CAC9C;AAKD,wBAAgB,eAAe,CAC7B,IAAI,EAAE,QAAQ,EACd,QAAQ,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,GAChD,GAAG,EAAE,CAeP;AAED,eAAO,MAAM,kBAAkB,EAAE,sBAAsB,CAAC,kBAAkB,CAgFzE,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAKpD,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,QAAQ,GAAG,GAAG,EAAE,CA0B/D;AAED,eAAO,MAAM,eAAe,EAAE,sBAAsB,CAAC,eAAe,CAiCnE,CAAC;AAEF,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;CACf;AAED,eAAO,MAAM,oBAAoB,EAAE,sBAAsB,CAAC,oBAAoB,CAsC7E,CAAC;AAEF,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,eAAO,MAAM,gBAAgB,EAAE,sBAAsB,CAAC,gBAAgB,CAoCrE,CAAC;AAEF,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,aAAa,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7D,kBAAkB,EAAE,aAAa,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IAC/E,WAAW,CAAC,EAAE,IAAI,CAAC,eAAe,EAAE,eAAe,GAAG,OAAO,CAAC,CAAC;CAChE;AAoFD,eAAO,MAAM,gBAAgB,EAAE,sBAAsB,CAAC,gBAAgB,CA0GrE,CAAC"}
@@ -1,9 +1,10 @@
1
1
  import ts from 'typescript';
2
2
  import { matchPattern, ownerOnly, reviewedAllowlist, } from '../../shapes/shapes.js';
3
- import { allowlistProblems, importAliasesProblem, namesProblem, reviewedProblems, textProblem, isList, } from '../../support/facts.js';
3
+ import { allowlistProblems, importAliasesProblem, namesProblem, reviewedProblems, pathProblem, textProblem, isList, } from '../../support/facts.js';
4
4
  import { fromStatements, namesModule } from '../../support/imports.js';
5
5
  import { escapeRegExp, isUnder } from '../../support/text.js';
6
- import { isPlainString } from '../../support/ast.js';
6
+ import { isPlainString, lineOfNode, unwrap } from '../../support/ast.js';
7
+ import { awaited, bindingsOf, calledExport, factoryFunction, parameterName, returnedExpressions, } from '../../support/mock-factories.js';
7
8
  import { aliasList, resolveSpecifier } from '../../support/resolve-specifier.js';
8
9
  const APP_CODE = { roots: ['app'], kinds: ['code'], tests: false };
9
10
  const dottedPattern = (spelling) => spelling
@@ -366,28 +367,13 @@ export const dateOfBirthGuard = {
366
367
  ];
367
368
  },
368
369
  };
369
- export function moduleMockCalls(code, module) {
370
- const opener = new RegExp(`vi\\.(?:mock|doMock)\\(\\s*(['"\`])${escapeRegExp(module)}\\1`, 'g');
371
- const calls = [];
372
- for (let m = opener.exec(code); m; m = opener.exec(code)) {
373
- const open = code.indexOf('(', m.index);
374
- let depth = 0;
375
- let end = open;
376
- for (; end < code.length; end++) {
377
- if (code[end] === '(')
378
- depth++;
379
- else if (code[end] === ')' && --depth === 0)
380
- break;
381
- }
382
- calls.push({ call: code.slice(m.index, end + 1), index: m.index });
383
- }
384
- return calls;
385
- }
386
- export function spreadsImportOriginal(call) {
387
- if (/\.\.\.\s*\(\s*await\s+importOriginal\b/.test(call))
370
+ const MOCK_CALLS = new Set(['vi.mock', 'vi.doMock']);
371
+ function spreadsImportOriginal(code, original = 'importOriginal') {
372
+ const name = escapeRegExp(original);
373
+ if (new RegExp(`\\.\\.\\.\\s*\\(\\s*await\\s+${name}\\b`).test(code))
388
374
  return true;
389
- const binding = /const\s+([A-Za-z_$][\w$]*)\s*=\s*await\s+importOriginal\b/.exec(call);
390
- return binding !== null && new RegExp(`\\.\\.\\.\\s*${escapeRegExp(binding[1])}\\b`).test(call);
375
+ const binding = new RegExp(`const\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*await\\s+${name}\\b`).exec(code);
376
+ return binding !== null && new RegExp(`\\.\\.\\.\\s*${escapeRegExp(binding[1])}\\b`).test(code);
391
377
  }
392
378
  function factoryLoads(call) {
393
379
  const loads = [];
@@ -407,20 +393,24 @@ function functionText(file, name) {
407
393
  }
408
394
  return null;
409
395
  }
410
- function ownerKeepsRealExports(context, owner, aliases) {
396
+ function ownerKeepsRealExports(context, owner, aliases, passesThrough) {
411
397
  if (!context.reader.exists(owner))
412
398
  return false;
413
399
  const file = context.file(owner);
414
400
  if (spreadsImportOriginal(file.code))
415
401
  return true;
402
+ const calls = (local) => new RegExp(`\\b${escapeRegExp(local)}\\s*[<(]`).test(file.code);
416
403
  for (const site of file.imports) {
417
404
  if (site.typeOnly)
418
405
  continue;
419
406
  const target = resolveSpecifier(context.reader, owner, site.spec, aliases);
407
+ for (const name of site.named)
408
+ if (!name.typeOnly && passesThrough(target, name.imported) && calls(name.local))
409
+ return true;
420
410
  if (target === site.spec || !context.reader.exists(target))
421
411
  continue;
422
412
  for (const name of site.named) {
423
- if (name.typeOnly || !new RegExp(`\\b${escapeRegExp(name.local)}\\s*[<(]`).test(file.code))
413
+ if (name.typeOnly || !calls(name.local))
424
414
  continue;
425
415
  const helper = functionText(context.file(target), name.imported);
426
416
  if (helper !== null && spreadsImportOriginal(helper))
@@ -429,6 +419,25 @@ function ownerKeepsRealExports(context, owner, aliases) {
429
419
  }
430
420
  return false;
431
421
  }
422
+ function returnsThroughPassThrough(fn, resolve, passesThrough) {
423
+ const original = parameterName(fn);
424
+ const bindings = bindingsOf(fn, resolve);
425
+ const returns = returnedExpressions(fn);
426
+ return (returns.length > 0 &&
427
+ returns.every((returned) => {
428
+ const call = returned ? awaited(returned) : null;
429
+ if (!call || !ts.isCallExpression(call))
430
+ return false;
431
+ const target = calledExport(call, bindings, resolve);
432
+ const [first] = call.arguments;
433
+ const passed = first ? unwrap(first) : null;
434
+ return (target !== null &&
435
+ passesThrough(target.path, target.name) &&
436
+ passed !== null &&
437
+ ts.isIdentifier(passed) &&
438
+ passed.text === original);
439
+ }));
440
+ }
432
441
  export const partialMockGuard = {
433
442
  id: 'partialMocks',
434
443
  absentWhen: false,
@@ -440,6 +449,13 @@ export const partialMockGuard = {
440
449
  textProblem(r?.module, 'a package root', problems);
441
450
  textProblem(r?.why, `package root ${String(r?.module)}'s why`, problems);
442
451
  }
452
+ if (!isList(facts.passThroughHelpers))
453
+ problems.push('passThroughHelpers is missing (declare [] when there is none)');
454
+ else
455
+ for (const helper of facts.passThroughHelpers) {
456
+ pathProblem(helper?.owner, "a passThroughHelpers entry's owner", problems);
457
+ namesProblem(helper?.names, `passThroughHelpers entry ${String(helper?.owner)}'s names`, problems);
458
+ }
443
459
  if (facts.sharedFakes !== undefined) {
444
460
  importAliasesProblem(facts.sharedFakes?.importAliases, 'sharedFakes.importAliases', problems);
445
461
  if (!isList(facts.sharedFakes?.fakes))
@@ -451,15 +467,20 @@ export const partialMockGuard = {
451
467
  return [
452
468
  {
453
469
  title: "a mock of a listed package root keeps the root's real exports",
454
- description: 'A `vi.mock` of a listed package root spreads the real module: ' +
455
- "`vi.mock(import('<root>'), async (importOriginal) => ({ ...(await importOriginal()), <override> }))`. A factory that " +
456
- 'returns only the exports one test steers deletes the rest, and code that needs them fails away from the ' +
457
- "assertion. A mock whose factory loads the root's shared fake passes when that fake spreads the real module. " +
470
+ description: 'A `vi.mock` of a listed package root, named by a string or through `import()`, keeps the real ' +
471
+ 'module: its factory spreads its own `importOriginal` ' +
472
+ '(`({ ...(await importOriginal()), <override> })`), returns on every path through a listed ' +
473
+ 'pass-through helper handed that `importOriginal` (`passThroughModule(importOriginal, { <override> })`), ' +
474
+ "or loads the root's shared fake that keeps the real module. A factory that returns only the " +
475
+ 'exports one test steers — through a stub helper too — deletes the rest, and code that needs them ' +
476
+ 'fails away from the assertion. ' +
458
477
  facts.packageRoots.map((r) => `${r.module}: ${r.why}.`).join(' '),
459
478
  run(context) {
460
479
  const findings = [];
480
+ const roots = new Set(facts.packageRoots.map((r) => r.module));
461
481
  const mocked = new Set();
462
482
  const aliases = aliasList(facts.sharedFakes?.importAliases ?? {});
483
+ const passesThrough = (path, name) => facts.passThroughHelpers.some((h) => h.owner === path && h.names.includes(name));
463
484
  const ownerOf = (module, rootName) => facts.sharedFakes?.fakes.find((f) => f.module === module &&
464
485
  context.reader.exists(f.owner) &&
465
486
  context.file(f.owner).rootName === rootName)?.owner;
@@ -467,7 +488,7 @@ export const partialMockGuard = {
467
488
  const owner = ownerOf(module, file.rootName);
468
489
  return (owner !== undefined &&
469
490
  factoryLoads(call).some((spec) => resolveSpecifier(context.reader, file.path, spec, aliases) === owner) &&
470
- ownerKeepsRealExports(context, owner, aliases));
491
+ ownerKeepsRealExports(context, owner, aliases, passesThrough));
471
492
  };
472
493
  const files = context.files({
473
494
  roots: ['app', 'functions'],
@@ -476,16 +497,25 @@ export const partialMockGuard = {
476
497
  optionalRoots: ['functions'],
477
498
  });
478
499
  for (const file of files) {
479
- for (const { module } of facts.packageRoots) {
480
- for (const { call, index } of moduleMockCalls(file.code, module)) {
481
- mocked.add(module);
482
- if (!spreadsImportOriginal(call) && !loadsKeepingOwner(file, module, call))
483
- findings.push({
484
- file: file.path,
485
- line: file.lineOf(index),
486
- detail: `a full-module mock of ${module}`,
487
- });
488
- }
500
+ const resolve = (spec) => resolveSpecifier(context.reader, file.path, spec, aliases);
501
+ for (const site of file.imports) {
502
+ if (site.kind !== 'mock' || !MOCK_CALLS.has(site.callee ?? ''))
503
+ continue;
504
+ if (!roots.has(site.spec) || !ts.isCallExpression(site.node))
505
+ continue;
506
+ mocked.add(site.spec);
507
+ const call = site.node.getText(file.sourceFile);
508
+ const factory = site.node.arguments[1];
509
+ const fn = factory ? factoryFunction(factory) : null;
510
+ if (spreadsImportOriginal(call, (fn && parameterName(fn)) ?? 'importOriginal') ||
511
+ (fn !== null && returnsThroughPassThrough(fn, resolve, passesThrough)) ||
512
+ loadsKeepingOwner(file, site.spec, call))
513
+ continue;
514
+ findings.push({
515
+ file: file.path,
516
+ line: lineOfNode(file, site.node),
517
+ detail: `a full-module mock of ${site.spec}`,
518
+ });
489
519
  }
490
520
  }
491
521
  for (const { module } of facts.packageRoots) {