Skip to content

Commit e877883

Browse files
feat(dev-tools): add ts-transform-remove-glsl-comments (#455)
1 parent 43e2ec0 commit e877883

16 files changed

+362
-32
lines changed

.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
dist/
2+
test-case-*.ts

.prettierignore

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
**/dist*/**/*.js
2+
test-case-*.ts

modules/dev-tools/package.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@
3636
"./ts-transform-append-extension": {
3737
"require": "./dist/ts-plugins/ts-transform-append-extension/index.cjs",
3838
"import": "./dist/ts-plugins/ts-transform-append-extension/index.js"
39+
},
40+
"./ts-transform-remove-glsl-comments": {
41+
"require": "./dist/ts-plugins/ts-transform-remove-glsl-comments/index.cjs",
42+
"import": "./dist/ts-plugins/ts-transform-remove-glsl-comments/index.js"
3943
}
4044
},
4145
"types": "./dist/index.d.ts",
@@ -86,6 +90,7 @@
8690
"eslint-plugin-react-hooks": "^4.0.0",
8791
"glob": "^7.1.4",
8892
"lerna": "^3.14.1",
93+
"minimatch": "^3.0.0",
8994
"prettier": "3.0.3",
9095
"prettier-check": "2.0.0",
9196
"tape": "^4.11.0",
@@ -94,7 +99,6 @@
9499
"ts-node": "~10.9.0",
95100
"ts-patch": "^3.1.2",
96101
"tsconfig-paths": "^4.1.1",
97-
"url": "^0.11.0",
98102
"vite": "^4.0.1",
99103
"vite-plugin-html": "^3.2.0"
100104
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* TypeScript transform to remove comments and unnecessary white space from GLSL source.
3+
* A template string is considered GLSL source if:
4+
a) the file matches the pattern specified in the plugin config; or
5+
b) it is tagged as glsl`...`
6+
* Usage with ts-patch:
7+
{
8+
"plugins": [
9+
{
10+
"transform": "ocular-dev-tools/ts-transform-remove-glsl-comments",
11+
"pattern": ["*.glsl.ts"]
12+
}
13+
]
14+
}
15+
*/
16+
import * as path from 'path';
17+
import type {Program, TransformationContext, SourceFile, Node} from 'typescript';
18+
import type {TransformerExtras, PluginConfig} from 'ts-patch';
19+
import minimatch from 'minimatch';
20+
21+
// inline comment is only safe to remove if it's followed by a return (i.e. end of comment)
22+
const INLINE_COMMENT_REGEX = /\s*\/\/.*[\n\r]/g;
23+
const BLOCK_COMMENT_REGEX = /\s*\/\*(\*(?!\/)|[^*])*\*\//g;
24+
const WHITESPACE_REGEX = /\s*[\n\r]\s*/gm;
25+
const DEFAULT_PATTERNS = [];
26+
27+
type RemoveGLSLCommentsPluginConfig = PluginConfig & {
28+
/** Glob patterns of shader files to include. */
29+
pattern?: string[];
30+
};
31+
32+
export default function (
33+
program: Program,
34+
pluginConfig: RemoveGLSLCommentsPluginConfig,
35+
{ts}: TransformerExtras
36+
) {
37+
const {pattern = DEFAULT_PATTERNS} = pluginConfig;
38+
39+
return (ctx: TransformationContext) => {
40+
const {factory} = ctx;
41+
42+
return (sourceFile: SourceFile) => {
43+
const isShaderFile = matchFilePath(sourceFile.fileName, pattern);
44+
45+
function replaceShaderString(node: Node): Node {
46+
if (ts.isNoSubstitutionTemplateLiteral(node)) {
47+
const text = node.rawText ?? '';
48+
// Convert source text to string content
49+
const newText = filterShaderSource(text);
50+
if (newText === text) {
51+
return node;
52+
}
53+
return factory.createNoSubstitutionTemplateLiteral(newText, newText);
54+
}
55+
if (ts.isTemplateLiteralToken(node)) {
56+
const text = node.rawText ?? '';
57+
const newText = filterShaderSource(text);
58+
if (newText === text) {
59+
return node;
60+
}
61+
if (ts.isTemplateHead(node)) {
62+
return factory.createTemplateHead(newText, newText);
63+
}
64+
if (ts.isTemplateMiddle(node)) {
65+
return factory.createTemplateMiddle(newText, newText);
66+
}
67+
if (ts.isTemplateTail(node)) {
68+
return factory.createTemplateTail(newText, newText);
69+
}
70+
return node;
71+
}
72+
return ts.visitEachChild(node, replaceShaderString, ctx);
73+
}
74+
75+
function visit(node: Node): Node {
76+
if (
77+
ts.isTaggedTemplateExpression(node) &&
78+
// First child is the tag identifier
79+
node.getChildAt(0).getText() === 'glsl'
80+
) {
81+
// Strip the template tag
82+
return replaceShaderString(node.getChildAt(1));
83+
}
84+
if (isShaderFile && ts.isTemplateLiteral(node)) {
85+
return replaceShaderString(node);
86+
}
87+
return ts.visitEachChild(node, visit, ctx);
88+
}
89+
return ts.visitNode(sourceFile, visit);
90+
};
91+
};
92+
}
93+
94+
function matchFilePath(filePath: string, includePatterns: string[]): boolean {
95+
const relPath = path.relative(process.env.PWD ?? '', filePath);
96+
for (const pattern of includePatterns) {
97+
if (minimatch(relPath, pattern)) {
98+
return true;
99+
}
100+
}
101+
return false;
102+
}
103+
104+
function filterShaderSource(source: string): string {
105+
return source
106+
.replace(INLINE_COMMENT_REGEX, '\n')
107+
.replace(BLOCK_COMMENT_REGEX, '')
108+
.replace(WHITESPACE_REGEX, '\n');
109+
}

