shaderkit 0.1.10 → 0.1.12
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 -21
- package/README.md +185 -185
- package/dist/index.cjs +5 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.js +4 -983
- package/dist/index.js.map +1 -1
- package/package.json +50 -44
- package/src/constants.ts +901 -901
- package/src/index.ts +3 -3
- package/src/minifier.ts +115 -100
- package/src/tokenizer.ts +78 -78
- package/dist/index.mjs +0 -984
- package/dist/index.mjs.map +0 -1
package/src/index.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export * from './constants'
|
|
2
|
-
export * from './minifier'
|
|
3
|
-
export * from './tokenizer'
|
|
1
|
+
export * from './constants'
|
|
2
|
+
export * from './minifier'
|
|
3
|
+
export * from './tokenizer'
|
package/src/minifier.ts
CHANGED
|
@@ -1,100 +1,115 @@
|
|
|
1
|
-
import { type Token, tokenize } from './tokenizer'
|
|
2
|
-
|
|
3
|
-
export type MangleMatcher = (token: Token, index: number, tokens: Token[]) => boolean
|
|
4
|
-
|
|
5
|
-
export interface MinifyOptions {
|
|
6
|
-
/** Whether to rename variables. Will call a {@link MangleMatcher} if specified. Default is `false`. */
|
|
7
|
-
mangle: boolean | MangleMatcher
|
|
8
|
-
/** A map to read and write renamed variables to when mangling. */
|
|
9
|
-
mangleMap: Map<string, string>
|
|
10
|
-
/** Whether to rename external variables such as uniforms or varyings. Default is `false`. */
|
|
11
|
-
mangleExternals: boolean
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const isWord = RegExp.prototype.test.bind(/^\w/)
|
|
15
|
-
const isSymbol = RegExp.prototype.test.bind(/[^\w\\]/)
|
|
16
|
-
const isName = RegExp.prototype.test.bind(/^[_A-Za-z]/)
|
|
17
|
-
const isStorage = RegExp.prototype.test.bind(/^(uniform|in|out|attribute|varying|,)$/)
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Minifies a string of GLSL or WGSL code.
|
|
21
|
-
*/
|
|
22
|
-
export function minify(
|
|
23
|
-
code: string,
|
|
24
|
-
{ mangle = false, mangleMap = new Map(), mangleExternals = false }: Partial<MinifyOptions> = {},
|
|
25
|
-
): string {
|
|
26
|
-
// Escape newlines after directives, skip comments
|
|
27
|
-
code = code.replace(/(^\s*#[^\\]*?)(\n|\/[\/\*])/gm, '$1\\$2')
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
let
|
|
34
|
-
let
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
1
|
+
import { type Token, tokenize } from './tokenizer'
|
|
2
|
+
|
|
3
|
+
export type MangleMatcher = (token: Token, index: number, tokens: Token[]) => boolean
|
|
4
|
+
|
|
5
|
+
export interface MinifyOptions {
|
|
6
|
+
/** Whether to rename variables. Will call a {@link MangleMatcher} if specified. Default is `false`. */
|
|
7
|
+
mangle: boolean | MangleMatcher
|
|
8
|
+
/** A map to read and write renamed variables to when mangling. */
|
|
9
|
+
mangleMap: Map<string, string>
|
|
10
|
+
/** Whether to rename external variables such as uniforms or varyings. Default is `false`. */
|
|
11
|
+
mangleExternals: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const isWord = RegExp.prototype.test.bind(/^\w/)
|
|
15
|
+
const isSymbol = RegExp.prototype.test.bind(/[^\w\\]/)
|
|
16
|
+
const isName = RegExp.prototype.test.bind(/^[_A-Za-z]/)
|
|
17
|
+
const isStorage = RegExp.prototype.test.bind(/^(binding|group|layout|uniform|in|out|attribute|varying|,)$/)
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Minifies a string of GLSL or WGSL code.
|
|
21
|
+
*/
|
|
22
|
+
export function minify(
|
|
23
|
+
code: string,
|
|
24
|
+
{ mangle = false, mangleMap = new Map(), mangleExternals = false }: Partial<MinifyOptions> = {},
|
|
25
|
+
): string {
|
|
26
|
+
// Escape newlines after directives, skip comments
|
|
27
|
+
code = code.replace(/(^\s*#[^\\]*?)(\n|\/[\/\*])/gm, '$1\\$2')
|
|
28
|
+
|
|
29
|
+
const mangleCache = new Map()
|
|
30
|
+
const exclude = new Set<string>(mangleMap.values())
|
|
31
|
+
const tokens: Token[] = tokenize(code).filter((token) => token.type !== 'whitespace' && token.type !== 'comment')
|
|
32
|
+
|
|
33
|
+
let mangleIndex: number = 0
|
|
34
|
+
let blockIndex: number | null = null
|
|
35
|
+
let minified: string = ''
|
|
36
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
37
|
+
const token = tokens[i]
|
|
38
|
+
|
|
39
|
+
// Pad alphanumeric tokens
|
|
40
|
+
if (isWord(token.value) && isWord(tokens[i - 1]?.value)) minified += ' '
|
|
41
|
+
|
|
42
|
+
// Mark enter/leave block-scope
|
|
43
|
+
if (token.value === '{' && isName(tokens[i - 1]?.value)) blockIndex = i - 1
|
|
44
|
+
else if (token.value === '}') blockIndex = null
|
|
45
|
+
|
|
46
|
+
// Pad symbols around #define and three.js #include (white-space sensitive)
|
|
47
|
+
if (
|
|
48
|
+
isSymbol(token.value) &&
|
|
49
|
+
((tokens[i - 2]?.value === '#' && tokens[i - 1]?.value === 'include') ||
|
|
50
|
+
(tokens[i - 3]?.value === '#' && tokens[i - 2]?.value === 'define'))
|
|
51
|
+
)
|
|
52
|
+
minified += ' '
|
|
53
|
+
|
|
54
|
+
// Mangle declarations and their references
|
|
55
|
+
if (
|
|
56
|
+
token.type === 'identifier' &&
|
|
57
|
+
// Filter variable names
|
|
58
|
+
token.value !== 'main' &&
|
|
59
|
+
(typeof mangle === 'boolean' ? mangle : mangle(token, i, tokens))
|
|
60
|
+
) {
|
|
61
|
+
const namespace = tokens[i - 1]?.value === '}' && tokens[i + 1]?.value === ';'
|
|
62
|
+
const storage = isStorage(tokens[i - 1]?.value) || isStorage(tokens[i - 2]?.value)
|
|
63
|
+
let renamed = mangleMap.get(token.value) ?? mangleCache.get(token.value)
|
|
64
|
+
if (
|
|
65
|
+
// no-op
|
|
66
|
+
!renamed &&
|
|
67
|
+
// Skip struct properties
|
|
68
|
+
blockIndex == null &&
|
|
69
|
+
// Is declaration, reference, namespace, or comma-separated list
|
|
70
|
+
(isName(tokens[i - 1]?.value) ||
|
|
71
|
+
// uniform Type { ... } name;
|
|
72
|
+
namespace ||
|
|
73
|
+
// float foo, bar;
|
|
74
|
+
tokens[i - 1]?.value === ',' ||
|
|
75
|
+
// fn (arg: type) -> void
|
|
76
|
+
tokens[i + 1]?.value === ':') &&
|
|
77
|
+
// Skip shader externals when disabled
|
|
78
|
+
(mangleExternals || !storage)
|
|
79
|
+
) {
|
|
80
|
+
while (!renamed || exclude.has(renamed)) {
|
|
81
|
+
renamed = ''
|
|
82
|
+
mangleIndex++
|
|
83
|
+
|
|
84
|
+
let j = mangleIndex
|
|
85
|
+
while (j > 0) {
|
|
86
|
+
renamed = String.fromCharCode(97 + ((j - 1) % 26)) + renamed
|
|
87
|
+
j = Math.floor(j / 26)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Write shader externals and preprocessor defines to mangleMap for multiple passes
|
|
92
|
+
// TODO: do so via scope tracking
|
|
93
|
+
const isExternal =
|
|
94
|
+
// Shader externals
|
|
95
|
+
(mangleExternals && storage) ||
|
|
96
|
+
// Defines
|
|
97
|
+
tokens[i - 2]?.value === '#' ||
|
|
98
|
+
// Namespaced uniform structs
|
|
99
|
+
namespace ||
|
|
100
|
+
// WGSL entrypoints via @stage or @workgroup_size(...)
|
|
101
|
+
(tokens[i - 1]?.value === 'fn' && (tokens[i - 2]?.value === ')' || tokens[i - 3]?.value === '@'))
|
|
102
|
+
const cache = isExternal ? mangleMap : mangleCache
|
|
103
|
+
cache.set(token.value, renamed)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
minified += renamed ?? token.value
|
|
107
|
+
} else {
|
|
108
|
+
if (token.value === '#' && tokens[i - 1]?.value !== '\\') minified += '\n#'
|
|
109
|
+
else if (token.value === '\\') minified += '\n'
|
|
110
|
+
else minified += token.value
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return minified.trim()
|
|
115
|
+
}
|
package/src/tokenizer.ts
CHANGED
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
import { WGSL_KEYWORDS, WGSL_SYMBOLS, GLSL_KEYWORDS, GLSL_SYMBOLS } from './constants'
|
|
2
|
-
|
|
3
|
-
export type TokenType = 'whitespace' | 'comment' | 'symbol' | 'bool' | 'float' | 'int' | 'identifier' | 'keyword'
|
|
4
|
-
|
|
5
|
-
export interface Token<T = TokenType, V = string> {
|
|
6
|
-
type: T
|
|
7
|
-
value: V
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
// Checks for WGSL-specific `fn foo(`, `var bar =`, and `let baz =`
|
|
11
|
-
const isWGSL = RegExp.prototype.test.bind(/\bfn\s+\w+\s*\(|\b(var|let)\s+\w+\s*[:=]/)
|
|
12
|
-
|
|
13
|
-
const isFloat = RegExp.prototype.test.bind(/^(\d+\.\d*|\d*\.\d+)([eEpP][-+]?\d+)?[fFhH]?$/)
|
|
14
|
-
const isInt = RegExp.prototype.test.bind(/^(0[xX][\w\d]+|\d+)[iIuU]?$/)
|
|
15
|
-
const isBool = RegExp.prototype.test.bind(/^(true|false)$/)
|
|
16
|
-
|
|
17
|
-
const ZERO = 48
|
|
18
|
-
const NINE = 57
|
|
19
|
-
const A = 65
|
|
20
|
-
const Z = 90
|
|
21
|
-
const LF = 10
|
|
22
|
-
const CR = 13
|
|
23
|
-
const TAB = 9
|
|
24
|
-
const SPACE = 32
|
|
25
|
-
const UNDERSCORE = 95
|
|
26
|
-
const SLASH = 47
|
|
27
|
-
const STAR = 42
|
|
28
|
-
const HASH = 35
|
|
29
|
-
const AT = 64
|
|
30
|
-
|
|
31
|
-
const isDigit = (c: number) => ZERO <= c && c <= NINE
|
|
32
|
-
const isAlpha = (c: number) => ((c &= ~32), A <= c && c <= Z)
|
|
33
|
-
const isLine = (c: number) => c === LF || c === CR
|
|
34
|
-
const isSpace = (c: number) => isLine(c) || c === TAB || c === SPACE
|
|
35
|
-
const isIdent = (c: number) => isAlpha(c) || isDigit(c) || c === UNDERSCORE
|
|
36
|
-
const isMacro = (c: number) => c === HASH || c === AT
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Tokenizes a string of GLSL or WGSL code.
|
|
40
|
-
*/
|
|
41
|
-
export function tokenize(code: string, index: number = 0): Token[] {
|
|
42
|
-
const tokens: Token[] = []
|
|
43
|
-
|
|
44
|
-
let prev: number = -1
|
|
45
|
-
const [KEYWORDS, SYMBOLS] = isWGSL(code) ? [WGSL_KEYWORDS, WGSL_SYMBOLS] : [GLSL_KEYWORDS, GLSL_SYMBOLS]
|
|
46
|
-
while (index < code.length) {
|
|
47
|
-
let value = code[index]
|
|
48
|
-
const char = code.charCodeAt(index++)
|
|
49
|
-
|
|
50
|
-
if (isSpace(char)) {
|
|
51
|
-
while (isSpace(code.charCodeAt(index))) value += code[index++]
|
|
52
|
-
tokens.push({ type: 'whitespace', value })
|
|
53
|
-
} else if (isDigit(char)) {
|
|
54
|
-
while (isFloat(value + code[index]) || isInt(value + code[index])) value += code[index++]
|
|
55
|
-
if (isFloat(value)) tokens.push({ type: 'float', value })
|
|
56
|
-
else tokens.push({ type: 'int', value })
|
|
57
|
-
} else if (isIdent(char)) {
|
|
58
|
-
while (isIdent(code.charCodeAt(index))) value += code[index++]
|
|
59
|
-
if (isBool(value)) tokens.push({ type: 'bool', value })
|
|
60
|
-
else if (KEYWORDS.includes(isMacro(prev) ? String.fromCharCode(prev) + value : value))
|
|
61
|
-
tokens.push({ type: 'keyword', value })
|
|
62
|
-
else tokens.push({ type: 'identifier', value })
|
|
63
|
-
} else if (char === SLASH && (code.charCodeAt(index) === SLASH || code.charCodeAt(index) === STAR)) {
|
|
64
|
-
const terminator = code.charCodeAt(index) === STAR ? '*/' : '\n'
|
|
65
|
-
while (!value.endsWith(terminator)) value += code[index++]
|
|
66
|
-
tokens.push({ type: 'comment', value })
|
|
67
|
-
} else {
|
|
68
|
-
for (const symbol of SYMBOLS) {
|
|
69
|
-
if (symbol.length > value.length && code.startsWith(symbol, index - 1)) value = symbol
|
|
70
|
-
}
|
|
71
|
-
index += value.length - 1
|
|
72
|
-
tokens.push({ type: 'symbol', value })
|
|
73
|
-
}
|
|
74
|
-
prev = char
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return tokens
|
|
78
|
-
}
|
|
1
|
+
import { WGSL_KEYWORDS, WGSL_SYMBOLS, GLSL_KEYWORDS, GLSL_SYMBOLS } from './constants'
|
|
2
|
+
|
|
3
|
+
export type TokenType = 'whitespace' | 'comment' | 'symbol' | 'bool' | 'float' | 'int' | 'identifier' | 'keyword'
|
|
4
|
+
|
|
5
|
+
export interface Token<T = TokenType, V = string> {
|
|
6
|
+
type: T
|
|
7
|
+
value: V
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Checks for WGSL-specific `fn foo(`, `var bar =`, and `let baz =`
|
|
11
|
+
const isWGSL = RegExp.prototype.test.bind(/\bfn\s+\w+\s*\(|\b(var|let)\s+\w+\s*[:=]/)
|
|
12
|
+
|
|
13
|
+
const isFloat = RegExp.prototype.test.bind(/^(\d+\.\d*|\d*\.\d+)([eEpP][-+]?\d+)?[fFhH]?$/)
|
|
14
|
+
const isInt = RegExp.prototype.test.bind(/^(0[xX][\w\d]+|\d+)[iIuU]?$/)
|
|
15
|
+
const isBool = RegExp.prototype.test.bind(/^(true|false)$/)
|
|
16
|
+
|
|
17
|
+
const ZERO = 48
|
|
18
|
+
const NINE = 57
|
|
19
|
+
const A = 65
|
|
20
|
+
const Z = 90
|
|
21
|
+
const LF = 10
|
|
22
|
+
const CR = 13
|
|
23
|
+
const TAB = 9
|
|
24
|
+
const SPACE = 32
|
|
25
|
+
const UNDERSCORE = 95
|
|
26
|
+
const SLASH = 47
|
|
27
|
+
const STAR = 42
|
|
28
|
+
const HASH = 35
|
|
29
|
+
const AT = 64
|
|
30
|
+
|
|
31
|
+
const isDigit = (c: number) => ZERO <= c && c <= NINE
|
|
32
|
+
const isAlpha = (c: number) => ((c &= ~32), A <= c && c <= Z)
|
|
33
|
+
const isLine = (c: number) => c === LF || c === CR
|
|
34
|
+
const isSpace = (c: number) => isLine(c) || c === TAB || c === SPACE
|
|
35
|
+
const isIdent = (c: number) => isAlpha(c) || isDigit(c) || c === UNDERSCORE
|
|
36
|
+
const isMacro = (c: number) => c === HASH || c === AT
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Tokenizes a string of GLSL or WGSL code.
|
|
40
|
+
*/
|
|
41
|
+
export function tokenize(code: string, index: number = 0): Token[] {
|
|
42
|
+
const tokens: Token[] = []
|
|
43
|
+
|
|
44
|
+
let prev: number = -1
|
|
45
|
+
const [KEYWORDS, SYMBOLS] = isWGSL(code) ? [WGSL_KEYWORDS, WGSL_SYMBOLS] : [GLSL_KEYWORDS, GLSL_SYMBOLS]
|
|
46
|
+
while (index < code.length) {
|
|
47
|
+
let value = code[index]
|
|
48
|
+
const char = code.charCodeAt(index++)
|
|
49
|
+
|
|
50
|
+
if (isSpace(char)) {
|
|
51
|
+
while (isSpace(code.charCodeAt(index))) value += code[index++]
|
|
52
|
+
tokens.push({ type: 'whitespace', value })
|
|
53
|
+
} else if (isDigit(char)) {
|
|
54
|
+
while (isFloat(value + code[index]) || isInt(value + code[index])) value += code[index++]
|
|
55
|
+
if (isFloat(value)) tokens.push({ type: 'float', value })
|
|
56
|
+
else tokens.push({ type: 'int', value })
|
|
57
|
+
} else if (isIdent(char)) {
|
|
58
|
+
while (isIdent(code.charCodeAt(index))) value += code[index++]
|
|
59
|
+
if (isBool(value)) tokens.push({ type: 'bool', value })
|
|
60
|
+
else if (KEYWORDS.includes(isMacro(prev) ? String.fromCharCode(prev) + value : value))
|
|
61
|
+
tokens.push({ type: 'keyword', value })
|
|
62
|
+
else tokens.push({ type: 'identifier', value })
|
|
63
|
+
} else if (char === SLASH && (code.charCodeAt(index) === SLASH || code.charCodeAt(index) === STAR)) {
|
|
64
|
+
const terminator = code.charCodeAt(index) === STAR ? '*/' : '\n'
|
|
65
|
+
while (!value.endsWith(terminator)) value += code[index++]
|
|
66
|
+
tokens.push({ type: 'comment', value })
|
|
67
|
+
} else {
|
|
68
|
+
for (const symbol of SYMBOLS) {
|
|
69
|
+
if (symbol.length > value.length && code.startsWith(symbol, index - 1)) value = symbol
|
|
70
|
+
}
|
|
71
|
+
index += value.length - 1
|
|
72
|
+
tokens.push({ type: 'symbol', value })
|
|
73
|
+
}
|
|
74
|
+
prev = char
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return tokens
|
|
78
|
+
}
|