Migrate


skuba migrate help

Echoes the available skuba migrations.

skuba migrate help

skuba migrate node

skuba includes migrations to upgrade your project to the active LTS version of Node.js. This is intended to minimise effort required to keep up with annual Node.js releases.

The following files are scanned:

  • .node-version
  • .nvmrc
  • package.jsons
  • tsconfig.jsons
  • Buildkite pipelines in .buildkite/ directories
  • CDK files in infra/ directories
  • Dockerfiles & Docker Compose files
  • Serverless files

skuba may not be able to upgrade all projects, and typically works best when a project closely matches a built-in template. Check your project for files that may have been missed, review and test the modified code as appropriate before releasing to production, and open an issue if your project files were corrupted by the migration. Exercise particular caution with monorepos, as some may have employed unique configurations that the migration has not accounted for.

The migration will attempt to proceed if your project:

  • Specifies a Node.js version in .node-version, .nvmrc, and/or package.json#/engines/node

  • Does not include a package.json#/files field

    This field implies that your project is an npm package. See below for considerations when manually upgrading npm packages.

  • Specifies a project type in package.json#/skuba/type that is not package

    Well-known project types currently include application and package. While we intend to improve support for monorepo projects in a future version, you may enable migrations in the interim by setting your root /package.json project type to root.

skuba upgrades your tsconfig.jsons in line with the official Node Target Mapping guidance. tsconfig.jsons contain two options that are linked to Node.js versions:

  • lib configures the language features available to your source code.

    For example, including ES2024 allows you to use the Object.groupBy() static method. The features available in each new ECMAScript version are summarised on node.green.

  • target configures the transpilation behaviour of the TypeScript compiler.

    Back-end applications typically synchronise lib with their Node.js runtime. In this scenario, there is no need to transpile language features and target can match the ECMAScript version in lib.

    On the other hand, you may wish to use recent language features when authoring your npm packages while retaining support for package consumers on older Node.js runtimes. In this scenario, see the note below on transpilation for npm packages.

As of skuba 14, for npm packages, we will attempt to upgrade your targets to be 1 major version behind the current LTS Node.js version.

For example, when upgrading a project to Node.js 24, we will upgrade npm packages to target Node.js 22.

To ensure accurate detection of npm packages, set the skuba.type field in your package.json to package for npm packages and application for applications.

The following fields are modified for npm packages:

  • package.json#/engines/node

    The engines property propagates to your package consumers. For example, if you specify a minimum Node.js version of 22, it will prevent your package from being installed in a Node.js 20 environment:

    {
      "engines": {
        "node": ">=22"
      }
    }
    

    Take care with the engines property of an npm package; modifications typically necessitate a new major release per semantic versioning.

  • tsconfig.json#/target

    Refer to the official Node Target Mapping guidance and ensure that the transpilation target corresponds to the minimum Node.js version in engines.

    For monorepo projects, check whether your npm packages inherit from another tsconfig.json. You may need to define explicit overrides for npm packages like so:

      {
    +   "compilerOptions": {
    +     "removeComments": false,
    +     "target": "ES2023" // Continue to support package consumers on Node.js 20
    +   },
        "extends": "../../tsconfig.json"
      }
    

skuba format and skuba lint will automatically run these migrations as patches.

As of skuba 14, skuba migrate attempts to upgrade underlying infrastructure packages for compatibility with the new Node.js version. These include aws-cdk-lib, datadog-cdk-constructs-v2, osls, serverless, serverless-plugin-datadog and @types/node.

skuba migrate node24

Attempts to automatically upgrade your project to Node.js 24 and your package targets to Node.js 22.14.0+.

skuba migrate node24

Node.js 24 includes breaking changes. For more information on the upgrade, refer to:

skuba migrate node22

Attempts to automatically upgrade your project to Node.js 22.

skuba migrate node22

Node.js 22 includes breaking changes. For more information on the upgrade, refer to:

You may need to manually upgrade CDK and Serverless package versions as appropriate to support nodejs22.x, and @types/node to major version 22.

skuba migrate node20

Attempts to automatically upgrade your project to Node.js 20.

skuba migrate node20

Node.js 20 includes breaking changes. For more information on the upgrade, refer to:

