sourcemonkey 0.0.0-stage → 0.5.0

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.
Files changed (367) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -2
  3. package/dist/bytes.js +37 -0
  4. package/dist/channel-placeholder.js +5 -0
  5. package/dist/collection-scan.js +17 -0
  6. package/dist/collections.html +353 -0
  7. package/dist/compare.html +358 -0
  8. package/dist/composed-source.js +205 -0
  9. package/dist/connect-prompt.html +47 -0
  10. package/dist/debug/icons.html +211 -0
  11. package/dist/debug/storage.html +163 -0
  12. package/dist/debug-log.js +75 -0
  13. package/dist/diagnostic-location.js +64 -0
  14. package/dist/eval-hook.js +308 -0
  15. package/dist/extension/background.js +5263 -0
  16. package/dist/extension/channel.js +133 -0
  17. package/dist/extension/code-links.js +187 -0
  18. package/dist/extension/code-tabs.js +460 -0
  19. package/dist/extension/collection-dialog.js +619 -0
  20. package/dist/extension/collection-display.js +322 -0
  21. package/dist/extension/collections.js +2905 -0
  22. package/dist/extension/compare.js +867 -0
  23. package/dist/extension/concurrency.js +186 -0
  24. package/dist/extension/connect-grants.js +109 -0
  25. package/dist/extension/connect-prompt-page.js +70 -0
  26. package/dist/extension/connect-prompt.js +266 -0
  27. package/dist/extension/copy-button.js +63 -0
  28. package/dist/extension/copy-dialog.js +502 -0
  29. package/dist/extension/copy-preference.js +66 -0
  30. package/dist/extension/debug/icons.js +203 -0
  31. package/dist/extension/debug/storage.js +365 -0
  32. package/dist/extension/deep-link.js +77 -0
  33. package/dist/extension/delete-dialog.js +432 -0
  34. package/dist/extension/delete-icon.js +40 -0
  35. package/dist/extension/details-tooltips.js +66 -0
  36. package/dist/extension/directories.js +679 -0
  37. package/dist/extension/export-backup.js +407 -0
  38. package/dist/extension/export-dialog.js +142 -0
  39. package/dist/extension/file-access.js +74 -0
  40. package/dist/extension/file-changes.js +346 -0
  41. package/dist/extension/frame-info.js +85 -0
  42. package/dist/extension/github-provider.js +336 -0
  43. package/dist/extension/glyph.js +84 -0
  44. package/dist/extension/gm-bindings.js +601 -0
  45. package/dist/extension/gm-download.js +419 -0
  46. package/dist/extension/gm-menu.js +228 -0
  47. package/dist/extension/gm-notification.js +141 -0
  48. package/dist/extension/gm-originator-map.js +23 -0
  49. package/dist/extension/gm-tab-data.js +121 -0
  50. package/dist/extension/gm-tab.js +160 -0
  51. package/dist/extension/gm-url-change.js +111 -0
  52. package/dist/extension/gm-url-scheme.js +27 -0
  53. package/dist/extension/gm-values.js +176 -0
  54. package/dist/extension/gm-window-control.js +104 -0
  55. package/dist/extension/gm-xhr-headers.js +333 -0
  56. package/dist/extension/gm-xhr.js +584 -0
  57. package/dist/extension/injector.js +3145 -0
  58. package/dist/extension/install-capture.js +277 -0
  59. package/dist/extension/line-diff.js +233 -0
  60. package/dist/extension/local-copy.js +560 -0
  61. package/dist/extension/local-delete.js +276 -0
  62. package/dist/extension/location-kinds.js +108 -0
  63. package/dist/extension/log-format.js +158 -0
  64. package/dist/extension/managed-changes.js +361 -0
  65. package/dist/extension/managed-items.js +57 -0
  66. package/dist/extension/match-index.js +375 -0
  67. package/dist/extension/missing-scripts.js +66 -0
  68. package/dist/extension/more-menu.js +109 -0
  69. package/dist/extension/move-dialog.js +197 -0
  70. package/dist/extension/offscreen.js +37 -0
  71. package/dist/extension/options.js +504 -0
  72. package/dist/extension/page-heading.js +170 -0
  73. package/dist/extension/page-links.js +199 -0
  74. package/dist/extension/parse-check-client.js +70 -0
  75. package/dist/extension/parse-check.js +161 -0
  76. package/dist/extension/parse-sandbox-host.js +172 -0
  77. package/dist/extension/parse-sandbox.js +175 -0
  78. package/dist/extension/popup-commands-open.js +41 -0
  79. package/dist/extension/popup-view.js +387 -0
  80. package/dist/extension/popup.js +703 -0
  81. package/dist/extension/prism-userscript.js +53 -0
  82. package/dist/extension/refresh-error.js +136 -0
  83. package/dist/extension/refresh-targets.js +160 -0
  84. package/dist/extension/related-versions.js +1144 -0
  85. package/dist/extension/resize-observer-loop.js +19 -0
  86. package/dist/extension/row-cache.js +387 -0
  87. package/dist/extension/script-blobs.js +413 -0
  88. package/dist/extension/script-details.js +658 -0
  89. package/dist/extension/script-enablement.js +285 -0
  90. package/dist/extension/script-identity.js +310 -0
  91. package/dist/extension/script-keys.js +54 -0
  92. package/dist/extension/script-log-id.js +155 -0
  93. package/dist/extension/script-log.js +290 -0
  94. package/dist/extension/script-rejection.js +104 -0
  95. package/dist/extension/script-sites.js +158 -0
  96. package/dist/extension/script-store.js +3507 -0
  97. package/dist/extension/script-updater.js +991 -0
  98. package/dist/extension/script-url-hash.js +61 -0
  99. package/dist/extension/select-all.js +37 -0
  100. package/dist/extension/select-open-guard.js +90 -0
  101. package/dist/extension/site-icon.js +1261 -0
  102. package/dist/extension/sm-dev-link.js +302 -0
  103. package/dist/extension/solo-click.js +58 -0
  104. package/dist/extension/sticky-numbers.js +188 -0
  105. package/dist/extension/storage-breakdown.js +253 -0
  106. package/dist/extension/storage-scan.js +86 -0
  107. package/dist/extension/storage-usage.js +194 -0
  108. package/dist/extension/structural-keys.js +29 -0
  109. package/dist/extension/sw-teardown.js +36 -0
  110. package/dist/extension/tab-status.js +403 -0
  111. package/dist/extension/toolbar-summary.js +105 -0
  112. package/dist/extension/user-scripts-access.js +43 -0
  113. package/dist/extension/userscript.js +3672 -0
  114. package/dist/extension/value-store.js +204 -0
  115. package/dist/extension/version-probe-fetch.js +68 -0
  116. package/dist/extension/worker-client.js +109 -0
  117. package/dist/extension/worker-ops.js +171 -0
  118. package/dist/extension/zip-writer.js +24 -0
  119. package/dist/external-files.js +878 -0
  120. package/dist/file-loader.js +436 -0
  121. package/dist/file-locations.js +439 -0
  122. package/dist/generated-wrappers.js +5 -0
  123. package/dist/github-auth.js +148 -0
  124. package/dist/github-urls.js +46 -0
  125. package/dist/gm-api-catalog.js +217 -0
  126. package/dist/gm-connect.js +273 -0
  127. package/dist/gm-xhr-types.js +5 -0
  128. package/dist/http-read.js +142 -0
  129. package/dist/icons/icon-128.png +0 -0
  130. package/dist/icons/icon-16.png +0 -0
  131. package/dist/icons/icon-48.png +0 -0
  132. package/dist/local-file-overrides.js +558 -0
  133. package/dist/manifest.json +38 -0
  134. package/dist/markup-response.js +126 -0
  135. package/dist/match-targets.js +305 -0
  136. package/dist/menu-captions.js +22 -0
  137. package/dist/offscreen.html +39 -0
  138. package/dist/options.html +197 -0
  139. package/dist/parse-check-protocol.js +51 -0
  140. package/dist/parse-location.js +171 -0
  141. package/dist/parse-sandbox.html +56 -0
  142. package/dist/phase-timing.js +155 -0
  143. package/dist/popup.html +197 -0
  144. package/dist/public-suffixes.js +6957 -0
  145. package/dist/run-context.js +31 -0
  146. package/dist/runtime-messages.js +34 -0
  147. package/dist/sandboxed-page.js +35 -0
  148. package/dist/script-build.js +787 -0
  149. package/dist/script-display.js +383 -0
  150. package/dist/script-icon.js +43 -0
  151. package/dist/script-links.js +204 -0
  152. package/dist/script-source-url.js +214 -0
  153. package/dist/script-world.js +65 -0
  154. package/dist/sm-dev-protocol.js +84 -0
  155. package/dist/source-map.js +177 -0
  156. package/dist/stack-link.js +138 -0
  157. package/dist/styles.css +825 -0
  158. package/dist/unsafe-window-use.js +229 -0
  159. package/dist/url-match.js +734 -0
  160. package/dist/userscript-header.js +248 -0
  161. package/dist/userscript.html +1165 -0
  162. package/dist/vendor/fflate.LICENSE +21 -0
  163. package/dist/vendor/fflate.js +2692 -0
  164. package/dist/vendor/prism/prism-line-numbers.css +1 -0
  165. package/dist/vendor/prism/prism-line-numbers.js +1 -0
  166. package/dist/vendor/prism/prism.css +1 -0
  167. package/dist/vendor/prism/prism.js +1946 -0
  168. package/dist/version-compare.js +141 -0
  169. package/dist/version-probe.js +120 -0
  170. package/lib/build-info.json +1 -0
  171. package/lib/bytes.d.ts +3 -0
  172. package/lib/bytes.js +37 -0
  173. package/lib/channel-placeholder.d.ts +1 -0
  174. package/lib/channel-placeholder.js +5 -0
  175. package/lib/collection-scan.d.ts +8 -0
  176. package/lib/collection-scan.js +17 -0
  177. package/lib/composed-source.d.ts +28 -0
  178. package/lib/composed-source.js +205 -0
  179. package/lib/debug-log.d.ts +9 -0
  180. package/lib/debug-log.js +75 -0
  181. package/lib/diagnostic-location.d.ts +8 -0
  182. package/lib/diagnostic-location.js +64 -0
  183. package/lib/eval-hook.d.ts +34 -0
  184. package/lib/eval-hook.js +308 -0
  185. package/lib/external-files.d.ts +111 -0
  186. package/lib/external-files.js +878 -0
  187. package/lib/file-loader.d.ts +48 -0
  188. package/lib/file-loader.js +436 -0
  189. package/lib/file-locations.d.ts +44 -0
  190. package/lib/file-locations.js +439 -0
  191. package/lib/generated-wrappers.d.ts +11 -0
  192. package/lib/generated-wrappers.js +5 -0
  193. package/lib/github-auth.d.ts +12 -0
  194. package/lib/github-auth.js +148 -0
  195. package/lib/github-urls.d.ts +1 -0
  196. package/lib/github-urls.js +46 -0
  197. package/lib/gm-api-catalog.d.ts +24 -0
  198. package/lib/gm-api-catalog.js +217 -0
  199. package/lib/gm-connect.d.ts +34 -0
  200. package/lib/gm-connect.js +273 -0
  201. package/lib/gm-xhr-types.d.ts +68 -0
  202. package/lib/gm-xhr-types.js +5 -0
  203. package/lib/harness/browser-extensions.d.ts +73 -0
  204. package/lib/harness/browser-extensions.js +285 -0
  205. package/lib/harness/browser-lang.d.ts +20 -0
  206. package/lib/harness/browser-lang.js +86 -0
  207. package/lib/harness/browser-launch.d.ts +14 -0
  208. package/lib/harness/browser-launch.js +184 -0
  209. package/lib/harness/checks.d.ts +110 -0
  210. package/lib/harness/checks.js +321 -0
  211. package/lib/harness/cli.d.ts +2 -0
  212. package/lib/harness/cli.js +2259 -0
  213. package/lib/harness/command-args.d.ts +91 -0
  214. package/lib/harness/command-args.js +369 -0
  215. package/lib/harness/command-lines.d.ts +80 -0
  216. package/lib/harness/command-lines.js +180 -0
  217. package/lib/harness/command-list.d.ts +206 -0
  218. package/lib/harness/command-list.js +1475 -0
  219. package/lib/harness/console-commands.d.ts +463 -0
  220. package/lib/harness/console-commands.js +3216 -0
  221. package/lib/harness/download-watch.d.ts +21 -0
  222. package/lib/harness/download-watch.js +156 -0
  223. package/lib/harness/extension-backend.d.ts +41 -0
  224. package/lib/harness/extension-backend.js +100 -0
  225. package/lib/harness/extension-host.d.ts +112 -0
  226. package/lib/harness/extension-host.js +552 -0
  227. package/lib/harness/extension-launch.d.ts +67 -0
  228. package/lib/harness/extension-launch.js +285 -0
  229. package/lib/harness/extension-link.d.ts +315 -0
  230. package/lib/harness/extension-link.js +607 -0
  231. package/lib/harness/external-manager.d.ts +146 -0
  232. package/lib/harness/external-manager.js +414 -0
  233. package/lib/harness/fixture.d.ts +165 -0
  234. package/lib/harness/fixture.js +461 -0
  235. package/lib/harness/frame-sessions.d.ts +71 -0
  236. package/lib/harness/frame-sessions.js +248 -0
  237. package/lib/harness/frames.d.ts +68 -0
  238. package/lib/harness/frames.js +216 -0
  239. package/lib/harness/help-format.d.ts +17 -0
  240. package/lib/harness/help-format.js +127 -0
  241. package/lib/harness/host.d.ts +352 -0
  242. package/lib/harness/host.js +423 -0
  243. package/lib/harness/index.d.ts +8 -0
  244. package/lib/harness/index.js +10 -0
  245. package/lib/harness/input-file.d.ts +18 -0
  246. package/lib/harness/input-file.js +61 -0
  247. package/lib/harness/json-output.d.ts +565 -0
  248. package/lib/harness/json-output.js +71 -0
  249. package/lib/harness/manager-actions.d.ts +21 -0
  250. package/lib/harness/manager-actions.js +129 -0
  251. package/lib/harness/manager-commands.d.ts +142 -0
  252. package/lib/harness/manager-commands.js +1196 -0
  253. package/lib/harness/manager-fetch.d.ts +33 -0
  254. package/lib/harness/manager-fetch.js +239 -0
  255. package/lib/harness/managers.d.ts +24 -0
  256. package/lib/harness/managers.js +27 -0
  257. package/lib/harness/output-style.d.ts +125 -0
  258. package/lib/harness/output-style.js +322 -0
  259. package/lib/harness/page-commands.d.ts +134 -0
  260. package/lib/harness/page-commands.js +1141 -0
  261. package/lib/harness/page-output.d.ts +67 -0
  262. package/lib/harness/page-output.js +139 -0
  263. package/lib/harness/page-watch.d.ts +171 -0
  264. package/lib/harness/page-watch.js +607 -0
  265. package/lib/harness/parse-check-node.d.ts +3 -0
  266. package/lib/harness/parse-check-node.js +132 -0
  267. package/lib/harness/path-args.d.ts +22 -0
  268. package/lib/harness/path-args.js +148 -0
  269. package/lib/harness/playwright-chromium.d.ts +16 -0
  270. package/lib/harness/playwright-chromium.js +86 -0
  271. package/lib/harness/prepare.d.ts +168 -0
  272. package/lib/harness/prepare.js +364 -0
  273. package/lib/harness/prompt-editor.d.ts +17 -0
  274. package/lib/harness/prompt-editor.js +354 -0
  275. package/lib/harness/prompt-history.d.ts +13 -0
  276. package/lib/harness/prompt-history.js +89 -0
  277. package/lib/harness/rebuild-advice.d.ts +6 -0
  278. package/lib/harness/rebuild-advice.js +39 -0
  279. package/lib/harness/run-log.d.ts +47 -0
  280. package/lib/harness/run-log.js +140 -0
  281. package/lib/harness/run-logs.d.ts +132 -0
  282. package/lib/harness/run-logs.js +241 -0
  283. package/lib/harness/run.d.ts +194 -0
  284. package/lib/harness/run.js +1973 -0
  285. package/lib/harness/runs.d.ts +229 -0
  286. package/lib/harness/runs.js +726 -0
  287. package/lib/harness/save-html.d.ts +39 -0
  288. package/lib/harness/save-html.js +89 -0
  289. package/lib/harness/saved-pages.d.ts +120 -0
  290. package/lib/harness/saved-pages.js +275 -0
  291. package/lib/harness/script-mark.d.ts +17 -0
  292. package/lib/harness/script-mark.js +56 -0
  293. package/lib/harness/snapshot.d.ts +143 -0
  294. package/lib/harness/snapshot.js +474 -0
  295. package/lib/harness/stuck-tabs.d.ts +19 -0
  296. package/lib/harness/stuck-tabs.js +131 -0
  297. package/lib/harness/tab-refs.d.ts +20 -0
  298. package/lib/harness/tab-refs.js +80 -0
  299. package/lib/harness/tampermonkey.d.ts +71 -0
  300. package/lib/harness/tampermonkey.js +356 -0
  301. package/lib/harness/tm-ids.d.ts +18 -0
  302. package/lib/harness/tm-ids.js +88 -0
  303. package/lib/harness/violentmonkey.d.ts +34 -0
  304. package/lib/harness/violentmonkey.js +162 -0
  305. package/lib/harness/wait.d.ts +77 -0
  306. package/lib/harness/wait.js +458 -0
  307. package/lib/http-read.d.ts +30 -0
  308. package/lib/http-read.js +142 -0
  309. package/lib/local-file-overrides.d.ts +35 -0
  310. package/lib/local-file-overrides.js +558 -0
  311. package/lib/manifest.json +38 -0
  312. package/lib/markup-response.d.ts +11 -0
  313. package/lib/markup-response.js +126 -0
  314. package/lib/match-targets.d.ts +18 -0
  315. package/lib/match-targets.js +305 -0
  316. package/lib/menu-captions.d.ts +1 -0
  317. package/lib/menu-captions.js +22 -0
  318. package/lib/parse-check-protocol.d.ts +27 -0
  319. package/lib/parse-check-protocol.js +51 -0
  320. package/lib/parse-location.d.ts +12 -0
  321. package/lib/parse-location.js +171 -0
  322. package/lib/phase-timing.d.ts +12 -0
  323. package/lib/phase-timing.js +155 -0
  324. package/lib/public-suffixes.d.ts +1 -0
  325. package/lib/public-suffixes.js +6957 -0
  326. package/lib/run-context.d.ts +2 -0
  327. package/lib/run-context.js +31 -0
  328. package/lib/runtime-messages.d.ts +10 -0
  329. package/lib/runtime-messages.js +34 -0
  330. package/lib/sandboxed-page.d.ts +5 -0
  331. package/lib/sandboxed-page.js +35 -0
  332. package/lib/script-build.d.ts +111 -0
  333. package/lib/script-build.js +787 -0
  334. package/lib/script-display.d.ts +51 -0
  335. package/lib/script-display.js +383 -0
  336. package/lib/script-icon.d.ts +16 -0
  337. package/lib/script-icon.js +43 -0
  338. package/lib/script-links.d.ts +24 -0
  339. package/lib/script-links.js +204 -0
  340. package/lib/script-source-url.d.ts +8 -0
  341. package/lib/script-source-url.js +214 -0
  342. package/lib/script-world.d.ts +6 -0
  343. package/lib/script-world.js +65 -0
  344. package/lib/sm-dev-protocol.d.ts +251 -0
  345. package/lib/sm-dev-protocol.js +84 -0
  346. package/lib/source-map.d.ts +32 -0
  347. package/lib/source-map.js +177 -0
  348. package/lib/stack-link.d.ts +15 -0
  349. package/lib/stack-link.js +138 -0
  350. package/lib/unsafe-window-use.d.ts +1 -0
  351. package/lib/unsafe-window-use.js +229 -0
  352. package/lib/url-match.d.ts +40 -0
  353. package/lib/url-match.js +734 -0
  354. package/lib/userscript-header.d.ts +33 -0
  355. package/lib/userscript-header.js +248 -0
  356. package/lib/version-compare.d.ts +6 -0
  357. package/lib/version-compare.js +141 -0
  358. package/lib/version-probe.d.ts +17 -0
  359. package/lib/version-probe.js +120 -0
  360. package/package.json +55 -3
  361. package/skills/SourceMonkey-dev/SKILL.md +868 -0
  362. package/skills/SourceMonkey-dev/references/cdp.md +293 -0
  363. package/skills/SourceMonkey-dev/references/commands.md +297 -0
  364. package/skills/SourceMonkey-dev/references/other-managers.md +63 -0
  365. package/skills/SourceMonkey-dev/references/specs.md +46 -0
  366. package/skills/SourceMonkey-friction/SKILL.md +103 -0
  367. package/skills/SourceMonkey-friction/references/template.md +59 -0
