{"id":14155891,"url":"https://github.com/DubstepJS/core","last_synced_at":"2025-08-06T02:30:54.011Z","repository":{"id":33118101,"uuid":"140321328","full_name":"DubstepJS/core","owner":"DubstepJS","description":"A batteries-included step runner library, suitable for creating migration tooling, codemods, scaffolding CLIs, etc.","archived":false,"fork":false,"pushed_at":"2024-12-06T04:50:05.000Z","size":430,"stargazers_count":40,"open_issues_count":17,"forks_count":10,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-12-07T08:44:09.959Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/DubstepJS.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-07-09T17:41:48.000Z","updated_at":"2024-07-07T21:15:05.000Z","dependencies_parsed_at":"2023-10-19T21:34:45.616Z","dependency_job_id":"ef3664d2-5244-4b37-aa22-cc1078aff872","html_url":"https://github.com/DubstepJS/core","commit_stats":{"total_commits":101,"total_committers":10,"mean_commits":10.1,"dds":0.594059405940594,"last_synced_commit":"b301185a3225c3b63a40201ccb1be022476687b6"},"previous_names":[],"tags_count":35,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DubstepJS%2Fcore","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DubstepJS%2Fcore/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DubstepJS%2Fcore/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DubstepJS%2Fcore/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/DubstepJS","download_url":"https://codeload.github.com/DubstepJS/core/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":228829007,"owners_count":17978131,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-17T08:05:04.441Z","updated_at":"2024-12-09T03:30:46.230Z","avatar_url":"https://github.com/DubstepJS.png","language":"JavaScript","funding_links":[],"categories":["others"],"sub_categories":[],"readme":"# Dubstep\n\n[![Build status](https://badge.buildkite.com/843d29b4898b20cd38d3a6509875979fbbd43314095540ed6c.svg?branch=master)](https://buildkite.com/uberopensource/at-dubstep-slash-core)\n\nA batteries-included step runner library, suitable for creating migration tooling, codemods, scaffolding CLIs, etc.\n\nDubstep has utility functions for file system operations, Babel-based codemodding, Git operations and others.\n\nLicense: MIT\n\n[Installation](#installation) | [Usage](#usage) | [API](#api) | [Recipes](#recipes) | [Motivation](#motivation)\n\n---\n\n## Installation\n\n```sh\nyarn add @dubstep/core\n```\n\n---\n\n## Usage\n\n```js\nimport {\n  Stepper,\n  step,\n  gitClone,\n  findFiles,\n  withTextFile,\n  getRestorePoint,\n  removeFile,\n  createRestorePoint,\n} from '@dubstep/core';\nimport inquirer from 'inquirer';\n\nasync function run() {\n  const state = {name: ''};\n  const stepper = new Stepper([\n    step('name', async () =\u003e {\n      state.name = await inquirer.prompt({message: 'Name:', type: 'input'});\n    }),\n    step('clone', async () =\u003e {\n      gitClone('some-scaffold-template.git', state.name);\n    }),\n    step('customize', async () =\u003e {\n      const files = await findFiles('**/*.js', f =\u003e /src/.test(f));\n      for (const file of files) {\n        withTextFile(file, text =\u003e text.replace(/{{name}}/g, state.name));\n      }\n    }),\n  ]);\n  stepper\n    .run({from: await getRestorePoint(restoreFile)})\n    .then(() =\u003e removeFile(reportFile))\n    .catch(e =\u003e createRestorePoint(reportFile, e));\n}\nrun();\n```\n\n## API\n\n[Core](#core) | [Utilities](#utilities) | [top](#dubstep)\n\nAll API entities are available as non-default import specifiers, e.g. `import {Stepper} from '@dubstep/core'`;\n\nUtilities can also be imported individually, e.g. `import {findFiles} from '@dubstep/core/find-files'`;\n\n### Core\n\n#### Stepper\n\n```js\nimport {Stepper} from '@dubstep/core';\n\nclass Stepper {\n  constructor(preset: Preset)\n  run(options: StepperOptions): Promise\u003cany\u003e // rejects w/ StepperError\n  on(type: 'progress', handler: StepperEventHandler)\n  off(type: 'progress', handler: StepperEventHandler)\n}\n\ntype Preset = Array\u003cStep\u003e\ntype StepperOptions = ?{from: ?number, to: ?number}\ntype StepperEventHandler = ({index: number, total: number, step: string}) =\u003e void\n```\n\nA stepper can take a list of steps, run them in series and emit progress events.\n\n#### step\n\n```js\nimport {step} from '@dubstep/core';\n\ntype step = (name: string, step: AsyncFunction) =\u003e Step;\n\ntype Step = {name: string, step: AsyncFunction};\ntype AsyncFunction = () =\u003e Promise\u003cany\u003e;\n```\n\nA step consists of a descriptive name and an async function.\n\n#### StepperError\n\n```js\nimport {StepperError} from '@dubstep/core';\n\nclass StepperError extends Error {\n  constructor(error: Error, step: string, index: number),\n  step: string,\n  index: number,\n  message: string,\n  stack: string,\n}\n```\n\nA stepper error indicates what step failed. It can be used for resuming execution via restore points.\n\n### Utilities\n\n[File system](#file-system) | [Babel](#babel) | [Git](#git) | [Restore points](#restore-points) | [Misc](#misc)\n\n#### File system\n\n##### findFiles\n\n```js\nimport {findFiles} from '@dubstep/core';\n\ntype findFiles = (glob?: string, filter?: string =\u003e boolean) =\u003e Promise\u003cArray\u003cstring\u003e\u003e;\n```\n\nResolves to a list of file names that match `glob` and match the condition from the `filter` function. Respects .gitignore.\n\n##### moveFile\n\n```js\nimport {moveFile} from '@dubstep/core';\n\ntype moveFile = (oldName: string, newName: string) =\u003e Promise\u003cany\u003e;\n```\n\nMoves an existing file or directory to the location specified by `newName`. If the file specified by `oldName` doesn't exist, it no-ops.\n\n##### readFile\n\n```js\nimport {readFile} from '@dubstep/core';\n\ntype readFile = (file: string) =\u003e Promise\u003cstring\u003e;\n```\n\nReads the specified file into a UTF-8 string. If the file doesn't exist, the function throws a ENOENT error.\n\n##### removeFile\n\n```js\nimport {removeFile} from '@dubstep/core';\n\ntype removeFile = (file: string) =\u003e Promise\u003cany\u003e;\n```\n\nRemoves the specified file. If the file doesn't exist, it no-ops.\n\n##### withIgnoreFile\n\n```js\nimport {withIgnoreFile} from '@dubstep/core';\n\ntype withIgnoreFile = (file: string, fn: IgnoreFileMutation) =\u003e Promise\u003cany\u003e;\ntype IgnoreFileMutation = (data: Array\u003cstring\u003e) =\u003e Promise\u003c?Array\u003cstring\u003e\u003e;\n```\n\nOpens a file, parses each line into a string, and calls `fn` with the array of lines. Then, writes the return value or the array back into the file.\n\nIf the file does not exist, `fn` is called with an empty array, and the file is created (including missing directories).\n\n##### withJsFile\n\n```js\nimport {withJsFile} from '@dubstep/core';\n\ntype withJsFile = (file: string, fn: JsFileMutation, options: ParserOptions) =\u003e Promise\u003cany\u003e;\ntype JsFileMutation = (program: BabelPath, file: string) =\u003e Promise\u003cany\u003e;\ntype ParserOptions = ?{mode: ?('typescript' | 'flow')};\n```\n\nOpens a file, parses each line into a Babel BabelPath, and calls `fn` with BabelPath. Then, writes the modified AST back into the file.\n\nIf the file does not exist, `fn` is called with a empty program BabelPath, and the file is created (including missing directories).\n\nSee the [Babel handbook](https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md) for more information on `BabelPath`'s API.\n\n##### withJsFiles\n\n```js\nimport {withJsFiles} from '@dubstep/core';\n\ntype withJsFiles = (glob: string, fn: JsFileMutation, options: ParserOptions) =\u003e Promise\u003cany\u003e;\ntype JsFileMutation = (program: BabelPath, file: string) =\u003e Promise\u003cany\u003e;\ntype ParserOptions = ?{mode: ?('typescript' | 'flow')};\n```\n\nRuns `withJsFile` only on files that match `glob`.\n\nSee the [Babel handbook](https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md) for more information on `BabelPath`'s API.\n\n##### withJsonFile\n\n```js\nimport {withJsonFile} from '@dubstep/core';\n\ntype withJsonFile = (file: string, fn: JsonFileMutation) =\u003e Promise\u003cany\u003e;\ntype JsonFileMutation = (data: any) =\u003e Promise\u003cany\u003e;\n```\n\nOpens a file, parses each line into a Javascript data structure, and calls `fn` with it. Then, writes the return value or modified data structure back into the file.\n\nIf the file does not exist, `fn` is called with an empty object, and the file is created (including missing directories).\n\n##### withTextFile\n\n```js\nimport {withTextFile} from '@dubstep/core';\n\ntype withTextFile = (file: string, fn: TextFileMutation) =\u003e Promise\u003cany\u003e;\ntype TextFileMutation = (data: string) =\u003e Promise\u003c?string\u003e;\n```\n\nOpens a file, parses each line into a string, and calls `fn` with it. Then, writes the return value back into the file.\n\nIf the file does not exist, `fn` is called with an empty string, and the file is created.\n\n##### writeFile\n\n```js\nimport {writeFile} from '@dubstep/core';\n\ntype writeFile = (file: string, data: string) =\u003e Promise\u003cany\u003e;\n```\n\nWrites `data` to `file`. If the file doesn't exist, it's created (including missing directories)\n\n---\n\n### Babel\n\n##### ensureJsImports\n\n```js\nimport {ensureJsImports} from '@dubstep/core';\n\ntype ensureJsImports = (path: BabelPath, code: string, options: ParserOptions) =\u003e Array\u003cObject\u003cstring, string\u003e\u003e;\ntype ParserOptions = ?{mode: ?('typescript' | 'flow')};\n```\n\nIf an import declaration in `code` is missing in the program, it's added. If it's already present, specifiers are added if not present. Note that the `BabelPath` should be for a Program node, and that it is mutated in-place.\n\nReturns a list of maps of specifier local names. The default specifier is bound to the key `default`.\n\nIf a specifier is already declared in `path`, but there's a conflicting specifier in `code`, the one in `path` is retained and returned in the output map. For example:\n\n```js\n// default specifier is already declared as `a`, but trying to redeclare it as `foo`\nensureJsImports(parseJs(`import a from 'a';`), `import foo from 'a'`);\n// \u003e {default: 'a'};\n```\n\nA `BabelPath` can be obtained from `withJsFile`, `withJsFiles` or `parseJs`.\n\n##### visitJsImport\n\n```js\nimport {visitJsImport} from '@dubstep/core';\n\ntype visitJsImport = (\n  path: BabelPath,\n  code: string,\n  handler: (importPath: BabelPath, refPaths: Array\u003cBabelPath\u003e) =\u003e void),\n  options: ParserOptions,\n: void\ntype ParserOptions = ?{mode: ?('typescript' | 'flow')};\n```\n\nThis function is useful when applying codemods to specific modules which requires modifying the ast surrounding\nspecific modules and their usage. This module works robustly across various styles of importing. For example:\n\n```js\nvisitJsImport(\n  parseJs(`\n    import {a} from 'a';\n    a('test')\n    console.log(a);\n  `),\n  `import {a} from 'a';`,\n  (importPath, refPaths) =\u003e {\n    // importPath corresponds to the ImportDeclaration from 'a';\n    // refPaths is a list of BabelPaths corresponding to the usage of the a variable\n  }\n);\n```\n\n##### hasImport\n\n```js\nimport {hasImport} from '@dubstep/core';\n\ntype hasImport = (path: BabelPath\u003cProgram\u003e, code: string, options: ParserOptions) =\u003e boolean\ntype ParserOptions = ?{mode: ?('typescript' | 'flow')};\n```\n\nChecks if a given program node contains an import matching a string.\n\n```js\nhasImport(\n  parseJs(`\n    import {a} from 'a';\n    console.log(a);\n  `),\n  `import {a} from 'a';`\n); // true\n```\n\n##### collapseImports\n\n```js\nimport {collapseImports} from '@dubstep/core';\n\ntype collapseImports = (path: BabelPath\u003cProgram\u003e) =\u003e BabelPath\u003cProgram\u003e\n```\n\nThis function collapses multiple import declarations with the same source into a single\nimport statement by combining the specifiers. For example:\n\n```js\nimport A, {B} from 'a';\nimport {C, D} from 'a';\n\n// =\u003e \n\nimport A, {B, C, D} from 'a';\n```\n\n##### generateJs\n\n```js\nimport {generateJs} from '@dubstep/core';\n\ntype generateJs = (path: BabelPath) =\u003e string;\n```\n\nConverts a Program `BabelPath` into a Javascript code string.\nA `BabelPath` can be obtained from `withJsFile`, `withJsFiles` or `parseJs`.\n\n##### insertJsAfter\n\n```js\nimport {insertJsAfter} from '@dubstep/core';\n\ntype insertJsAfter = (path: BabelPath, target: string, code: string, wildcards: Array\u003cstring\u003e, options: ParserOptions) =\u003e void\ntype ParserOptions = ?{mode: ?('typescript' | 'flow')};\n```\n\nInserts the statements in `code` after the `target` statement, transferring expressions contained in the `wildcards` list. Note that `path` should be a BabelPath to a Program node..\n\n```js\nconst path = parseJs(`const a = 1;`);\ninsertJsAfter(path, `const a = $VALUE`, `const b = 2;`, ['$VALUE']);\n\n// before\nconst a = 1;\n\n// after\nconst a = 1;\nconst b = 2;\n```\n\nIt also supports spread wildcards:\n\n```js\nconst path = parseJs(`const a = f(1, 2, 3);`);\ninsertJsAfter(path, `const a = f(...$ARGS)`, `const b = 2;`, ['$ARGS']);\n\n// before\nconst a = f(1, 2, 3);\n\n// after\nconst a = f(1, 2, 3);\nconst b = 2;\n```\n\n##### insertJsBefore\n\n```js\nimport {insertJsBefore} from '@dubstep/core';\n\ntype insertJsBefore = (path: BabelPath, target: string, code: string, wildcards: Array\u003cstring\u003e, options: ParserOptions) =\u003e void\ntype ParserOptions = ?{mode: ?('typescript' | 'flow')};\n```\n\nInserts the statements in `code` before the `target` statement, transferring expressions contained in the `wildcards` list. Note that `path` should be a BabelPath to a Program node..\n\n```js\nconst path = parseJs(`const a = 1;`);\ninsertJsBefore(path, `const a = $VALUE`, `const b = 2;`, ['$VALUE']);\n\n// before\nconst a = 1;\n\n// after\nconst b = 2;\nconst a = 1;\n```\n\nIt also supports spread wildcards:\n\n```js\nconst path = parseJs(`const a = f(1, 2, 3);`);\ninsertJsBefore(path, `const a = f(...$ARGS)`, `const b = 2;`, ['$ARGS']);\n\n// before\nconst a = f(1, 2, 3);\n\n// after\nconst b = 2;\nconst a = f(1, 2, 3);\n```\n\n##### parseJs\n\n```js\nimport {parseJs} from '@dubstep/core';\n\ntype parseJs = (code: string, options: ParserOptions) =\u003e BabelPath;\ntype ParserOptions = ?{mode: ?('typescript' | 'flow')};\n```\n\nParses a Javascript code string into a `BabelPath`. The default `mode` is `typescript`. The parser configuration follows [Postel's Law](https://en.wikipedia.org/wiki/Robustness_principle), i.e. it accepts all syntax options supported by Babel in order to maximize its versatility.\n\nSee the [Babel handbook](https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md) for more information on `BabelPath`'s API.\n\n##### parseStatement \n\n```js\nimport {parseStatement} from '@dubstep/core';\n\ntype parseStatement = (code: string, options: ParserOptions) =\u003e Node;\ntype ParserOptions = ?{mode: ?('typescript' | 'flow')};\n```\n\nParses a Javascript code statement into a `Node`. Similar to `parseJs` but extracts the statement node.\n\n##### removeJsImports\n\n```js\nimport {removeJsImports} from '@dubstep/core';\n\ntype removeJsImports = (path: BabelPath, code: string, options: ParserOptions) =\u003e void\ntype ParserOptions = ?{mode: ?('typescript' | 'flow')};\n```\n\nRemoves the specifiers declared in `code` for the relevant source. If the import declaration no longer has specifiers after that, the declaration is also removed. Note that `path` should be a BabelPath for a Program node.\n\nIn addition, it removes all statements that reference the removed specifier local binding name.\n\nA `BabelPath` can be obtained from `withJsFile`, `withJsFiles` or `parseJs`.\n\n##### replaceJs\n\n```js\nimport {replaceJs} from '@dubstep/core';\n\ntype replaceJs = (path: BabelPath, source: string, target: string, wildcards: Array\u003cstring\u003e, options: ParserOptions) =\u003e void;\ntype ParserOptions = ?{mode: ?('typescript' | 'flow')};\n```\n\nReplaces code matching `source` with the code in `target`, transferring expressions contained in the `wildcards` list. Note that `path` should be a BabelPath to a Program node.\n\n```js\nreplaceJs(\n  parseJs(`complex.pattern('foo', () =\u003e 'user code')`),\n  `complex.pattern('foo', $CALLBACK)`,\n  `differentPattern($CALLBACK)`,\n  ['$CALLBACK']\n);\n\ncomplex.pattern('foo', () =\u003e 'user code'); // before\ndifferentPattern(() =\u003e 'user code'); // after\n```\n\nIt also supports spread wildcards:\n\n```js\nreplaceJs(\n  parseJs('foo.bar(1, 2, 3);'),\n  `foo.bar(...$ARGS)`,\n  `transformed(...$ARGS)`,\n  ['$ARGS']\n);\n\nfoo.bar(1, 2, 3); // before\ntransformed(1, 2, 3); // after\n```\n\n##### t\n\n```js\nimport {t} from '@dubstep/core';\n\nt.arrayExpression([]);\n// etc.\n```\n\nThis is a flow-typed version of the exports from `@babel/types`. It is useful for creating AST nodes and asserting on existing AST nodes.\n\n---\n\n#### Git\n\n##### gitClone\n\n```js\nimport {gitClone} from '@dubstep/core';\n\ntype gitClone = (repo: string, target: string) =\u003e Promise\u003cany\u003e;\n```\n\nClones a repo into the `target` directory. If the directory exists, it no-ops.\n\n##### gitCommit\n\n```js\nimport {gitCommit} from '@dubstep/core';\n\ntype gitCommit = (message: string) =\u003e Promise\u003cany\u003e;\n```\n\nCreates a local commit containing all modified files with the specified message (but does not push it to origin).\n\n---\n\n#### Restore points\n\n##### createRestorePoint\n\n```js\nimport {createRestorePoint} from '@dubstep/core';\n\ntype createRestorePoint = (file: string, e: StepperError) =\u003e Promise\u003cany\u003e;\n```\n\nCreates a restore file that stores `StepperError` information.\n\n##### getRestorePoint\n\n```js\nimport {getRestorePoint} from '@dubstep/core';\n\ntype getRestorePoint = (file: string) =\u003e Promise\u003cnumber\u003e;\n```\n\nResolves to the index of the failing step recorded in a restore file.\n\n---\n\n#### Misc\n\n##### exec\n\n```js\nimport {exec} from '@dubstep/core';\n\ntype exec = (command: string, options: Object) =\u003e Promise\u003cstring\u003e;\n```\n\nRuns a CLI command in the shell and resolves to `stdout` output. Options provided are passed directly into [execa](https://github.com/sindresorhus/execa#options).\n\n\n---\n\n## Recipes\n\n### Preset composition\n\n```js\nconst migrateA = [\n  step('foo', async () =\u003e gitClone('some-repo.git', 'my-thing')),\n  step('bar', async () =\u003e moveFile('a', 'b')),\n];\n```\n\nA task that needs to run the `migrateA` preset but also need to run a similar task `migrateB` could be expressed in terms of a new preset:\n\n```js\nconst migrateAll = [...migrateA, ...migrateB];\n```\n\nWe retain full programmatic control over the steps, and can compose presets with a high level of granularity:\n\n```js\nconst migrateAndCommit = [\n  ...migrateA,\n  async () =\u003e gitCommit('migrate a'),\n  ...migrateB,\n  async () =\u003e gitCommit('migrate b'),\n];\n```\n\n### Restore points\n\nIf a step in a preset fails, it may be desirable to resume execution of the preset from the failing step (as opposed to restarting from scratch). Resuming a preset can be useful, for example, if a manual step is needed in the middle of a migration in order to unblock further steps.\n\n```js\nconst restoreFile = 'migration-report.json';\nnew Stepper([\n  /* ... */\n])\n  .run({\n    from: await getRestorePoint(restoreFile),\n  })\n  .then(() =\u003e removeFile(restoreFile), e =\u003e createRestorePoint(restoreFile, e));\n```\n\n### Javascript codemods\n\n```js\n// fix-health-path-check.js\nexport const fixHealthPathCheck = async () =\u003e {\n  await withJsFiles(\n    '.',\n    f =\u003e f.match(/src\\/.\\*\\.js/),\n    path =\u003e {\n      return replaceJs(path, `ctx.url === '/health'`, `ctx.path === '/health'`);\n    }\n  );\n};\n\n// index.js\nimport {fixHealthPathCheck} from './fix-health-path-check';\nnew Stepper([\n  step('fix health path check', () =\u003e fixHealthPathCheck()),\n  // ...\n]).run();\n```\n\n### Codemods with state\n\n```js\n// fix-health-path-check.js\nexport const fixHealthPathCheck = async ({path}) =\u003e {\n  const old = '/health';\n  withJsFiles(\n    '.',\n    f =\u003e f.match(/src\\/.*\\.js/),\n    path =\u003e {\n      replaceJs(path, `ctx.url === '${old}'`, `ctx.path === '${path}'`);\n    }\n  );\n  return old;\n};\n\n// index.js\nimport {fixHealthPathCheck} from './fix-health-path-check';\nconst state = {path: '', old: ''};\nnew Stepper([\n  step('get path', async () =\u003e {\n    state.path = await inquirer.prompt({\n      message: 'Replace with what',\n      type: 'input',\n    });\n  }),\n  step('fix health path check', () =\u003e {\n    state.old = await fixIt({path: state.path});\n  }),\n  step('show old', async () =\u003e {\n    console.log(state.old);\n  }),\n]).run();\n```\n\n### Leveraging Babel APIs (e.g. @babel/template)\n\n```js\nimport template from '@babel/template';\n\nexport const compatPluginRenderPageSkeleton = ({pageSkeletonConfig}) =\u003e {\n  const build = template(`foo($VALUE)`);\n  withJsFiles(\n    '.',\n    f =\u003e f.match(/src\\/.\\*\\.js/),\n    path =\u003e {\n      path.traverse({\n        FunctionExpression(path) {\n          if (someCondition(path)) {\n            path.replaceWith(build({VALUE: 1}));\n          }\n        },\n      });\n    }\n  );\n};\n```\n\n### Complex state management\n\nSince the core step runner library is agnostic of state, it's possible to use state management libraries like Redux to make complex state machines more maintainable, and to leverage the ecosystem for things like file persistence.\n\n```js\nconst rootReducer = (state, action) =\u003e ({\n  who: action.type === 'IDENTIFY' ? action.who : state.who || '',\n});\nconst store = redux.createStore(\n  rootReducer,\n  createPersistenceEnhancer('state.json') // save to disk on every action\n);\nnew Stepper([\n  step('who', async () =\u003e {\n    const who = await inquirer.prompt({message: 'who?', type: 'input'});\n    store.dispatch({type: 'IDENTIFY', who});\n  }),\n  step('resumable', async () =\u003e {\n    const {who} = store.getState(); // restore state from disk if needed\n    console.log(who);\n  }),\n]).run();\n```\n\n---\n\n## Motivation\n\nMaintaining Javascript codebases at scale can present some unique challenges. In large enough organizations, it's not uncommon to have dozens or even hundreds of projects. Even if projects were diligently maintained, it's common for a major version bump in a dependency to require code migrations. Keeping a large number of projects up-to-date with security updates and minimizing waste of development time on repetitive, generalizable code migrations are just some of the ways Dubstep can help maintain high code quality and productivity in large organizations.\n\nDubstep aims to provide reusable well-tested utilities for file operations, Javascript codemodding and other tasks related to codebase migrations and upgrades. It can also be used to implement granular scaffolding CLIs.\n\n### Prior art\n\nDubstep aims to be a one-stop shop for generic codebase transformation tasks. It was designed by taking into consideration experience with the strengths and weaknesses of several existing tools.\n\nJavascript-specific codemodding tools such as jscodeshift or babel-codemods can be limited when it comes to cross-file concerns and these tools don't provide adequate platforms for transformations that fall outside of the scope of Javascript parsing (for example, they can't handle JSON or .gitignore files).\n\nShell commands are often used for tasks involving heavy file manipulation, but large shell scripts typically suffer from poor portability/readability/testability. The Unix composition paradigm is also inefficient for composing Babel AST transformations.\n\nPicking-and-choosing NPM packages can offer a myriad of functionality, but there's no cohesiveness between packages, and they often expose inconsistent API styles (e.g. callback-based). Dubstep can easily integrate with NPM packages by leveraging ES2017 async/await in its interfaces.\n\n### Why migration tooling\n\nGenerallly speaking, there are two schools of thought when it comes to maintaining years-long projects at large organizations.\n\nThe \"don't fix what ain't broken\" approach is a conservative risk management strategy. It has the benefit that it requires little maintenance effort from a project owner, but it has the downside that projects become a source of technology fragmentation over time. With this approach, all technical debt that accumulates over the years eventually needs to be paid in one big lump sum (i.e. an expensive rewrite).\n\nAnother downside is that this approach doesn't work well with a push maintenance model. Typically, dependencies in small projects are managed via a pull model, i.e. the project owner updates dependencies at their own convenience. However, in typical large cross-team monorepos, dependencies are managed via a push model, i.e. whoever makes a change to a library is responsible to rolling out version bumps and relevant codemods to all downstreams using that library.\n\nThe \"always update\" approach aims to keep codebases always running on the latest-and-greatest versions of their dependencies, and to reduce duplication of effort (e.g. in bug fixes across duplicated code or fragmented/similar technologies). This approach pays off technical debt incrementally, but consistently across codebases, with the help of tooling to ensure that quality in codebases remains high as improvements are made to upstream libraries (both as patches and breaking changes). The downside of this approach is that it requires a higher investment in terms of maintenance effort, but this is typically offset by offloading the cost of migrations/codemods to a platform/infrastructure team, rather than having every project team waste time on similar/repetitive manual migration tasks.\n\nRegardless of which maintenance model an organization uses, migration tooling can be useful anywhere non-trivial improvements need to be made. Some examples include moving away from proprietary frameworks towards easier-to-hire-for open source ones, or moving away from undesirable technologies towards desirable ones (e.g. if a company decides to migrate from Angular 1.x to React for whatever reason).\n\n---\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FDubstepJS%2Fcore","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FDubstepJS%2Fcore","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FDubstepJS%2Fcore/lists"}