#!/usr/bin/env node /** * Copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // @ts-check /** * Use the following command to typescheck this file: * npx tsc --target es2020 --watch --checkjs --noemit --moduleResolution node workspace.js */ const fs = require('fs'); const path = require('path'); const readJSON = async (filePath) => JSON.parse(await fs.promises.readFile(filePath, 'utf8')); const writeJSON = async (filePath, json) => { await fs.promises.writeFile(filePath, JSON.stringify(json, null, 2) + '\n'); } class PWPackage { constructor(descriptor) { this.name = descriptor.name; this.path = descriptor.path; this.files = descriptor.files; this.packageJSONPath = path.join(this.path, 'package.json'); this.packageJSON = JSON.parse(fs.readFileSync(this.packageJSONPath, 'utf8')); this.isPrivate = !!this.packageJSON.private; } } class Workspace { /** * @param {string} rootDir * @param {PWPackage[]} packages */ constructor(rootDir, packages) { this._rootDir = rootDir; this._packages = packages; } /** * @returns {PWPackage[]} */ packages() { return this._packages; } async version() { const workspacePackageJSON = await readJSON(path.join(this._rootDir, 'package.json')); return workspacePackageJSON.version; } /** * @param {string} version */ async setVersion(version) { if (version.startsWith('v')) throw new Error('version must not start with "v"'); // 1. update workspace's package.json (playwright-internal) with the new version const workspacePackageJSON = await readJSON(path.join(this._rootDir, 'package.json')); workspacePackageJSON.version = version; await writeJSON(path.join(this._rootDir, 'package.json'), workspacePackageJSON); // 2. make workspace consistent. await this.ensureConsistent(); } async ensureConsistent() { let hasChanges = false; const maybeWriteJSON = async (jsonPath, json) => { const oldJson = await readJSON(jsonPath); if (JSON.stringify(json) === JSON.stringify(oldJson)) return; hasChanges = true; console.warn('Updated', jsonPath); await writeJSON(jsonPath, json); }; const workspacePackageJSON = await readJSON(path.join(this._rootDir, 'package.json')); const packageLockPath = path.join(this._rootDir, 'package-lock.json'); const packageLock = JSON.parse(await fs.promises.readFile(packageLockPath, 'utf8')); const version = workspacePackageJSON.version; // Make sure package-lock version is consistent with root package.json version. packageLock.version = version; packageLock.packages[""].version = version; for (const pkg of this._packages) { // 1. Copy package files. for (const file of pkg.files) { const fromPath = path.join(this._rootDir, file); const toPath = path.join(pkg.path, file); await fs.promises.mkdir(path.dirname(pkg.path), { recursive: true }); await fs.promises.copyFile(fromPath, toPath); } // 2. Make sure package-lock and package's package.json are consistent. // All manual package-lock management is a workaround for // https://github.com/npm/cli/issues/3940 const pkgLockEntry = packageLock['packages']['packages/' + path.basename(pkg.path)]; const depLockEntry = packageLock['dependencies'][pkg.name]; if (!pkg.isPrivate) { pkgLockEntry.version = version; pkg.packageJSON.version = version; pkg.packageJSON.repository = workspacePackageJSON.repository; pkg.packageJSON.engines = workspacePackageJSON.engines; pkg.packageJSON.homepage = workspacePackageJSON.homepage; pkg.packageJSON.author = workspacePackageJSON.author; pkg.packageJSON.license = workspacePackageJSON.license; } for (const otherPackage of this._packages) { if (pkgLockEntry.dependencies && pkgLockEntry.dependencies[otherPackage.name]) pkgLockEntry.dependencies[otherPackage.name] = version; if (pkgLockEntry.devDependencies && pkgLockEntry.devDependencies[otherPackage.name]) pkgLockEntry.devDependencies[otherPackage.name] = version; if (depLockEntry.requires && depLockEntry.requires[otherPackage.name]) depLockEntry.requires[otherPackage.name] = version; if (pkg.packageJSON.dependencies && pkg.packageJSON.dependencies[otherPackage.name]) pkg.packageJSON.dependencies[otherPackage.name] = version; if (pkg.packageJSON.devDependencies && pkg.packageJSON.devDependencies[otherPackage.name]) pkg.packageJSON.devDependencies[otherPackage.name] = version; } await maybeWriteJSON(pkg.packageJSONPath, pkg.packageJSON); } await maybeWriteJSON(packageLockPath, packageLock); return hasChanges; } } const ROOT_PATH = path.join(__dirname, '..'); const LICENCE_FILES = ['NOTICE', 'LICENSE']; const workspace = new Workspace(ROOT_PATH, [ new PWPackage({ name: 'playwright', path: path.join(ROOT_PATH, 'packages', 'playwright'), // We copy README.md additionally for Playwright so that it looks nice on NPM. files: [...LICENCE_FILES, 'README.md'], }), new PWPackage({ name: 'playwright-core', path: path.join(ROOT_PATH, 'packages', 'playwright-core'), files: LICENCE_FILES, }), new PWPackage({ name: '@playwright/test', path: path.join(ROOT_PATH, 'packages', 'playwright-test'), files: LICENCE_FILES, }), new PWPackage({ name: 'playwright-webkit', path: path.join(ROOT_PATH, 'packages', 'playwright-webkit'), files: LICENCE_FILES, }), new PWPackage({ name: 'playwright-firefox', path: path.join(ROOT_PATH, 'packages', 'playwright-firefox'), files: LICENCE_FILES, }), new PWPackage({ name: 'playwright-chromium', path: path.join(ROOT_PATH, 'packages', 'playwright-chromium'), files: LICENCE_FILES, }), new PWPackage({ name: 'html-reporter', path: path.join(ROOT_PATH, 'packages', 'html-reporter'), files: [], }), new PWPackage({ name: '@playwright/experimental-ct-react', path: path.join(ROOT_PATH, 'packages', 'playwright-ct-react'), files: ['LICENSE'], }), new PWPackage({ name: '@playwright/experimental-ct-solid', path: path.join(ROOT_PATH, 'packages', 'playwright-ct-solid'), files: ['LICENSE'], }), new PWPackage({ name: '@playwright/experimental-ct-svelte', path: path.join(ROOT_PATH, 'packages', 'playwright-ct-svelte'), files: ['LICENSE'], }), new PWPackage({ name: '@playwright/experimental-ct-vue', path: path.join(ROOT_PATH, 'packages', 'playwright-ct-vue'), files: ['LICENSE'], }), new PWPackage({ name: '@playwright/experimental-ct-vue2', path: path.join(ROOT_PATH, 'packages', 'playwright-ct-vue2'), files: ['LICENSE'], }), ]); if (require.main === module) { parseCLI(); } else { module.exports = {workspace}; } function die(message, exitCode = 1) { console.error(message); process.exit(exitCode); } async function parseCLI() { const commands = { '--ensure-consistent': async () => { const hasChanges = await workspace.ensureConsistent(); if (hasChanges) die(`\n ERROR: workspace is inconsistent! Run '//utils/workspace.js --ensure-consistent' and commit changes!`); }, '--list-public-package-paths': () => { for (const pkg of workspace.packages()) { if (!pkg.isPrivate) console.log(pkg.path); } }, '--get-version': async (version) => { console.log(await workspace.version()); }, '--set-version': async (version) => { if (!version) die('ERROR: Please specify version! e.g. --set-version 1.99.2'); await workspace.setVersion(version); }, '--help': () => { console.log([ `Available commands:`, ...Object.keys(commands).map(cmd => ' ' + cmd), ].join('\n')); }, }; const handler = commands[process.argv[2]]; if (!handler) die('ERROR: wrong usage! Run with --help to list commands'); await handler(process.argv[3]); }