@assembly-js/app-foundation 0.1.1 → 0.1.2
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/CHANGELOG.md +3 -0
- package/bin/scan-client-calls.mjs +37 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,9 @@ bullets ship. Write `[fix]`, `[behavior]` and `[action]` bullets for the app's o
|
|
|
17
17
|
A patch release never changes what an app's build requires; a minor release may add or change a
|
|
18
18
|
`bin/check-*.mjs` script. CI fails a PR that changes a check script without a minor bump over main.
|
|
19
19
|
|
|
20
|
+
## 0.1.2
|
|
21
|
+
- [new] The client-call scan now reads the app's installed `@assembly-js/node-sdk` for the exact method names, so a method added with a new verb is still caught. Nothing in the app changes.
|
|
22
|
+
|
|
20
23
|
## 0.1.1
|
|
21
24
|
- [fix] The app's `@assembly-js/app-bridge` is raised to 2.0.0 on update, so host navigation, the paint beacon and the cookie-outcome line work instead of silently doing nothing.
|
|
22
25
|
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
// Parse-only like scan-routes: ts.createSourceFile, no Program, no type check,
|
|
21
21
|
// so an app that does not typecheck still scans. Files are parsed lazily, only
|
|
22
22
|
// when an entry reaches them.
|
|
23
|
-
import { readFileSync } from 'node:fs';
|
|
23
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
24
|
+
import { createRequire } from 'node:module';
|
|
24
25
|
import path from 'node:path';
|
|
25
26
|
import { CLIENT_SAFE_SDK_METHODS } from './client-safe-sdk-methods.mjs';
|
|
26
27
|
import { NON_CLIENT_EXPORTS } from './foundation-exports.mjs';
|
|
@@ -39,7 +40,7 @@ import {
|
|
|
39
40
|
wrapperName,
|
|
40
41
|
} from './scan-routes.mjs';
|
|
41
42
|
|
|
42
|
-
export const SCANNER_VERSION =
|
|
43
|
+
export const SCANNER_VERSION = 3;
|
|
43
44
|
|
|
44
45
|
export const CLASS_BREAKS = 'BREAKS';
|
|
45
46
|
export const CLASS_CLEAN = 'CLEAN';
|
|
@@ -89,10 +90,32 @@ const PROMISE_CHAIN_METHODS = new Set(['then', 'catch', 'finally']);
|
|
|
89
90
|
// `getV1Comments`), so a call with one of these shapes is an SDK call whatever the receiver is
|
|
90
91
|
// called. Recognising the method instead of the source is what keeps a client the name-based
|
|
91
92
|
// tagging never saw from reading CLEAN.
|
|
92
|
-
const SDK_METHOD_SHAPE = /^(?:(?:list|retrieve|create|update|delete|send|cancel|add|mark|request|download)[A-Z]|(?:get|put|post)V1)/;
|
|
93
|
-
// `fetch`
|
|
94
|
-
// non-SDK endpoints such as the AI proxy.
|
|
95
|
-
const SDK_METHOD_EXTRAS = new Set(['fetch']);
|
|
93
|
+
export const SDK_METHOD_SHAPE = /^(?:(?:list|retrieve|create|update|delete|send|cancel|add|mark|request|download)[A-Z]|(?:get|put|post)V1)/;
|
|
94
|
+
// `fetch` and `sendWebhook` are the flat client methods the codegen services do not declare, and
|
|
95
|
+
// the verb pattern cannot match; `fetch` is how apps reach non-SDK endpoints such as the AI proxy.
|
|
96
|
+
export const SDK_METHOD_EXTRAS = new Set(['fetch', 'sendWebhook']);
|
|
97
|
+
const SDK_SERVICES_DIR = '../codegen/api/services';
|
|
98
|
+
const SDK_SERVICE_FILE = /Service\.d\.ts$/;
|
|
99
|
+
const SDK_STATIC_METHOD = /static (\w+)\(/g;
|
|
100
|
+
|
|
101
|
+
// The installed SDK's own method list is exact where the verb pattern only guesses, so a new verb
|
|
102
|
+
// is covered without a scanner change. It is resolved from the app because the app's version is
|
|
103
|
+
// the one its calls run against; null falls back to the pattern.
|
|
104
|
+
export function loadSdkMethods(root) {
|
|
105
|
+
try {
|
|
106
|
+
const entry = createRequire(path.join(root, 'package.json')).resolve('@assembly-js/node-sdk');
|
|
107
|
+
const dir = path.resolve(path.dirname(entry), SDK_SERVICES_DIR);
|
|
108
|
+
const methods = new Set();
|
|
109
|
+
for (const file of readdirSync(dir).filter((f) => SDK_SERVICE_FILE.test(f))) {
|
|
110
|
+
for (const m of readFileSync(path.join(dir, file), 'utf8').matchAll(SDK_STATIC_METHOD)) methods.add(m[1]);
|
|
111
|
+
}
|
|
112
|
+
if (methods.size === 0) return null;
|
|
113
|
+
for (const extra of SDK_METHOD_EXTRAS) methods.add(extra);
|
|
114
|
+
return methods;
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
96
119
|
// Browser and Node methods that fit the verb pattern on any receiver (a media query list, a
|
|
97
120
|
// ref, a form), so a call on one says nothing about the SDK.
|
|
98
121
|
const NON_SDK_METHODS = new Set([
|
|
@@ -155,7 +178,7 @@ function scriptKind(ts, file) {
|
|
|
155
178
|
// `calls` are every method call on a tagged receiver, `defeaters` are the
|
|
156
179
|
// constructs that stop the scan following a viewer receiver, `specifiers` the
|
|
157
180
|
// module specifiers it imports.
|
|
158
|
-
export function collectFileFacts(ts, file, text) {
|
|
181
|
+
export function collectFileFacts(ts, file, text, sdkMethods = null) {
|
|
159
182
|
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, scriptKind(ts, file));
|
|
160
183
|
const calls = [];
|
|
161
184
|
const defeaters = [];
|
|
@@ -486,7 +509,7 @@ export function collectFileFacts(ts, file, text) {
|
|
|
486
509
|
if (ts.isPropertyAccessExpression(callee)) name = callee.name.text;
|
|
487
510
|
else if (ts.isElementAccessExpression(callee) && isLiteralText(callee.argumentExpression)) name = callee.argumentExpression.text;
|
|
488
511
|
if (name == null || NON_SDK_METHODS.has(name)) return;
|
|
489
|
-
if (!SDK_METHOD_SHAPE.test(name) && !SDK_METHOD_EXTRAS.has(name)) return;
|
|
512
|
+
if (sdkMethods ? !sdkMethods.has(name) : !SDK_METHOD_SHAPE.test(name) && !SDK_METHOD_EXTRAS.has(name)) return;
|
|
490
513
|
const receiver = unwrap(callee.expression).node;
|
|
491
514
|
if (provenanceOf(receiver, scope)) return;
|
|
492
515
|
const root = rootIdentifier(receiver);
|
|
@@ -827,6 +850,7 @@ function classifyEntries(ts, root, files, read) {
|
|
|
827
850
|
// projection pays only for the files it reaches.
|
|
828
851
|
export function createAnalysis(root) {
|
|
829
852
|
const ts = loadTypeScript();
|
|
853
|
+
const sdkMethods = loadSdkMethods(root);
|
|
830
854
|
const files = listFiles(root).filter((f) => SOURCE_FILE.test(f) && !DECLARATION_FILE.test(f)).sort(byteCompare);
|
|
831
855
|
const fileSet = new Set(files);
|
|
832
856
|
const texts = new Map();
|
|
@@ -858,7 +882,7 @@ export function createAnalysis(root) {
|
|
|
858
882
|
|
|
859
883
|
const factsFor = (file) => {
|
|
860
884
|
if (!facts.has(file)) {
|
|
861
|
-
const raw = collectFileFacts(ts, file, read(file));
|
|
885
|
+
const raw = collectFileFacts(ts, file, read(file), sdkMethods);
|
|
862
886
|
const imports = [];
|
|
863
887
|
const defeaters = [...raw.defeaters];
|
|
864
888
|
for (const spec of raw.specifiers) {
|
|
@@ -875,7 +899,7 @@ export function createAnalysis(root) {
|
|
|
875
899
|
return facts.get(file);
|
|
876
900
|
};
|
|
877
901
|
|
|
878
|
-
return { root, files, ts, read, facts: factsFor, classifyEntries: () => classifyEntries(ts, root, files, read) };
|
|
902
|
+
return { root, files, ts, read, sdkMethodMode: sdkMethods ? 'exact' : 'pattern', facts: factsFor, classifyEntries: () => classifyEntries(ts, root, files, read) };
|
|
879
903
|
}
|
|
880
904
|
|
|
881
905
|
// Maps every file to the byte-smallest entry that loads it (`entry`) and to every entry that
|
|
@@ -927,8 +951,9 @@ export function projectClientReachable(analysis, allowlist = CLIENT_SAFE_SDK_MET
|
|
|
927
951
|
}
|
|
928
952
|
|
|
929
953
|
export function scanClientCalls(root, { allowlist = CLIENT_SAFE_SDK_METHODS, appId } = {}) {
|
|
930
|
-
const
|
|
931
|
-
|
|
954
|
+
const analysis = createAnalysis(root);
|
|
955
|
+
const app = { app: appId ?? path.basename(path.resolve(root)), ...projectClientReachable(analysis, allowlist) };
|
|
956
|
+
return { scannerVersion: SCANNER_VERSION, sdkMethodMode: analysis.sdkMethodMode, apps: [app] };
|
|
932
957
|
}
|
|
933
958
|
|
|
934
959
|
export const serializeClientCalls = (result) => JSON.stringify(result, null, 2) + '\n';
|