Frontend Development 24 min read

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.

ByteFE
ByteFE
ByteFE
Building an ES Module Package with tsc, tscpaths, and AST Tools for Asset and Style Handling

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.scss

Simplifying 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/assets

This 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.js

Find 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.js

package.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 node

Command‑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

cliTypeScriptASTnpmtscjscodeshiftpath-alias
ByteFE
Written by

ByteFE

Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.