|
| 1 | +// @ts-check |
| 2 | + |
| 3 | +import { cpSync, existsSync, lstatSync, readFileSync } from 'node:fs'; |
| 4 | +import { spawnSync } from 'node:child_process'; |
| 5 | +import { basename, dirname, resolve, sep } from 'node:path'; |
| 6 | +import { fileURLToPath } from 'node:url'; |
| 7 | + |
| 8 | +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); |
| 9 | +const nodeModulesPath = resolve(repoRoot, 'node_modules'); |
| 10 | +const npmExecutable = process.platform === 'win32' ? 'npm.cmd' : 'npm'; |
| 11 | + |
| 12 | +/** @typedef {{ folder: string; reason: string }} Candidate */ |
| 13 | + |
| 14 | +/** @param {string} message */ |
| 15 | +function log(message) { |
| 16 | + console.info(`[setup-fast] ${message}`); |
| 17 | +} |
| 18 | + |
| 19 | +/** @param {string[]} args */ |
| 20 | +function runNpm(args) { |
| 21 | + log(`Running: npm ${args.join(' ')}`); |
| 22 | + const result = spawnSync(npmExecutable, args, { |
| 23 | + cwd: repoRoot, |
| 24 | + stdio: 'inherit' |
| 25 | + }); |
| 26 | + if (result.error) { |
| 27 | + throw result.error; |
| 28 | + } |
| 29 | + if ((result.status ?? 1) !== 0) { |
| 30 | + process.exit(result.status ?? 1); |
| 31 | + } |
| 32 | +} |
| 33 | + |
| 34 | +/** |
| 35 | + * @param {Candidate[]} candidates |
| 36 | + * @param {string | undefined} folder |
| 37 | + * @param {string} reason |
| 38 | + */ |
| 39 | +function addCandidate(candidates, folder, reason) { |
| 40 | + if (!folder) { |
| 41 | + return; |
| 42 | + } |
| 43 | + if (candidates.some(candidate => candidate.folder === folder)) { |
| 44 | + return; |
| 45 | + } |
| 46 | + candidates.push({ folder, reason }); |
| 47 | + log(`Candidate found (${reason}): ${folder}`); |
| 48 | +} |
| 49 | + |
| 50 | +function detectMainSiblingFolder() { |
| 51 | + const currentFolderName = basename(repoRoot); |
| 52 | + if (!currentFolderName.startsWith('xterm.js') || currentFolderName === 'xterm.js') { |
| 53 | + log(`Current folder is "${currentFolderName}", skipping xterm.js sibling lookup.`); |
| 54 | + return undefined; |
| 55 | + } |
| 56 | + const siblingFolder = resolve(dirname(repoRoot), 'xterm.js'); |
| 57 | + log(`Current folder is "${currentFolderName}", sibling main folder candidate: ${siblingFolder}`); |
| 58 | + return siblingFolder; |
| 59 | +} |
| 60 | + |
| 61 | +function detectWorktreeMainFolder() { |
| 62 | + const gitPath = resolve(repoRoot, '.git'); |
| 63 | + if (!existsSync(gitPath)) { |
| 64 | + log('No .git entry found at repo root.'); |
| 65 | + return undefined; |
| 66 | + } |
| 67 | + const gitStat = lstatSync(gitPath); |
| 68 | + if (!gitStat.isFile()) { |
| 69 | + log('.git is not a file, this repo does not appear to be a worktree checkout.'); |
| 70 | + return undefined; |
| 71 | + } |
| 72 | + const gitFileContent = readFileSync(gitPath, 'utf8').trim(); |
| 73 | + const gitDirMatch = /^gitdir:\s*(.+)$/m.exec(gitFileContent); |
| 74 | + if (!gitDirMatch) { |
| 75 | + log('Could not parse gitdir from .git file.'); |
| 76 | + return undefined; |
| 77 | + } |
| 78 | + |
| 79 | + const gitDirPath = resolve(repoRoot, gitDirMatch[1].trim()); |
| 80 | + log(`Parsed gitdir from .git file: ${gitDirPath}`); |
| 81 | + |
| 82 | + const normalizedGitDirPath = gitDirPath.replace(/\\/g, '/'); |
| 83 | + const worktreeMarker = '/.git/worktrees/'; |
| 84 | + const markerIndex = normalizedGitDirPath.indexOf(worktreeMarker); |
| 85 | + if (markerIndex === -1) { |
| 86 | + log('gitdir path does not contain /.git/worktrees/, skipping worktree main folder lookup.'); |
| 87 | + return undefined; |
| 88 | + } |
| 89 | + |
| 90 | + const normalizedMainFolder = normalizedGitDirPath.slice(0, markerIndex); |
| 91 | + const mainFolder = sep === '/' ? normalizedMainFolder : normalizedMainFolder.split('/').join(sep); |
| 92 | + log(`Worktree main folder candidate: ${mainFolder}`); |
| 93 | + return mainFolder; |
| 94 | +} |
| 95 | + |
| 96 | +function resolveSourceFolder() { |
| 97 | + /** @type {Candidate[]} */ |
| 98 | + const candidates = []; |
| 99 | + addCandidate(candidates, detectMainSiblingFolder(), 'xterm.js sibling'); |
| 100 | + addCandidate(candidates, detectWorktreeMainFolder(), 'worktree main repo'); |
| 101 | + |
| 102 | + for (const candidate of candidates) { |
| 103 | + const candidateNodeModulesPath = resolve(candidate.folder, 'node_modules'); |
| 104 | + if (existsSync(candidateNodeModulesPath)) { |
| 105 | + log(`Using candidate (${candidate.reason}) with node_modules: ${candidate.folder}`); |
| 106 | + return candidate.folder; |
| 107 | + } |
| 108 | + log(`Candidate skipped (${candidate.reason}), node_modules missing: ${candidateNodeModulesPath}`); |
| 109 | + } |
| 110 | + |
| 111 | + log('No candidate folder with node_modules was found.'); |
| 112 | + return undefined; |
| 113 | +} |
| 114 | + |
| 115 | +if (!existsSync(nodeModulesPath)) { |
| 116 | + log(`node_modules missing: ${nodeModulesPath}`); |
| 117 | + const sourceFolder = resolveSourceFolder(); |
| 118 | + if (sourceFolder) { |
| 119 | + const sourceNodeModulesPath = resolve(sourceFolder, 'node_modules'); |
| 120 | + log(`Copying node_modules from ${sourceNodeModulesPath} to ${nodeModulesPath}`); |
| 121 | + try { |
| 122 | + cpSync(sourceNodeModulesPath, nodeModulesPath, { recursive: true }); |
| 123 | + log('node_modules copy completed.'); |
| 124 | + } catch (error) { |
| 125 | + const message = error instanceof Error ? error.message : String(error); |
| 126 | + log(`node_modules copy failed: ${message}`); |
| 127 | + log('Falling back to npm ci.'); |
| 128 | + runNpm(['ci']); |
| 129 | + } |
| 130 | + } else { |
| 131 | + log('No source folder available, running npm ci.'); |
| 132 | + runNpm(['ci']); |
| 133 | + } |
| 134 | +} else { |
| 135 | + log(`node_modules already exists: ${nodeModulesPath}`); |
| 136 | +} |
| 137 | + |
| 138 | +runNpm(['run', 'setup']); |
0 commit comments