Skip to content

Node.js tool for updating your ES module and CommonJS specifiers.

License

Notifications You must be signed in to change notification settings

knightedcodemonkey/specifier

Repository files navigation

CI codecov NPM version

Node.js tool for updating your ESM and CJS specifiers.

Supports:

  • Latest ECMAScript
  • TypeScript
  • JSX

Example

The following script will update all specifiers in dist/index.js with an AST node.type of StringLiteral, and that also end with a .js extension, to end with an .mjs extension instead. Finally, it writes the updated source to dist/index.mjs.

script.js

import { writeFile } from 'node:fs/promises'
import { specifier } from '@knighted/specifier'

const { code, error } = await specifier.update('dist/index.js', ({ type, value }) => {
  if (type === 'StringLiteral') {
    return value.replace(/([^.]+)\.js$/, '$1.mjs')
  }
})

if (code && !error) {
  await writeFile('dist/index.mjs', code)
}

You can also provide an object where the keys are regular expressions and the values are the replacements to make when the regular expression matches.

The following does the same as before.

const { code, error } = await specifier.mapper('dist/index.js', {
  '([^.]+)\\.js$': '$1.mjs'
})

The order of the keys in the map object matters, the first match found will be used, so the most specific rule should be defined first. Additionally, you can substitute captured regex groups using numbered backreferences.

You can also check out the reference implementation.

async specifier.update(filename, callback)

Updates specifiers in filename using the values returned from callback, and returns the updated file content. The callback is called for each specifier found, and the returned value is used to update the related specifier value. If the callback returns anything other than a string, the return value will be ignored and the specifier not updated.

Signature

(filename: string, callback: Callback) => Promise<Update>

Where the other types are defined as such.

type Callback = (spec: Spec) => string | undefined
interface Spec {
  type: 'StringLiteral' | 'TemplateLiteral' | 'BinaryExpression' | 'NewExpression'
  start: number
  end: number
  value: string
  loc: SourceLocation
}
interface Position {
  line: number
  column: number
}
interface SourceLocation {
  start: Position
  end: Position
}
interface Update {
  code?: string
  map?: SourceMap | null
  error?: UpdateError
}
interface UpdateError {
  error: boolean
  msg: string
  filename?: string
  syntaxError?: {
    code: string
    reasonCode: string
  }
}

The Spec.value will not include any surrounding quotes or backticks when the type is StringLiteral or TemplateLiteral, and the return value from callback is not expected to include them either.

async specifier.mapper(filename, regexMap)

Updates specifiers in filename using the provided regexMap object and returns the updated file content. The value of the first key to match in regexMap is used, so more specific rules should be defined first. Numbered backreferences of captured groups can be used.

Signature

(filename: string, regexMap: {[regex: string]: string}) => Promise<Update>

Where Update is the same from specifier.update.

async specifier.updateSrc(code, callbackOrMap, opts)

Updates specifiers in source code using the values defined from callbackOrMap, and returns an Update that includes a map if opts.sourceMap was passed. If the provided source code is from a TypeScript declaration file you should pass true for opts.dts to support parsing of ambient contexts.

Signature

(code: string, callbackOrMap: Callback | RegexMap, opts?: Opts) => Promise<Update>
interface Opts {
  dts?: boolean;
  sourceMap?: boolean;
}
interface RegexMap {
  [regex: string]: string
}

Where the other types have the same definition from specifier.update.

async specifier.updateAst(ast, code, callback)

Updates specifiers in a Babel AST with source code using the value returned from callback, and returns an Update.

Signature

(ast: ParseResult<File>, code: string, callback: Callback) => Promise<Update>

Where the other types have the same definition from specifier.update.