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.
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ import { PluginOption } from 'vite'
2
+
3
+ export function threlteMinify(): PluginOption