zshy

repository·main·Indexed 22 days ago

https://github.com/colinhacks/zshy

A bundler-free build tool for TypeScript libraries that uses tsc to perform dual-module (ESM and CJS) builds. zshy automates the generation of package.json exports, handles file extension rewriting for imports, and supports wildcard entrypoints. It includes features for CJS interop, path alias resolution, import.meta shimming, and automatic jsr.json export updates.

Tokens
3.8K
Snippets
13
Records
19
Agent score
78%

What's inside zshy

  1. Run a zshy build

    main

    Execute the build using npx zshy. It is recommended to add a build script to your package.json.

    Via npx:

    npx zshy

    Via npm script:

    1. Add to package.json:
    "scripts": {
      "build": "zshy"
    }
    1. Run:
    npm run build

    Use the --dry-run flag to simulate the build without writing or updating any files.

  2. Update `jsr.json` exports

    main
    If your project contains a jsr.json file, zshy can automatically update its exports field. Because JSR does not support wildcard exports, zshy expands any wildcard patterns defined in your configuration into explicit source entrypoints within jsr.json.
  3. Configure zshy entrypoints in package.json

    main

    Specify your TypeScript entrypoints in the zshy field of your package.json. You can provide a single string for one entrypoint or an object with an exports map for multiple entrypoints, including support for wildcards.

    Single entrypoint:

    {
      "name": "my-pkg",
      "zshy": "./src/index.ts"
    }

    Multiple entrypoints with wildcards:

    {
      "name": "my-pkg",
      "zshy": {
        "exports": {
          ".": "./src/index.ts",
          "./utils": "./src/utils.ts",
          "./plugins/*": "./src/plugins/*",
          "./components/**/*": "./src/components/**/*"
        }
      }
    }

    Wildcard Rules:

    • Keys must end in /*.
    • Values should use /* for shallow matches or /**/* for deep recursive matches.
    • Do not include file extensions in the value; zshy matches .ts, .tsx, .cts, and .mts files.
    {
      "zshy": {
        "exports": {
          ".": "./src/index.ts",
          "./utils": "./src/utils.ts",
          "./plugins/*": "./src/plugins/*",
          "./components/**/*": "./src/components/**/*"
        }
      }
    }
  4. Configure the `bin` field for CLIs

    main

    When building a CLI package, zshy can automatically generate the bin field in your package.json based on your configuration. This ensures that the executable paths correctly point to the generated output files (handling extensions like .js, .mjs, or .cjs based on your ESM/CJS settings).

    You can specify bin in your configuration as either a single string (mapping to the package name) or an object mapping command names to source file paths.

  5. Configure ProjectOptions for compilation

    main

    The ProjectOptions interface defines how zshy transforms and emits your project. Key configuration areas include:

    • Output Format: Use format ('cjs' or 'esm') and ext ('cjs' | 'js' | 'mjs') to control the module system and file extensions.
    • TypeScript Integration: compilerOptions must include module, moduleResolution, and outDir. You can also provide paths and baseUrl for TypeScript path mapping resolution.
    • CJS Interop: If building for CommonJS (format: 'cjs'), setting cjsInterop: true enables transformations for single default exports.
    • Asset Management: rootDir is used to resolve asset imports found during transformation so they can be copied to the outDir.
    • Execution Modes: dryRun: true allows you to simulate the build and inspect writtenFiles and copiedAssets in the BuildContext without actually writing to disk.
    export interface ProjectOptions {
      configPath: string;
      compilerOptions: ts.CompilerOptions & Required<Pick<ts.CompilerOptions, "module" | "moduleResolution" | "outDir">>;
      ext: "cjs" | "js" | "mjs";
      format: "cjs" | "esm";
      pkgJsonDir: string; // Add package root for relative path display
      rootDir: string; // Add source root for asset copying
      verbose: boolean;
      dryRun: boolean;
      cjsInterop?: boolean; // Enable CJS interop for single default exports
      paths?: Record<string, string[]>; // TypeScript paths configuration
      baseUrl?: string; // TypeScript baseUrl configuration
    }
  6. Configure build failure thresholds

    main

    You can control whether the build process exits with an error code based on compilation results using the failThreshold setting:

    • never: The build will always exit with code 0, even if there are errors or warnings (it will only log a warning).
    • warn: The build will exit with code 1 if there are any warnings.
    • (Default/Implicit): The build will exit with code 1 if there are any errors.
  7. Configure CLI binaries with zshy

    main
    If your package is a CLI, specify the entrypoint in package.json#/zshy/bin. zshy will automatically generate the `
  8. Use createImportMetaShimTransformer for CJS builds

    main

    The createImportMetaShimTransformer function returns a TypeScript transformer factory designed to shim import.meta properties for CommonJS (CJS) compatibility. When used during a build process, it replaces ESM-specific import.meta access with CJS equivalents:

    • import.meta.url is transformed into a call to pathToFileURL(__filename) using require('url').
    • import.meta.dirname is transformed into __dirname.
    • import.meta.filename is transformed into __filename.
    import * as ts from "typescript";
    import { createImportMetaShimTransformer } from "./tx-import-meta-shim";
    
    // Example usage in a custom TypeScript transformation pipeline
    const transformer = createImportMetaShimTransformer();
    // This transformer can then be passed to ts.transform() along with your source file
  9. Compile a project with compileProject()

    main

    The compileProject function is the primary entry point for programmatic compilation using zshy. It orchestrates the TypeScript compilation process, applies custom transformers (like extension rewriting and CJS interop), handles asset copying, and manages error reporting.

    To use it, you must provide a ProjectOptions object, an array of entry point file paths, and a BuildContext object to track the build state.

    import { compileProject, type ProjectOptions, type BuildContext } from './src/compile.js';
    
    const options: ProjectOptions = {
      configPath: './tsconfig.json',
      compilerOptions: {
        module: ts.ModuleKind.ESNext,
        moduleResolution: ts.ModuleResolutionKind.Node16,
        outDir: './dist',
        // ... other ts.CompilerOptions
      },
      ext: 'mjs',
      format: 'esm',
      pkgJsonDir: './',
      rootDir: './src',
      verbose: true,
      dryRun: false
    };
    
    const ctx: BuildContext = {
      writtenFiles: new Set(),
      copiedAssets: new Set(),
      errorCount: 0,
      warningCount: 0
    };
    
    const entryPoints = ['./src/index.ts'];
    
    await compileProject(options, entryPoints, ctx);
  10. Create a TypeScript transformer with createPathsResolverTransformer

    main

    The createPathsResolverTransformer function creates a TypeScript transformer that resolves path aliases (defined in tsconfig.json's paths property) into relative paths during the build process. This allows you to use path aliases in your source code while ensuring the output contains valid relative imports.

    To use this, you must provide a PathsConfig object that describes your project structure and path mappings.

    import { createPathsResolverTransformer, PathsConfig } from 'zshy/tx-paths-resolver';
    
    const config: PathsConfig = {
      baseUrl: './',
      paths: {
        '@/*': ['src/*']
      },
      tsconfigDir: '/path/to/project',
      rootDir: '/path/to/project'
    };
    
    const transformer = createPathsResolverTransformer(config);
    // Use this transformer in your TypeScript build pipeline