Skip to content

Commit a331579

Browse files
chore(atomic): add SVG transformer (#4867)
Update the Lit build process to be support SVG imports ## Explanation We are essentially creating a compiler which takes a list of TypeScript files (Lit components and dependencies) and compiles them to their corresponding JavaScript keeping the same file structure as in `/src`. [Typescript doc ](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API#a-minimal-compiler) To that compiler, I have added a SVG custom transformer. ### Example The following typescript file ```ts import Tick from '../../../images/checkbox.svg'; () => console.log(Tick); ``` was transpiled to ```js import Tick from '../../../images/checkbox.svg'; () => console.log(Tick); ``` With the SVG transformer, we have ```js const Tick = "<svg viewBox=\"0 0 12 9\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M1.5 5L4.6 7.99999L11 1\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n</svg>"; () => console.log(Tick); ``` ### Pseudo-code 1. Load the TypeScript configuration file (tsconfig.lit.json) 2. Create a default CompilerHost which uses the file system to get files (using `program.emit` function). 3. To that program, provide a customTransformers which will essentially visit recursively tree nodes and check for `svg` import statements. 4. When a SVG import statement is encountered, replace it with a "create variable statement" using the `createVariableStatement` typescript built-in method. Ensure to right-hand side of the assignation is the content of the .svg file https://coveord.atlassian.net/browse/KIT-3865 --------- Co-authored-by: GitHub Actions Bot <> Co-authored-by: Louis Bompart <[email protected]>
1 parent 9cac893 commit a331579

File tree

4 files changed

+192
-2
lines changed

4 files changed

+192
-2
lines changed

packages/atomic/project.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"commands": [
3535
"node --max_old_space_size=6144 ../../node_modules/@stencil/core/bin/stencil build",
3636
"node ./scripts/stencil-proxy.mjs",
37-
"tsc -p tsconfig.lit.json",
37+
"node ./scripts/build.mjs --config=tsconfig.lit.json",
3838
"esbuild src/autoloader/index.ts --format=esm --outfile=dist/atomic/autoloader/index.esm.js",
3939
"esbuild src/autoloader/index.ts --format=cjs --outfile=dist/atomic/autoloader/index.cjs.js"
4040
],

packages/atomic/scripts/build.mjs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import {dirname, basename} from 'path';
2+
import {argv} from 'process';
3+
import {
4+
readConfigFile,
5+
getLineAndCharacterOfPosition,
6+
sys,
7+
parseJsonConfigFileContent,
8+
getPreEmitDiagnostics,
9+
createProgram,
10+
flattenDiagnosticMessageText,
11+
} from 'typescript';
12+
import svgTransformer from './svg-transform.mjs';
13+
14+
const args = argv.slice(2);
15+
const configArg = args.find((arg) => arg.startsWith('--config='));
16+
if (configArg === undefined) {
17+
throw new Error('Missing --config=[PATH] argument');
18+
}
19+
const tsConfigPath = configArg.split('=')[1];
20+
21+
function loadTsConfig(configPath) {
22+
const configFile = readConfigFile(configPath, sys.readFile);
23+
if (configFile.error) {
24+
throw new Error(
25+
`Error loading tsconfig file: ${configFile.error.messageText}`
26+
);
27+
}
28+
return parseJsonConfigFileContent(
29+
configFile.config,
30+
sys,
31+
dirname(configPath)
32+
);
33+
}
34+
35+
function emit(program) {
36+
const targetSourceFile = undefined;
37+
const cancellationToken = undefined;
38+
const writeFile = undefined;
39+
const emitOnlyDtsFiles = false;
40+
const customTransformers = {
41+
before: [svgTransformer],
42+
};
43+
44+
return program.emit(
45+
targetSourceFile,
46+
cancellationToken,
47+
writeFile,
48+
emitOnlyDtsFiles,
49+
customTransformers
50+
);
51+
}
52+
53+
/**
54+
* Compiles TypeScript files using a custom transformer.
55+
*
56+
* This function mimics the behavior of running `tsc -p tsconfig.json` but applies a custom SVG transformer
57+
* to all TypeScript files. It loads the TypeScript configuration from the specified `tsconfig.json` file,
58+
* creates a TypeScript program, and emits the compiled JavaScript files with the custom transformer applied.
59+
*
60+
* Info: https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API#a-minimal-compiler
61+
*/
62+
function compileWithTransformer() {
63+
console.log('Using tsconfig:', basename(tsConfigPath));
64+
const {options, fileNames} = loadTsConfig(tsConfigPath);
65+
const program = createProgram(fileNames, options);
66+
const emitResult = emit(program);
67+
68+
const allDiagnostics = getPreEmitDiagnostics(program).concat(
69+
emitResult.diagnostics
70+
);
71+
72+
allDiagnostics.forEach((diagnostic) => {
73+
if (diagnostic.file) {
74+
const {line, character} = getLineAndCharacterOfPosition(
75+
diagnostic.file,
76+
diagnostic.start
77+
);
78+
const message = flattenDiagnosticMessageText(
79+
diagnostic.messageText,
80+
'\n'
81+
);
82+
83+
console.log(
84+
`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`
85+
);
86+
} else {
87+
console.error(flattenDiagnosticMessageText(diagnostic.messageText, '\n'));
88+
}
89+
});
90+
91+
let exitCode = emitResult.emitSkipped ? 1 : 0;
92+
console.log(`Process exiting with code '${exitCode}'.`);
93+
process.exit(exitCode);
94+
}
95+
96+
compileWithTransformer();
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import {readFileSync} from 'fs';
2+
import {basename, dirname, join, resolve} from 'path';
3+
import {
4+
NodeFlags,
5+
isImportDeclaration,
6+
visitEachChild,
7+
visitNode,
8+
} from 'typescript';
9+
10+
/**
11+
* Creates a TypeScript variable statement for an SVG import.
12+
*
13+
* This function generates a TypeScript variable statement that assigns the SVG content as a string literal
14+
* to a variable. It is used as part of a custom TypeScript transformer to inline SVG content in the transpiled
15+
* JavaScript files.
16+
*
17+
* @example
18+
* The following TypeScript source file:
19+
* ```ts
20+
* // src/components/component.ts
21+
* import Tick from '../../../images/checkbox.svg';
22+
* () => console.log(Tick);
23+
* ```
24+
*
25+
* Will be transpiled to (note that the SVG import statement has been replaced with the SVG content):
26+
* ```js
27+
* // dist/components/component.js
28+
* const Tick = "<svg viewBox=\"0 0 12 9\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> ... </svg>";
29+
* () => console.log(Tick);
30+
* ```
31+
*
32+
* @param {NodeFactory} factory - The TypeScript factory object used to create AST nodes.
33+
* @param {string} svgContent - The content of the SVG file as a string.
34+
* @param {string} variableName - The name of the variable to which the SVG content will be assigned.
35+
* @returns {VariableStatement} A TypeScript variable statement that assigns the SVG content to the variable.
36+
* @throws If the variable name is not defined.
37+
*/
38+
function createStatement(factory, svgContent, variableName) {
39+
const bindingName = undefined;
40+
const exclamationToken = undefined;
41+
const modifiers = [];
42+
const {
43+
createVariableStatement,
44+
createVariableDeclarationList,
45+
createVariableDeclaration,
46+
createStringLiteral,
47+
} = factory;
48+
49+
if (variableName === undefined) {
50+
throw new Error(
51+
`Variable name is not defined for the import statement ${node.getText()}`
52+
);
53+
}
54+
55+
return createVariableStatement(
56+
modifiers,
57+
createVariableDeclarationList(
58+
[
59+
createVariableDeclaration(
60+
variableName,
61+
bindingName,
62+
exclamationToken,
63+
createStringLiteral(svgContent)
64+
),
65+
],
66+
NodeFlags.Const
67+
)
68+
);
69+
}
70+
71+
/**
72+
* Custom SVG transformer to handle .svg imports.
73+
*/
74+
export default function svgTransformer(context) {
75+
const {factory} = context;
76+
77+
function visit(node) {
78+
if (isImportDeclaration(node)) {
79+
const importPath = node.moduleSpecifier.text;
80+
if (importPath.endsWith('.svg')) {
81+
console.log('Replacing SVG import:', basename(importPath));
82+
const dir = dirname(node.getSourceFile().fileName);
83+
const svgPath = resolve(dir, importPath);
84+
const svgContent = readFileSync(svgPath, 'utf8');
85+
const variableName = node.importClause?.name?.escapedText;
86+
87+
return createStatement(factory, svgContent, variableName);
88+
}
89+
}
90+
return visitEachChild(node, visit, context);
91+
}
92+
93+
return (sourceFile) => visitNode(sourceFile, visit);
94+
}

packages/atomic/scripts/watch.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ function rebuild() {
55
const commands = [
66
'node --max_old_space_size=6144 ../../node_modules/@stencil/core/bin/stencil build',
77
'node ./scripts/stencil-proxy.mjs',
8-
'tsc -p tsconfig.lit.json',
8+
'node ./scripts/build.mjs --config=tsconfig.lit.json',
99
'esbuild src/autoloader/index.ts --format=esm --outfile=dist/atomic/autoloader/index.esm.js',
1010
'esbuild src/autoloader/index.ts --format=cjs --outfile=dist/atomic/autoloader/index.cjs.js',
1111
];

0 commit comments

Comments
 (0)