modules/dev-tools/test/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ import './lib/configuration.spec';
33

44
import './ts-plugins/ts-transform-version-inline.spec';
55
import './ts-plugins/ts-transform-append-extension.spec';
6+
import './ts-plugins/ts-transform-remove-glsl-comments/index.spec';

modules/dev-tools/test/ts-plugins/test-transformer.ts

+50-1
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import type {PluginConfig} from 'ts-patch';
55
* Transpile ts code with TypeScript compiler API
66
*/
77
export function transpile({
8+
sourceFileName = 'test.ts',
89
source,
910
transformer,
1011
config = {},
1112
outputType = 'js'
1213
}: {
14+
sourceFileName?: string;
1315
source: string;
1416
transformer: Function;
1517
config?: PluginConfig;
@@ -25,7 +27,7 @@ export function transpile({
2527
}
2628
});
2729

28-
project.createSourceFile('test.ts', source);
30+
project.createSourceFile(sourceFileName, source);
2931

3032
const customTransformers: ts.CustomTransformers = {};
3133
const transform: ts.TransformerFactory<ts.SourceFile> = transformer(
@@ -49,3 +51,50 @@ export function transpile({
4951

5052
return result.getFiles()[0].text;
5153
}
54+
55+
/**
56+
* Compare two pieces of source code. Returns a description of the difference, or null if identical.
57+
*/
58+
export function assertSourceEqual(
59+
actual: string,
60+
expected: string,
61+
options: {
62+
/** If true, ignore difference in indent
63+
* @default true
64+
*/
65+
ignoreIndent?: boolean;
66+
/** If true, ignore empty lines
67+
* @default true
68+
*/
69+
ignoreEmptyLines?: boolean;
70+
} = {}
71+
): true | Error {
72+
const {ignoreIndent = true, ignoreEmptyLines = true} = options;
73+
const actualLines = actual.split('\n');
74+
const expectedLines = expected.split('\n');
75+
let i1 = 0;
76+
let i2 = 0;
77+
78+
while (i1 < actualLines.length || i2 < expectedLines.length) {
79+
let t1 = actualLines[i1] ?? '';
80+
let t2 = expectedLines[i2] ?? '';
81+
if (ignoreIndent) {
82+
t1 = t1.trimStart();
83+
t2 = t2.trimStart();
84+
}
85+
if (t1 === t2) {
86+
i1++;
87+
i2++;
88+
} else if (ignoreEmptyLines && !t1) {
89+
i1++;
90+
} else if (ignoreEmptyLines && !t2) {
91+
i2++;
92+
} else {
93+
return new Error(`Mismatch at line ${i1}
94+
Actual: ${t1}
95+
Expected: ${t2}
96+
`);
97+
}
98+
}
99+
return true;
100+
}

modules/dev-tools/test/ts-plugins/ts-transform-append-extension.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import test from 'tape-promise/tape';
2-
import {transpile} from './test-transformer.js';
2+
import {transpile, assertSourceEqual} from './test-transformer.js';
33
// @ts-expect-error Aliased import, remapped to valid path in esm-loader
44
import appendExtension from 'ocular-dev-tools/ts-plugins/ts-transform-append-extension';
55

@@ -50,7 +50,7 @@ test('ts-transform-append-extension', (t) => {
5050
outputType: testCase.config.afterDeclarations ? 'd.ts' : 'js'
5151
});
5252

53-
t.is(result.trim(), testCase.output, testCase.title);
53+
t.is(assertSourceEqual(result, testCase.output), true, testCase.title);
5454
}
5555

5656
t.end();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import {fileURLToPath} from 'node:url';
4+
5+
import test from 'tape-promise/tape';
6+
import {transpile, assertSourceEqual} from '../test-transformer.js';
7+
// @ts-expect-error Aliased import, remapped to valid path in esm-loader
8+
import removeGLSLComments from 'ocular-dev-tools/ts-plugins/ts-transform-remove-glsl-comments';
9+
10+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
11+
12+
function loadSourceFromFile(fileName: string): string {
13+
return fs.readFileSync(path.join(__dirname, fileName), 'utf8');
14+
}
15+
16+
const testCases = [
17+
{
18+
title: 'no comments',
19+
fileName: 'test.glsl.ts',
20+
config: {pattern: ['**/*.glsl.ts']},
21+
input: 'test-case-0.ts',
22+
output: 'test-case-0-expected.ts'
23+
},
24+
{
25+
title: 'remove comments from template literal',
26+
fileName: 'test.glsl.ts',
27+
config: {pattern: ['**/*.glsl.ts']},
28+
input: 'test-case-1.ts',
29+
output: 'test-case-1-expected.ts'
30+
},
31+
{
32+
title: 'excluded by file name',
33+
fileName: 'test.ts',
34+
config: {},
35+
input: 'test-case-1.ts',
36+
output: 'test-case-1.ts'
37+
},
38+
{
39+
title: 'included by template tag',
40+
fileName: 'test.ts',
41+
config: {},
42+
input: 'test-case-2.ts',
43+
output: 'test-case-2-expected.ts'
44+
}
45+
];
46+
47+
test('ts-transform-remove-glsl-comments', (t) => {
48+
for (const testCase of testCases) {
49+
const result = transpile({
50+
sourceFileName: testCase.fileName,
51+
source: loadSourceFromFile(testCase.input),
52+
transformer: removeGLSLComments,
53+
config: testCase.config
54+
});
55+
const expected = loadSourceFromFile(testCase.output);
56+
57+
t.is(assertSourceEqual(result, expected, {ignoreEmptyLines: false}), true, testCase.title);
58+
}
59+
60+
t.end();
61+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export function getTime() {
2+
// Template literal that is not shader
3+
return `The time is ${Date.now()}`;
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export function getTime() {
2+
// Template literal that is not shader
3+
return `The time is ${Date.now()}`;
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Constants
2+
const COORDINATE_SYSTEM = {
3+
CARTESIAN: 0,
4+
LNGLAT: 1
5+
};
6+
const constantDefinitions = Object.keys(COORDINATE_SYSTEM)
7+
.map((key) => `const int COORDINATE_SYSTEM_${key} = ${COORDINATE_SYSTEM[key]};`)
8+
.join('\n');
9+
// Vertex shader
10+
export const vs = `\
11+
#version 300 es
12+
${constantDefinitions}
13+
in vec4 position;
14+
in vec4 color;
15+
uniform mat4 pMatrix;
16+
uniform mat4 mMatrix;
17+
uniform float opacity;
18+
out vec4 vColor;
19+
main() {
20+
gl_Position = pMatrix * mMatrix * position;
21+
vColor = vec4(color, color.a * opacity);
22+
}
23+
`;
24+
// Fragment shader
25+
export const fs = `\
26+
#version 300 es
27+
in vec4 vColor;
28+
main() {
29+
if (vColor.a == 0.0) {
30+
discard;
31+
}
32+
gl_FragColor = vColor;
33+
}
34+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Constants
2+
const COORDINATE_SYSTEM = {
3+
CARTESIAN: 0,
4+
LNGLAT: 1
5+
};
6+
const constantDefinitions = Object.keys(COORDINATE_SYSTEM)
7+
.map((key) => `const int COORDINATE_SYSTEM_${key} = ${COORDINATE_SYSTEM[key]};`)
8+
.join('\n');
9+
// Vertex shader
10+
export const vs = `\
11+
#version 300 es
12+
${constantDefinitions}
13+
14+
in vec4 position;
15+
in vec4 color;
16+
17+
uniform mat4 pMatrix; // Projection matrix
18+
uniform mat4 mMatrix; // Model matrix
19+
uniform float opacity;
20+
21+
out vec4 vColor;
22+
23+
main() {
24+
gl_Position = pMatrix * mMatrix * position;
25+
vColor = vec4(color, /* inline comment */ color.a * opacity);
26+
}
27+
`;
28+
// Fragment shader
29+
export const fs = `\
30+
#version 300 es
31+
32+
in vec4 vColor;
33+
34+
main() {
35+
if (vColor.a == 0.0) {
36+
/*
37+
Remove transparent fragment
38+
*/
39+
discard;
40+
}
41+
gl_FragColor = vColor;
42+
}
43+
`;

0 commit comments

Comments
 (0)