Skip to content

Commit 525662c

Browse files
authored
Merge pull request #13 from dev-five-git/wrap-url
Wrap url
2 parents e077905 + 3769861 commit 525662c

File tree

7 files changed

+163
-104
lines changed

7 files changed

+163
-104
lines changed

src/__tests__/code.test.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,21 @@ import {
88
mock,
99
spyOn,
1010
} from 'bun:test'
11-
import { registerCodegen, run, runCommand } from '../code'
1211
import * as devupModule from '../commands/devup'
1312
import * as exportAssetsModule from '../commands/exportAssets'
1413
import * as exportComponentsModule from '../commands/exportComponents'
1514

16-
beforeAll(() => {
15+
let codeModule: typeof import('../code-impl')
16+
17+
beforeAll(async () => {
1718
;(globalThis as { figma?: unknown }).figma = {
1819
editorType: 'dev',
1920
mode: 'codegen',
2021
command: 'noop',
2122
codegen: { on: mock(() => {}) },
2223
closePlugin: mock(() => {}),
2324
} as unknown as typeof figma
25+
codeModule = await import('../code-impl')
2426
})
2527

2628
beforeEach(() => {
@@ -61,7 +63,7 @@ describe('runCommand', () => {
6163
closePlugin,
6264
} as unknown as typeof figma
6365

64-
await runCommand(figmaMock as typeof figma)
66+
await codeModule.runCommand(figmaMock as typeof figma)
6567

6668
switch (fn) {
6769
case 'exportDevup':
@@ -131,7 +133,7 @@ describe('registerCodegen', () => {
131133
codegen: { on: mock(() => {}) },
132134
closePlugin: mock(() => {}),
133135
} as unknown as typeof figma
134-
registerCodegen(figmaMock)
136+
codeModule.registerCodegen(figmaMock)
135137
expect(figmaMock.codegen.on).toHaveBeenCalledWith(
136138
'generate',
137139
expect.any(Function),
@@ -145,23 +147,38 @@ describe('registerCodegen', () => {
145147
})
146148
})
147149

148-
it('should not register codegen if figma is not defined', () => {
149-
run(undefined as unknown as typeof figma)
150+
it('should not register codegen if figma is not defined', async () => {
151+
codeModule.run(undefined as unknown as typeof figma)
150152
expect(devupModule.exportDevup).not.toHaveBeenCalled()
151153
expect(devupModule.importDevup).not.toHaveBeenCalled()
152154
expect(exportAssetsModule.exportAssets).not.toHaveBeenCalled()
153155
expect(exportComponentsModule.exportComponents).not.toHaveBeenCalled()
154156
})
155157

156-
it('should run command', () => {
158+
it('should run command', async () => {
157159
const figmaMock = {
158160
editorType: 'figma',
159161
command: 'export-devup',
160162
closePlugin: mock(() => {}),
161163
} as unknown as typeof figma
162-
run(figmaMock as typeof figma)
164+
codeModule.run(figmaMock as typeof figma)
163165
expect(devupModule.exportDevup).toHaveBeenCalledWith('json')
164166
expect(devupModule.importDevup).not.toHaveBeenCalled()
165167
expect(exportAssetsModule.exportAssets).not.toHaveBeenCalled()
166168
expect(exportComponentsModule.exportComponents).not.toHaveBeenCalled()
167169
})
170+
171+
it('auto-runs on module load when figma is present', async () => {
172+
const codegenOn = mock(() => {})
173+
;(globalThis as { figma?: unknown }).figma = {
174+
editorType: 'dev',
175+
mode: 'codegen',
176+
command: 'noop',
177+
codegen: { on: codegenOn },
178+
closePlugin: mock(() => {}),
179+
} as unknown as typeof figma
180+
181+
await import(`../code?with-figma=${Date.now()}`)
182+
183+
expect(codegenOn).toHaveBeenCalledWith('generate', expect.any(Function))
184+
})

src/code-impl.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { Codegen } from './codegen/Codegen'
2+
import { exportDevup, importDevup } from './commands/devup'
3+
import { exportAssets } from './commands/exportAssets'
4+
import { exportComponents } from './commands/exportComponents'
5+
6+
export function registerCodegen(ctx: typeof figma) {
7+
if (ctx.editorType === 'dev' && ctx.mode === 'codegen') {
8+
ctx.codegen.on('generate', async ({ node, language, ...rest }) => {
9+
console.info(rest, node)
10+
switch (language) {
11+
case 'devup-ui': {
12+
const time = Date.now()
13+
const codegen = new Codegen(node)
14+
await codegen.run()
15+
const componentsCodes = codegen.getComponentsCodes()
16+
console.info(`[benchmark] devup-ui end ${Date.now() - time}ms`)
17+
return [
18+
...(node.type === 'COMPONENT' ||
19+
node.type === 'COMPONENT_SET' ||
20+
node.type === 'INSTANCE'
21+
? []
22+
: [
23+
{
24+
title: node.name,
25+
language: 'TYPESCRIPT',
26+
code: codegen.getCode(),
27+
} as const,
28+
]),
29+
...(componentsCodes.length > 0
30+
? ([
31+
{
32+
title: `${node.name} - Components`,
33+
language: 'TYPESCRIPT',
34+
code: componentsCodes.map((code) => code[1]).join('\n\n'),
35+
},
36+
{
37+
title: `${node.name} - Components CLI`,
38+
language: 'BASH',
39+
code: componentsCodes
40+
.map(
41+
([componentName, code]) =>
42+
`echo '${code}' > ${componentName}.tsx`,
43+
)
44+
.join('\n'),
45+
},
46+
] as const)
47+
: []),
48+
]
49+
}
50+
}
51+
return []
52+
})
53+
}
54+
}
55+
56+
export function runCommand(ctx: typeof figma = figma) {
57+
switch (ctx.command) {
58+
case 'export-devup':
59+
exportDevup('json').finally(() => ctx.closePlugin())
60+
break
61+
case 'export-devup-without-treeshaking':
62+
exportDevup('json', false).finally(() => ctx.closePlugin())
63+
break
64+
case 'export-devup-excel':
65+
exportDevup('excel').finally(() => ctx.closePlugin())
66+
break
67+
case 'export-devup-excel-without-treeshaking':
68+
exportDevup('excel', false).finally(() => ctx.closePlugin())
69+
break
70+
case 'import-devup':
71+
importDevup('json').finally(() => ctx.closePlugin())
72+
break
73+
case 'import-devup-excel':
74+
importDevup('excel').finally(() => ctx.closePlugin())
75+
break
76+
case 'export-assets':
77+
exportAssets().finally(() => ctx.closePlugin())
78+
break
79+
case 'export-components':
80+
exportComponents().finally(() => ctx.closePlugin())
81+
break
82+
}
83+
}
84+
85+
export function run(ctx: typeof figma) {
86+
if (typeof ctx !== 'undefined') {
87+
registerCodegen(ctx)
88+
runCommand(ctx)
89+
}
90+
}
91+
92+
export function autoRun(ctx: typeof figma | undefined = figma) {
93+
if (typeof ctx !== 'undefined') {
94+
run(ctx)
95+
}
96+
}

src/code.ts

Lines changed: 2 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,3 @@
1-
import { Codegen } from './codegen/Codegen'
2-
import { exportDevup, importDevup } from './commands/devup'
3-
import { exportAssets } from './commands/exportAssets'
4-
import { exportComponents } from './commands/exportComponents'
1+
import { autoRun } from './code-impl'
52

6-
export function registerCodegen(ctx: typeof figma = figma) {
7-
if (ctx.editorType === 'dev' && ctx.mode === 'codegen') {
8-
ctx.codegen.on('generate', async ({ node, language }) => {
9-
switch (language) {
10-
case 'devup-ui': {
11-
const time = Date.now()
12-
const codegen = new Codegen(node)
13-
await codegen.run()
14-
const componentsCodes = codegen.getComponentsCodes()
15-
console.info(`[benchmark] devup-ui end ${Date.now() - time}ms`)
16-
return [
17-
...(node.type === 'COMPONENT' ||
18-
node.type === 'COMPONENT_SET' ||
19-
node.type === 'INSTANCE'
20-
? []
21-
: [
22-
{
23-
title: node.name,
24-
language: 'TYPESCRIPT',
25-
code: codegen.getCode(),
26-
} as const,
27-
]),
28-
...(componentsCodes.length > 0
29-
? ([
30-
{
31-
title: `${node.name} - Components`,
32-
language: 'TYPESCRIPT',
33-
code: componentsCodes.map((code) => code[1]).join('\n\n'),
34-
},
35-
{
36-
title: `${node.name} - Components CLI`,
37-
language: 'BASH',
38-
code: componentsCodes
39-
.map(
40-
([componentName, code]) =>
41-
`echo '${code}' > ${componentName}.tsx`,
42-
)
43-
.join('\n'),
44-
},
45-
] as const)
46-
: []),
47-
]
48-
}
49-
}
50-
return []
51-
})
52-
}
53-
}
54-
55-
export function runCommand(ctx: typeof figma = figma) {
56-
switch (ctx.command) {
57-
case 'export-devup':
58-
exportDevup('json').finally(() => ctx.closePlugin())
59-
break
60-
case 'export-devup-without-treeshaking':
61-
exportDevup('json', false).finally(() => ctx.closePlugin())
62-
break
63-
case 'export-devup-excel':
64-
exportDevup('excel').finally(() => ctx.closePlugin())
65-
break
66-
case 'export-devup-excel-without-treeshaking':
67-
exportDevup('excel', false).finally(() => ctx.closePlugin())
68-
break
69-
case 'import-devup':
70-
importDevup('json').finally(() => ctx.closePlugin())
71-
break
72-
case 'import-devup-excel':
73-
importDevup('excel').finally(() => ctx.closePlugin())
74-
break
75-
case 'export-assets':
76-
exportAssets().finally(() => ctx.closePlugin())
77-
break
78-
case 'export-components':
79-
exportComponents().finally(() => ctx.closePlugin())
80-
break
81-
}
82-
}
83-
84-
export function run(ctx: typeof figma) {
85-
if (typeof ctx !== 'undefined') {
86-
registerCodegen(ctx)
87-
runCommand(ctx)
88-
}
89-
}
90-
91-
run((globalThis as { figma?: unknown }).figma as typeof figma)
3+
autoRun(typeof figma === 'undefined' ? undefined : figma)

src/codegen/Codegen.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
getDevupComponentByNode,
1010
getDevupComponentByProps,
1111
} from './utils/get-devup-component'
12+
import { buildCssUrl } from './utils/wrap-url'
1213

1314
export class Codegen {
1415
components: Map<
@@ -80,7 +81,7 @@ export class Codegen {
8081
const maskColor = await checkSameColor(node)
8182
if (maskColor) {
8283
// support mask image icon
83-
props.maskImage = `url(${props.src})`
84+
props.maskImage = buildCssUrl(props.src)
8485
props.maskRepeat = 'no-repeat'
8586
props.maskSize = 'contain'
8687
props.bg = maskColor
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { describe, expect, test } from 'bun:test'
2+
import { buildCssUrl } from '../wrap-url'
3+
4+
describe('buildCssUrl', () => {
5+
test('keeps simple paths unquoted', () => {
6+
expect(buildCssUrl('/icons/logo.svg')).toBe('url(/icons/logo.svg)')
7+
})
8+
9+
test('wraps paths with spaces', () => {
10+
expect(buildCssUrl('/icons/logo icon.svg')).toBe(
11+
"url('/icons/logo icon.svg')",
12+
)
13+
})
14+
15+
test('escapes single quotes inside path', () => {
16+
expect(buildCssUrl("/icons/John's icon.svg")).toBe(
17+
"url('/icons/John\\'s icon.svg')",
18+
)
19+
})
20+
})

src/codegen/utils/paint-to-css.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { rgbaToHex } from '../../utils/rgba-to-hex'
33
import { checkAssetNode } from './check-asset-node'
44
import { fmtPct } from './fmtPct'
55
import { solidToString } from './solid-to-string'
6+
import { buildCssUrl } from './wrap-url'
67

78
interface Point {
89
x: number
@@ -48,13 +49,13 @@ function convertImage(fill: ImagePaint): string {
4849

4950
switch (fill.scaleMode) {
5051
case 'FILL':
51-
return `url(/icons/${imageName}) center/cover no-repeat`
52+
return `${buildCssUrl(`/icons/${imageName}`)} center/cover no-repeat`
5253
case 'FIT':
53-
return `url(/icons/${imageName}) center/contain no-repeat`
54+
return `${buildCssUrl(`/icons/${imageName}`)} center/contain no-repeat`
5455
case 'CROP':
55-
return `url(/icons/${imageName}) center/cover no-repeat`
56+
return `${buildCssUrl(`/icons/${imageName}`)} center/cover no-repeat`
5657
case 'TILE':
57-
return `url(/icons/${imageName}) repeat`
58+
return `${buildCssUrl(`/icons/${imageName}`)} repeat`
5859
}
5960
}
6061

@@ -195,7 +196,8 @@ async function convertPattern(fill: PatternPaint): Promise<string> {
195196
const position = [horizontalPosition, verticalPosition]
196197
.filter(Boolean)
197198
.join(' ')
198-
return `url(/icons/${imageName}.${imageExtension})${position ? ` ${position}` : ''} repeat`
199+
const url = buildCssUrl(`/icons/${imageName}.${imageExtension}`)
200+
return `${url}${position ? ` ${position}` : ''} repeat`
199201
}
200202

201203
function convertPosition(

src/codegen/utils/wrap-url.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Build a CSS url() value. If the path contains whitespace or characters
3+
* that commonly require quoting, wrap it in single quotes and escape any
4+
* existing single quotes.
5+
*/
6+
export function buildCssUrl(path: string): string {
7+
const normalized = path.trim()
8+
const needsQuotes = /[\s'"()]/.test(normalized)
9+
const escaped = normalized.replace(/'/g, "\\'")
10+
return `url(${needsQuotes ? `'${escaped}'` : escaped})`
11+
}

0 commit comments

Comments
 (0)