theatre/compat-tests/scripts/scripts.mjs
Aria Minaei e856ee54ff Use a temp directory to install the compat test fixtures
This is a way to run `npm install` on the compat test fixtures without the node_modules at the root of the repo interfering with the node's module resolution (and that of parcel/webpack/etc).

It's hacky because ideally we'd just put each test in its own docker container for simplicity. We tried that in the private repo, but the complexity is not worth the benefit.
2023-08-01 17:47:17 +02:00

397 lines
12 KiB
JavaScript

/**
* Utility functions for the compatibility tests
*/
import prettier from 'prettier'
import path from 'path'
import {globby, argv, YAML, $, fs, cd, os, within} from 'zx'
import onCleanup from 'node-cleanup'
import * as verdaccioPackage from 'verdaccio'
/**
* @param {string} pkg
* @returns boolean
*/
const isTheatreDependency = (pkg) =>
pkg.startsWith('@theatre/') || pkg === 'theatric'
const verbose = !!argv['verbose']
if (!verbose) {
$.verbose = false
console.log(
'Running in quiet mode. Add --verbose to see the output of all commands.',
)
}
// 'verdaccio' is not an es module so we have to do this:
// @ts-ignore
const startVerdaccioServer = verdaccioPackage.default.default
const config = {
VERDACCIO_PORT: 4823,
VERDACCIO_HOST: `localhost`,
get VERDACCIO_URL() {
return `http://${config.VERDACCIO_HOST}:${config.VERDACCIO_PORT}/`
},
PATH_TO_COMPAT_TESTS_ROOT: path.join(__dirname, '..'),
MONOREPO_ROOT: path.join(__dirname, '../..'),
}
/**
* Set environment variables so that yarn and npm use verdaccio as the registry.
* These are only set for the current process.
*/
process.env.YARN_NPM_PUBLISH_REGISTRY = config.VERDACCIO_URL
process.env.YARN_UNSAFE_HTTP_WHITELIST = config.VERDACCIO_HOST
process.env.YARN_NPM_AUTH_IDENT = 'test:test'
process.env.NPM_CONFIG_REGISTRY = config.VERDACCIO_URL
const tempVersion =
'0.0.1-COMPAT.' +
(typeof argv['version'] === 'number'
? argv['version'].toString()
: // a random integer between 1 and 50000
(Math.floor(Math.random() * 50000) + 1).toString())
const keepAlive = !!argv['keep-alive']
/**
* This script starts verdaccio and publishes all the packages in the monorepo to it, then
* it runs `npm install` on all the test packages, and finally it closes verdaccio.
*/
export async function installFixtures() {
onCleanup((exitCode, signal) => {
onCleanup.uninstall()
restoreTestPackageJsons()
process.kill(process.pid, signal)
return false
})
console.log(
`Using temporary version: ${tempVersion} . Use --version=[NUMBER] to change.`,
)
console.log('Patching package.json files in ./test-*')
const restoreTestPackageJsons = await patchTestPackageJsons()
console.log('Starting verdaccio')
const verdaccioServer = await startVerdaccio(config.VERDACCIO_PORT)
console.log(`Verdaccio is running on ${config.VERDACCIO_URL}`)
console.log('Releasing @theatre/* packages to verdaccio')
await releaseToVerdaccio()
console.log('Running `$ npm install` on test packages')
await runNpmInstallOnTestPackages()
console.log('All fixtures installed successfully')
if (keepAlive) {
console.log('Keeping verdaccio alive. Press Ctrl+C to exit.')
// wait for ctrl+c
await new Promise((resolve) => {})
} else {
console.log('Closing verdaccio. Use --keep-alive to keep it running.')
restoreTestPackageJsons()
await verdaccioServer.close()
}
console.log('Done')
}
async function runNpmInstallOnTestPackages() {
const packagePaths = await getCompatibilityTestSetups()
for (const pathToPackageDir of packagePaths) {
await fs.remove(path.join(pathToPackageDir, 'node_modules'))
await fs.remove(path.join(pathToPackageDir, 'package-lock.json'))
cd(path.join(pathToPackageDir, '../'))
const tempPath = fs.mkdtempSync(
path.join(os.tmpdir(), 'theatre-compat-test-'),
)
await fs.copy(pathToPackageDir, tempPath)
cd(path.join(tempPath))
try {
console.log('Running npm install on ' + pathToPackageDir + '...')
await $`npm install --registry ${config.VERDACCIO_URL} --loglevel ${
verbose ? 'warn' : 'error'
} --fund false`
console.log('npm install finished successfully in' + tempPath)
await fs.move(
path.join(tempPath, 'node_modules'),
path.join(pathToPackageDir, 'node_modules'),
)
await fs.move(
path.join(tempPath, 'package-lock.json'),
path.join(pathToPackageDir, 'package-lock.json'),
)
} catch (error) {
console.error(`Failed to install dependencies for ${pathToPackageDir}
Try running \`npm install\` in that directory manually via:
cd ${pathToPackageDir}
npm install --registry ${config.VERDACCIO_URL}
Original error: ${error}`)
} finally {
await fs.remove(tempPath)
}
}
}
/**
* Takes an absolute path to a package.json file and replaces all of its
* dependencies on `@theatre/*` packatges to `version`.
*
* @param {string} pathToPackageJson absolute path to the package.json file
* @param {string} version The version to set all `@theatre/*` dependencies to
*/
async function patchTheatreDependencies(pathToPackageJson, version) {
const originalFileContent = fs.readFileSync(pathToPackageJson, {
encoding: 'utf-8',
})
// get the package.json file's content
const packageJson = JSON.parse(originalFileContent)
// find all dependencies on '@theatre/*' packages and replace them with the local version
for (const dependencyType of [
'dependencies',
'devDependencies',
'peerDependencies',
]) {
const dependencies = packageJson[dependencyType]
if (dependencies) {
for (const dependencyName of Object.keys(dependencies)) {
if (isTheatreDependency(dependencyName)) {
dependencies[dependencyName] = version
}
}
}
}
// run the json through prettier
const jsonStringPrettified = prettier.format(
JSON.stringify(packageJson, null, 2),
{
parser: 'json',
filepath: pathToPackageJson,
},
)
// write the modified package.json file
fs.writeFileSync(pathToPackageJson, jsonStringPrettified, {encoding: 'utf-8'})
}
async function patchTestPackageJsons() {
const packagePaths = (await getCompatibilityTestSetups()).map(
(pathToPackageDir) => path.join(pathToPackageDir, 'package.json'),
)
// replace all dependencies on @theatre/* packages with the local version
for (const pathToPackageJson of packagePaths) {
patchTheatreDependencies(pathToPackageJson, tempVersion)
}
return () => {
// replace all dependencies on @theatre/* packages with the 0.0.1-COMPAT.1
for (const pathToPackageJson of packagePaths) {
patchTheatreDependencies(pathToPackageJson, '0.0.1-COMPAT.1')
}
}
}
/**
* Starts the verdaccio server and returns a promise that resolves when the serve is up and ready
*
* Credit: https://github.com/storybookjs/storybook/blob/92b23c080d03433765cbc7a60553d036a612a501/scripts/run-registry.ts
*/
const startVerdaccio = (port) => {
let resolved = false
return Promise.race([
new Promise((resolve) => {
const config = {
...YAML.parse(
fs.readFileSync(path.join(__dirname, '../verdaccio.yml'), 'utf8'),
),
}
if (verbose) {
config.logs.level = 'warn'
}
const cache = path.join(__dirname, '../.verdaccio-cache')
config.self_path = cache
const onReady = (webServer) => {
webServer.listen(port, () => {
resolved = true
resolve(webServer)
})
}
startVerdaccioServer(config, 6000, cache, '1.0.0', 'verdaccio', onReady)
}),
new Promise((_, rej) => {
setTimeout(() => {
if (!resolved) {
resolved = true
rej(new Error(`TIMEOUT - verdaccio didn't start within 10s`))
}
}, 10000)
}),
])
}
const packagesToPublish = [
'@theatre/core',
'@theatre/studio',
'@theatre/dataverse',
'@theatre/react',
'@theatre/browser-bundles',
'@theatre/r3f',
'theatric',
]
/**
* Assigns a new version to each of @theatre/* packages. If there a package depends on another package in this monorepo,
* this function makes sure the dependency version is fixed at "version"
*
* @param {{name: string, location: string}[]} workspacesListObjects - An Array of objects containing information about the workspaces
* @param {string} version - Version of the latest commit (or any other string)
* @returns {Promise<() => void>} - An async function that restores the package.json files to their original version
*/
async function writeVersionsToPackageJSONs(workspacesListObjects, version) {
/**
* An array of functions each of which restores a certain package.json to its original state
* @type {Array<() => void>}
*/
const restores = []
for (const workspaceData of workspacesListObjects) {
const pathToPackage = path.resolve(
config.MONOREPO_ROOT,
workspaceData.location,
'./package.json',
)
const originalFileContent = fs.readFileSync(pathToPackage, {
encoding: 'utf-8',
})
const originalJson = JSON.parse(originalFileContent)
restores.push(() => {
fs.writeFileSync(pathToPackage, originalFileContent, {encoding: 'utf-8'})
})
let {dependencies, peerDependencies, devDependencies} = originalJson
// Normally we don't have to override the package versions in dependencies because yarn would already convert
// all the "workspace:*" versions to a fixed version before publishing. However, packages like @theatre/studio
// have a peerDependency on @theatre/core set to "*" (meaning they would work with any version of @theatre/core).
// This is not the desired behavior in pre-release versions, so here, we'll fix those "*" versions to the set version.
for (const deps of [dependencies, peerDependencies, devDependencies]) {
if (!deps) continue
for (const wpObject of workspacesListObjects) {
if (deps[wpObject.name]) {
deps[wpObject.name] = version
}
}
}
const newJson = {
...originalJson,
version,
dependencies,
peerDependencies,
devDependencies,
}
fs.writeFileSync(pathToPackage, JSON.stringify(newJson, undefined, 2), {
encoding: 'utf-8',
})
}
return () =>
restores.forEach((fn) => {
fn()
})
}
/**
* Builds all the @theatre/* packages with version number 0.0.1-COMPAT.1 and publishes
* them all to the verdaccio registry
*/
async function releaseToVerdaccio() {
cd(config.MONOREPO_ROOT)
// @ts-ignore ignore
process.env.THEATRE_IS_PUBLISHING = true
const workspacesListString = await $`yarn workspaces list --json`
const workspacesListObjects = workspacesListString.stdout
.split(os.EOL)
// strip out empty lines
.filter(Boolean)
.map((x) => JSON.parse(x))
const restorePackages = await writeVersionsToPackageJSONs(
workspacesListObjects,
tempVersion,
)
// Restore the package.json files to their original state when the process is killed
process.on('SIGINT', async function cleanup(a) {
restorePackages()
})
try {
await $`yarn clean`
await $`yarn build`
await Promise.all(
packagesToPublish.map(async (workspaceName) => {
const npmTag = 'compat'
await $`yarn workspace ${workspaceName} npm publish --access public --tag ${npmTag}`
}),
)
} finally {
restorePackages()
}
}
/**
* Get all the setups from `./compat-tests/`
*
* @returns {Promise<Array<string>>} An array containing the absolute paths to the compatibility test setups
*/
export async function getCompatibilityTestSetups() {
const fixturePackageJsonFiles = await globby(
'./fixtures/*/package/package.json',
{
cwd: config.PATH_TO_COMPAT_TESTS_ROOT,
gitignore: false,
onlyFiles: true,
},
)
return fixturePackageJsonFiles.map((entry) => {
return path.join(config.PATH_TO_COMPAT_TESTS_ROOT, entry, '../')
})
}
/**
* Deletes ../test-*\/(node_modules|package-lock.json|yarn.lock)
*/
export async function clean() {
const toDelete = await globby(
'./fixtures/*/package/(node_modules|yarn.lock|package-lock.json|.parcel-cache)',
{
cwd: config.PATH_TO_COMPAT_TESTS_ROOT,
// node_modules et al are gitignored, but we still want to clean them
gitignore: false,
// include directories too
onlyFiles: false,
},
)
return await Promise.all(
toDelete.map((fileOrDir) => {
console.log('deleting', fileOrDir)
return fs.remove(path.join(config.PATH_TO_COMPAT_TESTS_ROOT, fileOrDir))
}),
)
}