threlte-minify 0.0.1
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/LICENSE +21 -0
- package/README.md +81 -0
- package/package.json +62 -0
- package/plugin/__tests__/compile.spec.ts +92 -0
- package/plugin/__tests__/extractImports.spec.ts +66 -0
- package/plugin/__tests__/findImportAlias.spec.ts +150 -0
- package/plugin/__tests__/hasDotComponent.spec.ts +79 -0
- package/plugin/__tests__/index.spec.ts +100 -0
- package/plugin/__tests__/insertImports.spec.ts +62 -0
- package/plugin/__tests__/replaceDotComponents.spec.ts +241 -0
- package/plugin/__tests__/stripScriptTags.spec.ts +48 -0
- package/plugin/__tests__/viteBuild.spec.ts +133 -0
- package/plugin/compile.js +76 -0
- package/plugin/extractExistingImports.js +29 -0
- package/plugin/findImportAlias.js +26 -0
- package/plugin/hasDotComponent.js +53 -0
- package/plugin/index.d.ts +3 -0
- package/plugin/index.js +49 -0
- package/plugin/insertImports.js +29 -0
- package/plugin/replaceDotComponents.js +112 -0
- package/plugin/stripScriptTags.js +8 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { insertImports } from '../insertImports.js'
|
|
3
|
+
|
|
4
|
+
describe('insertImports', () => {
|
|
5
|
+
it('inserts new imports into a component with existing imports from the same module', () => {
|
|
6
|
+
const content = `
|
|
7
|
+
import { Mesh } from 'three'
|
|
8
|
+
`
|
|
9
|
+
const newImports = new Set(['Group', 'Material'])
|
|
10
|
+
const result = insertImports(newImports, content)
|
|
11
|
+
expect(result?.code).toContain(`import { Mesh } from 'three'`)
|
|
12
|
+
expect(result?.code).toContain(`import { Group, Material } from 'three'`)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('inserts new imports into a component with existing imports from different modules', () => {
|
|
16
|
+
const content = `
|
|
17
|
+
import { someOtherThing } from 'another-module'
|
|
18
|
+
`
|
|
19
|
+
const newImports = new Set(['Mesh', 'Group'])
|
|
20
|
+
const result = insertImports(newImports, content)
|
|
21
|
+
expect(result?.code).toContain(`import { someOtherThing } from 'another-module'`)
|
|
22
|
+
expect(result?.code).toContain(`import { Mesh, Group } from 'three'`)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('does not insert duplicate imports', () => {
|
|
26
|
+
const content = `
|
|
27
|
+
import { Mesh } from 'three'
|
|
28
|
+
`
|
|
29
|
+
const newImports = new Set(['Mesh', 'Group'])
|
|
30
|
+
const result = insertImports(newImports, content)
|
|
31
|
+
expect(result?.code).not.toContain(`import { Mesh, Mesh`)
|
|
32
|
+
expect(result?.code).toContain(`import { Mesh } from 'three'`)
|
|
33
|
+
expect(result?.code).toContain(`import { Group } from 'three'`)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('handles an empty set of new imports', () => {
|
|
37
|
+
const content = `
|
|
38
|
+
import { Mesh } from 'three'
|
|
39
|
+
`
|
|
40
|
+
const newImports = new Set<string>()
|
|
41
|
+
const result = insertImports(newImports, content)
|
|
42
|
+
// No changes
|
|
43
|
+
expect(result?.code).toEqual(content)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should not insert imports if all new imports already exist in the component', () => {
|
|
47
|
+
const content = `
|
|
48
|
+
import { Mesh, Group } from 'three'
|
|
49
|
+
`
|
|
50
|
+
const newImports = new Set(['Mesh', 'Group'])
|
|
51
|
+
const result = insertImports(newImports, content)
|
|
52
|
+
// No changes
|
|
53
|
+
expect(result?.code).toEqual(content)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('does not insert duplicates when the existing import uses compact spacing', () => {
|
|
57
|
+
const content = `import{Mesh as THRELTE_MINIFY__Mesh}from"three"`
|
|
58
|
+
const newImports = new Set(['Mesh as THRELTE_MINIFY__Mesh'])
|
|
59
|
+
const result = insertImports(newImports, content)
|
|
60
|
+
expect(result.code).toBe(content)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { replaceDotComponents } from '../replaceDotComponents.js'
|
|
3
|
+
|
|
4
|
+
describe('replaceDotComponents', () => {
|
|
5
|
+
it('replaces <T.SomeClass> with <T is={THRELTE_MINIFY__SomeClass}>', () => {
|
|
6
|
+
const content = `<script>
|
|
7
|
+
import { T } from '@threlte/core'
|
|
8
|
+
</script>
|
|
9
|
+
<T.SomeClass></T.SomeClass>`
|
|
10
|
+
const imports = new Set<string>()
|
|
11
|
+
const result = replaceDotComponents(imports, content)
|
|
12
|
+
expect(result.code).toBe(`<script>
|
|
13
|
+
import { T } from '@threlte/core'
|
|
14
|
+
</script>
|
|
15
|
+
<T is={THRELTE_MINIFY__SomeClass}></T>`)
|
|
16
|
+
expect(imports.has('SomeClass as THRELTE_MINIFY__SomeClass')).toBe(true)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('handles empty components', () => {
|
|
20
|
+
const content = ''
|
|
21
|
+
const imports = new Set<string>()
|
|
22
|
+
const result = replaceDotComponents(imports, content)
|
|
23
|
+
expect(result.code).toBe('')
|
|
24
|
+
expect(imports.size).toBe(0)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('handles nested components', () => {
|
|
28
|
+
const content = `<script>
|
|
29
|
+
import { T } from '@threlte/core'
|
|
30
|
+
</script>
|
|
31
|
+
<T.Outer><T.Inner /></T.Outer>`
|
|
32
|
+
const imports = new Set<string>()
|
|
33
|
+
const result = replaceDotComponents(imports, content)
|
|
34
|
+
expect(result.code).toBe(`<script>
|
|
35
|
+
import { T } from '@threlte/core'
|
|
36
|
+
</script>
|
|
37
|
+
<T is={THRELTE_MINIFY__Outer}><T is={THRELTE_MINIFY__Inner} /></T>`)
|
|
38
|
+
expect(imports.has('Outer as THRELTE_MINIFY__Outer')).toBe(true)
|
|
39
|
+
expect(imports.has('Inner as THRELTE_MINIFY__Inner')).toBe(true)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('handles mixed components', () => {
|
|
43
|
+
const content = `
|
|
44
|
+
<script>
|
|
45
|
+
import { T } from '@threlte/core'
|
|
46
|
+
</script>
|
|
47
|
+
<T
|
|
48
|
+
is={group}
|
|
49
|
+
bind:ref
|
|
50
|
+
{...props}
|
|
51
|
+
>
|
|
52
|
+
<T.Group rotation.x={Math.PI / 2}>
|
|
53
|
+
<T.Mesh
|
|
54
|
+
scale.y={-1}
|
|
55
|
+
rotation.x={-Math.PI / 2}
|
|
56
|
+
material={shadowMaterial}
|
|
57
|
+
geometry={planeGeometry}
|
|
58
|
+
/>
|
|
59
|
+
|
|
60
|
+
<T
|
|
61
|
+
is={shadowCamera}
|
|
62
|
+
manual
|
|
63
|
+
/>
|
|
64
|
+
|
|
65
|
+
<slot ref={group} />
|
|
66
|
+
</T.Group>
|
|
67
|
+
</T>
|
|
68
|
+
`
|
|
69
|
+
const imports = new Set<string>()
|
|
70
|
+
const result = replaceDotComponents(imports, content)
|
|
71
|
+
expect(result.code).toBe(`
|
|
72
|
+
<script>
|
|
73
|
+
import { T } from '@threlte/core'
|
|
74
|
+
</script>
|
|
75
|
+
<T
|
|
76
|
+
is={group}
|
|
77
|
+
bind:ref
|
|
78
|
+
{...props}
|
|
79
|
+
>
|
|
80
|
+
<T is={THRELTE_MINIFY__Group} rotation.x={Math.PI / 2}>
|
|
81
|
+
<T is={THRELTE_MINIFY__Mesh}
|
|
82
|
+
scale.y={-1}
|
|
83
|
+
rotation.x={-Math.PI / 2}
|
|
84
|
+
material={shadowMaterial}
|
|
85
|
+
geometry={planeGeometry}
|
|
86
|
+
/>
|
|
87
|
+
|
|
88
|
+
<T
|
|
89
|
+
is={shadowCamera}
|
|
90
|
+
manual
|
|
91
|
+
/>
|
|
92
|
+
|
|
93
|
+
<slot ref={group} />
|
|
94
|
+
</T>
|
|
95
|
+
</T>
|
|
96
|
+
`)
|
|
97
|
+
expect(imports.has('Group as THRELTE_MINIFY__Group')).toBe(true)
|
|
98
|
+
expect(imports.has('Mesh as THRELTE_MINIFY__Mesh')).toBe(true)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('replaces components with attributes', () => {
|
|
102
|
+
const content = `
|
|
103
|
+
<script>
|
|
104
|
+
import { T } from '@threlte/core'
|
|
105
|
+
</script>
|
|
106
|
+
<T.SomeClass attribute="value" {prop} {...$$restProps}>
|
|
107
|
+
<slot />
|
|
108
|
+
</T.SomeClass>
|
|
109
|
+
`
|
|
110
|
+
const imports = new Set<string>()
|
|
111
|
+
const result = replaceDotComponents(imports, content)
|
|
112
|
+
expect(result.code).toBe(`
|
|
113
|
+
<script>
|
|
114
|
+
import { T } from '@threlte/core'
|
|
115
|
+
</script>
|
|
116
|
+
<T is={THRELTE_MINIFY__SomeClass} attribute="value" {prop} {...$$restProps}>
|
|
117
|
+
<slot />
|
|
118
|
+
</T>
|
|
119
|
+
`)
|
|
120
|
+
expect(imports.has('SomeClass as THRELTE_MINIFY__SomeClass')).toBe(true)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('replaces closing tags', () => {
|
|
124
|
+
const content = `<script>
|
|
125
|
+
import { T } from '@threlte/core'
|
|
126
|
+
</script>
|
|
127
|
+
<T.SomeClass></T.SomeClass>`
|
|
128
|
+
const imports = new Set<string>()
|
|
129
|
+
const result = replaceDotComponents(imports, content)
|
|
130
|
+
expect(result.code).toBe(`<script>
|
|
131
|
+
import { T } from '@threlte/core'
|
|
132
|
+
</script>
|
|
133
|
+
<T is={THRELTE_MINIFY__SomeClass}></T>`)
|
|
134
|
+
expect(imports.has('SomeClass as THRELTE_MINIFY__SomeClass')).toBe(true)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('does not affect components without .', () => {
|
|
138
|
+
const content = `<T is={SomeClass} />`
|
|
139
|
+
const imports = new Set<string>()
|
|
140
|
+
const result = replaceDotComponents(imports, content)
|
|
141
|
+
expect(result.code).toBe(`<T is={SomeClass} />`)
|
|
142
|
+
expect(imports.size).toBe(0)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('handles import { x as y }', () => {
|
|
146
|
+
const content = `
|
|
147
|
+
<script>
|
|
148
|
+
import { T as C } from '@threlte/core'
|
|
149
|
+
</script>
|
|
150
|
+
<C.SomeClass />
|
|
151
|
+
`
|
|
152
|
+
const imports = new Set<string>()
|
|
153
|
+
const result = replaceDotComponents(imports, content)
|
|
154
|
+
expect(result.code).toBe(`
|
|
155
|
+
<script>
|
|
156
|
+
import { T as C } from '@threlte/core'
|
|
157
|
+
</script>
|
|
158
|
+
<C is={THRELTE_MINIFY__SomeClass} />
|
|
159
|
+
`)
|
|
160
|
+
expect(imports.size).toBe(1)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('handles aliases that contain regex characters', () => {
|
|
164
|
+
const content = `
|
|
165
|
+
<script>
|
|
166
|
+
import { T as T$ } from '@threlte/core'
|
|
167
|
+
</script>
|
|
168
|
+
<T$.SomeClass />
|
|
169
|
+
`
|
|
170
|
+
const imports = new Set<string>()
|
|
171
|
+
const result = replaceDotComponents(imports, content)
|
|
172
|
+
expect(result.code).toBe(`
|
|
173
|
+
<script>
|
|
174
|
+
import { T as T$ } from '@threlte/core'
|
|
175
|
+
</script>
|
|
176
|
+
<T$ is={THRELTE_MINIFY__SomeClass} />
|
|
177
|
+
`)
|
|
178
|
+
expect(imports.has('SomeClass as THRELTE_MINIFY__SomeClass')).toBe(true)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('uses a unique generated alias when the default one already exists', () => {
|
|
182
|
+
const content = `
|
|
183
|
+
<script>
|
|
184
|
+
const THRELTE_MINIFY__Mesh = 1
|
|
185
|
+
import { T } from '@threlte/core'
|
|
186
|
+
</script>
|
|
187
|
+
<T.Mesh />
|
|
188
|
+
`
|
|
189
|
+
const imports = new Set<string>()
|
|
190
|
+
const result = replaceDotComponents(imports, content)
|
|
191
|
+
expect(result.code).toContain(`<T is={THRELTE_MINIFY__Mesh_1} />`)
|
|
192
|
+
expect(imports.has('Mesh as THRELTE_MINIFY__Mesh_1')).toBe(true)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('does not replace components when T is not imported from @threlte/core', () => {
|
|
196
|
+
const content = `
|
|
197
|
+
<script>
|
|
198
|
+
import * as T from './local.js'
|
|
199
|
+
</script>
|
|
200
|
+
<T.Mesh />
|
|
201
|
+
`
|
|
202
|
+
const imports = new Set<string>()
|
|
203
|
+
const result = replaceDotComponents(imports, content)
|
|
204
|
+
expect(result.code).toBe(content)
|
|
205
|
+
expect(imports.size).toBe(0)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('does not replace comment or text content that looks like a dot component', () => {
|
|
209
|
+
const content = `
|
|
210
|
+
<script>
|
|
211
|
+
import { T } from '@threlte/core'
|
|
212
|
+
</script>
|
|
213
|
+
<!-- <T.Mesh /> -->
|
|
214
|
+
<p>{'<T.Mesh />'}</p>
|
|
215
|
+
`
|
|
216
|
+
const imports = new Set<string>()
|
|
217
|
+
const result = replaceDotComponents(imports, content)
|
|
218
|
+
expect(result.code).toBe(content)
|
|
219
|
+
expect(imports.size).toBe(0)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('does not replace <T.> in the script section', () => {
|
|
223
|
+
const content = `
|
|
224
|
+
<script>
|
|
225
|
+
import { T } from '@threlte/core'
|
|
226
|
+
const str = '<T.Component />'
|
|
227
|
+
</script>
|
|
228
|
+
<T.Mesh></T.Mesh>
|
|
229
|
+
`
|
|
230
|
+
const imports = new Set<string>()
|
|
231
|
+
const result = replaceDotComponents(imports, content)
|
|
232
|
+
expect(result.code).toBe(`
|
|
233
|
+
<script>
|
|
234
|
+
import { T } from '@threlte/core'
|
|
235
|
+
const str = '<T.Component />'
|
|
236
|
+
</script>
|
|
237
|
+
<T is={THRELTE_MINIFY__Mesh}></T>
|
|
238
|
+
`)
|
|
239
|
+
expect(imports.size).toBe(1)
|
|
240
|
+
})
|
|
241
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { stripScriptTags } from '../stripScriptTags.js'
|
|
3
|
+
|
|
4
|
+
describe('stripScriptTags', () => {
|
|
5
|
+
it('Does not remove content that is not script tags', () => {
|
|
6
|
+
const input = `<div><p>Paragraph</p></div>`
|
|
7
|
+
const expected = `<div><p>Paragraph</p></div>`
|
|
8
|
+
expect(stripScriptTags(input)).toBe(expected)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('Removes a simple script tag', () => {
|
|
12
|
+
const input = `<script>let name = 'world'</script><h1>Hello {name}!</h1>`
|
|
13
|
+
const expected = `<h1>Hello {name}!</h1>`
|
|
14
|
+
expect(stripScriptTags(input)).toBe(expected)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('Removes multiple script tags', () => {
|
|
18
|
+
const input = `<script>let name = 'world';</script><style></style><h1>Hello {name}!</h1><script>console.log('test')</script>`
|
|
19
|
+
const expected = `<style></style><h1>Hello {name}!</h1>`
|
|
20
|
+
expect(stripScriptTags(input)).toBe(expected)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('Removes script tags with attributes', () => {
|
|
24
|
+
const input = `<script context="module">import { foo } from 'bar'</script><script lang='ts'></script><h1>Hello</h1>`
|
|
25
|
+
const expected = `<h1>Hello</h1>`
|
|
26
|
+
expect(stripScriptTags(input)).toBe(expected)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('Removes script tags nested in other tags', () => {
|
|
30
|
+
const input = `<div><script>let name = 'world';</script></div>`
|
|
31
|
+
const expected = `<div></div>`
|
|
32
|
+
expect(stripScriptTags(input)).toBe(expected)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('Removes script tags with unusual spacing', () => {
|
|
36
|
+
const input = `<script >let name = 'world';</script><h1>Hello {name}!</h1>`
|
|
37
|
+
const expected = `<h1>Hello {name}!</h1>`
|
|
38
|
+
expect(stripScriptTags(input)).toBe(expected)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should handle script tags with newlines inside', () => {
|
|
42
|
+
const input = `<script>
|
|
43
|
+
let name = 'world';
|
|
44
|
+
</script><h1>Hello {name}!</h1>`
|
|
45
|
+
const expected = `<h1>Hello {name}!</h1>`
|
|
46
|
+
expect(stripScriptTags(input)).toBe(expected)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
4
|
+
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
|
5
|
+
import { build } from 'vite'
|
|
6
|
+
import { threlteMinify } from '../index.js'
|
|
7
|
+
|
|
8
|
+
const tempDirs = new Set<string>()
|
|
9
|
+
|
|
10
|
+
const collectFiles = async (dir: string): Promise<string[]> => {
|
|
11
|
+
const { readdir } = await import('node:fs/promises')
|
|
12
|
+
const entries = await readdir(dir, { withFileTypes: true })
|
|
13
|
+
const files = await Promise.all(
|
|
14
|
+
entries.map(async (entry) => {
|
|
15
|
+
const fullPath = join(dir, entry.name)
|
|
16
|
+
if (entry.isDirectory()) {
|
|
17
|
+
return collectFiles(fullPath)
|
|
18
|
+
}
|
|
19
|
+
return [fullPath]
|
|
20
|
+
})
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
return files.flat()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const createFixture = async () => {
|
|
27
|
+
const tempRoot = join(process.cwd(), '.vitest-tmp')
|
|
28
|
+
await mkdir(tempRoot, { recursive: true })
|
|
29
|
+
|
|
30
|
+
const root = await mkdtemp(join(tempRoot, 'threlte-minify-vite-'))
|
|
31
|
+
tempDirs.add(root)
|
|
32
|
+
|
|
33
|
+
await mkdir(join(root, 'src'), { recursive: true })
|
|
34
|
+
await writeFile(
|
|
35
|
+
join(root, 'index.html'),
|
|
36
|
+
`<!doctype html>
|
|
37
|
+
<html lang="en">
|
|
38
|
+
<body>
|
|
39
|
+
<div id="app"></div>
|
|
40
|
+
<script type="module" src="/src/main.js"></script>
|
|
41
|
+
</body>
|
|
42
|
+
</html>`
|
|
43
|
+
)
|
|
44
|
+
await writeFile(
|
|
45
|
+
join(root, 'src', 'main.js'),
|
|
46
|
+
`import { mount } from 'svelte'
|
|
47
|
+
import App from './App.svelte'
|
|
48
|
+
|
|
49
|
+
mount(App, {
|
|
50
|
+
target: document.getElementById('app'),
|
|
51
|
+
})`
|
|
52
|
+
)
|
|
53
|
+
await writeFile(
|
|
54
|
+
join(root, 'src', 'App.svelte'),
|
|
55
|
+
`<script>
|
|
56
|
+
import { Canvas, T } from '@threlte/core'
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<Canvas>
|
|
60
|
+
<T.Mesh>
|
|
61
|
+
<T.BoxGeometry />
|
|
62
|
+
</T.Mesh>
|
|
63
|
+
</Canvas>`
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return root
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
afterEach(async () => {
|
|
70
|
+
await Promise.all(
|
|
71
|
+
[...tempDirs].map(async (dir) => {
|
|
72
|
+
tempDirs.delete(dir)
|
|
73
|
+
await rm(dir, { recursive: true, force: true })
|
|
74
|
+
})
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('vite build integration', () => {
|
|
79
|
+
it('builds a fixture app and applies the Svelte transform before compilation', async () => {
|
|
80
|
+
const root = await createFixture()
|
|
81
|
+
let capturedSvelte = ''
|
|
82
|
+
const outDir = join(root, 'dist')
|
|
83
|
+
|
|
84
|
+
await build({
|
|
85
|
+
configFile: false,
|
|
86
|
+
logLevel: 'silent',
|
|
87
|
+
root,
|
|
88
|
+
publicDir: false,
|
|
89
|
+
plugins: [
|
|
90
|
+
threlteMinify(),
|
|
91
|
+
{
|
|
92
|
+
name: 'capture-threlte-minify-output',
|
|
93
|
+
enforce: 'pre',
|
|
94
|
+
transform(code, id) {
|
|
95
|
+
if (id.endsWith('/src/App.svelte')) {
|
|
96
|
+
capturedSvelte = code
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return null
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
svelte(),
|
|
103
|
+
],
|
|
104
|
+
build: {
|
|
105
|
+
minify: false,
|
|
106
|
+
outDir,
|
|
107
|
+
rollupOptions: {
|
|
108
|
+
input: join(root, 'index.html'),
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
expect(capturedSvelte).toContain(`Mesh as THRELTE_MINIFY__Mesh`)
|
|
114
|
+
expect(capturedSvelte).toContain(`BoxGeometry as THRELTE_MINIFY__BoxGeometry`)
|
|
115
|
+
expect(capturedSvelte).toContain(`<T is={THRELTE_MINIFY__Mesh}>`)
|
|
116
|
+
expect(capturedSvelte).toContain(`<T is={THRELTE_MINIFY__BoxGeometry} />`)
|
|
117
|
+
|
|
118
|
+
const files = await collectFiles(outDir)
|
|
119
|
+
const jsFiles = files.filter((file) => file.endsWith('.js'))
|
|
120
|
+
expect(jsFiles.length).toBeGreaterThan(0)
|
|
121
|
+
|
|
122
|
+
const bundle = (
|
|
123
|
+
await Promise.all(
|
|
124
|
+
jsFiles.map(async (file) => {
|
|
125
|
+
return readFile(file, 'utf8')
|
|
126
|
+
})
|
|
127
|
+
)
|
|
128
|
+
).join('\n')
|
|
129
|
+
|
|
130
|
+
expect(bundle).toContain('return Mesh;')
|
|
131
|
+
expect(bundle).toContain('return BoxGeometry;')
|
|
132
|
+
}, 30000)
|
|
133
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import MagicString from 'magic-string'
|
|
2
|
+
import { insertImports } from './insertImports.js'
|
|
3
|
+
import { parse, preprocess } from 'svelte/compiler'
|
|
4
|
+
import { replaceDotComponents } from './replaceDotComponents.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
*
|
|
8
|
+
* @param {Set<string>} imports
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
const createThreeImport = (imports) => {
|
|
12
|
+
return `import { ${[...imports].join(', ')} } from 'three'`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
*
|
|
17
|
+
* @param {string} code
|
|
18
|
+
* @param {Set<string>} imports
|
|
19
|
+
* @param {string=} filename
|
|
20
|
+
* @returns {{ code: string; map: import('magic-string').SourceMap }}
|
|
21
|
+
*/
|
|
22
|
+
const injectInstanceScript = (code, imports, filename) => {
|
|
23
|
+
const ast = parse(code)
|
|
24
|
+
const str = new MagicString(code, { filename })
|
|
25
|
+
const instanceScript = `<script>\n${createThreeImport(imports)}\n</script>`
|
|
26
|
+
const insertAt = ast.module ? ast.module.end : 0
|
|
27
|
+
const prefix = insertAt === 0 ? '' : '\n'
|
|
28
|
+
const suffix = insertAt === 0 ? '\n' : ''
|
|
29
|
+
|
|
30
|
+
str.appendLeft(insertAt, `${prefix}${instanceScript}${suffix}`)
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
code: str.toString(),
|
|
34
|
+
map: str.generateMap(),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
*
|
|
40
|
+
* @param {string} source
|
|
41
|
+
* @param {string} filename
|
|
42
|
+
* @returns {Promise<import('svelte/compiler').Processed>}
|
|
43
|
+
*/
|
|
44
|
+
export const compile = async (source, filename) => {
|
|
45
|
+
/** @type {Set<string>} */
|
|
46
|
+
const imports = new Set()
|
|
47
|
+
|
|
48
|
+
const processed = await preprocess(
|
|
49
|
+
source,
|
|
50
|
+
[
|
|
51
|
+
{
|
|
52
|
+
name: 'threlte-minify',
|
|
53
|
+
|
|
54
|
+
markup: ({ content }) => {
|
|
55
|
+
return replaceDotComponents(imports, content, filename)
|
|
56
|
+
},
|
|
57
|
+
script: ({ content, attributes }) => {
|
|
58
|
+
if (attributes.context === 'module' || 'module' in attributes) return
|
|
59
|
+
const result = insertImports(imports, content, filename)
|
|
60
|
+
imports.clear()
|
|
61
|
+
return result
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
{ filename }
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if (imports.size === 0) {
|
|
69
|
+
return processed
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
...processed,
|
|
74
|
+
...injectInstanceScript(processed.code, imports, filename),
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* @param {string} code
|
|
4
|
+
* @param {string} moduleName
|
|
5
|
+
* @returns {string[]}
|
|
6
|
+
*/
|
|
7
|
+
export const extractExistingImports = (code, moduleName) => {
|
|
8
|
+
const escapedModuleName = moduleName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
9
|
+
const regex = new RegExp(`import\\s*\\{([^}]+)\\}\\s*from\\s*['"]${escapedModuleName}['"]`, 'ug')
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @type {RegExpExecArray | null}
|
|
13
|
+
*/
|
|
14
|
+
let match = null
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @type {Set<string>}
|
|
18
|
+
*/
|
|
19
|
+
const imports = new Set()
|
|
20
|
+
|
|
21
|
+
while ((match = regex.exec(code)) !== null) {
|
|
22
|
+
const [, result] = match
|
|
23
|
+
for (const item of result.split(',')) {
|
|
24
|
+
imports.add(item.trim())
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return [...imports]
|
|
29
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* @param {string} content
|
|
4
|
+
* @param {string} alias
|
|
5
|
+
* @returns {string | null}
|
|
6
|
+
*/
|
|
7
|
+
export const findImportAlias = (content, alias) => {
|
|
8
|
+
const regex = /import\s*{([^}]+)}\s*from\s*["']@threlte\/core["']/g
|
|
9
|
+
const matches = [...content.matchAll(regex)]
|
|
10
|
+
|
|
11
|
+
for (const match of matches) {
|
|
12
|
+
const imports = match[1].split(',')
|
|
13
|
+
for (const imp of imports) {
|
|
14
|
+
const [imported, asAlias] = imp
|
|
15
|
+
.trim()
|
|
16
|
+
.split(/\s+as\s+/)
|
|
17
|
+
.map((str) => {
|
|
18
|
+
return str.trim()
|
|
19
|
+
})
|
|
20
|
+
if (imported === alias) {
|
|
21
|
+
return asAlias ?? imported
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { findImportAlias } from './findImportAlias.js'
|
|
2
|
+
import { parse } from 'svelte/compiler'
|
|
3
|
+
import { stripScriptTags } from './stripScriptTags.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
*
|
|
7
|
+
* @param {string} code
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
export const hasDotComponent = (code) => {
|
|
11
|
+
const alias = findImportAlias(code, 'T')
|
|
12
|
+
|
|
13
|
+
if (!alias) {
|
|
14
|
+
return false
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!stripScriptTags(code).includes(`<${alias}.`)) {
|
|
18
|
+
return false
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let hasMatch = false
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
*
|
|
25
|
+
* @param {unknown} node
|
|
26
|
+
* @returns {void}
|
|
27
|
+
*/
|
|
28
|
+
const visit = (node) => {
|
|
29
|
+
if (!node || typeof node !== 'object' || hasMatch) return
|
|
30
|
+
|
|
31
|
+
if (Array.isArray(node)) {
|
|
32
|
+
for (const child of node) {
|
|
33
|
+
visit(child)
|
|
34
|
+
}
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (node.type === 'InlineComponent' && node.name.startsWith(`${alias}.`)) {
|
|
39
|
+
hasMatch = true
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const value of Object.values(node)) {
|
|
44
|
+
if (value && typeof value === 'object') {
|
|
45
|
+
visit(value)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
visit(parse(code).html)
|
|
51
|
+
|
|
52
|
+
return hasMatch
|
|
53
|
+
}
|