@@ -0,0 +1,787 @@
1
+ // Building a script row from its text: parsing and validating the
2
+ // metadata block, deriving its matching rules and startup notes,
3
+ // loading its `@require` / `@resource` files, and the parse check.
4
+ //
5
+ // The half of the install pipeline that touches no storage, so the
6
+ // development harness (src/harness/prepare.ts) can run it under Node
7
+ // without the store around it. script-store.ts calls it for every add
8
+ // and refresh, and re-exports what its callers know it for.
9
+ import { getValue, getValues, isContextMenuRunAt, parseHeader, splitHeaderBody, unsupportedRunAt, NO_METADATA_BLOCK, } from './userscript-header.js';
10
+ import { attachFiles, checkScriptIcon, distillRequires, distillResources, } from './external-files.js';
11
+ import { localSearchRoots, localRootsReady } from './local-file-overrides.js';
12
+ import { iconDeclFromHeader } from './script-icon.js';
13
+ import { canonicalizeGrant, findApi, resolveGrants } from './gm-api-catalog.js';
14
+ import { hasNoConnectRules } from './gm-connect.js';
15
+ import { sourceUsesUnsafeWindow } from './unsafe-window-use.js';
16
+ import { resolveWorld } from './script-world.js';
17
+ import { ignoredMatchMessage, matchPatternProblem, unusableMatchMessage, } from './url-match.js';
18
+ import { validLocation, } from './diagnostic-location.js';
19
+ import { sha256Hex } from './bytes.js';
20
+ import { dlog } from './debug-log.js';
21
+ import { timePhase } from './phase-timing.js';
22
+ // Compute the universal change-detection token for a fetched script
23
+ // source. Uses the Web Crypto SubtleCrypto SHA-256 — available
24
+ // uniformly in service workers, page contexts, and Node ≥19, so the
25
+ // same helper works in production and in unit tests with no
26
+ // per-environment polyfill. Async by necessity (SubtleCrypto has no
27
+ // sync form on the web platform); buildScriptFromText and
28
+ // rebuildCollection (script-store.ts) are async in turn.
29
+ export async function hashSource(text) {
30
+ return sha256Hex(text);
31
+ }
32
+ // Pull `@match` / `@exclude` / `@include` lists out of a parsed header.
33
+ // These feed the future page-matching/injection logic.
34
+ export function distillMatching(header) {
35
+ if (!header)
36
+ return { matches: [], excludes: [], includes: [] };
37
+ return {
38
+ matches: getValues(header, 'match'),
39
+ excludes: getValues(header, 'exclude'),
40
+ includes: getValues(header, 'include'),
41
+ };
42
+ }
43
+ // Derive parse-time diagnostics from the metadata block (plus one fact
44
+ // about the body: whether it names `unsafeWindow`). Producers
45
+ // so far: unknown `@grant` names, `unsafeWindow` in a script that asks
46
+ // to run isolated, `GM_xmlhttpRequest` with no
47
+ // `@connect`, an unsupported `@run-at`, and bad `@match` patterns. Add others here as they come up (e.g.
48
+ // deprecated header tags, soft `@resource` failures,
49
+ // `@grant`-but-never-called dead grants). Anything that blocks
50
+ // `validateHeader` belongs in there instead — error rows skip the
51
+ // startup-logs attachment in `buildScriptFromText`.
52
+ // Said when a script asks for `unsafeWindow` (by `@grant` or by using
53
+ // the name) and also asks to run isolated, naming the header that did
54
+ // (`@inject-into content` or `@sandbox DOM`). Exported so the tests and
55
+ // any other surface recognize it by identity rather than by matching
56
+ // its wording.
57
+ export function isolatedUnsafeWindowWarning(cause) {
58
+ return 'This script cannot reach the page through unsafeWindow. '
59
+ + `${cause} makes the script run isolated from main page content, so the `
60
+ + `page's variables read as undefined. Remove ${cause} to reach them.`;
61
+ }
62
+ // The header line that puts a script in the isolated world, in the form
63
+ // a message names it, or undefined when it runs in the page.
64
+ function isolatingHeader(header) {
65
+ if (resolveWorld(header) !== 'USER_SCRIPT')
66
+ return undefined;
67
+ const injectInto = (getValue(header, 'inject-into') ?? '').trim().toLowerCase();
68
+ return injectInto === 'content' ? '@inject-into content' : '@sandbox DOM';
69
+ }
70
+ // Said when a script grants an API that `@connect` gates (today only
71
+ // `GM_xmlhttpRequest`) but declares no `@connect` line: each other site
72
+ // it contacts needs the user's answer to a prompt (see gm-connect.ts).
73
+ // `grant` is the name as the script wrote it (`GM.xmlHttpRequest` or the
74
+ // legacy form).
75
+ export function noConnectInfo(grant) {
76
+ return `This script has no @connect lines, so SourceMonkey asks before its ${grant} requests reach another site. `
77
+ + 'Add @connect for each host it contacts, or @connect * to allow any host.';
78
+ }
79
+ // A manual script (`@run-at context-menu`): it matches pages like any
80
+ // other, but never runs on page load, only when the person asks for it
81
+ // in the popup or sm-dev fires it. See docs/manual-scripts.md.
82
+ export function isManualScript(script) {
83
+ return isContextMenuRunAt(script.header);
84
+ }
85
+ // Said for a `@run-at` value we don't honor (a typo). `context-menu`
86
+ // isn't one: it makes the script manual (see `CONTEXT_MENU_RUN_AT`).
87
+ export function unsupportedRunAtWarning(value) {
88
+ return `@run-at ${value} is not supported. The script runs at document-idle instead.`;
89
+ }
90
+ export function extractStartupLogs(header, relativePath,
91
+ // `sourceUsesUnsafeWindow(bodyText)` — the body names `unsafeWindow`,
92
+ // grant or no grant. Passed in rather than derived here so the scan
93
+ // happens once per load, on the same text the row stores.
94
+ usesUnsafeWindow = false) {
95
+ if (!header)
96
+ return [];
97
+ const logs = [];
98
+ const grants = getValues(header, 'grant');
99
+ const resolved = resolveGrants(grants);
100
+ const isolatedBy = isolatingHeader(header);
101
+ // The two ways a script asks for the page's window: the grant line,
102
+ // and naming `unsafeWindow` in its body without one. Everything below
103
+ // treats them alike — what the script needs is the same either way.
104
+ const wantsUnsafeWindow = resolved.granted.has('unsafeWindow') || usesUnsafeWindow;
105
+ if (grants.length > 0) {
106
+ // `none` wins over every other `@grant` line in `resolveGrants`: no
107
+ // bindings and no sandbox `window`. The author
108
+ // probably meant the other lines, so say what happened to them.
109
+ // Only the grants we'd otherwise have honored; a typo'd name gets
110
+ // its own "unsupported" line below and shouldn't be counted twice.
111
+ const overridden = grants
112
+ .map((g) => g.trim())
113
+ .filter((g) => g !== '' && g.toLowerCase() !== 'none' && canonicalizeGrant(g) !== null);
114
+ if (resolved.hasNone && overridden.length > 0) {
115
+ const log = {
116
+ level: 'warn',
117
+ message: `@grant none overrides @grant ${overridden.join(', ')}, so the script runs in the ` +
118
+ 'page with no GM APIs. Remove the none line to use them.',
119
+ };
120
+ const line = headerTagLine(header, 'grant', 'none');
121
+ if (line !== undefined)
122
+ log.location = { file: relativePath, line };
123
+ logs.push(log);
124
+ }
125
+ if (resolved.unknown.length > 0) {
126
+ const log = {
127
+ level: 'warn',
128
+ message: `Ignoring unsupported @grant ${resolved.unknown.join(', ')}`,
129
+ };
130
+ // One message can name several bad grants, and they may be spread
131
+ // over several `@grant` lines — so the location is the FIRST one,
132
+ // which is where a reader fixing them starts. The others are named
133
+ // in the message and are a line or two away.
134
+ const line = headerTagLine(header, 'grant', resolved.unknown[0]);
135
+ if (line !== undefined)
136
+ log.location = { file: relativePath, line };
137
+ logs.push(log);
138
+ }
139
+ }
140
+ // `unsafeWindow` means "the page's real window". That only exists in
141
+ // MAIN world, and `@inject-into content` / `@sandbox DOM` pin the
142
+ // script to the isolated USER_SCRIPT world, where `unsafeWindow` can
143
+ // only be the isolated realm's own `window`: page globals read as
144
+ // `undefined`. We keep the binding (matching Violentmonkey's content
145
+ // mode) but say so out loud, because the alternative is a script that
146
+ // quietly does nothing. See docs/injection.md#execution-world.
147
+ if (wantsUnsafeWindow && isolatedBy) {
148
+ logs.push({ level: 'warn', message: isolatedUnsafeWindowWarning(isolatedBy) });
149
+ }
150
+ // A granted API that needs `@connect`, with none declared. One row
151
+ // even when both spellings are granted: they are one API.
152
+ if (hasNoConnectRules(header)) {
153
+ const needsConnect = grants
154
+ .map((g) => g.trim())
155
+ .find((g) => {
156
+ const name = canonicalizeGrant(g);
157
+ return name !== null && resolved.granted.has(name)
158
+ && (findApi(name)?.requires ?? []).includes('connect');
159
+ });
160
+ if (needsConnect) {
161
+ const log = { level: 'info', message: noConnectInfo(needsConnect) };
162
+ const line = headerTagLine(header, 'grant', needsConnect);
163
+ if (line !== undefined)
164
+ log.location = { file: relativePath, line };
165
+ logs.push(log);
166
+ }
167
+ }
168
+ const runAt = unsupportedRunAt(header);
169
+ if (runAt !== undefined) {
170
+ const log = { level: 'warn', message: unsupportedRunAtWarning(runAt) };
171
+ const line = headerTagLine(header, 'run-at', runAt);
172
+ if (line !== undefined)
173
+ log.location = { file: relativePath, line };
174
+ logs.push(log);
175
+ }
176
+ // A `@match` Chrome's grammar refuses. One of these would take the
177
+ // whole registration down (Chrome refuses the spec, not the pattern),
178
+ // so `buildRegistrationSpec` drops it — and this is what says so, on
179
+ // the line that declared it.
180
+ //
181
+ // One row per bad pattern, unlike the grants above: each has its own
182
+ // reason and its own line, so collapsing them would leave the
183
+ // location pointing at one of several unrelated problems.
184
+ for (const pattern of getValues(header, 'match')) {
185
+ const problem = matchPatternProblem(pattern);
186
+ if (!problem)
187
+ continue;
188
+ const log = { level: 'warn', message: ignoredMatchMessage(problem) };
189
+ const line = headerTagLine(header, 'match', pattern);
190
+ if (line !== undefined)
191
+ log.location = { file: relativePath, line };
192
+ logs.push(log);
193
+ }
194
+ return logs;
195
+ }
196
+ // The line of the `@<tag>` declaration whose value is `value`.
197
+ //
198
+ // No "near enough" fallback to the first `@<tag>` line: that line is
199
+ // usually a perfectly good declaration, and pointing at valid code is
200
+ // worse than pointing at nothing — it sends the reader somewhere with
201
+ // nothing wrong with it. `HeaderTag.value` is trimmed by `TAG_LINE`
202
+ // and the caller reports the trimmed text, so the match holds.
203
+ function headerTagLine(header, tag, value) {
204
+ for (const t of header.tags) {
205
+ if (t.tag !== tag || t.locale)
206
+ continue;
207
+ if (t.value.trim() === value)
208
+ return t.line;
209
+ }
210
+ return undefined;
211
+ }
212
+ // The messages `validateHeader` can return. Exported for the same
213
+ // reason the split errors are (see `userscript-header.ts`): the corpus
214
+ // validator groups failures by message, so these are the strings it
215
+ // matches against rather than literals maintained in two places.
216
+ //
217
+ // Same phrasing rule too — each has to read correctly alone in the
218
+ // viewer's message bar and after the preview's `This file cannot be
219
+ // installed: ` prefix.
220
+ export const NO_NAME = 'No @name in the metadata block';
221
+ export const NO_MATCH_OR_INCLUDE = 'No @match or @include in the metadata block, so this script would never run on any page';
222
+ // No longer produced: manual scripts are supported now. Kept so
223
+ // `retiredHeaderProblemIds` can find the rows an older build refused
224
+ // with it.
225
+ export const RETIRED_CONTEXT_MENU_UNSUPPORTED = '@run-at context-menu (run from the right-click menu) is not supported, so this script can\'t run';
226
+ // Opens every parse-check verdict (`Syntax error: <what V8 said>`).
227
+ // Exported for the same reason as the constants above: the corpus
228
+ // validator groups its survey by the message AFTER this prefix, so it
229
+ // has to strip exactly what we wrote. It used to carry its own regex,
230
+ // which stopped matching the moment this string was recased — and a
231
+ // stale recognizer mislabels a whole bucket instead of failing.
232
+ export const SYNTAX_ERROR_PREFIX = 'Syntax error: ';
233
+ // Verify the parsed header has the fields we require to consider a
234
+ // script `ok`. Returns null on success, otherwise a human-readable
235
+ // problem. Required: an `@name` plus at least one usable
236
+ // `@match` / `@include` (otherwise the script can never match a page).
237
+ //
238
+ // The missing-tag failures are about something that ISN'T there, so
239
+ // none of them has a line of its own. They point at the
240
+ // `// ==UserScript==` marker instead: that's the block the missing tag
241
+ // belongs in, and where the reader has to go to add it. The unusable
242
+ // `@match` case is the exception — the offending text is right there in
243
+ // the file, so it gets that line.
244
+ export function validateHeader(header) {
245
+ // No block at all — the whole file is the answer, so no line.
246
+ if (!header)
247
+ return { message: NO_METADATA_BLOCK };
248
+ const line = header.startLine;
249
+ if (!getValue(header, 'name'))
250
+ return { message: NO_NAME, line };
251
+ const matches = getValues(header, 'match');
252
+ const includes = getValues(header, 'include');
253
+ if (matches.length === 0 && includes.length === 0) {
254
+ return { message: NO_MATCH_OR_INCLUDE, line };
255
+ }
256
+ // Patterns Chrome's grammar refuses are dropped rather than run (see
257
+ // `matchPatternProblem`), so a script whose every `@match` is one of
258
+ // those — with no `@include` to fall back on — is in the same
259
+ // position as one that declared no pattern at all: it can never match
260
+ // a page. Say so with the FIRST bad pattern's own reason, which is
261
+ // more use than the generic sentence above and points at a real line.
262
+ //
263
+ // Only the first: the others' reasons ride on `startupLogs` for an
264
+ // `ok` script, and an `invalid` row doesn't carry those. A reader who
265
+ // fixes this one sees the next on the following load, which is the
266
+ // same one-at-a-time path every other header failure takes.
267
+ if (includes.length === 0) {
268
+ const problems = matches.map((m) => matchPatternProblem(m));
269
+ const first = problems[0];
270
+ if (first && problems.every((p) => p !== null)) {
271
+ return {
272
+ message: unusableMatchMessage(first),
273
+ line: headerTagLine(header, 'match', first.pattern) ?? line,
274
+ };
275
+ }
276
+ }
277
+ return null;
278
+ }
279
+ // Compose a `StoredScript` from freshly fetched source text. Always
280
+ // records the new content (so the viewer reflects what's on disk),
281
+ // then determines status by running it through split + parse +
282
+ // validate. Any failure along that chain lands in `errorMessage` while
283
+ // the content fields stay populated. Async because hashing the source
284
+ // for the universal change-detection token uses SubtleCrypto.
285
+ export async function buildScriptFromText(args) {
286
+ const split = splitHeaderBody(args.text);
287
+ const header = parseHeader(args.text);
288
+ const matching = distillMatching(header);
289
+ // UTF-16 string length ≠ file byte length for non-ASCII content;
290
+ // TextEncoder gives us the wire-accurate UTF-8 byte count.
291
+ const size = new TextEncoder().encode(args.text).length;
292
+ const contentHash = await hashSource(args.text);
293
+ let status = 'ok';
294
+ let errorMessage;
295
+ let errorLine;
296
+ if (split.error) {
297
+ status = 'invalid';
298
+ errorMessage = split.error;
299
+ // UNTERMINATED_METADATA_BLOCK gets the line that opened the block
300
+ // that never closed. NO_METADATA_BLOCK gets nothing — there is no
301
+ // marker to point at, and the answer is the file as a whole.
302
+ errorLine = split.headerStartLine;
303
+ }
304
+ else {
305
+ const problem = validateHeader(header);
306
+ if (problem) {
307
+ status = 'invalid';
308
+ errorMessage = problem.message;
309
+ errorLine = problem.line;
310
+ }
311
+ }
312
+ // Same bytes as last time → preserve the prior load timestamp so
313
+ // the collections page's "what changed" highlight stays accurate even
314
+ // when the cheap skip paths didn't fire. Status still reflects the
315
+ // current parse outcome (parser fixes between refreshes are rare
316
+ // but shouldn't be silently masked).
317
+ const unchanged = args.prior?.contentHash === contentHash;
318
+ const loadedAt = unchanged && args.prior ? args.prior.loadedAt : args.now;
319
+ const entry = {
320
+ id: args.id,
321
+ collectionId: args.collectionId,
322
+ relativePath: args.relativePath,
323
+ fileUrl: args.fileUrl,
324
+ status,
325
+ size,
326
+ contentHash,
327
+ headerText: split.headerText,
328
+ bodyText: split.bodyText,
329
+ header,
330
+ matching,
331
+ loadedAt,
332
+ // New rows default enabled; existing rows preserve their prior
333
+ // value via the merge in rebuildCollection / refreshScript
334
+ // (script-store.ts).
335
+ enabled: true,
336
+ };
337
+ if (errorMessage) {
338
+ setScriptError(entry, errorMessage, locationIn(args.relativePath, errorLine));
339
+ }
340
+ if (args.httpEtag)
341
+ entry.httpEtag = args.httpEtag;
342
+ if (args.httpLastModified)
343
+ entry.httpLastModified = args.httpLastModified;
344
+ // Only persist leadingText when non-empty so the vast majority of
345
+ // scripts (which start at `// ==UserScript==`) don't carry an
346
+ // empty-string field.
347
+ if (split.leadingText)
348
+ entry.leadingText = split.leadingText;
349
+ // Parse-time diagnostics. Only attached when status is `ok` — error
350
+ // rows don't run and never reach the wrapper, so the diagnostics
351
+ // would just be dead bytes in storage. Same shape filter for the
352
+ // empty case keeps the persisted row free of empty arrays.
353
+ if (status === 'ok') {
354
+ // Derived once, here, from the text this row stores — the world
355
+ // decision and the emitted bindings both read the persisted flag.
356
+ const usesUnsafeWindow = sourceUsesUnsafeWindow(split.bodyText);
357
+ if (usesUnsafeWindow)
358
+ entry.usesUnsafeWindow = true;
359
+ const startupLogs = extractStartupLogs(header, args.relativePath, usesUnsafeWindow);
360
+ if (startupLogs.length > 0)
361
+ entry.startupLogs = startupLogs;
362
+ }
363
+ return entry;
364
+ }
365
+ // Apply a hard fetch failure (network error, 404, unreadable file,
366
+ // disconnected mount) to an existing entry, preserving the prior
367
+ // content either way.
368
+ //
369
+ // - prior was `ok`: this is a *refresh* failure, not a content
370
+ // problem — we have a last-known-good copy. Keep it running
371
+ // (`status: 'ok'`) and record `refreshError` so the UI can flag
372
+ // REFRESH FAILED. A transient logout / network blip must never
373
+ // silently disable a working script.
374
+ // - prior was anything else (already invalid): no good copy to
375
+ // protect, so flip to `invalid` with the message as before.
376
+ export function applyFetchError(existing, errorMessage) {
377
+ if (existing.status === 'ok') {
378
+ return { ...existing, refreshError: errorMessage };
379
+ }
380
+ const next = { ...existing, status: 'invalid' };
381
+ // No location: we never got the bytes, so there is no content this
382
+ // message is about. Routed through the setter so the prior row's
383
+ // location can't survive into a message it has nothing to do with.
384
+ setScriptError(next, errorMessage, undefined);
385
+ return next;
386
+ }
387
+ // Write a row's error message and its location together. They are one
388
+ // fact stored in two fields, and every path that changes one has to
389
+ // change the other: a location left behind by a previous failure would
390
+ // point the reader at a line that has nothing to do with what the row
391
+ // now says. Passing `undefined` for the message clears both.
392
+ //
393
+ // The location is validated on the way in rather than at each of the
394
+ // surfaces that renders it — these rows are persisted and reloaded, and
395
+ // a click target is not the place to discover a malformed line number.
396
+ export function setScriptError(entry, message, location) {
397
+ if (message === undefined)
398
+ delete entry.errorMessage;
399
+ else
400
+ entry.errorMessage = message;
401
+ const valid = validLocation(location);
402
+ if (valid)
403
+ entry.errorLocation = valid;
404
+ else
405
+ delete entry.errorLocation;
406
+ }
407
+ // A place in the script's own source. `file` is the relative path
408
+ // because that is the identity the source map gives a script's own body
409
+ // (`buildParseCheckSource` passes it as `mainUrl`) and what the viewer
410
+ // matches its main tab against — see `DiagnosticLocation`.
411
+ //
412
+ // Returns a location even with no line: "this file" is still somewhere
413
+ // to go, and it is the honest answer for a file with no metadata block
414
+ // at all.
415
+ export function locationIn(file, line) {
416
+ return line === undefined ? { file } : { file, line };
417
+ }
418
+ // Build an `invalid` entry for a brand-new (never-loaded-successfully)
419
+ // script whose first fetch failed. No content to preserve, so the
420
+ // content fields stay empty and size is null.
421
+ export function buildInitialInvalidEntry(args) {
422
+ return {
423
+ id: args.id,
424
+ collectionId: args.collectionId,
425
+ relativePath: args.relativePath,
426
+ fileUrl: args.fileUrl,
427
+ status: 'invalid',
428
+ errorMessage: args.errorMessage,
429
+ size: null,
430
+ headerText: '',
431
+ bodyText: '',
432
+ header: null,
433
+ matching: { matches: [], excludes: [], includes: [] },
434
+ loadedAt: args.now,
435
+ enabled: true,
436
+ };
437
+ }
438
+ // Resolve + fetch a script's `@require` bodies AND `@resource` blobs,
439
+ // attaching both lists onto the StoredScript. On any failure preserves
440
+ // the prior's last-known-good state and flips status to 'invalid' with a
441
+ // message naming the offending URL — mirrors `applyFetchError`'s
442
+ // content-preservation policy for body-fetch failures, so a flaky
443
+ // dependency doesn't clobber the user's working version. Without
444
+ // declared deps a userscript usually breaks worse than not injecting,
445
+ // so erroring out is safer than running headless.
446
+ //
447
+ // `recheck === 'preserve'` reuses every cached prior verbatim and only
448
+ // fetches refs the script newly declares. `recheck === 'all'` refetches
449
+ // every declared file (used by explicit Refresh). The fetchCache is
450
+ // shared across all scripts in a single refresh pass so two scripts
451
+ // referencing the same canonical URL share one fetch — and a require
452
+ // + a resource pointing at the same URL also share a single fetch.
453
+ //
454
+ // Exported for the development harness (`src/harness/prepare.ts`),
455
+ // which runs this half of the pipeline under Node for `sm-dev` and the
456
+ // corpus validator. The point is to exercise the production path, so
457
+ // it calls this rather than restating the sequence.
458
+ export async function applyFiles(built, prior, recheck, fetchCache, now,
459
+ // What this script's collection says about local stand-ins — whether
460
+ // they're allowed, and the collection's own root. Defaults to
461
+ // disabled so a caller outside the extension keeps the plain
462
+ // declared-URL behavior.
463
+ localFiles = { enabled: false },
464
+ // Whether to wait for the parse verdict before returning. Defaults to
465
+ // 'await' so every caller that hasn't thought about it keeps today's
466
+ // behavior; see `ParseCheckMode`.
467
+ parseCheck = 'await',
468
+ // A scratch script's pushed bodies (sm-dev's local files), which the
469
+ // store reads from the script's `scratchFiles:` row on every pass,
470
+ // like a local stand-in, since a push may have changed them.
471
+ pushedFiles) {
472
+ const priorRequires = new Map((prior?.requires ?? []).map((r) => [r.url, r]));
473
+ const priorResources = new Map((prior?.resources ?? []).map((r) => [r.url, r]));
474
+ // Configured roots come from storage, so make sure that read has
475
+ // landed before asking — a refresh racing SW startup would otherwise
476
+ // resolve as if none were set and persist that.
477
+ if (localFiles.enabled)
478
+ await localRootsReady();
479
+ const localRoots = localSearchRoots(built.fileUrl, localFiles);
480
+ // Before the shortcut below, which returns early: the icon has its
481
+ // own rule for when to look again. Not for a scratch script: sm-dev
482
+ // pushes its `@require` / `@resource` files but not its icon, and
483
+ // has already checked the icon on disk itself.
484
+ if (!pushedFiles)
485
+ await applyIconCheck(built, prior, recheck, fetchCache, localRoots, localFiles.lookup);
486
+ // `recheck: 'preserve'` short-circuits when both lists are identical
487
+ // to the prior's: no resolve work, no fetch, just keep what we had.
488
+ // This is the hot path on opportunistic refresh.
489
+ //
490
+ // Skipped entirely when the script has local search roots: a stand-in
491
+ // has to be re-read every pass or an edit on disk wouldn't surface
492
+ // until the user clicked Refresh, which is the workflow the whole
493
+ // feature exists for. The per-entry loaders still preserve cached
494
+ // REMOTE entries in that case, so only local reads are paid for.
495
+ if (recheck === 'preserve' && prior && localRoots.length === 0 && !pushedFiles) {
496
+ const newReqRefs = distillRequires(built.header);
497
+ const sameReqs = newReqRefs.length === (prior.requires?.length ?? 0) &&
498
+ newReqRefs.every((ref, i) => prior.requires?.[i]?.ref === ref);
499
+ const newResDecls = distillResources(built.header);
500
+ const sameRess = newResDecls.length === (prior.resources?.length ?? 0) &&
501
+ newResDecls.every((d, i) => {
502
+ const p = prior.resources?.[i];
503
+ return p && p.name === d.name && p.ref === d.ref;
504
+ });
505
+ if (sameReqs && sameRess) {
506
+ if (prior.requires)
507
+ built.requires = prior.requires;
508
+ else
509
+ delete built.requires;
510
+ if (prior.resources)
511
+ built.resources = prior.resources;
512
+ else
513
+ delete built.resources;
514
+ // Still route through the parse check. `built` is a fresh row,
515
+ // so returning here directly would drop `parseCheckedHash` and
516
+ // make every later refresh re-compile content that never moved.
517
+ // The check itself is usually free on this path: the deps are
518
+ // the prior's by construction, so the fingerprint matches unless
519
+ // the BODY changed — which is exactly when a re-check is right.
520
+ return timePhase('parse-check', () => applyParseCheck(built, prior, parseCheck));
521
+ }
522
+ }
523
+ const result = await timePhase('deps', () => attachFiles({
524
+ scriptFileUrl: built.fileUrl,
525
+ header: built.header,
526
+ priorRequires,
527
+ priorResources,
528
+ recheck,
529
+ fetchCache,
530
+ now,
531
+ localRoots,
532
+ ...(localFiles.lookup ? { localLookup: localFiles.lookup } : {}),
533
+ ...(pushedFiles ? { pushedFiles } : {}),
534
+ }));
535
+ if (result.errorMessage) {
536
+ // Preserve the last-known-good script state when we have one.
537
+ // This rolls the *entire* refresh back to the prior — the new
538
+ // body isn't applied either, because injecting a fresh body
539
+ // against partial/stale dependencies would be unsafe.
540
+ //
541
+ // A failed @require/@resource fetch against a prior `ok` row is a
542
+ // *refresh* failure, not a content problem: keep the script
543
+ // running on its cached deps (`status: 'ok'`) and record
544
+ // `refreshError` so the UI flags REFRESH FAILED. A flaky dep host
545
+ // (or a logged-out CDN) must never silently disable a working
546
+ // script. Note we can only catch dep *fetch* failures this way —
547
+ // a 200 returning a login page in place of a @require is caught by
548
+ // the require markup gate (HTML rejected) in external-files;
549
+ // @resource bodies are arbitrary bytes and can't be validated.
550
+ if (prior && prior.status === 'ok') {
551
+ return { ...prior, refreshError: result.errorMessage };
552
+ }
553
+ built.status = 'invalid';
554
+ // The file that failed has no content to point into, so the
555
+ // location is the `@require` / `@resource` line that named it —
556
+ // which is the line the reader has to change anyway.
557
+ setScriptError(built, result.errorMessage, locationIn(built.relativePath, result.errorLine));
558
+ delete built.requires;
559
+ delete built.resources;
560
+ return built;
561
+ }
562
+ if (result.requires.length > 0)
563
+ built.requires = result.requires;
564
+ else
565
+ delete built.requires;
566
+ if (result.resources.length > 0)
567
+ built.resources = result.resources;
568
+ else
569
+ delete built.resources;
570
+ // A dependency whose content changed is fresh content for the script
571
+ // too: what runs is the composed body. Everything that asks "is there
572
+ // a newer copy than the page ran" compares `loadedAt` (the reload
573
+ // swap, marking open tabs for a reload, an old wrapper's report), so
574
+ // without this a library edit reached none of them until the next
575
+ // reload after the re-registration.
576
+ if (prior && dependenciesChanged(prior, built))
577
+ built.loadedAt = Math.max(built.loadedAt, now);
578
+ return timePhase('parse-check', () => applyParseCheck(built, prior, parseCheck));
579
+ }
580
+ // Whether the `@require` / `@resource` content differs between two rows.
581
+ // Compared on the content hashes, so a re-read of the same bytes, or the
582
+ // same bytes from another place, is no change.
583
+ function dependenciesChanged(a, b) {
584
+ const key = (s) => JSON.stringify([
585
+ (s.requires ?? []).map((r) => [r.url, r.contentHash]),
586
+ (s.resources ?? []).map((r) => [r.name, r.url, r.contentHash]),
587
+ ]);
588
+ return key(a) !== key(b);
589
+ }
590
+ // Record the `@icon` check on `built`, and a problem it found as a
591
+ // load-time warning on the `@icon` line.
592
+ async function applyIconCheck(built, prior, recheck, fetchCache, localRoots, localLookup) {
593
+ // A row reused whole (the "unchanged" refresh path starts from a copy
594
+ // of the prior) already carries the last check's warning, so drop it
595
+ // before adding this one.
596
+ const stale = iconCheckLog(built);
597
+ if (stale && built.startupLogs) {
598
+ const kept = built.startupLogs.filter((l) => l.message !== stale.message);
599
+ if (kept.length > 0)
600
+ built.startupLogs = kept;
601
+ else
602
+ delete built.startupLogs;
603
+ }
604
+ delete built.iconCheck;
605
+ // Invalid rows don't run and carry no startup logs; see `startupLogs`.
606
+ if (built.status !== 'ok')
607
+ return;
608
+ const check = await timePhase('icon-check', () => checkScriptIcon({
609
+ header: built.header,
610
+ scriptFileUrl: built.fileUrl,
611
+ prior: prior?.iconCheck,
612
+ recheck,
613
+ fetchCache,
614
+ localRoots,
615
+ ...(localLookup ? { localLookup } : {}),
616
+ }));
617
+ if (!check)
618
+ return;
619
+ built.iconCheck = check;
620
+ const log = iconCheckLog(built);
621
+ if (log)
622
+ built.startupLogs = [...(built.startupLogs ?? []), log];
623
+ }
624
+ // A stored row's startup logs, derived again: the header checks, plus
625
+ // the stored `@icon` warning. That one comes from a download, not the
626
+ // header, so it is carried over rather than re-derived. For the
627
+ // backfills that rebuild a row's logs without rebuilding the row.
628
+ export function rederivedStartupLogs(script, usesUnsafeWindow) {
629
+ const logs = [...extractStartupLogs(script.header, script.relativePath, usesUnsafeWindow)];
630
+ const iconLog = iconCheckLog(script);
631
+ if (iconLog)
632
+ logs.push(iconLog);
633
+ return logs;
634
+ }
635
+ // The load-time warning for a stored `@icon` problem, or null when
636
+ // there is none.
637
+ function iconCheckLog(script) {
638
+ const message = script.iconCheck?.message;
639
+ if (!message)
640
+ return null;
641
+ return {
642
+ level: 'warn',
643
+ message,
644
+ location: locationIn(script.relativePath, iconDeclFromHeader(script.header)?.line),
645
+ };
646
+ }
647
+ // Exported (read-only by convention) for script-store.ts's rechecks.
648
+ export let parseChecker = null;
649
+ export function setParseChecker(fn) {
650
+ parseChecker = fn;
651
+ }
652
+ // Fingerprint of everything that can affect whether the injected entry
653
+ // parses: the body, each `@require` body in order, and our own
654
+ // extension version.
655
+ //
656
+ // The body's `contentHash` covers the WHOLE file including the header,
657
+ // so `@grant` / `@require` / `@inject-into` edits all move the key.
658
+ //
659
+ // The version leg is there because how WE compose and wrap the user
660
+ // program decides what parses, and that can change between releases
661
+ // with nothing about the script itself changing. (Moving the user
662
+ // program into its own nested scope, for one, turned scripts that
663
+ // redeclare a `GM_*` name from failures into passes; see
664
+ // `wrapCombinedEntry` in injector.ts.) Without the version in the key,
665
+ // a cached verdict would survive the upgrade.
666
+ export function parseCheckKey(script) {
667
+ const reqs = (script.requires ?? []).map((r) => r.contentHash).join(',');
668
+ return `${extensionVersion()}|${script.contentHash ?? ''}|${reqs}`;
669
+ }
670
+ // Guarded because this module is also loaded by the unit suite, which
671
+ // runs the compiled output under plain Node with no `chrome` global.
672
+ // An empty version there just means the version leg contributes
673
+ // nothing to the key, which is correct for a context that never runs
674
+ // the check anyway.
675
+ function extensionVersion() {
676
+ if (typeof chrome === 'undefined' || !chrome.runtime?.getManifest)
677
+ return '';
678
+ return chrome.runtime.getManifest().version;
679
+ }
680
+ // Compile the script's user program without running it, and flip the
681
+ // row to `invalid` if V8 refuses it.
682
+ //
683
+ // Why here: this is the last point in the load pipeline where the body
684
+ // and every `@require` are both present and final. Before this
685
+ // existed, a syntax error anywhere in the concatenated program was
686
+ // only discovered at injection time — on a page the script matched,
687
+ // reported by the seal entry, and only as "a @require or the script
688
+ // body has a syntax error". See [docs/parse-check.md].
689
+ //
690
+ // `invalid` is deliberate rather than `refreshError`: unlike a failed
691
+ // dependency fetch, this is not transient and re-running won't help —
692
+ // the content we hold genuinely cannot run. Callers upstream still
693
+ // apply their own preservation policy on top; `rebuildCollection`
694
+ // (script-store.ts) keeps
695
+ // a prior good copy running when an *http* refresh comes back invalid
696
+ // (a login page served in place of the script), while a file:// edit
697
+ // flags immediately, which is what a local dev wants.
698
+ //
699
+ // Fails open. `checkParses` returns null when the machinery is
700
+ // unavailable, and null must never become `invalid` — a working script
701
+ // must not break because our offscreen document didn't open. The seal
702
+ // entry remains the backstop for anything missed here.
703
+ export async function applyParseCheck(built, prior, mode = 'await') {
704
+ // Nothing to check on a row that already failed for another reason
705
+ // (missing file, bad header) — there may be no body at all.
706
+ if (built.status !== 'ok')
707
+ return built;
708
+ const key = parseCheckKey(built);
709
+ // Unchanged since the last check, so the previous verdict still
710
+ // holds. This is what keeps opportunistic refreshes from paying a
711
+ // round trip per script for content that didn't move. Applies in
712
+ // 'defer' mode too — reusing a cached verdict costs nothing, so a
713
+ // deferring caller only leaves the row unchecked when the content
714
+ // really moved.
715
+ if (prior?.parseCheckedHash === key && prior.status === 'ok') {
716
+ built.parseCheckedHash = key;
717
+ return built;
718
+ }
719
+ // Deferred: return the row with no `parseCheckedHash`, which is the
720
+ // same state a row is in when the check couldn't run. The caller runs
721
+ // `checkParsesInBackground` after its write, and `verifyParseChecks`
722
+ // catches the row before registration either way (both in
723
+ // script-store.ts).
724
+ if (mode === 'defer')
725
+ return built;
726
+ if (!parseChecker)
727
+ return built;
728
+ let verdict;
729
+ try {
730
+ verdict = await parseChecker(built);
731
+ }
732
+ catch (err) {
733
+ // The checker itself failed (a storage read for a storage-granting
734
+ // script's baked-in values, the offscreen document refusing to
735
+ // open). Not the script's fault; skip rather than accuse it.
736
+ dlog(`[SourceMonkey] parse check skipped for ${built.relativePath}: ${describeError(err)}`);
737
+ return built;
738
+ }
739
+ if (!verdict)
740
+ return built;
741
+ // `unknown` is the machinery saying it couldn't decide — a stack
742
+ // overflow in the parser, a sandbox that lost its CSP. It must not
743
+ // become `invalid`, and it must not be cached as a pass either.
744
+ if (verdict.status === 'unknown') {
745
+ dlog(`[SourceMonkey] parse check inconclusive for ${built.relativePath}: ${verdict.message ?? ''}`);
746
+ return built;
747
+ }
748
+ if (verdict.status === 'ok') {
749
+ built.parseCheckedHash = key;
750
+ return built;
751
+ }
752
+ built.status = 'invalid';
753
+ // The message carries no `(in …)` suffix any more — the location
754
+ // rides alongside it so surfaces can make it clickable, and each of
755
+ // them appends the suffix itself via `describeDiagnostic`. Anything
756
+ // reading `errorMessage` on its own therefore has to pair it with
757
+ // `errorLocation` to say the whole thing.
758
+ setScriptError(built,
759
+ // No "script source has a…" prefix: `errorLocation` names the file,
760
+ // and that file is routinely a `@require` rather than the script.
761
+ `${SYNTAX_ERROR_PREFIX}${verdict.message ?? 'unknown'}`, verdict.location);
762
+ // Don't record the fingerprint: the row is invalid, and a future
763
+ // refresh that produces the same bytes should re-derive that rather
764
+ // than inherit a stale pass. (`prior` above only short-circuits on a
765
+ // prior `ok`.)
766
+ delete built.parseCheckedHash;
767
+ return built;
768
+ }
769
+ // The fields a parse verdict owns, copied from the checked row onto
770
+ // the stored one. Everything else on the stored row is left as it is
771
+ // found — the check has no opinion about it.
772
+ export function applyVerdictFields(current, checked) {
773
+ const merged = { ...current, status: checked.status };
774
+ setScriptError(merged, checked.errorMessage, checked.errorLocation);
775
+ if (checked.parseCheckedHash)
776
+ merged.parseCheckedHash = checked.parseCheckedHash;
777
+ else
778
+ delete merged.parseCheckedHash;
779
+ return merged;
780
+ }
781
+ // Coerce a caught throw into a printable string. Exported so other
782
+ // modules (notably the injector) don't have to duplicate the helper.
783
+ export function describeError(err) {
784
+ if (err instanceof Error)
785
+ return err.message;
786
+ return String(err);
787
+ }