You may need to manually upgrade CDK and Serverless package versions as appropriate to support nodejs20.x, and @types/node to major version 20.

skuba migrate esm

Attempts to automatically migrate your project from CommonJS to ESM. Before running the migration, follow the migration steps.

skuba migrate esm

The following changes are made:

  • type module is added to package.json files
  • CommonJS syntax is replaced with ESM syntax in source files, test files, and configuration files
  • AWS CDK worker and Serverless files are migrated to ESM format
  • ESLint config files and Prettier config files are migrated to ESM format
  • Jest is replaced with Vitest as the test runner
    • The sku codemod is run along with additional transformations to fix additional cases
    • aws-sdk-client-mock-jest → aws-sdk-client-mock-vitest + @types/node
    • @shopify/jest-koa-mocks → @skuba-lib/vitest-koa-mocks + @types/node
    • --runInBand → --maxWorkers=1 in package.json test scripts and Buildkite pipelines
    • jest.config.*ts files are migrated to vitest.config.ts on a best-effort basis
    • Jest hooks are migrated to Vitest hooks on a best-effort basis

Due to the complexities of test code and configurations, the migration may not be able to modify all files in your project.

If you are running this migration for a non-skuba application, you will need to manually install vitest, and @vitest/coverage-istanbul as dev dependencies.

Post Migration Steps

  1. Run skuba lint and attempt to address any lint errors that may be caused by the migration. The most common failure points with skuba test runs can normally be addressed by fixing the lint errors first.

If you notice there are changes you can make prior to running the skuba migration, we suggest making those changes first and then re-running the migration for the ease of reviewing the migration changes.

If you notice any repeatable issues that the migration has not accounted for, please open an issue or reach out in #skuba-support.

  1. Review vitest.config.ts migrations

Review your generated vitest.config.ts and Vitest setup files against the original jest.config.ts and Jest setup files to verify all configuration has been carried across, paying close attention to any custom settings or patterns that the migration may have missed. Once satisfied, delete the jest.config.ts and any Jest setup files.

The migration may also leave some manual steps for you to complete within your vitest.config.ts files, such as updating existing regexp patterns to glob patterns in your test configuration.

// vitest.config.ts
export default defineConfig({
  test: {
    exclude: ['\\.int\\.test'], // TODO: Update these regexp pattern strings to globs
  },
});

These should be easily migrated by hand or with the assistance of an AI agent such as Copilot with a prompt such as

Address the TODO comments in vitest.config files
  1. Run skuba test and attempt to address any test errors that may be caused by the migration

  2. Run and deploy your project as normal, and monitor for any issues that may be caused by the migration.

  3. If your project deploys a package, ensure you test the published package in a downstream project to confirm it works as expected.

FAQ and Tips

Config consolidation

If you have multiple jest.config.ts files, you may be able to consolidate these into a single vitest.config.ts file with multiple projects

Example:

If you have the following Jest config files:

  • jest.config.ts
  • jest.config.int.ts where integration tests must be run with --runInBand due to shared resources

You may be able to consolidate these into a single vitest.config.ts file with multiple projects like so:

// vitest.config.ts
export default defineConfig(
  Vitest.mergePreset({
    ssr: {
      resolve: {
        conditions: ['@seek/YOUR_REPO/source'],
      },
    },
    test: {
      env: {
        ENVIRONMENT: 'test',
      },
      projects: [
        {
          extends: true,
          test: {
            name: 'unit',
            exclude: ['**/*.int.test.ts'],
          },
        },
        {
          extends: true,
          test: {
            name: 'integration',
            fileParallelism: false, // Equivalent to --runInBand
            setupFiles: ['vitest.setup.int.ts'],
            include: ['**/*.int.test.ts'],
          },
        },
      ],
    },
  }),
);
Performance

By default, Vitest runs every test in isolation to provide a side-effect free testing environment. However, this is not always necessary and can lead to slower test runs compared to Jest.

Follow the Vitest improving performance guide to optimise your Vitest configuration

Matchers not matching on errors

Vitest matches deeper than Jest so you may need to adjust your test assertions. Previously, you were able to match on error messages with Jest like so but you may need to adjust your tests to match on the error object instead of just the message with Vitest:

