Skip to content

Commit

Permalink
chore(atomic): add SVG transformer (#4867)
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
y-lakhdar and louis-bompart authored Jan 21, 2025
1 parent 9cac893 commit a331579
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 2 deletions.
2 changes: 1 addition & 1 deletion packages/atomic/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"commands": [
"node --max_old_space_size=6144 ../../node_modules/@stencil/core/bin/stencil build",
"node ./scripts/stencil-proxy.mjs",
"tsc -p tsconfig.lit.json",
"node ./scripts/build.mjs --config=tsconfig.lit.json",
"esbuild src/autoloader/index.ts --format=esm --outfile=dist/atomic/autoloader/index.esm.js",
"esbuild src/autoloader/index.ts --format=cjs --outfile=dist/atomic/autoloader/index.cjs.js"
],
Expand Down
96 changes: 96 additions & 0 deletions packages/atomic/scripts/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {dirname, basename} from 'path';
import {argv} from 'process';
import {
readConfigFile,
getLineAndCharacterOfPosition,
sys,
parseJsonConfigFileContent,
getPreEmitDiagnostics,
createProgram,
flattenDiagnosticMessageText,
} from 'typescript';
import svgTransformer from './svg-transform.mjs';

const args = argv.slice(2);
const configArg = args.find((arg) => arg.startsWith('--config='));
if (configArg === undefined) {
throw new Error('Missing --config=[PATH] argument');
}
const tsConfigPath = configArg.split('=')[1];

function loadTsConfig(configPath) {
const configFile = readConfigFile(configPath, sys.readFile);
if (configFile.error) {
throw new Error(
`Error loading tsconfig file: ${configFile.error.messageText}`
);
}
return parseJsonConfigFileContent(
configFile.config,
sys,
dirname(configPath)
);
}

function emit(program) {
const targetSourceFile = undefined;
const cancellationToken = undefined;
const writeFile = undefined;
const emitOnlyDtsFiles = false;
const customTransformers = {
before: [svgTransformer],
};

return program.emit(
targetSourceFile,
cancellationToken,
writeFile,
emitOnlyDtsFiles,
customTransformers
);
}

/**
* Compiles TypeScript files using a custom transformer.
*
* This function mimics the behavior of running `tsc -p tsconfig.json` but applies a custom SVG transformer
* to all TypeScript files. It loads the TypeScript configuration from the specified `tsconfig.json` file,
* creates a TypeScript program, and emits the compiled JavaScript files with the custom transformer applied.
*
* Info: https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API#a-minimal-compiler
*/
function compileWithTransformer() {
console.log('Using tsconfig:', basename(tsConfigPath));
const {options, fileNames} = loadTsConfig(tsConfigPath);
const program = createProgram(fileNames, options);
const emitResult = emit(program);

const allDiagnostics = getPreEmitDiagnostics(program).concat(
emitResult.diagnostics
);

allDiagnostics.forEach((diagnostic) => {
if (diagnostic.file) {
const {line, character} = getLineAndCharacterOfPosition(
diagnostic.file,
diagnostic.start
);
const message = flattenDiagnosticMessageText(
diagnostic.messageText,
'\n'
);

console.log(
`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`
);
} else {
console.error(flattenDiagnosticMessageText(diagnostic.messageText, '\n'));
}
});

let exitCode = emitResult.emitSkipped ? 1 : 0;
console.log(`Process exiting with code '${exitCode}'.`);
process.exit(exitCode);
}

compileWithTransformer();
94 changes: 94 additions & 0 deletions packages/atomic/scripts/svg-transform.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import {readFileSync} from 'fs';
import {basename, dirname, join, resolve} from 'path';
import {
NodeFlags,
isImportDeclaration,
visitEachChild,
visitNode,
} from 'typescript';

/**
* Creates a TypeScript variable statement for an SVG import.
*
* This function generates a TypeScript variable statement that assigns the SVG content as a string literal
* to a variable. It is used as part of a custom TypeScript transformer to inline SVG content in the transpiled
* JavaScript files.
*
* @example
* The following TypeScript source file:
* ```ts
* // src/components/component.ts
* import Tick from '../../../images/checkbox.svg';
* () => console.log(Tick);
* ```
*
* Will be transpiled to (note that the SVG import statement has been replaced with the SVG content):
* ```js
* // dist/components/component.js
* const Tick = "<svg viewBox=\"0 0 12 9\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> ... </svg>";
* () => console.log(Tick);
* ```
*
* @param {NodeFactory} factory - The TypeScript factory object used to create AST nodes.
* @param {string} svgContent - The content of the SVG file as a string.
* @param {string} variableName - The name of the variable to which the SVG content will be assigned.
* @returns {VariableStatement} A TypeScript variable statement that assigns the SVG content to the variable.
* @throws If the variable name is not defined.
*/
function createStatement(factory, svgContent, variableName) {
const bindingName = undefined;
const exclamationToken = undefined;
const modifiers = [];
const {
createVariableStatement,
createVariableDeclarationList,
createVariableDeclaration,
createStringLiteral,
} = factory;

if (variableName === undefined) {
throw new Error(
`Variable name is not defined for the import statement ${node.getText()}`
);
}

return createVariableStatement(
modifiers,
createVariableDeclarationList(
[
createVariableDeclaration(
variableName,
bindingName,
exclamationToken,
createStringLiteral(svgContent)
),
],
NodeFlags.Const
)
);
}

/**
* Custom SVG transformer to handle .svg imports.
*/
export default function svgTransformer(context) {
const {factory} = context;

function visit(node) {
if (isImportDeclaration(node)) {
const importPath = node.moduleSpecifier.text;
if (importPath.endsWith('.svg')) {
console.log('Replacing SVG import:', basename(importPath));
const dir = dirname(node.getSourceFile().fileName);
const svgPath = resolve(dir, importPath);
const svgContent = readFileSync(svgPath, 'utf8');
const variableName = node.importClause?.name?.escapedText;

return createStatement(factory, svgContent, variableName);
}
}
return visitEachChild(node, visit, context);
}

return (sourceFile) => visitNode(sourceFile, visit);
}
2 changes: 1 addition & 1 deletion packages/atomic/scripts/watch.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ function rebuild() {
const commands = [
'node --max_old_space_size=6144 ../../node_modules/@stencil/core/bin/stencil build',
'node ./scripts/stencil-proxy.mjs',
'tsc -p tsconfig.lit.json',
'node ./scripts/build.mjs --config=tsconfig.lit.json',
'esbuild src/autoloader/index.ts --format=esm --outfile=dist/atomic/autoloader/index.esm.js',
'esbuild src/autoloader/index.ts --format=cjs --outfile=dist/atomic/autoloader/index.cjs.js',
];
Expand Down

0 comments on commit a331579

Please sign in to comment.