shaderkit 0.0.2 → 0.1.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/README.md +183 -1
- package/dist/index.js +563 -122
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +563 -122
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/src/constants.ts +496 -19
- package/src/minifier.ts +39 -72
- package/src/tokenizer.ts +53 -44
package/src/minifier.ts
CHANGED
|
@@ -9,111 +9,78 @@ export interface MinifyOptions {
|
|
|
9
9
|
mangleMap: Map<string, string>
|
|
10
10
|
/** Whether to rename external variables such as uniforms or varyings. Default is `false`. */
|
|
11
11
|
mangleExternals: boolean
|
|
12
|
-
/** Whether to rename properties in structs or uniform buffers. Default is `false`. */
|
|
13
|
-
mangleProperties: boolean
|
|
14
12
|
}
|
|
15
13
|
|
|
14
|
+
const isWord = RegExp.prototype.test.bind(/^\w/)
|
|
15
|
+
const isName = RegExp.prototype.test.bind(/^[A-Za-z]/)
|
|
16
|
+
const isStorage = RegExp.prototype.test.bind(/^(uniform|in|out|attribute|varying|,)$/)
|
|
17
|
+
|
|
16
18
|
/**
|
|
17
|
-
* Minifies a string of GLSL code.
|
|
19
|
+
* Minifies a string of GLSL or WGSL code.
|
|
18
20
|
*/
|
|
19
21
|
export function minify(
|
|
20
22
|
code: string,
|
|
21
|
-
{
|
|
22
|
-
mangle = false,
|
|
23
|
-
mangleMap = new Map(),
|
|
24
|
-
mangleExternals = false,
|
|
25
|
-
mangleProperties = false,
|
|
26
|
-
}: Partial<MinifyOptions> = {},
|
|
23
|
+
{ mangle = false, mangleMap = new Map(), mangleExternals = false }: Partial<MinifyOptions> = {},
|
|
27
24
|
): string {
|
|
25
|
+
// Escape newlines after directives, skip comments
|
|
26
|
+
code = code.replace(/(^\s*#[^\\]*?)(\n|\/[\/\*])/gm, '$1\\$2')
|
|
27
|
+
|
|
28
28
|
const exclude = new Set<string>(mangleMap.values())
|
|
29
|
-
const tokens: Token[] = tokenize(code).filter((token) => token.type !== 'comment')
|
|
29
|
+
const tokens: Token[] = tokenize(code).filter((token) => token.type !== 'whitespace' && token.type !== 'comment')
|
|
30
30
|
|
|
31
31
|
let mangleIndex: number = 0
|
|
32
32
|
let blockIndex: number | null = null
|
|
33
|
-
let prefix: string | null = null
|
|
34
33
|
let minified: string = ''
|
|
35
34
|
for (let i = 0; i < tokens.length; i++) {
|
|
36
35
|
const token = tokens[i]
|
|
37
|
-
if (/keyword|identifier/.test(token.type)) {
|
|
38
|
-
if (/keyword|identifier/.test(tokens[i - 1]?.type)) minified += ' '
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
37
|
+
// Pad alphanumeric tokens
|
|
38
|
+
if (isWord(token.value) && isWord(tokens[i - 1]?.value)) minified += ' '
|
|
39
|
+
|
|
40
|
+
// Mark enter/leave block-scope
|
|
41
|
+
if (token.value === '{' && isName(tokens[i - 1]?.value)) blockIndex = i - 1
|
|
42
|
+
else if (token.value === '}') blockIndex = null
|
|
47
43
|
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
// Mangle declarations and their references
|
|
45
|
+
if (isName(token.value)) {
|
|
46
|
+
let renamed = mangleMap.get(token.value)
|
|
50
47
|
if (
|
|
51
48
|
// no-op
|
|
52
49
|
!renamed &&
|
|
50
|
+
// Skip struct properties
|
|
51
|
+
blockIndex == null &&
|
|
53
52
|
// Filter variable names
|
|
54
|
-
|
|
53
|
+
token.value !== 'main' &&
|
|
55
54
|
(typeof mangle === 'boolean' ? mangle : mangle(token, i, tokens)) &&
|
|
56
55
|
// Is declaration, reference, namespace, or comma-separated list
|
|
57
56
|
token.type === 'identifier' &&
|
|
58
|
-
(
|
|
57
|
+
(isName(tokens[i - 1]?.value) || /}|,/.test(tokens[i - 1]?.value) || tokens[i + 1]?.value === ':') &&
|
|
58
|
+
// Skip shader externals when disabled
|
|
59
|
+
(mangleExternals || (!isStorage(tokens[i - 1]?.value) && !isStorage(tokens[i - 2]?.value)))
|
|
59
60
|
) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// Skip shader externals when disabled
|
|
64
|
-
(blockIndex != null
|
|
65
|
-
? // Struct member
|
|
66
|
-
(mangleExternals && mangleProperties) ||
|
|
67
|
-
!/(struct|uniform|in|out|attribute|varying)/.test(tokens[blockIndex - 1]?.value)
|
|
68
|
-
: // Struct header, fully specified uniform, or comma-separated list
|
|
69
|
-
mangleExternals ||
|
|
70
|
-
(!/(uniform|in|out|attribute|varying|,)/.test(tokens[i - 1]?.value) &&
|
|
71
|
-
!/(uniform|in|out|attribute|varying)/.test(tokens[i - 2]?.value)))
|
|
72
|
-
) {
|
|
73
|
-
while (!renamed || exclude.has(renamed)) {
|
|
74
|
-
renamed = ''
|
|
75
|
-
mangleIndex++
|
|
61
|
+
while (!renamed || exclude.has(renamed)) {
|
|
62
|
+
renamed = ''
|
|
63
|
+
mangleIndex++
|
|
76
64
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
65
|
+
let j = mangleIndex
|
|
66
|
+
while (j > 0) {
|
|
67
|
+
renamed = String.fromCharCode(97 + ((j % 26) - 1)) + renamed
|
|
68
|
+
j = Math.floor(j / 26)
|
|
82
69
|
}
|
|
83
|
-
|
|
84
|
-
mangleMap.set(key, renamed)
|
|
85
70
|
}
|
|
86
71
|
|
|
87
|
-
|
|
88
|
-
// If UBOs don't specify a suffix, their inner declarations are global
|
|
89
|
-
if (tokens[i + 1]?.value === '{') {
|
|
90
|
-
let j = i
|
|
91
|
-
while (tokens[j].value !== '}') j++
|
|
92
|
-
const suffix = tokens[j + 1].value
|
|
93
|
-
if (suffix !== ';') prefix = suffix
|
|
94
|
-
blockIndex = i
|
|
95
|
-
} else if (tokens[i + 2]?.value === '}') {
|
|
96
|
-
prefix = null
|
|
97
|
-
blockIndex = null
|
|
98
|
-
}
|
|
72
|
+
mangleMap.set(token.value, renamed)
|
|
99
73
|
}
|
|
100
74
|
|
|
101
75
|
minified += renamed ?? token.value
|
|
102
|
-
} else if (token.value === '#') {
|
|
103
|
-
// Don't pad consecutive directives
|
|
104
|
-
if (tokens[i - 1]?.value !== '\\') minified += '\n'
|
|
105
|
-
|
|
106
|
-
// Join preprocessor directives
|
|
107
|
-
while (tokens[i].value !== '\\') {
|
|
108
|
-
if ((tokens[i].type !== 'symbol' || tokens[i - 2]?.value === '#') && tokens[i - 1]?.type !== 'symbol')
|
|
109
|
-
minified += ' '
|
|
110
|
-
minified += tokens[i].value
|
|
111
|
-
i++
|
|
112
|
-
}
|
|
113
76
|
|
|
114
|
-
|
|
77
|
+
// three.js has a white-space sensitive RegExp
|
|
78
|
+
// https://github.com/mrdoob/three.js/blob/dev/src/renderers/webgl/WebGLProgram.js#L206
|
|
79
|
+
if (token.value === 'include') minified += ' '
|
|
115
80
|
} else {
|
|
116
|
-
minified +=
|
|
81
|
+
if (token.value === '#' && tokens[i - 1]?.value !== '\\') minified += '\n#'
|
|
82
|
+
else if (token.value === '\\') minified += '\n'
|
|
83
|
+
else minified += token.value
|
|
117
84
|
}
|
|
118
85
|
}
|
|
119
86
|
|
package/src/tokenizer.ts
CHANGED
|
@@ -1,64 +1,73 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SYMBOLS, WGSL_KEYWORDS, GLSL_KEYWORDS } from './constants'
|
|
2
2
|
|
|
3
|
-
export type TokenType = 'comment' | 'symbol' | 'bool' | 'float' | 'int' | '
|
|
3
|
+
export type TokenType = 'whitespace' | 'comment' | 'symbol' | 'bool' | 'float' | 'int' | 'identifier' | 'keyword'
|
|
4
4
|
|
|
5
5
|
export interface Token<T = TokenType, V = string> {
|
|
6
6
|
type: T
|
|
7
7
|
value: V
|
|
8
8
|
}
|
|
9
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)\b/)
|
|
12
|
+
|
|
13
|
+
const isFloat = RegExp.prototype.test.bind(/\.|[eEpP][-+]?\d|[fFhH]$/)
|
|
14
|
+
const isBool = RegExp.prototype.test.bind(/^(true|false)$/)
|
|
15
|
+
|
|
16
|
+
const ZERO = 48
|
|
17
|
+
const NINE = 57
|
|
18
|
+
const A = 65
|
|
19
|
+
const Z = 90
|
|
20
|
+
const LF = 10
|
|
21
|
+
const CR = 13
|
|
22
|
+
const TAB = 9
|
|
23
|
+
const SPACE = 32
|
|
24
|
+
const PLUS = 43
|
|
25
|
+
const MINUS = 45
|
|
26
|
+
const DOT = 46
|
|
27
|
+
const UNDERSCORE = 95
|
|
28
|
+
const SLASH = 47
|
|
29
|
+
const STAR = 42
|
|
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 isNumber = (c: number) => isAlpha(c) || isDigit(c) || c === PLUS || c === MINUS || c === DOT
|
|
36
|
+
const isIdent = (c: number) => isAlpha(c) || isDigit(c) || c === UNDERSCORE
|
|
37
|
+
|
|
10
38
|
/**
|
|
11
|
-
* Tokenizes a string of GLSL code.
|
|
39
|
+
* Tokenizes a string of GLSL or WGSL code.
|
|
12
40
|
*/
|
|
13
41
|
export function tokenize(code: string, index: number = 0): Token[] {
|
|
14
42
|
const tokens: Token[] = []
|
|
15
43
|
|
|
44
|
+
const KEYWORDS = isWGSL(code) ? WGSL_KEYWORDS : GLSL_KEYWORDS
|
|
16
45
|
while (index < code.length) {
|
|
17
|
-
let
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
// Skip whitespace
|
|
21
|
-
if (/\s/.test(char)) {
|
|
22
|
-
index++
|
|
23
|
-
continue
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Escape newlines after directives, skip comments
|
|
27
|
-
if (char === '#') {
|
|
28
|
-
let j = index
|
|
29
|
-
while (code[j] !== '\n' && !/\/[\/\*]/.test(code[j] + code[j + 1])) j++
|
|
30
|
-
code = code.substring(0, j) + '\\' + code.substring(j)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Parse values and identifiers
|
|
34
|
-
while ((/\d/.test(char) ? /[\d\.\-\w]/ : /\w/).test(code[index])) value += code[index++]
|
|
46
|
+
let value = code[index]
|
|
47
|
+
const char = code.charCodeAt(index++)
|
|
35
48
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
value
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
if (isSpace(char)) {
|
|
50
|
+
while (isSpace(code.charCodeAt(index))) value += code[index++]
|
|
51
|
+
tokens.push({ type: 'whitespace', value })
|
|
52
|
+
} else if (isDigit(char)) {
|
|
53
|
+
while (isNumber(code.charCodeAt(index))) value += code[index++]
|
|
54
|
+
if (isFloat(value)) tokens.push({ type: 'float', value })
|
|
55
|
+
else tokens.push({ type: 'int', value })
|
|
56
|
+
} else if (isAlpha(char)) {
|
|
57
|
+
while (isIdent(code.charCodeAt(index))) value += code[index++]
|
|
58
|
+
if (isBool(value)) tokens.push({ type: 'bool', value })
|
|
59
|
+
else if (KEYWORDS.includes(value)) tokens.push({ type: 'keyword', value })
|
|
60
|
+
else tokens.push({ type: 'identifier', value })
|
|
61
|
+
} else if (char === SLASH && (code.charCodeAt(index) === SLASH || code.charCodeAt(index) === STAR)) {
|
|
62
|
+
const isMultiline = code.charCodeAt(index) === STAR
|
|
48
63
|
while (!value.endsWith(isMultiline ? '*/' : '\n')) value += code[index++]
|
|
49
64
|
tokens.push({ type: 'comment', value })
|
|
50
|
-
} else if (!/\w/.test(char)) {
|
|
51
|
-
tokens.push({ type: 'symbol', value })
|
|
52
|
-
} else if (/\d/.test(char)) {
|
|
53
|
-
if (/[uU]/.test(value)) tokens.push({ type: 'uint', value })
|
|
54
|
-
else if (/\.|[eE]-?\d/.test(value)) tokens.push({ type: 'float', value })
|
|
55
|
-
else tokens.push({ type: 'int', value })
|
|
56
|
-
} else if (/^(true|false)$/.test(value)) {
|
|
57
|
-
tokens.push({ type: 'bool', value })
|
|
58
|
-
} else if (GLSL_KEYWORDS.includes(tokens[tokens.length - 1]?.value === '#' ? `#${value}` : value)) {
|
|
59
|
-
tokens.push({ type: 'keyword', value })
|
|
60
65
|
} else {
|
|
61
|
-
|
|
66
|
+
for (const symbol of SYMBOLS) {
|
|
67
|
+
if (symbol.length > value.length && code.startsWith(symbol, index - 1)) value = symbol
|
|
68
|
+
}
|
|
69
|
+
index += value.length - 1
|
|
70
|
+
tokens.push({ type: 'symbol', value })
|
|
62
71
|
}
|
|
63
72
|
}
|
|
64
73
|
|