testPackIntoPackage

tcmts.cli.test.e2e.pack.testPackIntoPackage · #aomxeg5a · term

Pack definitions into a package: bins, entry points, types, docs, dependencies at their locked versions.

import { installedPackage, tempDir } from "#codebase/tcmts/cli/test/e2e/harness";
import { setUp } from "#codebase/tcmts/cli/test/e2e/pack";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";

export const testPackIntoPackage = () => {
  const { dir, spawn, tcm } = setUp();
  const out = tcm(
    "pack",
    "greet",
    "ui=ui",
    "--name",
    "greeter@1.2.0",
    "--bin",
    "greeter=greet.main",
    "--files",
    "README.md",
  );
  assert.match(
    out,
    /packed greeter@1\.2\.0 into \.tcm\/pack\/greeter: \d+ module\(s\), depends on @types\/react, escape-string-regexp, react/,
  );
  const pkgDir = path.join(dir, ".tcm", "pack", "greeter");
  const pkg = JSON.parse(fs.readFileSync(path.join(pkgDir, "package.json"), "utf8"));
  const locked = (name: string) =>
    /\d+\.\d+\.\d+/.exec(
      tcm("packages")
        .split("\n")
        .find((l) => l.trimStart().startsWith(`${name} `))!,
    )![0];
  assert.deepEqual(pkg.dependencies, {
    "@types/react": locked("@types/react"),
    "escape-string-regexp": locked("escape-string-regexp"),
    react: locked("react"),
  });
  assert.deepEqual(pkg.bin, { greeter: "bin/greeter.js" });
  assert.deepEqual(Object.keys(pkg.exports), [".", "./ui"]);
  assert.equal(fs.readFileSync(path.join(pkgDir, "README.md"), "utf8"), "# greeter\n");
  // packages by name, not the cache's aliases; documentation in the declarations
  const modules = fs
    .readdirSync(path.join(pkgDir, "modules"))
    .map((f) => fs.readFileSync(path.join(pkgDir, "modules", f), "utf8"))
    .join("\n");
  assert.match(modules, /from "escape-string-regexp"/);
  assert.match(modules, /from "react\/jsx-runtime"/);
  assert.match(modules, /import\("react"\)\.JSX\.Element/);
  assert.doesNotMatch(modules, /npm__|\.tsx?"/);
  assert.match(
    modules,
    /\/\*\*\n \* A greeting, and a pattern that finds it\.\n \*\/\ndeclare function greet/,
  );

  // installed from its tarball, like any package
  const user = tempDir("pack user");
  const packed = spawn("npm", ["pack", "--silent", "--pack-destination", user], pkgDir);
  assert.equal(packed.status, 0, packed.out);
  fs.writeFileSync(
    path.join(user, "package.json"),
    JSON.stringify({ name: "user", private: true, type: "module" }),
  );
  const installed = spawn(
    "npm",
    ["install", "./greeter-1.2.0.tgz", "--no-audit", "--no-fund", "--loglevel=error"],
    user,
  );
  assert.equal(installed.status, 0, installed.out);

  // the bin runs the definition as tcm run does; a number it returns is the exit code
  const greeter = path.join(user, "node_modules", ".bin", "greeter");
  assert.deepEqual(spawn(greeter, ["a.b"], user), { status: 0, out: "hello, a.b hello, a\\.b\n" });
  assert.equal(spawn(greeter, [], user).status, 2);

  // the definitions by name, typed
  fs.writeFileSync(
    path.join(user, "use.ts"),
    `import { greet, finder, type Greeting } from "greeter";\nimport { Badge } from "greeter/ui";\nconst g: Greeting = greet("x");\nconsole.log(finder(g).test("hello, x"), typeof Badge);\n`,
  );
  assert.deepEqual(spawn(process.execPath, ["use.ts"], user), {
    status: 0,
    out: "true function\n",
  });
  fs.writeFileSync(path.join(user, "bad.ts"), `import { greet } from "greeter";\ngreet(1);\n`);
  // checked the way a user's project would check it: tsc
  const compilerOptions = {
    module: "nodenext",
    strict: true,
    noEmit: true,
    allowImportingTsExtensions: true,
    skipLibCheck: true,
    types: [],
  };
  fs.writeFileSync(
    path.join(user, "tsconfig.json"),
    JSON.stringify({ compilerOptions, files: ["use.ts", "bad.ts"] }),
  );
  const tsc = path.join(installedPackage("typescript"), "bin", "tsc");
  const errors = spawn(process.execPath, [tsc, "-p", ".", "--pretty", "false"], user)
    .out.trim()
    .split("\n");
  assert.deepEqual(errors, [
    "bad.ts(2,7): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.",
  ]);
}

uses