#!/usr/bin/env node
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.install = install;
const node_child_process_1 = require("node:child_process");
const node_fs_1 = require("node:fs");
const node_module_1 = require("node:module");
const node_path_1 = require("node:path");
const yaml_1 = require("yaml");
const common_1 = require("./common");
const PACKAGE_NAME = '@zyno-io/ts-server-foundation';
const INSTALL_COMMAND = 'tsf-install';
const PACKAGE_MANAGER_RERUN_ENV = 'TSF_INSTALL_PACKAGE_MANAGER_RERUN';
const PACKAGE_TYPE_COMPILER_PLUGIN = '@zyno-io/ts-server-foundation/type-compiler';
const TSCONFIG_FILE_PATTERN = /^tsconfig(?:\..*)?\.json$/;
const SKIPPED_TSCONFIG_DIRS = new Set(['.git', '.hg', '.svn', '.yarn', 'node_modules', 'dist', 'build', 'coverage']);
function install(options = {}) {
    const projectDir = options.projectDir ?? findInstallProjectRoot();
    const packageJsonPath = (0, node_path_1.join)(projectDir, 'package.json');
    const pkg = readPackageJson(packageJsonPath);
    const workspaceRoot = findWorkspaceRoot(projectDir) ?? projectDir;
    const workspacePkg = (0, node_path_1.resolve)(workspaceRoot) === (0, node_path_1.resolve)(projectDir) ? pkg : readPackageJson((0, node_path_1.join)(workspaceRoot, 'package.json'));
    const packageManager = detectPackageManager(workspaceRoot, workspacePkg);
    const baselineVersions = readBaselineVersions();
    const yarnConfigChanged = packageManager.manager === 'yarn' && ensureYarnPnpCompatibility(workspaceRoot, baselineVersions.opentelemetryCore);
    const postinstallChanged = ensurePostinstallScript(pkg);
    const changedPackages = new Map();
    let compilerSetupChanged = false;
    let tsconfigChanged = false;
    const compilerWorkspaces = findCompilerWorkspaces(workspaceRoot, workspacePkg, packageJsonPath, pkg);
    if (postinstallChanged)
        changedPackages.set((0, node_path_1.resolve)(packageJsonPath), pkg);
    for (const workspace of compilerWorkspaces) {
        const packageChanged = ensureCompilerSetup(workspace.pkg, baselineVersions, packageManager.manager === 'yarn');
        if (packageChanged) {
            changedPackages.set(workspace.packageJsonPath, workspace.pkg);
            compilerSetupChanged = true;
        }
        tsconfigChanged = ensureTsconfigCompilerPlugins(workspace.dir) || tsconfigChanged;
    }
    for (const [changedPackageJsonPath, changedPkg] of changedPackages)
        writePackageJson(changedPackageJsonPath, changedPkg);
    if (postinstallChanged)
        console.log('tsf-install: updated postinstall script');
    if (compilerSetupChanged)
        console.log('tsf-install: updated TypeScript compiler setup');
    if (yarnConfigChanged)
        console.log("tsf-install: updated Yarn Plug'n'Play compatibility");
    if (tsconfigChanged)
        console.log('tsf-install: updated tsconfig compiler plugin');
    if ((compilerSetupChanged || yarnConfigChanged) && options.runPackageManager !== false && process.env[PACKAGE_MANAGER_RERUN_ENV] !== '1') {
        const status = runPackageManagerInstall(packageManager.installDir, packageManager.manager);
        if (status !== 0)
            return status;
    }
    return options.runPackageManager === false ? 0 : prepareCompilerWorkspaces(compilerWorkspaces);
}
function ensureCompilerSetup(pkg, versions, yarn) {
    let changed = false;
    changed = ensureDevDependency(pkg, 'ttsc', versions.ttsc) || changed;
    changed = ensureDevDependency(pkg, 'typescript', versions.typescript) || changed;
    if (yarn)
        changed = ensureYarnCompilerPackagesUnplugged(pkg) || changed;
    changed =
        (pkg.tsf?.compiler === true && !hasFoundationPackageDependency(pkg)
            ? ensureDevDependency(pkg, PACKAGE_NAME, versions.framework)
            : setDependencyVersionIfPresent(pkg, PACKAGE_NAME, versions.framework)) || changed;
    return changed;
}
function ensureYarnCompilerPackagesUnplugged(pkg) {
    let changed = false;
    pkg.dependenciesMeta ??= {};
    for (const name of [
        'ttsc',
        'protoc',
        'ts-proto',
        '@ttsc/darwin-arm64',
        '@ttsc/darwin-x64',
        '@ttsc/linux-arm',
        '@ttsc/linux-arm64',
        '@ttsc/linux-x64',
        '@ttsc/win32-arm64',
        '@ttsc/win32-x64'
    ]) {
        const metadata = (pkg.dependenciesMeta[name] ??= {});
        if (metadata.unplugged !== true) {
            metadata.unplugged = true;
            changed = true;
        }
    }
    return changed;
}
function findInstallProjectRoot() {
    const packageJsonPath = process.env.npm_package_json;
    if (packageJsonPath && (0, node_path_1.basename)(packageJsonPath) === 'package.json' && (0, node_fs_1.existsSync)(packageJsonPath)) {
        return (0, node_path_1.dirname)((0, node_path_1.resolve)(packageJsonPath));
    }
    return (0, common_1.findProjectRoot)();
}
function readPackageJson(path) {
    return JSON.parse((0, node_fs_1.readFileSync)(path, 'utf8'));
}
function writePackageJson(path, pkg) {
    (0, node_fs_1.writeFileSync)(path, `${JSON.stringify(pkg, null, 4)}\n`);
}
function readBaselineVersions() {
    const packageRoot = (0, common_1.findPackageRoot)();
    const frameworkVersion = readPackageJson((0, node_path_1.join)(packageRoot, 'package.json')).version;
    if (!frameworkVersion)
        throw new Error(`Could not determine ${PACKAGE_NAME} version`);
    return {
        framework: frameworkVersion,
        opentelemetryCore: (0, common_1.readPackageDependencyVersion)(packageRoot, '@opentelemetry/core'),
        ttsc: (0, common_1.readPackageDependencyVersion)(packageRoot, 'ttsc'),
        typescript: (0, common_1.readPackageDependencyVersion)(packageRoot, 'typescript')
    };
}
function ensureYarnPnpCompatibility(projectRoot, opentelemetryCoreVersion) {
    const configPath = (0, node_path_1.join)(projectRoot, '.yarnrc.yml');
    const document = (0, yaml_1.parseDocument)((0, node_fs_1.existsSync)(configPath) ? (0, node_fs_1.readFileSync)(configPath, 'utf8') : '');
    if (document.errors.length > 0)
        throw document.errors[0];
    const nodeLinker = document.get('nodeLinker');
    if (nodeLinker !== undefined && nodeLinker !== 'pnp')
        return false;
    const dependencyPath = ['packageExtensions', '@sentry/node@*', 'dependencies', '@opentelemetry/core'];
    if (document.getIn(dependencyPath) === opentelemetryCoreVersion)
        return false;
    document.setIn(dependencyPath, opentelemetryCoreVersion);
    (0, node_fs_1.writeFileSync)(configPath, String(document));
    return true;
}
function ensurePostinstallScript(pkg) {
    pkg.scripts ??= {};
    const existing = pkg.scripts.postinstall?.trim();
    if (!existing) {
        pkg.scripts.postinstall = INSTALL_COMMAND;
        return true;
    }
    if (hasInstallCommand(existing))
        return false;
    pkg.scripts.postinstall = `${existing} && ${INSTALL_COMMAND}`;
    return true;
}
function hasInstallCommand(script) {
    return script
        .split(/\s*(?:&&|\|\||;)\s*/)
        .some(command => /^(?:(?:npx|yarn|bunx)\s+|pnpm\s+(?:exec\s+)?|npm\s+exec\s+)?tsf-install(?:\s|$)/.test(command));
}
function ensureDevDependency(pkg, name, version) {
    let changed = false;
    if (pkg.dependencies?.[name] !== undefined) {
        delete pkg.dependencies[name];
        changed = true;
    }
    pkg.devDependencies ??= {};
    if (pkg.devDependencies[name] !== version) {
        pkg.devDependencies[name] = version;
        changed = true;
    }
    return changed;
}
function findCompilerWorkspaces(workspaceRoot, workspaceRootPkg, projectPackageJsonPath, projectPkg) {
    const resolvedProjectPackageJsonPath = (0, node_path_1.resolve)(projectPackageJsonPath);
    const rootPackageJsonPath = (0, node_path_1.resolve)(workspaceRoot, 'package.json');
    const packages = new Map();
    packages.set(rootPackageJsonPath, {
        dir: (0, node_path_1.resolve)(workspaceRoot),
        packageJsonPath: rootPackageJsonPath,
        pkg: rootPackageJsonPath === resolvedProjectPackageJsonPath ? projectPkg : workspaceRootPkg
    });
    const workspaces = getWorkspacePatterns(workspaceRootPkg);
    if (!workspaces?.length)
        return projectPkg.tsf?.compiler === false ? [] : [...packages.values()];
    const excludedPaths = new Set(workspaces.filter(pattern => pattern.startsWith('!')).flatMap(pattern => globWorkspacePackageJsonPaths(workspaceRoot, pattern.slice(1))));
    for (const packageJsonPath of workspaces
        .filter(pattern => !pattern.startsWith('!'))
        .flatMap(pattern => globWorkspacePackageJsonPaths(workspaceRoot, pattern))
        .sort()) {
        if (excludedPaths.has(packageJsonPath))
            continue;
        packages.set(packageJsonPath, {
            dir: (0, node_path_1.dirname)(packageJsonPath),
            packageJsonPath,
            pkg: packageJsonPath === resolvedProjectPackageJsonPath ? projectPkg : readPackageJson(packageJsonPath)
        });
    }
    return [...packages.values()].filter(workspace => usesTsfCompiler(workspace.pkg));
}
function findWorkspaceRoot(projectDir) {
    let dir = (0, node_path_1.resolve)(projectDir);
    while (true) {
        const pkg = readPackageJsonIfExists((0, node_path_1.join)(dir, 'package.json'));
        if (pkg && getWorkspacePatterns(pkg)?.length)
            return dir;
        const parent = (0, node_path_1.dirname)(dir);
        if (parent === dir)
            return undefined;
        dir = parent;
    }
}
function getWorkspacePatterns(pkg) {
    return Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
}
function globWorkspacePackageJsonPaths(workspaceRoot, workspacePattern) {
    const pattern = workspacePattern.replace(/\/$/, '');
    return (0, node_fs_1.globSync)(`${pattern}/package.json`, {
        cwd: workspaceRoot,
        exclude: ['**/node_modules/**']
    }).map(path => (0, node_path_1.resolve)(workspaceRoot, path));
}
function setDependencyVersionIfPresent(pkg, name, version) {
    let changed = false;
    for (const dependencies of [pkg.dependencies, pkg.devDependencies, pkg.optionalDependencies, pkg.peerDependencies]) {
        if (dependencies?.[name] === undefined || dependencies[name] === version)
            continue;
        dependencies[name] = version;
        changed = true;
    }
    return changed;
}
function ensureTsconfigCompilerPlugins(projectDir) {
    const patchedTsconfigs = new Set();
    let changed = false;
    for (const tsconfigPath of findTsconfigPaths(projectDir)) {
        const tsconfig = readTsconfigJson(tsconfigPath);
        if (extendsPatchedTsconfig(tsconfigPath, tsconfig, patchedTsconfigs))
            continue;
        const result = ensureTsconfigCompilerPlugin(tsconfigPath, tsconfig);
        if (result.hasCompilerSetup)
            patchedTsconfigs.add(tsconfigPath);
        changed = result.changed || changed;
    }
    return changed;
}
function ensureTsconfigCompilerPlugin(tsconfigPath, tsconfig) {
    let changed = false;
    tsconfig.compilerOptions ??= {};
    const plugins = Array.isArray(tsconfig.compilerOptions.plugins) ? tsconfig.compilerOptions.plugins : [];
    if (plugins !== tsconfig.compilerOptions.plugins) {
        tsconfig.compilerOptions.plugins = plugins;
        changed = true;
    }
    const existingPlugin = plugins.find(isTypeCompilerPlugin);
    if (!existingPlugin) {
        plugins.push({ transform: PACKAGE_TYPE_COMPILER_PLUGIN });
        changed = true;
    }
    else if (existingPlugin.transform !== PACKAGE_TYPE_COMPILER_PLUGIN) {
        existingPlugin.transform = PACKAGE_TYPE_COMPILER_PLUGIN;
        changed = true;
    }
    if (tsconfig.reflection !== true) {
        tsconfig.reflection = true;
        changed = true;
    }
    if (changed)
        (0, node_fs_1.writeFileSync)(tsconfigPath, `${JSON.stringify(tsconfig, null, 4)}\n`);
    return { changed, hasCompilerSetup: hasTsconfigCompilerSetup(tsconfig) };
}
function findTsconfigPaths(projectDir) {
    const root = (0, node_path_1.resolve)(projectDir);
    const paths = [];
    const visit = (dir) => {
        if (dir !== root && (0, node_fs_1.existsSync)((0, node_path_1.join)(dir, 'package.json')))
            return;
        for (const entry of (0, node_fs_1.readdirSync)(dir, { withFileTypes: true })) {
            if (entry.isDirectory()) {
                if (!SKIPPED_TSCONFIG_DIRS.has(entry.name))
                    visit((0, node_path_1.join)(dir, entry.name));
                continue;
            }
            if (entry.isFile() && TSCONFIG_FILE_PATTERN.test(entry.name))
                paths.push((0, node_path_1.resolve)(dir, entry.name));
        }
    };
    visit(root);
    return paths.sort(compareTsconfigPaths);
}
function absolutePathDepth(path) {
    return (0, node_path_1.resolve)(path)
        .split(/[\\/]+/)
        .filter(Boolean).length;
}
function compareTsconfigPaths(a, b) {
    const depthDiff = absolutePathDepth(a) - absolutePathDepth(b);
    if (depthDiff)
        return depthDiff;
    const basePriorityDiff = tsconfigBasePriority(a) - tsconfigBasePriority(b);
    if (basePriorityDiff)
        return basePriorityDiff;
    return a.localeCompare(b);
}
function tsconfigBasePriority(path) {
    return (0, node_path_1.basename)(path) === 'tsconfig.json' ? 0 : 1;
}
function readTsconfigJson(tsconfigPath) {
    return parseJsonC((0, node_fs_1.readFileSync)(tsconfigPath, 'utf8'));
}
function hasTsconfigCompilerSetup(tsconfig) {
    return (tsconfig.reflection === true &&
        Array.isArray(tsconfig.compilerOptions?.plugins) &&
        tsconfig.compilerOptions.plugins.some(isTypeCompilerPlugin));
}
function extendsPatchedTsconfig(tsconfigPath, tsconfig, patchedTsconfigs, seen = new Set()) {
    for (const extendedPath of resolveTsconfigExtends(tsconfigPath, tsconfig.extends)) {
        if (seen.has(extendedPath))
            continue;
        seen.add(extendedPath);
        if (patchedTsconfigs.has(extendedPath))
            return true;
        if (!(0, node_fs_1.existsSync)(extendedPath))
            continue;
        const extendedTsconfig = readTsconfigJson(extendedPath);
        if (hasTsconfigCompilerSetup(extendedTsconfig))
            return true;
        if (extendsPatchedTsconfig(extendedPath, extendedTsconfig, patchedTsconfigs, seen))
            return true;
    }
    return false;
}
function resolveTsconfigExtends(tsconfigPath, extended) {
    const values = typeof extended === 'string' ? [extended] : Array.isArray(extended) ? extended.filter(value => typeof value === 'string') : [];
    return values.map(value => resolveTsconfigExtendsPath(tsconfigPath, value)).filter((value) => value !== undefined);
}
function resolveTsconfigExtendsPath(tsconfigPath, extended) {
    if (!(0, node_path_1.isAbsolute)(extended) && !extended.startsWith('.'))
        return undefined;
    return resolveTsconfigPathCandidate((0, node_path_1.isAbsolute)(extended) ? extended : (0, node_path_1.resolve)((0, node_path_1.dirname)(tsconfigPath), extended));
}
function resolveTsconfigPathCandidate(path) {
    const candidates = path.endsWith('.json') ? [path] : [path, `${path}.json`, (0, node_path_1.join)(path, 'tsconfig.json')];
    for (const candidate of candidates) {
        if ((0, node_fs_1.existsSync)(candidate) && (0, node_fs_1.statSync)(candidate).isFile())
            return (0, node_path_1.resolve)(candidate);
    }
    return (0, node_path_1.resolve)(candidates[0]);
}
function isTypeCompilerPlugin(plugin) {
    if (!plugin || typeof plugin !== 'object')
        return false;
    const transform = plugin.transform;
    if (typeof transform !== 'string')
        return false;
    const normalized = transform.replace(/\\/g, '/');
    return normalized.endsWith('/dist/src/type-compiler/index.cjs') || normalized === PACKAGE_TYPE_COMPILER_PLUGIN;
}
function hasFoundationPackageDependency(pkg) {
    return (pkg.dependencies?.[PACKAGE_NAME] !== undefined ||
        pkg.devDependencies?.[PACKAGE_NAME] !== undefined ||
        pkg.peerDependencies?.[PACKAGE_NAME] !== undefined);
}
function usesTsfCompiler(pkg) {
    return pkg.tsf?.compiler ?? hasFoundationPackageDependency(pkg);
}
function parseJsonC(contents) {
    let output = '';
    let inString = false;
    let stringQuote = '';
    let escaped = false;
    for (let index = 0; index < contents.length; index++) {
        const char = contents[index];
        const next = contents[index + 1];
        if (inString) {
            output += char;
            if (escaped) {
                escaped = false;
            }
            else if (char === '\\') {
                escaped = true;
            }
            else if (char === stringQuote) {
                inString = false;
                stringQuote = '';
            }
            continue;
        }
        if (char === '"' || char === "'") {
            inString = true;
            stringQuote = char;
            output += char;
            continue;
        }
        if (char === '/' && next === '/') {
            while (index < contents.length && contents[index] !== '\n')
                index++;
            output += '\n';
            continue;
        }
        if (char === '/' && next === '*') {
            index += 2;
            while (index < contents.length && !(contents[index] === '*' && contents[index + 1] === '/'))
                index++;
            index++;
            continue;
        }
        output += char;
    }
    return JSON.parse(output);
}
function detectPackageManager(projectDir, pkg) {
    let dir = projectDir;
    while (true) {
        const lockfile = detectLockfilePackageManager(dir);
        if (lockfile)
            return { installDir: dir, manager: lockfile };
        const packageJson = dir === projectDir ? pkg : readPackageJsonIfExists((0, node_path_1.join)(dir, 'package.json'));
        if (packageJson) {
            const declared = getDeclaredPackageManager(packageJson);
            if (declared)
                return { installDir: dir, manager: declared };
        }
        const parent = (0, node_path_1.dirname)(dir);
        if (parent === dir)
            return { installDir: projectDir, manager: 'npm' };
        dir = parent;
    }
}
function readPackageJsonIfExists(path) {
    if (!(0, node_fs_1.existsSync)(path))
        return undefined;
    return readPackageJson(path);
}
function getDeclaredPackageManager(pkg) {
    const declared = pkg.packageManager?.split('@')[0];
    if (declared === 'yarn' || declared === 'npm' || declared === 'pnpm' || declared === 'bun')
        return declared;
}
function detectLockfilePackageManager(dir) {
    if ((0, node_fs_1.existsSync)((0, node_path_1.join)(dir, 'yarn.lock')))
        return 'yarn';
    if ((0, node_fs_1.existsSync)((0, node_path_1.join)(dir, 'package-lock.json')) || (0, node_fs_1.existsSync)((0, node_path_1.join)(dir, 'npm-shrinkwrap.json')))
        return 'npm';
    if ((0, node_fs_1.existsSync)((0, node_path_1.join)(dir, 'pnpm-lock.yaml')))
        return 'pnpm';
    if ((0, node_fs_1.existsSync)((0, node_path_1.join)(dir, 'bun.lockb')) || (0, node_fs_1.existsSync)((0, node_path_1.join)(dir, 'bun.lock')))
        return 'bun';
    if ((0, node_fs_1.existsSync)((0, node_path_1.join)(dir, '.yarnrc.yml')))
        return 'yarn';
}
function runPackageManagerInstall(projectDir, packageManager) {
    console.log(`tsf-install: running ${packageManager} install`);
    const result = (0, node_child_process_1.spawnSync)(packageManager, ['install'], {
        cwd: projectDir,
        stdio: 'inherit',
        env: {
            ...process.env,
            [PACKAGE_MANAGER_RERUN_ENV]: '1'
        }
    });
    if (result.error) {
        console.error(result.error.message);
        return 1;
    }
    return result.status ?? 1;
}
function prepareCompilerWorkspaces(workspaces) {
    for (const workspace of workspaces) {
        const tsconfig = findTsconfigPaths(workspace.dir).find(path => hasTsconfigCompilerSetup(readTsconfigJson(path)));
        if (!tsconfig)
            continue;
        const projectRequire = (0, node_module_1.createRequire)((0, node_path_1.join)(workspace.dir, 'package.json'));
        let ttscEntry;
        try {
            ttscEntry = projectRequire.resolve('ttsc');
        }
        catch (error) {
            if (error.code === 'MODULE_NOT_FOUND')
                continue;
            console.error(`tsf-install: could not resolve ttsc for ${workspace.dir}: ${error instanceof Error ? error.message : String(error)}`);
            return 1;
        }
        let TtscCompiler;
        try {
            ({ TtscCompiler } = projectRequire(ttscEntry));
        }
        catch (error) {
            console.error(`tsf-install: could not load ttsc for ${workspace.dir}: ${error instanceof Error ? error.message : String(error)}`);
            return 1;
        }
        try {
            console.log(`tsf-install: preparing type compiler for ${workspace.dir}`);
            new TtscCompiler({ cwd: workspace.dir, tsconfig }).prepare();
        }
        catch (error) {
            console.error(`tsf-install: failed to prepare type compiler for ${workspace.dir}: ${error instanceof Error ? error.message : String(error)}`);
            return 1;
        }
    }
    return 0;
}
function main(args = process.argv.slice(2)) {
    if (args.includes('--help') || args.includes('-h')) {
        console.log('Usage: tsf-install [--no-install]');
        console.log();
        console.log('Ensures a project has the baseline compiler setup required by ts-server-foundation.');
        return 0;
    }
    const runPackageManager = !takeFlag(args, '--no-install');
    if (args.length) {
        console.error(`Unknown option: ${args[0]}`);
        return 1;
    }
    return install({ runPackageManager });
}
function takeFlag(args, flag) {
    const index = args.indexOf(flag);
    if (index === -1)
        return false;
    args.splice(index, 1);
    return true;
}
if (require.main === module) {
    process.exit(main());
}
//# sourceMappingURL=tsf-install.js.map