Skip to content

Commit 35e094a

Browse files
committed
feat: use content script to rewrite async chunks
1 parent c07255c commit 35e094a

File tree

9 files changed

+213
-8
lines changed

9 files changed

+213
-8
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
# Changelog
44

5+
## 0.11.0
6+
7+
- Improved compatibility with Figma's lazy loading mechanism.
8+
- Improved rewriting rules.
9+
510
## 0.10.2
611

712
- Fixed an issue with CSS code generation when optimizing dimension code.

entrypoints/background.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
1-
const RULE_URL = 'https://ecomfe.github.io/tempad-dev/figma.json'
1+
import { RULES_URL } from '@/rewrite/shared'
2+
import rules from '@/public/rules/figma.json'
3+
import type { Rules } from './types'
24

35
const SYNC_ALARM = 'sync-rules'
46
const SYNC_INTERVAL_MINUTES = 10
57

68
async function fetchRules() {
79
try {
8-
const res = await fetch(RULE_URL, { cache: 'no-store' })
9-
if (!res.ok) {
10-
console.error('[tempad-dev] Failed to fetch rules:', res.statusText)
11-
return
10+
let newRules: Rules
11+
12+
if (import.meta.env.DEV) {
13+
newRules = rules as Rules
14+
console.log('[tempad-dev] Loaded local rules (dev).')
15+
} else {
16+
const res = await fetch(RULES_URL, { cache: 'no-store' })
17+
if (!res.ok) {
18+
console.error('[tempad-dev] Failed to fetch rules:', res.statusText)
19+
return
20+
}
21+
22+
newRules = (await res.json()) as Rules
1223
}
1324

14-
const newRules = await res.json()
1525
const oldIds = (await browser.declarativeNetRequest.getDynamicRules()).map(({ id }) => id)
1626

1727
await browser.declarativeNetRequest.updateEnabledRulesets({

entrypoints/rewrite.content.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import rules from '@/public/rules/figma.json'
2+
import { applyGroups, RULES_URL, REWRITE_RULE_ID } from '@/rewrite/shared'
3+
import { GROUPS } from '@/rewrite/config'
4+
import type { BlobHandle, CacheEntry, Rules } from './types'
5+
6+
export default defineContentScript({
7+
matches: [
8+
'https://www.figma.com/file/*',
9+
'https://www.figma.com/design/*',
10+
'https://www.figma.com/deck/*',
11+
'https://www.figma.com/proto/*'
12+
],
13+
runAt: 'document_start',
14+
world: 'MAIN',
15+
main() {
16+
function extractRegexFilter(source: Rules): RegExp | null {
17+
try {
18+
const rule = source.find((r) => r.id === REWRITE_RULE_ID)
19+
return rule?.condition?.regexFilter ? new RegExp(rule.condition.regexFilter, 'i') : null
20+
} catch {
21+
return null
22+
}
23+
}
24+
25+
async function loadRemoteRegex(url: string): Promise<RegExp | null> {
26+
try {
27+
const resp = await fetch(url, { credentials: 'omit', cache: 'no-cache' })
28+
if (!resp.ok) {
29+
return null
30+
}
31+
return extractRegexFilter(await resp.json())
32+
} catch {
33+
return null
34+
}
35+
}
36+
let targetRegex: RegExp | null = extractRegexFilter(rules as Rules)
37+
38+
loadRemoteRegex(RULES_URL).then((remoteRegex) => {
39+
if (remoteRegex) {
40+
targetRegex = remoteRegex
41+
console.log('[tempad-dev] Loaded remote rewrite rules.')
42+
} else {
43+
console.warn('[tempad-dev] Failed to fetch rewrite rules; using bundled rules.')
44+
}
45+
})
46+
47+
const { appendChild, insertBefore } = Element.prototype
48+
49+
const processedScripts = new WeakSet<HTMLScriptElement>()
50+
51+
const blobCache = new Map<string, CacheEntry>()
52+
53+
function shouldRewrite(url: string): boolean {
54+
return !!targetRegex && targetRegex.test(url)
55+
}
56+
57+
function isRewritableScript(node: Node): node is HTMLScriptElement {
58+
return (
59+
node instanceof HTMLScriptElement && typeof node.src === 'string' && node.src.length > 0
60+
)
61+
}
62+
63+
function releaseBlobUrl(src: string, usedUrl: string): void {
64+
const entry = blobCache.get(src)
65+
if (!entry || entry.url !== usedUrl) return
66+
entry.ref -= 1
67+
if (entry.ref <= 0) {
68+
try {
69+
URL.revokeObjectURL(entry.url)
70+
} catch {}
71+
blobCache.delete(src)
72+
}
73+
}
74+
75+
async function acquireBlobUrl(src: string): Promise<BlobHandle> {
76+
const existing = blobCache.get(src)
77+
if (existing) {
78+
existing.ref += 1
79+
return { url: existing.url, release: () => releaseBlobUrl(src, existing.url) }
80+
}
81+
82+
const resp = await fetch(src, { credentials: 'include', cache: 'force-cache' })
83+
const originalText = await resp.text()
84+
const { content, changed } = applyGroups(originalText, GROUPS)
85+
86+
if (changed) {
87+
console.log(`[tempad-dev] Rewrote async script: ${src}`)
88+
}
89+
90+
const blob = new Blob([content], { type: 'application/javascript; charset=utf-8' })
91+
const url = URL.createObjectURL(blob)
92+
blobCache.set(src, { url, ref: 1 })
93+
return { url, release: () => releaseBlobUrl(src, url) }
94+
}
95+
96+
function normalizedInsert(parent: Element, node: Node, before: Node | null): void {
97+
if (before) insertBefore.call(parent, node, before)
98+
else appendChild.call(parent, node)
99+
}
100+
101+
async function rewriteAndInsert(
102+
parent: Element,
103+
script: HTMLScriptElement,
104+
before: Node | null
105+
): Promise<void> {
106+
if (processedScripts.has(script)) {
107+
normalizedInsert(parent, script, before)
108+
return
109+
}
110+
processedScripts.add(script)
111+
112+
if (!shouldRewrite(script.src)) {
113+
normalizedInsert(parent, script, before)
114+
return
115+
}
116+
117+
try {
118+
const { url, release } = await acquireBlobUrl(script.src)
119+
script.removeAttribute('integrity')
120+
script.addEventListener('load', release, { once: true })
121+
script.addEventListener('error', release, { once: true })
122+
script.src = url
123+
} catch {}
124+
125+
normalizedInsert(parent, script, before)
126+
}
127+
128+
const newAppendChild: typeof Element.prototype.appendChild = function <T extends Node>(
129+
this: Element,
130+
node: T
131+
): T {
132+
if (isRewritableScript(node)) {
133+
void rewriteAndInsert(this, node, null)
134+
return node
135+
}
136+
appendChild.call(this, node)
137+
return node
138+
}
139+
140+
const newInsertBefore: typeof Element.prototype.insertBefore = function <T extends Node>(
141+
this: Element,
142+
node: T,
143+
before: Node | null
144+
): T {
145+
if (isRewritableScript(node)) {
146+
void rewriteAndInsert(this, node, before)
147+
return node
148+
}
149+
insertBefore.call(this, node, before)
150+
return node
151+
}
152+
153+
Element.prototype.appendChild = newAppendChild
154+
Element.prototype.insertBefore = newInsertBefore
155+
}
156+
})

entrypoints/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export interface CacheEntry {
2+
url: string
3+
ref: number
4+
}
5+
6+
export interface BlobHandle {
7+
url: string
8+
release: () => void
9+
}
10+
11+
export type Rules = NonNullable<
12+
Parameters<typeof browser.declarativeNetRequest.updateDynamicRules>[number]['addRules']
13+
>

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "tempad-dev",
33
"description": "Inspect panel on Figma, for everyone.",
44
"private": true,
5-
"version": "0.10.2",
5+
"version": "0.11.0",
66
"type": "module",
77
"scripts": {
88
"dev": "wxt",

public/rules/figma.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,23 @@
2828
"regexFilter": "/webpack-artifacts/assets/(?:figma_app[^.]+|[0-9a-zA-Z-]+)\\.min\\.js(?:\\.br)?$",
2929
"resourceTypes": ["script"]
3030
}
31+
},
32+
{
33+
"id": 99,
34+
"priority": 1000,
35+
"action": {
36+
"type": "modifyHeaders",
37+
"responseHeaders": [
38+
{ "header": "Access-Control-Allow-Origin", "operation": "set", "value": "*" },
39+
{ "header": "Access-Control-Allow-Methods", "operation": "set", "value": "GET, OPTIONS" },
40+
{ "header": "Access-Control-Allow-Headers", "operation": "set", "value": "*" }
41+
]
42+
},
43+
"condition": {
44+
"resourceTypes": ["xmlhttprequest"],
45+
"initiatorDomains": ["www.figma.com"],
46+
"requestDomains": ["ecomfe.github.io"],
47+
"regexFilter": "/tempad-dev/figma\\.json$"
48+
}
3149
}
3250
]

rewrite/figma.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ async function rewriteScript() {
2424
const { content: afterRules, changed } = applyGroups(original, GROUPS)
2525

2626
if (changed) {
27-
console.log(`Rewrote script: ${src}`)
27+
console.log(`[tempad-dev] Rewrote script: ${src}`)
2828
}
2929

3030
const content = afterRules.replaceAll('delete window.figma', 'window.figma = undefined')

rewrite/shared.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import type { Group } from './config'
22

3+
export const RULES_URL = 'https://ecomfe.github.io/tempad-dev/figma.json'
4+
export const REWRITE_RULE_ID = 2
5+
36
export function groupMatches(content: string, group: Group) {
47
const markers = group.markers || []
58
return markers.every((marker) => content.includes(marker))

0 commit comments

Comments
 (0)