diff --git a/crates/node/package.json b/crates/node/package.json index ad05012945db..325041ec94c4 100644 --- a/crates/node/package.json +++ b/crates/node/package.json @@ -50,12 +50,12 @@ }, "scripts": { "build": "pnpm run build:platform && pnpm run build:wasm", - "build:platform": "napi build --platform --release", + "build:platform": "node ../../scripts/run-task.mjs build -- napi build --platform --release", "postbuild:platform": "node ./scripts/move-artifacts.mjs", - "build:wasm": "napi build --release --target wasm32-wasip1-threads", + "build:wasm": "node ../../scripts/run-task.mjs build -- napi build --release --target wasm32-wasip1-threads", "postbuild:wasm": "node ./scripts/move-artifacts.mjs", "dev": "cargo watch --quiet --shell 'npm run build'", - "build:debug": "napi build --platform", + "build:debug": "node ../../scripts/run-task.mjs build -- napi build --platform", "version": "napi version" }, "optionalDependencies": { diff --git a/package.json b/package.json index c06e7c606009..d3205319eb09 100644 --- a/package.json +++ b/package.json @@ -32,19 +32,20 @@ ] }, "scripts": { + "check:env": "node ./scripts/preflight.mjs doctor", "format": "prettier --write .", "lint": "prettier --check . && turbo lint", - "build": "turbo build --filter=!./playgrounds/*", + "build": "node ./scripts/run-task.mjs build", "postbuild": "node ./scripts/pack-packages.mjs", - "dev": "turbo dev --filter=!./playgrounds/*", - "test": "cargo test && vitest run --hideSkippedTests", - "test:integrations": "vitest --root=./integrations", - "test:ui": "pnpm run --filter=tailwindcss test:ui && pnpm run --filter=@tailwindcss/browser test:ui", + "dev": "node ./scripts/run-task.mjs dev", + "test": "node ./scripts/run-task.mjs test", + "test:integrations": "node ./scripts/run-task.mjs test:integrations", + "test:ui": "node ./scripts/run-task.mjs test:ui", "tdd": "vitest --hideSkippedTests", "bench": "vitest bench", "version-packages": "node ./scripts/version-packages.mjs", - "vite": "pnpm run --filter=vite-playground dev", - "nextjs": "pnpm run --filter=nextjs-playground dev" + "vite": "node ./scripts/run-task.mjs vite", + "nextjs": "node ./scripts/run-task.mjs nextjs" }, "license": "MIT", "devDependencies": { diff --git a/scripts/preflight.mjs b/scripts/preflight.mjs new file mode 100644 index 000000000000..06a30ae1e33a --- /dev/null +++ b/scripts/preflight.mjs @@ -0,0 +1,185 @@ +import { spawnSync } from 'node:child_process' +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' +import { + cargoBin, + commandExists, + hasDetectedCargoBin, + readInstalledRustTargets, + rustEnv, +} from './rust-env.mjs' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const root = path.resolve(__dirname, '..') +const command = process.argv[2] ?? 'doctor' +const wasmTarget = 'wasm32-wasip1-threads' + +const contexts = { + doctor: { + label: 'pnpm run check:env', + needsRust: true, + needsWasmTarget: true, + }, + build: { + label: 'pnpm build', + needsRust: true, + needsWasmTarget: true, + }, + dev: { + label: 'pnpm dev', + needsRust: true, + needsWasmTarget: true, + }, + test: { + label: 'pnpm test', + needsRust: true, + needsWasmTarget: false, + }, + 'test:integrations': { + label: 'pnpm test:integrations', + needsRust: true, + needsWasmTarget: true, + needsBuildArtifacts: true, + }, + 'test:ui': { + label: 'pnpm test:ui', + needsRust: true, + needsWasmTarget: true, + needsBuildArtifacts: true, + }, + vite: { + label: 'pnpm vite', + needsRust: true, + needsWasmTarget: true, + needsBuildArtifacts: true, + needsBun: true, + }, + nextjs: { + label: 'pnpm nextjs', + needsRust: true, + needsWasmTarget: true, + needsBuildArtifacts: true, + }, +} + +const context = contexts[command] ?? contexts.doctor + +const requiredArtifacts = [ + 'crates/node/index.js', + 'packages/tailwindcss/dist/lib.js', +] + +function run(command, args) { + return spawnSync(command, args, { + cwd: root, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + env: rustEnv, + }) +} + +function formatList(items) { + return items.map((item) => `- ${item}`).join('\n') +} + +function relativeArtifact(artifact) { + return artifact.replace(/\\/g, '/') +} + +let errors = [] +let warnings = [] + +if (context.needsRust) { + let missingRustTools = ['cargo', 'rustc'].filter((tool) => !commandExists(tool)) + + if (missingRustTools.length > 0) { + errors.push({ + title: 'Missing required Rust tools', + body: [ + formatList(missingRustTools), + 'How to fix:', + '1. Install Rustup from https://rustup.rs/', + hasDetectedCargoBin() + ? `2. Rust appears to be installed in the detected Cargo bin directory (${cargoBin}), so rerun the command through the repo scripts such as \`pnpm build\` or \`pnpm run check:env\`.` + : '2. If you installed Rustup to a custom location, make sure `CARGO_HOME`, `RUSTUP_HOME`, or your PATH points at the installation.', + '3. Run `rustup default stable`.', + `4. Run \`rustup target add ${wasmTarget}\`.`, + ].join('\n'), + }) + } + + if (!commandExists('rustup')) { + warnings.push( + 'rustup was not found, so the script could not verify the installed WASM targets.', + ) + } else if (context.needsWasmTarget) { + let installedTargets = readInstalledRustTargets() + + if (installedTargets === null) { + warnings.push('Could not read the installed Rust targets from rustup.') + } else if (!installedTargets.has(wasmTarget)) { + errors.push({ + title: 'Missing required Rust target', + body: [`- ${wasmTarget}`, `Run \`rustup target add ${wasmTarget}\`.`].join('\n'), + }) + } + } +} + +if (context.needsBun && !commandExists('bun')) { + errors.push({ + title: 'Missing required tool for the Vite playground', + body: [ + '- bun', + 'The Vite playground uses `bun --bun vite`.', + 'Install Bun, or validate Vite changes with `pnpm build && pnpm test:integrations -- --run integrations/vite`.', + ].join('\n'), + }) +} + +if (context.needsBuildArtifacts) { + let missingArtifacts = requiredArtifacts.filter((artifact) => { + return !fs.existsSync(path.join(root, artifact)) + }) + + if (missingArtifacts.length > 0) { + errors.push({ + title: 'Missing build artifacts required by this command', + body: [ + formatList(missingArtifacts.map(relativeArtifact)), + 'Run `pnpm build` first, then retry this command.', + ].join('\n'), + }) + } +} + +if (errors.length > 0) { + console.error(`Environment preflight failed for ${context.label}.`) + console.error('') + + for (let error of errors) { + console.error(`${error.title}:`) + console.error(error.body) + console.error('') + } + + if (warnings.length > 0) { + console.error('Warnings:') + console.error(formatList(warnings)) + console.error('') + } + + process.exit(1) +} + +if (command === 'doctor') { + console.log(`Environment preflight passed for ${context.label}.`) + + if (warnings.length > 0) { + console.log('') + console.log('Warnings:') + console.log(formatList(warnings)) + } +} \ No newline at end of file diff --git a/scripts/run-task.mjs b/scripts/run-task.mjs new file mode 100644 index 000000000000..ccce677c40f7 --- /dev/null +++ b/scripts/run-task.mjs @@ -0,0 +1,72 @@ +import { spawnSync } from 'node:child_process' +import path from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' +import { isWindows, rustEnv } from './rust-env.mjs' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const root = path.resolve(__dirname, '..') +const cwd = process.cwd() + +const task = process.argv[2] +const passthrough = process.argv.slice(3) + +const predefinedCommands = { + build: [['turbo', ['build', '--filter=!./playgrounds/*']]], + dev: [['turbo', ['dev', '--filter=!./playgrounds/*']]], + test: [ + ['cargo', ['test']], + ['vitest', ['run', '--hideSkippedTests']], + ], + 'test:integrations': [['vitest', ['--root=./integrations']]], + 'test:ui': [ + ['pnpm', ['run', '--filter=tailwindcss', 'test:ui']], + ['pnpm', ['run', '--filter=@tailwindcss/browser', 'test:ui']], + ], + vite: [['pnpm', ['run', '--filter=vite-playground', 'dev']]], + nextjs: [['pnpm', ['run', '--filter=nextjs-playground', 'dev']]], +} + +if (!task || !predefinedCommands[task]) { + console.error(`Unknown task: ${task ?? '(missing)'}`) + process.exit(1) +} + +let commands = predefinedCommands[task] + +if (passthrough[0] === '--') { + if (!passthrough[1]) { + console.error('Missing command after `--`.') + process.exit(1) + } + + commands = [[passthrough[1], passthrough.slice(2)]] +} else if (commands && passthrough.length > 0) { + commands = commands.map((entry, index) => { + if (index !== commands.length - 1) return entry + return [entry[0], [...entry[1], ...passthrough]] + }) +} + +let preflight = spawnSync(process.execPath, [path.join(root, 'scripts', 'preflight.mjs'), task], { + cwd, + env: rustEnv, + stdio: 'inherit', +}) + +if (preflight.status !== 0) { + process.exit(preflight.status ?? 1) +} + +for (let [command, args] of commands) { + let result = spawnSync(command, args, { + cwd, + env: rustEnv, + stdio: 'inherit', + shell: isWindows, + }) + + if (result.status !== 0) { + process.exit(result.status ?? 1) + } +} \ No newline at end of file diff --git a/scripts/rust-env.mjs b/scripts/rust-env.mjs new file mode 100644 index 000000000000..fcb426af4544 --- /dev/null +++ b/scripts/rust-env.mjs @@ -0,0 +1,54 @@ +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import { spawnSync } from 'node:child_process' + +export const isWindows = process.platform === 'win32' + +const home = process.env.USERPROFILE ?? process.env.HOME ?? process.env.HOMEPATH ?? '' + +export const cargoHome = process.env.CARGO_HOME ?? (home ? path.join(home, '.cargo') : '') +export const rustupHome = process.env.RUSTUP_HOME ?? (home ? path.join(home, '.rustup') : '') +export const cargoBin = cargoHome ? path.join(cargoHome, 'bin') : '' + +export const mergedPath = cargoBin + ? [cargoBin, process.env.PATH ?? ''].filter(Boolean).join(path.delimiter) + : process.env.PATH ?? '' + +export const rustEnv = { + ...process.env, + ...(cargoHome ? { CARGO_HOME: cargoHome } : {}), + ...(rustupHome ? { RUSTUP_HOME: rustupHome } : {}), + PATH: mergedPath, +} + +export function commandExists(command, { cwd = process.cwd() } = {}) { + let result = spawnSync(command, ['--version'], { + cwd, + env: rustEnv, + stdio: 'ignore', + shell: isWindows, + }) + + return !result.error && result.status === 0 +} + +export function readInstalledRustTargets({ cwd = process.cwd() } = {}) { + let result = spawnSync('rustup', ['target', 'list', '--installed'], { + cwd, + env: rustEnv, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + shell: isWindows, + }) + + if (result.error || result.status !== 0) { + return null + } + + return new Set(result.stdout.split(/\r?\n/).filter(Boolean)) +} + +export function hasDetectedCargoBin() { + return Boolean(cargoBin) && fs.existsSync(cargoBin) +} \ No newline at end of file