@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 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 = 2;
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` is the one flat client method the verb pattern cannot match; it is how apps reach
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 app = { app: appId ?? path.basename(path.resolve(root)), ...projectClientReachable(createAnalysis(root), allowlist) };
931
- return { scannerVersion: SCANNER_VERSION, apps: [app] };
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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assembly-js/app-foundation",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Versioned foundation helpers and build context for Assembly generated apps",
5
5
  "type": "module",
6
6
  "files": [