- await expect(someFunction()).rejects.toThrow(new Error('some error message'));
+ await expect(someFunction()).rejects.toThrow(expect.objectContaining(new Error('some error message')));
// or
+ await expect(someFunction()).rejects.toThrow(new ActualError('some error message'));
Jest spies no longer work after the migration
  1. Run pnpm dlx @skuba-lib/detect-invalid-spies .

Spies work differently in Vitest compared to Jest. You can read more about the differences here in our @skuba-lib/detect-invalid-spies documentation.

This will identify any spies in your code that may be broken by the migration. If there are any issues detected, you will need to address these before proceeding with the migration.

For other Jest-specific patterns, you can refer to the migration guide provided by Vitest for more information on how to migrate your tests.

Cannot find module ‘some-module/type’ or its corresponding type declarations.ts(2307)

The ESLint rule introduced in previous skuba versions would quit evaluating imports very early to avoid long ESLint run times which means a few imports may be now invalid imports in ESM. The fix is as simple as adding a .js extension to the end of the import path:

- import { type } from '@seek/some-module/lib-types/types/type.generated';
+ import { type } from '@seek/some-module/lib-types/types/type.generated.js';
Jest Dynalite

If you were using jest-dynalite for testing DynamoDB interactions, you will need to switch to Vitest dynalite lite which provides similar functionality for Vitest.

The recommended setupFiles can slow down your test suite when run against all tests. To avoid this, configure a dedicated Vitest project for test files that use Dynalite.

Example:

// vitest.config.ts
export default defineConfig(
  Vitest.mergePreset({
    ssr: {
      resolve: {
        conditions: ['@seek/YOUR_REPO/source'],
      },
    },
    test: {
      env: {
        ENVIRONMENT: 'test',
      },
      projects: [
        {
          extends: true,
          test: {
            name: 'unit',
            exclude: ['**/*.dynalite.test.ts'],
          },
        },
        {
          extends: true,
          test: {
            name: 'dynalite',
            setupFiles: ['vitest-dynamodb-lite'],
            include: ['**/*.dynalite.test.ts'],
          },
        },
      ],
    },
  }),
);
DataDog Trace Headers

You may notice Datadog trace headers being emitted in your test output after the migration. This is because Vitest runs tests in a more realistic environment which may cause some of your code to execute differently compared to Jest.

+     "x-datadog-parent-id": "6421394243863276142",
+     "x-datadog-sampling-priority": "-1",
+     "x-datadog-tags": "_dd.p.tid=69f895eb00000000,_dd.p.ksr=0",
+     "x-datadog-trace-id": "6421394243863276142",

You can suppress these headers by adding the following to your Vitest setup file:

export default defineConfig({
  test: {
    env: {
      ENVIRONMENT: 'test',
+     DD_TRACE_ENABLED: 'false',
    },
  },
});
Esbuild

If you were using esbuild directly in your project, you may need to update your esbuild configuration to ensure it is compatible with ESM.

Of note, you may need to update the conditions, mainFields, format and external or plugins options in your esbuild configuration to ensure that it correctly resolves ESM modules.

  esbuild.build({
    // ...
    conditions: [
      '@seek/YOUR_REPO/source',
+     'module'
    ],
+   mainFields: ['module', 'main'],
+   format: 'esm',

+   external: ['pino']
    // or
+   plugins: [esbuildPluginPino()]
   });
Coverage reports are different after the migration

Vitest transforms your code differently to Jest which may result in different coverage reports after the migration. You may need to experiment with placing /* istanbul ignore */ comments in different places in your code to achieve the desired coverage report.

    transport:
-   /* istanbul ignore next */
      config.environment === 'local'
+       ? /* istanbul ignore next */ { target: 'pino-pretty' }
        : undefined,

We are unsure whether this is intended behaviour or if there is a bug in the Vitest Istanbul and v8 coverage providers.

You may also find some luck with using the /* istanbul ignore start */ and /* istanbul ignore end */ comments

For the keen observers, we have decided to ease the migration by firstly adopting the istanbul provider for coverage in Vitest instead of the default v8 provider. The v8 provider will be made the default in a future release once we have mostly migrated our codebase to ESM and can confirm it works as expected.