// Copyright 2024-2024, Pulumi 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. import { randomInt } from "crypto"; import execa from "execa"; import * as fs from "fs/promises"; import * as path from "path"; import * as process from "process"; import * as tmp from "tmp"; import { pack } from "./pack"; // Write a package.json that installs the local pulumi package and optional dependencies. async function writePackageJSON( dir: string, pulumiPackagePath: string, dependencies: Record<string, string | undefined>, ) { const packageJSON = { name: "install-package-tests", version: "1.0.0", license: "Apache-2.0", dependencies: { "@pulumi/pulumi": pulumiPackagePath, ...dependencies, }, }; await fs.writeFile(path.join(dir, "package.json"), JSON.stringify(packageJSON, undefined, 4)); } async function writeTSConfig(dir: string) { const tsconfigJSON = { compilerOptions: { strict: true, target: "es2016", module: "commonjs", moduleResolution: "node", declaration: true, resolveJsonModule: true, sourceMap: true, stripInternal: true, experimentalDecorators: true, pretty: true, noFallthroughCasesInSwitch: true, noImplicitReturns: true, forceConsistentCasingInFileNames: true, esModuleInterop: true, }, include: ["index.ts"], }; await fs.writeFile(path.join(dir, "tsconfig.json"), JSON.stringify(tsconfigJSON, undefined, 4)); } // A simple TypeScript Pulumi program to test that we can load and run TypeScript code. async function writeProgram(dir: string, projectName: string) { const indexTS = `import * as pulumi from "@pulumi/pulumi"; pulumi.runtime.serializeFunction(() => 42); export const test: number = 42; `; await fs.writeFile(path.join(dir, "index.ts"), indexTS); const project = `name: ${projectName} runtime: nodejs backend: url: 'file://~' `; await fs.writeFile(path.join(dir, "Pulumi.yaml"), project); } async function exec(command: string, args: string[], options: execa.Options): Promise<string> { const message = `$ ${command} ${args.join(" ")}'\n`; const result = await execa(command, args, options); return message + result.stdout; } async function main() { const sdkRoot = path.join(__dirname, "..", "..", ".."); const sdkRootBin = path.join(sdkRoot, "bin"); const tmpPackageDir = tmp.dirSync({ prefix: "pulumi-package-", unsafeCleanup: true }); try { // Add a random suffix to the package name to avoid any issues with yarn caching the tgz. const packageName = `pulumi-${randomInt(10000, 99999)}.tgz`; const pulumiPackagePath = path.join(tmpPackageDir.name, packageName); await pack(sdkRootBin, pulumiPackagePath); const packageManagers = [ { name: "npm", // This version doesn't install peer dependencies automatically. version: "^6.0.0", }, { name: "npm", version: "*", // Latest version. }, { name: "yarn", // This version doesn't install peer dependencies automatically. version: "^1.0.0", }, // We don't support yarn >= 2 yet. // { // packageManager: "yarn", // version: "*", // Latest version. // }, { name: "pnpm", version: "*", // Latest version. }, ]; // Dependencies to add to package.json. const dependencies = [ { // No explicit typescript or ts-node versions, use the vendored versions. typescript: undefined, "ts-node": undefined, }, { typescript: "~3.8.3", "ts-node": "^7.0.1", }, { typescript: "^4.0.0", "ts-node": undefined, }, { typescript: "^5.0.0", "ts-node": undefined, }, { typescript: "^5.0.0", "ts-node": "^10.0.0", }, ]; for (const pm of packageManagers) { for (const deps of dependencies) { const tmpDir = tmp.dirSync({ prefix: "install-test-", unsafeCleanup: true }); try { await runTest(tmpDir, pulumiPackagePath, pm.name, pm.version, deps); } finally { tmpDir.removeCallback(); } } } } finally { tmpPackageDir.removeCallback(); } } async function runTest( tmpDir: tmp.DirResult, pulumiPackagePath: string, packageManager: string, packageManagerVersion: string, peerDeps: Record<string, string | undefined>, ) { await writePackageJSON(tmpDir.name, pulumiPackagePath, peerDeps); await writeTSConfig(tmpDir.name); const projectName = `install-test-${packageManager}-${packageManagerVersion}`.replace(/[^a-zA-Z0-9]/g, "-"); await writeProgram(tmpDir.name, projectName); const dependencies = Object.entries(peerDeps) .filter(([_, v]) => v !== undefined) .map(([p, v]) => `${p}:${v}`) .join(", "); const dependenciesString = dependencies.length > 0 ? ` with ${dependencies}` : ""; let logs = ""; // Install the package manager to test. logs += await exec(`corepack`, ["enable"], { cwd: tmpDir.name }); logs += await exec(`corepack`, ["use", `${packageManager}@${packageManagerVersion}`], { cwd: tmpDir.name }); const env = { PULUMI_CONFIG_PASSPHRASE: "test", PULUMI_HOME: tmpDir.name, }; // Up and down a test stack to ensure we're able to load & run typescript code. const stackName = `install-test-${randomInt(10000, 99999)}`; try { logs += await exec("pulumi", ["stack", "init", stackName], { cwd: tmpDir.name, env, }); logs += await exec("pulumi", ["up", "--stack", stackName, "--skip-preview"], { cwd: tmpDir.name, env, }); console.log(`✅ ${packageManager}@${packageManagerVersion}${dependenciesString}`); } catch (err) { console.log( `❌ Failed to run test with ${packageManager}@${packageManagerVersion}${dependenciesString} in ${tmpDir.name}: ${err}`, ); console.log(`Captured stdout: ${logs}`); throw err; } finally { await exec("pulumi", ["destroy", "--stack", stackName, "--yes"], { cwd: tmpDir.name, env, }); await exec("pulumi", ["stack", "rm", stackName, "--yes"], { cwd: tmpDir.name, env, }); } } main().catch((error) => { console.error(error); process.exit(1); });