Skip to content

Bump node 22.x #316

@eleanorjboyd

Description

@eleanorjboyd

How to Update Node.js Version and Modernize Tooling in a VS Code Extension Workspace

This guide walks you through updating the Node.js version and modernizing dependencies and configuration for a VS Code extension project. These steps are based on recent changes made in the vscode-black-formatter extension.

1. Update Node.js Version

  • CI/CD Pipelines:

    • Update Node.js version in your CI/CD config files (e.g., GitHub Actions, Azure Pipelines, etc.)

      • Example for GitHub Actions:
        env:
          NODE_VERSION: 22.17.0
        jobs:
          build:
            steps:
              - uses: actions/setup-node@v4
                with:
                  node-version: ${{ env.NODE_VERSION }}
      • Example for Azure Pipelines:
        - task: NodeTool@0
          inputs:
            versionSpec: '22.17.0'
    • Replace Emoji-Based Working Directory Names:

      • If you see config like:
        special-working-directory: './🐍 🐛'
        special-working-directory-relative: '🐍 🐛'
        Replace '🐍 🐛' with a simple name, e.g.:
        special-working-directory: './testingDir'
        special-working-directory-relative: 'testingDir'
      • This avoids cross-platform issues and improves reliability in CI/CD and local testing.
  • Local Development:

    • Update your local Node.js version using nvm or your preferred version manager:
      nvm install 22.17.0
      nvm use 22.17.0

2. Update Dependencies

  • Open package.json and update major dependencies:
    • TypeScript, ESLint, Mocha, Sinon, etc.
    • Use npm outdated to see what can be updated.
    • Example:
      "devDependencies": {
        "typescript": "^5.9.2",
        "eslint": "^9.33.0",
        "mocha": "^11.7.1",
        "sinon": "^21.0.0",
        // ...other dependencies
      }
  • Run:
    npm install

3. (Optional) Modernize ESLint Configuration

If you want to update to the latest ESLint configuration style:

  • Remove old .eslintrc.json or .eslintrc.js.
  • Create a new eslint.config.mjs using the latest ESLint API:
    // @ts-check
    import eslint from '@eslint/js';
    import tseslint from 'typescript-eslint';
    
    export default tseslint.config(
      eslint.configs.recommended,
      ...tseslint.configs.recommended,
      {
        files: ['**/*.ts'],
        rules: {
          '@typescript-eslint/naming-convention': 'warn',
          'curly': 'warn',
          'eqeqeq': 'warn',
          'no-throw-literal': 'warn',
          'semi': 'off',
        },
      },
      {
        ignores: [
          'out/**',
          'dist/**',
          '**/*.d.ts',
        ],
      }
    );
  • Install new ESLint dependencies:
    npm install @eslint/js typescript-eslint eslint --save-dev

4. Update Python Requirements (if applicable)

  • If your extension uses Python, update requirements.txt and test requirements to latest versions.
  • Use pip-compile or uv to regenerate lock files.

5. Refactor Test and Build Scripts

  • Update test scripts to use new Node.js and dependency APIs.
  • Add verbose logging and error handling for extension activation and installation.
  • Ensure working directories are compatible across platforms (avoid emojis, use simple names like testingDir).

Important: Fixing spawnSync Usage for Node.js 22+

Node.js 22 introduced stricter requirements for the arguments passed to spawnSync. If you use code like:

const command = path.relative(EXTENSION_ROOT_DIR, cli);
cp.spawnSync(command, [...args, '--install-extension', 'ms-python.python'], { ... });

You may encounter errors about invalid arguments or command not found. To fix this:

  • Use the full path to the CLI executable (do not use path.relative).
  • Pass arguments as a flat array, and set shell: true only on Windows.
  • Add verbose logging for debugging.

Example: Robust Extension Installation for Node.js 22+

async function main() {
  try {
    // The folder containing the Extension Manifest package.json
    // Passed to `--extensionDevelopmentPath`
    const extensionDevelopmentPath = path.resolve(__dirname, '../');

    // The path to test runner
    // Passed to --extensionTestsPath
    const extensionTestsPath = path.resolve(__dirname, './unittest/index');
    const vscodeExecutablePath = await downloadAndUnzipVSCode('stable');
    const [cliPath, ...args] = resolveCliArgsFromVSCodeExecutablePath(vscodeExecutablePath);

    // Use cp.spawn / cp.exec for custom setup
    const isWin = process.platform === 'win32';
    if (isWin) {
      try {
        const installResult = cp.spawnSync(
          cliPath,
          [...args, '--install-extension', PVSC_EXTENSION_ID_FOR_TESTS, PVSC_ENVS_EXTENSION_ID_FOR_TESTS],
          {
            cwd: path.dirname(cliPath),
            encoding: 'utf8',
            stdio: 'inherit',
            shell: true,
          },
        );
        if (installResult.error) {
          console.error('Extension installation error:', installResult.error);
        }
        if (installResult.status !== 0) {
          console.error(`Extension installation failed with exit code: ${installResult.status}`);
        } else {
          console.log('Extension installation succeeded.');
        }
      } catch (ex) {
        console.error('Exception during extension installation:', ex);
      }
    } else {
      const installResult = cp.spawnSync(
        cliPath,
        [...args, '--install-extension', PVSC_EXTENSION_ID_FOR_TESTS, PVSC_ENVS_EXTENSION_ID_FOR_TESTS],
        {
          encoding: 'utf8',
          stdio: 'inherit',
        },
      );
      if (installResult.error) {
        console.error('Extension installation error:', installResult.error);
      }
      if (installResult.status !== 0) {
        console.error(`Extension installation failed with exit code: ${installResult.status}`);
      } else {
        console.log('Extension installation succeeded.');
      }
    }
    console.log('Extensions installed, ready to run tests.');

    // Run the extension test
    await runTests({
      // Use the specified `code` executable
      vscodeExecutablePath,
      extensionDevelopmentPath,
      extensionTestsPath,
    });
  } catch (err) {
    console.error('Failed to run tests');
    process.exit(1);
  }
}

This pattern ensures compatibility with Node.js 22+ and avoids common issues with extension installation in smoke tests. Always check for errors and exit codes, and use platform-specific options for Windows.

6. Clean Up and Validate

  • Remove unused imports and obsolete configuration.
  • Run all tests to ensure compatibility.
  • Commit and push your changes.

other notes:
update the checkout actions to v4 as well
add the github label "debt"


Tip: Review recent PRs in similar extensions for examples of dependency and config updates.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions