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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Micheal Parks
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # threlte-minify
2
+
3
+ A Vite plugin that produces smaller and faster [Threlte](https://threlte.xyz) apps.
4
+
5
+ > **Note:** This plugin is not compatible with Threlte's `extend` function.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm i -D threlte-minify
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ // vite.config.ts
17
+ import { defineConfig } from 'vite'
18
+ import { sveltekit } from '@sveltejs/kit/vite'
19
+ import { threlteMinify } from 'threlte-minify'
20
+
21
+ export default defineConfig({
22
+ plugins: [threlteMinify(), sveltekit()],
23
+ })
24
+ ```
25
+
26
+ ## Results
27
+
28
+ Tested on a simple scene with a few meshes and materials:
29
+
30
+ | Configuration | Size | Gzip |
31
+ | ---------------------------------------------------------------------------------- | --------: | --------: |
32
+ | No plugins | 810.88 kB | 213.82 kB |
33
+ | threlte-minify | 576.58 kB | 150.13 kB |
34
+ | threlte-minify + [three-minifier](https://github.com/nickyMcDonald/three-minifier) | 550.64 kB | 145.96 kB |
35
+
36
+ ## How it works
37
+
38
+ Threlte's `<T.Mesh>` syntax relies on a proxy that does `import * as THREE from 'three'` to resolve Three.js classes at runtime. This has two costs:
39
+
40
+ 1. **Bundle size** -- the wildcard import prevents tree-shaking, pulling in all of Three.js regardless of what you use.
41
+ 2. **Runtime speed** -- every `<T.Something>` goes through a proxy lookup to resolve the class.
42
+
43
+ This plugin preprocesses your Svelte components, transforming them from this:
44
+
45
+ ```svelte
46
+ <script>
47
+ import { T } from '@threlte/core'
48
+ </script>
49
+
50
+ <T.Mesh>
51
+ <T.BoxGeometry />
52
+ <T.MeshStandardMaterial color="orange" />
53
+ </T.Mesh>
54
+ ```
55
+
56
+ ...into this:
57
+
58
+ ```svelte
59
+ <script>
60
+ import { Mesh as THRELTE_MINIFY__Mesh } from 'three'
61
+ import { BoxGeometry as THRELTE_MINIFY__BoxGeometry } from 'three'
62
+ import { MeshStandardMaterial as THRELTE_MINIFY__MeshStandardMaterial } from 'three'
63
+ import { T } from '@threlte/core'
64
+ </script>
65
+
66
+ <T is={THRELTE_MINIFY__Mesh}>
67
+ <T is={THRELTE_MINIFY__BoxGeometry} />
68
+ <T
69
+ is={THRELTE_MINIFY__MeshStandardMaterial}
70
+ color="orange"
71
+ />
72
+ </T>
73
+ ```
74
+
75
+ By replacing proxy-resolved dot notation with direct imports, the bundler can tree-shake unused Three.js classes and the runtime skips the proxy entirely.
76
+
77
+ The plugin also removes the wildcard import from Threlte's internals to complete the tree-shaking.
78
+
79
+ ## License
80
+
81
+ MIT
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "threlte-minify",
3
+ "version": "0.0.1",
4
+ "description": "A vite plugin to better minify Threlte apps.",
5
+ "keywords": [
6
+ "Threlte"
7
+ ],
8
+ "license": "MIT",
9
+ "author": {
10
+ "name": "Micheal Parks",
11
+ "email": "michealparks1989@gmail.com",
12
+ "url": "https://parks.lol"
13
+ },
14
+ "exports": {
15
+ "import": "./plugin/index.js",
16
+ "types": "./plugin/index.d.ts"
17
+ },
18
+ "files": [
19
+ "./plugin/*"
20
+ ],
21
+ "scripts": {
22
+ "dev": "vite dev --host",
23
+ "build": "vite build",
24
+ "build:null": "vite build --mode null",
25
+ "build:threlte": "vite build --mode threlte",
26
+ "build:three": "vite build --mode three",
27
+ "preview": "vite preview",
28
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
29
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
30
+ "lint": "prettier --check . && eslint .",
31
+ "format": "prettier --write .",
32
+ "stats": "node stats.js",
33
+ "test": "vitest",
34
+ "test:watch": "vitest --watch"
35
+ },
36
+ "devDependencies": {
37
+ "@changesets/cli": "^2.30.0",
38
+ "@sveltejs/adapter-auto": "^3.2.1",
39
+ "@sveltejs/kit": "^2.55.0",
40
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
41
+ "@threlte/core": "^8.5.0",
42
+ "@threlte/extras": "^9.14.0",
43
+ "@types/three": "^0.183.0",
44
+ "@yushijinhun/three-minifier-rollup": "^0.4.0",
45
+ "eslint": "^9.0.0",
46
+ "eslint-config-prettier": "^10.0.0",
47
+ "eslint-plugin-svelte": "^3.0.0",
48
+ "prettier": "^3.2.5",
49
+ "prettier-plugin-svelte": "^3.2.3",
50
+ "svelte": "^5.55.0",
51
+ "svelte-check": "^4.0.0",
52
+ "three": "^0.183.0",
53
+ "typescript": "^5.4.5",
54
+ "vite": "^6.3.0",
55
+ "vitest": "^3.0.0"
56
+ },
57
+ "dependencies": {
58
+ "magic-string": "^0.30.10"
59
+ },
60
+ "type": "module",
61
+ "packageManager": "pnpm@10.29.3"
62
+ }
@@ -0,0 +1,92 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { compile } from '../compile.js'
3
+
4
+ describe('compile', () => {
5
+ const component = `
6
+ <script context="module" lang="ts">
7
+ import { Group } from 'three'
8
+ </script>
9
+ <script lang="ts">
10
+ import { Canvas, T } from '@threlte/core'
11
+ </script>
12
+
13
+ <Canvas>
14
+ <T.Group>
15
+ <T.Mesh>
16
+ <T is={Group}>
17
+
18
+ </T>
19
+ </T.Mesh>
20
+ </T.Group>
21
+ </Canvas>
22
+ `
23
+
24
+ it('Correctly modifies <T.> components during compilation', async () => {
25
+ const result = await compile(component, 'file.svelte')
26
+ expect(result.code).toBe(`
27
+ <script context="module" lang="ts">
28
+ import { Group } from 'three'
29
+ </script>
30
+ <script lang="ts">
31
+ import { Group as THRELTE_MINIFY__Group, Mesh as THRELTE_MINIFY__Mesh } from 'three'
32
+
33
+ import { Canvas, T } from '@threlte/core'
34
+ </script>
35
+
36
+ <Canvas>
37
+ <T is={THRELTE_MINIFY__Group}>
38
+ <T is={THRELTE_MINIFY__Mesh}>
39
+ <T is={Group}>
40
+
41
+ </T>
42
+ </T>
43
+ </T>
44
+ </Canvas>
45
+ `)
46
+ })
47
+
48
+ it('does not transform comment, style, or text literals that contain <T.>', async () => {
49
+ const source = `
50
+ <script>
51
+ import { T } from '@threlte/core'
52
+ </script>
53
+ <!-- <T.Mesh /> -->
54
+ <style>.x::before { content: "<T.Mesh />"; }</style>
55
+ <p>{'<T.Mesh />'}</p>
56
+ `
57
+ const result = await compile(source, 'file.svelte')
58
+ expect(result.code).toBe(source)
59
+ })
60
+
61
+ it('does not transform components when T is not imported from @threlte/core', async () => {
62
+ const source = `
63
+ <script>
64
+ import * as T from './local.js'
65
+ </script>
66
+ <T.Mesh />
67
+ `
68
+ const result = await compile(source, 'file.svelte')
69
+ expect(result.code).toBe(source)
70
+ })
71
+
72
+ it('is idempotent and returns a source map for transformed components', async () => {
73
+ const source = `
74
+ <script>
75
+ import { T } from '@threlte/core'
76
+ </script>
77
+ <T.Mesh />
78
+ `
79
+ const first = await compile(source, '/virtual/Test.svelte')
80
+ const second = await compile(first.code, '/virtual/Test.svelte')
81
+
82
+ expect(second.code).toBe(first.code)
83
+ expect(first.map).toMatchObject({
84
+ version: 3,
85
+ sources: ['Test.svelte'],
86
+ })
87
+ expect(second.map).toMatchObject({
88
+ version: 3,
89
+ sources: ['Test.svelte'],
90
+ })
91
+ })
92
+ })
@@ -0,0 +1,66 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { extractExistingImports } from '../extractExistingImports.js'
3
+
4
+ describe('extractExistingImports', () => {
5
+ it('returns an array of imports from the specified module', () => {
6
+ const code = `
7
+ import { Mesh, Group, type Material } from 'three'
8
+ `
9
+ const result = extractExistingImports(code, 'three')
10
+ expect(result).toEqual(['Mesh', 'Group', 'type Material'])
11
+ })
12
+
13
+ it('returns an empty array if no match for the specified module', () => {
14
+ const code = `
15
+ import { Mesh, Group, type Material } from 'other-module'
16
+ `
17
+ const result = extractExistingImports(code, 'three')
18
+ expect(result).toEqual([])
19
+ })
20
+
21
+ it('returns an empty array if there are no import statements', () => {
22
+ const code = ``
23
+ const result = extractExistingImports(code, 'three')
24
+ expect(result).toEqual([])
25
+ })
26
+
27
+ it('handles multiple import statements', () => {
28
+ const code = `
29
+ import { Mesh, Group, type Material } from 'three'
30
+ import { anotherThing } from 'another-module'
31
+ `
32
+ const result = extractExistingImports(code, 'three')
33
+ expect(result).toEqual(['Mesh', 'Group', 'type Material'])
34
+ })
35
+
36
+ it('handles strange whitespace', () => {
37
+ const code = `
38
+ import { Mesh , Group , type Material } from 'three'
39
+ `
40
+ const result = extractExistingImports(code, 'three')
41
+ expect(result).toEqual(['Mesh', 'Group', 'type Material'])
42
+ })
43
+
44
+ it('handles imports with type and as keywords', () => {
45
+ const code = `
46
+ import { Mesh, type Material, Object3D as ThreeObject } from 'three'
47
+ `
48
+ const result = extractExistingImports(code, 'three')
49
+ expect(result).toEqual(['Mesh', 'type Material', 'Object3D as ThreeObject'])
50
+ })
51
+
52
+ it('handles multiple import statements from the same module', () => {
53
+ const code = `
54
+ import { Mesh } from 'three'
55
+ import { Group, type Material } from 'three'
56
+ `
57
+ const result = extractExistingImports(code, 'three')
58
+ expect(result).toEqual(['Mesh', 'Group', 'type Material'])
59
+ })
60
+
61
+ it('handles imports without whitespace around braces or from', () => {
62
+ const code = `import{Mesh as THRELTE_MINIFY__Mesh,type Material}from"three"`
63
+ const result = extractExistingImports(code, 'three')
64
+ expect(result).toEqual(['Mesh as THRELTE_MINIFY__Mesh', 'type Material'])
65
+ })
66
+ })
@@ -0,0 +1,150 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { findImportAlias } from '../findImportAlias.js'
3
+
4
+ describe('findImportAlias', () => {
5
+ it('returns the alias when the import statement is correctly formatted', () => {
6
+ const svelteComponent = `
7
+ <script>
8
+ import { T as y } from '@threlte/core';
9
+ </script>
10
+ `
11
+ const alias = 'T'
12
+ const result = findImportAlias(svelteComponent, alias)
13
+ expect(result).toBe('y')
14
+ })
15
+
16
+ it('returns null when the import statement does not exist', () => {
17
+ const svelteComponent = `
18
+ <script>
19
+ // no import statement
20
+ </script>
21
+ `
22
+ const alias = 'T'
23
+ const result = findImportAlias(svelteComponent, alias)
24
+ expect(result).toBeNull()
25
+ })
26
+
27
+ it('handles no whitespace', () => {
28
+ const svelteComponent = `
29
+ <script>
30
+ import{T as y}from'@threlte/core';
31
+ </script>
32
+ `
33
+ const alias = 'T'
34
+ const result = findImportAlias(svelteComponent, alias)
35
+ expect(result).toBe('y')
36
+ })
37
+
38
+ it('returns null when the alias is not found in the import statement', () => {
39
+ const svelteComponent = `
40
+ <script>
41
+ import { X as y } from '@threlte/core';
42
+ </script>
43
+ `
44
+ const alias = 'T'
45
+ const result = findImportAlias(svelteComponent, alias)
46
+ expect(result).toBeNull()
47
+ })
48
+
49
+ it('handles extra whitespace', () => {
50
+ const svelteComponent = `
51
+ <script>
52
+ import { T as y } from '@threlte/core';
53
+ </script>
54
+ `
55
+ const alias = 'T'
56
+ const result = findImportAlias(svelteComponent, alias)
57
+ expect(result).toBe('y')
58
+ })
59
+
60
+ it('handles multiple import statements and find the correct one', () => {
61
+ const svelteComponent = `
62
+ <script>
63
+ import { A as b } from '@threlte/core';
64
+ import { T as y } from '@threlte/core';
65
+ import { C as d } from 'otherlib';
66
+ </script>
67
+ `
68
+ const alias = 'T'
69
+ const result = findImportAlias(svelteComponent, alias)
70
+ expect(result).toBe('y')
71
+ })
72
+
73
+ it('should return the correct alias when there are multiple imports with the same alias', () => {
74
+ const svelteComponent = `
75
+ <script>
76
+ import { T as b } from 'otherlib';
77
+ import { T as a } from '@threlte/core';
78
+ </script>
79
+ `
80
+ const alias = 'T'
81
+ const result = findImportAlias(svelteComponent, alias)
82
+ // Only the first match is considered
83
+ expect(result).toBe('a')
84
+ })
85
+
86
+ it('should handle import statements without "as"', () => {
87
+ const svelteComponent = `
88
+ <script>
89
+ import { T } from '@threlte/core';
90
+ </script>
91
+ `
92
+ const alias = 'T'
93
+ const result = findImportAlias(svelteComponent, alias)
94
+ expect(result).toBe('T')
95
+ })
96
+
97
+ it('should handle an empty component string', () => {
98
+ const svelteComponent = ``
99
+ const alias = 'T'
100
+ const result = findImportAlias(svelteComponent, alias)
101
+ expect(result).toBeNull()
102
+ })
103
+
104
+ it('should handle multiple imports from the same library', () => {
105
+ const svelteComponent = `
106
+ <script>
107
+ import { A as a, T as y, C as c } from '@threlte/core';
108
+ </script>
109
+ `
110
+ const alias = 'T'
111
+ const result = findImportAlias(svelteComponent, alias)
112
+ expect(result).toBe('y')
113
+ })
114
+
115
+ it('should handle multiple lines of imports from the same library', () => {
116
+ const svelteComponent = `
117
+ <script>
118
+ import {
119
+ A as a,
120
+ T as y,
121
+ C as c
122
+ } from '@threlte/core';
123
+ </script>
124
+ `
125
+ const alias = 'T'
126
+ const result = findImportAlias(svelteComponent, alias)
127
+ expect(result).toBe('y')
128
+ })
129
+
130
+ it('should handle import statement with double quotes', () => {
131
+ const svelteComponent = `
132
+ <script>
133
+ import { A as a, T as y, C as c } from "@threlte/core";
134
+ </script>
135
+ `
136
+ const alias = 'T'
137
+ const result = findImportAlias(svelteComponent, alias)
138
+ expect(result).toBe('y')
139
+ })
140
+
141
+ it('does not truncate aliases that contain "as"', () => {
142
+ const svelteComponent = `
143
+ <script>
144
+ import { T as task } from '@threlte/core';
145
+ </script>
146
+ `
147
+ const result = findImportAlias(svelteComponent, 'T')
148
+ expect(result).toBe('task')
149
+ })
150
+ })
@@ -0,0 +1,79 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { hasDotComponent } from '../hasDotComponent.js'
3
+
4
+ describe('hasDotComponent', () => {
5
+ it('returns true when the code contains a <T.>', () => {
6
+ const code = `
7
+ <script>
8
+ import { T } from '@threlte/core'
9
+ </script>
10
+ <T.Mesh></T.Mesh>
11
+ `
12
+ expect(hasDotComponent(code)).toBe(true)
13
+ })
14
+
15
+ it('returns true when the code contains multiple <T.> instances', () => {
16
+ const code = `
17
+ <script>
18
+ import { T } from '@threlte/core'
19
+ </script>
20
+ <T.Mesh></T.Mesh>
21
+ <T.Object3D attribute="value" {...$$restProps}></T.Object3D>
22
+ `
23
+ expect(hasDotComponent(code)).toBe(true)
24
+ })
25
+
26
+ it('returns false when the code does not contain any <T.Component>', () => {
27
+ const code = ``
28
+ expect(hasDotComponent(code)).toBe(false)
29
+ })
30
+
31
+ it('should return false when the code contains <T> but not <T.>', () => {
32
+ const code = `
33
+ <T></T>
34
+ <T is={something}></T>
35
+ <Threlte></Threlte>
36
+ `
37
+ expect(hasDotComponent(code)).toBe(false)
38
+ })
39
+
40
+ it('should return false when the code contains <T.> only in the script section', () => {
41
+ const code = `
42
+ <script>
43
+ const str = '<T.Mesh>'
44
+ </script>
45
+ `
46
+ expect(hasDotComponent(code)).toBe(false)
47
+ })
48
+
49
+ it('detects aliased T dot components', () => {
50
+ const code = `
51
+ <script>
52
+ import { T as C } from '@threlte/core'
53
+ </script>
54
+ <C.Mesh />
55
+ `
56
+ expect(hasDotComponent(code)).toBe(true)
57
+ })
58
+
59
+ it('returns false when T is not imported from @threlte/core', () => {
60
+ const code = `
61
+ <script>
62
+ import * as T from './local.js'
63
+ </script>
64
+ <T.Mesh />
65
+ `
66
+ expect(hasDotComponent(code)).toBe(false)
67
+ })
68
+
69
+ it('returns false when <T.> only appears in comments or text', () => {
70
+ const code = `
71
+ <script>
72
+ import { T } from '@threlte/core'
73
+ </script>
74
+ <!-- <T.Mesh /> -->
75
+ <p>{'<T.Mesh />'}</p>
76
+ `
77
+ expect(hasDotComponent(code)).toBe(false)
78
+ })
79
+ })
@@ -0,0 +1,100 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { resolve } from 'node:path'
3
+ import { describe, expect, it } from 'vitest'
4
+ import { threlteMinify } from '../index.js'
5
+
6
+ const getTransform = () => {
7
+ const plugin = threlteMinify()
8
+
9
+ if (!plugin || Array.isArray(plugin) || !('transform' in plugin) || !plugin.transform) {
10
+ throw new Error('Expected a Vite plugin with a transform hook')
11
+ }
12
+
13
+ return plugin.transform
14
+ }
15
+
16
+ const getExportedNames = (code: string) => {
17
+ const names = new Set<string>()
18
+
19
+ for (const match of code.matchAll(/export const (\w+)/g)) {
20
+ names.add(match[1])
21
+ }
22
+
23
+ for (const match of code.matchAll(/export \{\s*default as (\w+)\s*\}/g)) {
24
+ names.add(match[1])
25
+ }
26
+
27
+ return [...names].sort()
28
+ }
29
+
30
+ describe('threlteMinify', () => {
31
+ it('transforms aliased T dot components', async () => {
32
+ const transform = getTransform()
33
+ const result = await transform.call(
34
+ {},
35
+ `
36
+ <script>
37
+ import { T as C } from '@threlte/core'
38
+ </script>
39
+ <C.Mesh />
40
+ `,
41
+ '/virtual/Test.svelte'
42
+ )
43
+
44
+ expect(result).toMatchObject({
45
+ code: `
46
+ <script>
47
+ import { Mesh as THRELTE_MINIFY__Mesh } from 'three'
48
+
49
+ import { T as C } from '@threlte/core'
50
+ </script>
51
+ <C is={THRELTE_MINIFY__Mesh} />
52
+ `,
53
+ })
54
+
55
+ if (!result || typeof result === 'string') {
56
+ throw new Error('Expected an object transform result')
57
+ }
58
+
59
+ expect(JSON.parse(result.map)).toMatchObject({
60
+ version: 3,
61
+ sources: ['Test.svelte'],
62
+ })
63
+ })
64
+
65
+ it('does not transform non-Threlte T components', async () => {
66
+ const transform = getTransform()
67
+ const source = `
68
+ <script>
69
+ import * as T from './local.js'
70
+ </script>
71
+ <T.Mesh />
72
+ `
73
+ const result = await transform.call({}, source, '/virtual/Test.svelte')
74
+
75
+ expect(result).toBeUndefined()
76
+ })
77
+
78
+ it('preserves the installed @threlte/core T.js export contract when overwriting', async () => {
79
+ const transform = getTransform()
80
+ const id = resolve(process.cwd(), 'node_modules/@threlte/core/dist/components/T/T.js')
81
+ const source = await readFile(id, 'utf8')
82
+
83
+ expect(getExportedNames(source)).toEqual(['T', 'extend'])
84
+
85
+ const result = await transform.call({}, source, id)
86
+
87
+ if (!result || typeof result === 'string') {
88
+ throw new Error('Expected an object transform result')
89
+ }
90
+
91
+ expect(getExportedNames(result.code)).toEqual(['T', 'extend'])
92
+ expect(result.code).toContain(`export { default as T } from './T.svelte'`)
93
+ expect(result.code).toContain(`export const extend = () => {`)
94
+ expect(result.code).not.toContain(`import * as THREE from 'three'`)
95
+ expect(JSON.parse(result.map.toString())).toMatchObject({
96
+ version: 3,
97
+ sources: [''],
98
+ })
99
+ })
100
+ })