@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.
- package/dist/modules/firestore/client-guards.d.ts +5 -0
- package/dist/modules/firestore/client-guards.d.ts.map +1 -1
- package/dist/modules/firestore/client-guards.js +69 -0
- package/dist/modules/firestore/client-guards.js.map +1 -1
- package/dist/modules/firestore/guards.d.ts +2 -1
- package/dist/modules/firestore/guards.d.ts.map +1 -1
- package/dist/modules/firestore/guards.js +2 -1
- package/dist/modules/firestore/guards.js.map +1 -1
- package/dist/modules/frontend-actions/client.d.ts +4 -5
- package/dist/modules/frontend-actions/client.d.ts.map +1 -1
- package/dist/modules/frontend-actions/client.js +70 -40
- package/dist/modules/frontend-actions/client.js.map +1 -1
- package/dist/modules/frontend-reads/read-failures.d.ts +3 -0
- package/dist/modules/frontend-reads/read-failures.d.ts.map +1 -1
- package/dist/modules/frontend-reads/read-failures.js +170 -6
- package/dist/modules/frontend-reads/read-failures.js.map +1 -1
- package/dist/modules/repo/typed-mocks.d.ts.map +1 -1
- package/dist/modules/repo/typed-mocks.js +4 -76
- package/dist/modules/repo/typed-mocks.js.map +1 -1
- package/dist/support/mock-factories.d.ts +18 -0
- package/dist/support/mock-factories.d.ts.map +1 -0
- package/dist/support/mock-factories.js +80 -0
- package/dist/support/mock-factories.js.map +1 -0
- package/package.json +1 -1
|
@@ -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":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAK5B,OAAO,EAAE,iBAAiB,EAAY,MAAM,wBAAwB,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAEhD,MAAM,aAAa,GAAG,8BAA8B,CAAC;AACrD,MAAM,UAAU,GAAU,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAE5E,MAAM,UAAU,oBAAoB,CAAC,IAAc;IACjD,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC;IAC/B,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC;IAC9C,MAAM,UAAU,GAAG,IAAI,GAAG,EAAU,CAAC;IACrC,KAAK,MAAM,IAAI,IAAI,aAAa,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;QACrD,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,SAAS;QAC/D,IAAI,IAAI,CAAC,SAAS;YAAE,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACnD,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK;YAC3B,IAAI,IAAI,CAAC,QAAQ,KAAK,YAAY;gBAAE,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACtE,CAAC;IACD,MAAM,IAAI,GAAU,EAAE,CAAC;IACvB,MAAM,EAAE,GAAG,CAAC,IAAa,EAAE,MAAc,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;IAClG,WAAW,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;QAC3B,IAAI,EAAE,CAAC,oBAAoB,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,yBAAyB,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YAC/E,MAAM,GAAG,GACP,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;YACtF,IAAI,GAAG,KAAK,WAAW,IAAI,IAAI,CAAC,WAAW,CAAC,IAAI,KAAK,EAAE,CAAC,UAAU,CAAC,YAAY;gBAC7E,EAAE,CAAC,IAAI,EAAE,cAAc,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC/D,CAAC;aAAM,IAAI,EAAE,CAAC,6BAA6B,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YACpF,EAAE,CAAC,IAAI,EAAE,eAAe,CAAC,CAAC;QAC5B,CAAC;aAAM,IAAI,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC;YACrC,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC;YAC/B,IAAI,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;gBAAE,EAAE,CAAC,IAAI,EAAE,GAAG,MAAM,CAAC,IAAI,KAAK,CAAC,CAAC;iBACxF,IAAI,EAAE,CAAC,0BAA0B,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,KAAK,YAAY;gBACjF,EAAE,CAAC,IAAI,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC,CAAC,CAAC;IACH,OAAO,IAAI,CAAC;AACd,CAAC;AAMD,MAAM,CAAC,MAAM,qBAAqB,GAAkD;IAClF,EAAE,EAAE,4BAA4B;IAChC,UAAU,EAAE,qCAAqC;IACjD,YAAY,CAAC,KAAK;QAChB,MAAM,QAAQ,GAAa,EAAE,CAAC;QAC9B,gBAAgB,CAAC,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC;QAC1D,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,KAAK,CAAC,KAAK;QACT,OAAO;YACL,iBAAiB,CAAC;gBAChB,KAAK,EAAE,2DAA2D;gBAClE,WAAW,EACT,iHAAiH;oBACjH,6GAA6G;oBAC7G,6GAA6G;oBAC7G,4GAA4G;oBAC5G,oEAAoE;gBACtE,KAAK,EAAE,UAAU;gBACjB,KAAK,EAAE,oBAAoB;gBAC3B,SAAS,EAAE,KAAK,CAAC,SAAS;gBAC1B,OAAO,EAAE;oBACP,KAAK,EAAE;wBACL,0DAA0D;wBAC1D,oDAAoD;wBACpD,iEAAiE;wBACjE,2GAA2G;wBAC3G,qFAAqF;qBACtF;oBACD,MAAM,EAAE;wBACN,uEAAuE;wBACvE,2DAA2D;wBAC3D,gEAAgE;wBAChE,oEAAoE;wBACpE,0DAA0D;qBAC3D;iBACF;aACF,CAAC;SACH,CAAC;IACJ,CAAC;CACF,CAAC;AAOF,MAAM,UAAU,uBAAuB,CAAC,IAAc;IACpD,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC;IAC/B,MAAM,IAAI,GAAU,EAAE,CAAC;IACvB,KAAK,MAAM,IAAI,IAAI,aAAa,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC;QAC1E,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,SAAS;QAC7C,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACtC,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC3B,MAAM,QAAQ,GACZ,CAAC,IAAI,CAAC,UAAU;gBAChB,CAAC,IAAI,CAAC,QAAQ;oBACZ,CAAC,CAAC,IAAI,CAAC,WAAW;wBAChB,CAAC,IAAI,CAAC,SAAS;wBACf,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC;wBACrB,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;YAC5C,IAAI,CAAC,QAAQ;gBAAE,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,gBAAgB,IAAI,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC;QAChF,CAAC;aAAM,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAClC,IAAI,CAAC,IAAI,CAAC,QAAQ;gBAAE,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,mBAAmB,IAAI,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC;QACxF,CAAC;;YAAM,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,WAAW,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IACpE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,CAAC,MAAM,yBAAyB,GAAsD;IAC1F,EAAE,EAAE,sBAAsB;IAC1B,UAAU,EAAE,qCAAqC;IACjD,YAAY,CAAC,KAAK;QAChB,MAAM,QAAQ,GAAa,EAAE,CAAC;QAC9B,YAAY,CAAC,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC;QACtD,gBAAgB,CAAC,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,CAAC,CAAC;QAC5D,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,KAAK,CAAC,KAAK;QACT,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QACvF,OAAO;YACL,iBAAiB,CAAC;gBAChB,KAAK,EAAE,qDAAqD;gBAC5D,WAAW,EACT,8GAA8G;oBAC9G,4GAA4G;oBAC5G,+GAA+G;oBAC/G,iFAAiF;gBACnF,KAAK,EAAE,UAAU;gBACjB,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,uBAAuB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBACvE,SAAS,EAAE,KAAK,CAAC,UAAU;gBAC3B,OAAO,EAAE;oBACP,KAAK,EAAE;wBACL;4BACE,IAAI,EAAE,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY;4BACvC,IAAI,EAAE,mDAAmD;yBAC1D;wBACD;4BACE,IAAI,EAAE,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY;4BACvC,IAAI,EAAE,sDAAsD;yBAC7D;qBACF;oBACD,MAAM,EAAE;wBACN;4BACE,IAAI,EAAE,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY;4BACvC,IAAI,EAAE,sDAAsD;yBAC7D;wBACD;4BACE,IAAI,EAAE,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY;4BACvC,IAAI,EAAE,sDAAsD;yBAC7D;qBACF;iBACF;aACF,CAAC;SACH,CAAC;IACJ,CAAC;CACF,CAAC","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,
|
|
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,
|
|
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;
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
|
|
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 =
|
|
390
|
-
return binding !== null && new RegExp(`\\.\\.\\.\\s*${escapeRegExp(binding[1])}\\b`).test(
|
|
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 || !
|
|
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
|
|
455
|
-
|
|
456
|
-
'
|
|
457
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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) {
|