Building an ES Module Package with tsc, tscpaths, and AST Tools for Asset and Style Handling
This tutorial walks through using the TypeScript compiler (tsc) together with tscpaths and AST manipulation tools like jscodeshift to resolve path aliases, copy static assets, and convert style imports from SCSS/LESS to CSS when packaging a frontend library for npm distribution.
Opportunity
When setting up an open‑source project I needed to publish an ES module package that developers could install via npm . The project includes styles, and I wanted the output directory structure to match the source. I considered Rollup but chose the built‑in TypeScript compiler tsc , starting my journey of troubleshooting.
Pitfalls with tsc
While compiling with tsc I ran into three basic problems. Below is the directory layout that will be compiled:
|-- src
|-- assets
|-- test.png
|-- util
|-- classnames.ts
|-- index.tsx
|-- index.scssSimplifying Import Paths
I added path alias configuration in tsconfig.json like this:
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@Src/*": ["src/*"],
"@Utils/*": ["src/utils/*"],
"@Assets/*": ["src/assets/*"]
}
}
}Now imports such as util or assets can be written with the alias, e.g. in index.tsx :
import classNames from "@Utils/classnames";
import testPNG from "@Assets/test.png";Unfortunately tsc does not rewrite these aliases to relative paths. I discovered the tscpaths plugin and added it to the build script:
"scripts": {
"build": "tsc -p tsconfig.json && tscpaths -p tsconfig.json -s src -o dist,"
},The plugin scans the compiled .js files and converts the simplified import paths to relative ones.
Static Asset Not Bundled
After compilation the assets folder is missing because tsc only compiles TypeScript. To copy static files I use the copyfiles CLI tool:
copyfiles -f src/assets/* dist/assetsThis copies the asset directory into the distribution.
Style File Extension Issue
Importing a .scss file in index.tsx results in the compiled JavaScript still referencing .scss . For a library we need to expose .css files. I considered using tscpaths again, but realized a more robust solution is to manipulate the AST directly.
Using an AST approach, I can locate import declarations ending with .scss or .less and replace the extension with .css without affecting user code that may contain similar strings.
What is an AST?
If you are familiar with tools like ESLint , Babel , or Webpack , you already know the power of an Abstract Syntax Tree (AST). An AST represents source code as a tree of nodes, allowing precise transformations such as changing var to const or adjusting literals.
{
"type": "VariableDeclaration",
"kind": "const",
"declarations": [{
"type": "VariableDeclarator",
"id": {"type": "Identifier", "name": "a"},
"init": {"type": "Literal", "value": 1, "raw": "1"}
}]
}Tools like ESTree define the standard node types used by ESLint, Babel, and others.
Tooling
Below are three useful tools for working with ASTs:
AST Explorer
Visit https://astexplorer.net/ to paste JavaScript code and view its AST.
jscodeshift
jscodeshift, built on recast , provides a friendly API for traversing and modifying ASTs.
@babel/types
The @babel/types package lists all node types and their properties.
Type Name
Chinese Name
Description
Program
程序主体
Root of the code
VariableDeclaration
变量声明
let/const/var declarations
FunctionDeclaration
函数声明
function declarations
ExpressionStatement
表达式语句
e.g., console.log(1)
BlockStatement
块语句
code inside { }
BreakStatement
中断语句
break
ContinueStatement
持续语句
continue
ReturnStatement
返回语句
return
SwitchStatement
Switch 语句
switch
IfStatement
If 控制流语句
if/else
Identifier
标识符
variable names
ArrayExpression
数组表达式
e.g., [1,2,3]
StringLiteral
字符型字面量
string literals
NumericLiteral
数字型字面量
numeric literals
ImportDeclaration
引入声明
import statements
AST Node CRUD
We will build a simple environment to demonstrate create, read, update, and delete operations on AST nodes.
Setup
mkdir ast-demo
cd ast-demo
npm init -y
npm install jscodeshift --save
touch create.js delete.js update.js find.jsFind Nodes
const jf = require("jscodeshift");
const value = `
import React from 'react';
import { Button } from 'antd';
`;
const root = jf(value);
root.find(jf.ImportDeclaration, { source: { value: "antd" } })
.forEach(path => {
console.log(path.node.source.value);
});Running this prints antd .
Update Nodes
const jf = require("jscodeshift");
const value = `
import React from 'react';
import { Button, Input } from 'antd';
`;
const root = jf(value);
root.find(jf.ImportDeclaration, { source: { value: "antd" } })
.forEach(path => {
const { specifiers } = path.node;
specifiers.forEach(spec => {
if (spec.imported.name === "Button") {
spec.imported.name = "Select";
}
});
});
console.log(root.toSource());The output shows Button replaced by Select .
Add Nodes
const jf = require("jscodeshift");
const value = `
import React from 'react';
import { Button, Input } from 'antd';
`;
const root = jf(value);
root.find(jf.ImportDeclaration, { source: { value: "antd" } })
.forEach(path => {
const { specifiers } = path.node;
specifiers.push(jf.importSpecifier(jf.identifier("Select")));
});
console.log(root.toSource());The resulting code now imports Select as well.
Delete Nodes
const jf = require("jscodeshift");
const value = `
import React from 'react';
import { Button, Input } from 'antd';
`;
const root = jf(value);
root.find(jf.ImportDeclaration, { source: { value: "antd" } })
.forEach(path => {
jf(path).replaceWith("");
});
console.log(root.toSource());This removes the entire antd import line.
Putting It All Together – The tsccss CLI
The goal is a command‑line tool that scans the compiled dist directory and rewrites any .scss or .less style imports to .css . The implementation uses commander , globby , jscodeshift , and Node's fs module.
Project Initialization
# create project folder
mkdir tsccss
cd tsccss
npm init -y
# install dependencies
npm i commander globby jscodeshift --save
mkdir src
cd src
touch index.jspackage.json bin Field
{
"main": "src/index.js",
"bin": { "tsccss": "src/index.js" },
"files": ["src"]
}Add the shebang line at the top of src/index.js :
#! /usr/bin/env nodeCommand‑Line Options
const { program } = require("commander");
program.version("0.0.1")
.option("-o, --out
", "output root path")
.on("--help", () => {
console.log(`
You can add the following commands to npm scripts:
------------------------------------------------------
"compile": "tsccss -o dist"
------------------------------------------------------
`);
});
program.parse(process.argv);
const { out } = program.opts();
if (!out) { throw new Error("--out must be specified"); }Reading Files
const { resolve } = require("path");
const { sync } = require("globby");
const outRoot = resolve(process.cwd(), out);
const files = sync(`${outRoot}/**/!(*.d).{ts,tsx,js,jsx}`, { dot: true })
.map(x => resolve(x));AST Transformation Function
function transToCSS(str) {
const jf = require("jscodeshift");
const root = jf(str);
root.find(jf.ImportDeclaration).forEach(path => {
let value = "";
if (path && path.node && path.node.source) {
value = path.node.source.value;
}
const regex = /(scss|less)('|"|`)?$/i;
if (value && regex.test(value.toString())) {
path.node.source.value = value
.toString()
.replace(regex, (_, __, quote) => (quote ? `css${quote}` : "css"));
}
});
return root.toSource();
}Read‑Write Loop
const { readFileSync, writeFileSync } = require("fs");
for (let i = 0; i < files.length; i++) {
const file = files[i];
const content = readFileSync(file, "utf-8");
const resContent = transToCSS(content);
writeFileSync(file, resContent, "utf8");
}Running node src/index.js -o dist rewrites all style imports to .css in the compiled files.
Final Thoughts
This article demonstrates a practical use of AST manipulation to solve real‑world build problems, emphasizing that understanding the underlying technology expands the toolbox for developers.
References
[1] tscpaths – https://github.com/joonhocho/tscpaths
[2] copyfiles – https://github.com/calvinmetcalf/copyfiles
[3] ESTree – https://github.com/estree/estree
[4] jscodeshift – https://github.com/facebook/jscodeshift
[5] recast – https://github.com/benjamn/recast
[6] @babel/types – https://babeljs.io/docs/en/babel-types
[7] AST Explorer – https://astexplorer.net/
[8] AST Explorer – https://astexplorer.net/
[9] collection – https://github.com/facebook/jscodeshift/blob/master/src/Collection.js
[10] extensions – https://github.com/facebook/jscodeshift/tree/master/src/collections
[11] commander – https://github.com/tj/commander.js
[12] tsccss – https://github.com/vortesnail/tsccss
ByteFE
Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.