From ddea1b4f7d40eab64197fa666f59de7030f3117b Mon Sep 17 00:00:00 2001 From: Matt Hamlin Date: Tue, 14 May 2024 22:05:31 -0400 Subject: [PATCH] Setup test infra - jsonc-parser is all working fine it seems --- .../template-monorepo/libs/pkg-a/package.json | 8 + .../template-monorepo/libs/pkg-b/package.json | 8 + .../template-monorepo/libs/pkg-c/package.json | 8 + .../template-monorepo/package.json | 6 + .../__tests__/jsonc-parser.test.mjs | 46 ++++- .../__tests__/one-version.test.mjs | 172 +++++++++++++++++- packages/one-version/biome.jsonc | 1 + packages/one-version/one-version.mjs | 60 +++++- packages/one-version/utils/create-debug.mjs | 9 + packages/one-version/utils/jsonc-parser.mjs | 132 +++++++++++++- 10 files changed, 420 insertions(+), 30 deletions(-) create mode 100644 packages/one-version/__fixtures__/template-monorepo/libs/pkg-a/package.json create mode 100644 packages/one-version/__fixtures__/template-monorepo/libs/pkg-b/package.json create mode 100644 packages/one-version/__fixtures__/template-monorepo/libs/pkg-c/package.json create mode 100644 packages/one-version/__fixtures__/template-monorepo/package.json create mode 100644 packages/one-version/utils/create-debug.mjs diff --git a/packages/one-version/__fixtures__/template-monorepo/libs/pkg-a/package.json b/packages/one-version/__fixtures__/template-monorepo/libs/pkg-a/package.json new file mode 100644 index 0000000..a9facbc --- /dev/null +++ b/packages/one-version/__fixtures__/template-monorepo/libs/pkg-a/package.json @@ -0,0 +1,8 @@ +{ + "name": "pkg-a", + "dependencies": { + "a": "1.0.0", + "b": "2.3.1", + "c": "^3" + } +} diff --git a/packages/one-version/__fixtures__/template-monorepo/libs/pkg-b/package.json b/packages/one-version/__fixtures__/template-monorepo/libs/pkg-b/package.json new file mode 100644 index 0000000..118b05c --- /dev/null +++ b/packages/one-version/__fixtures__/template-monorepo/libs/pkg-b/package.json @@ -0,0 +1,8 @@ +{ + "name": "pkg-b", + "dependencies": { + "a": "1.0.0", + "b": "1.2.3", + "c": "^3" + } +} diff --git a/packages/one-version/__fixtures__/template-monorepo/libs/pkg-c/package.json b/packages/one-version/__fixtures__/template-monorepo/libs/pkg-c/package.json new file mode 100644 index 0000000..460674a --- /dev/null +++ b/packages/one-version/__fixtures__/template-monorepo/libs/pkg-c/package.json @@ -0,0 +1,8 @@ +{ + "name": "pkg-c", + "dependencies": { + "a": "1.0.0", + "b": "2.1.3", + "c": "^3" + } +} diff --git a/packages/one-version/__fixtures__/template-monorepo/package.json b/packages/one-version/__fixtures__/template-monorepo/package.json new file mode 100644 index 0000000..b591ff9 --- /dev/null +++ b/packages/one-version/__fixtures__/template-monorepo/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "workspaces": [ + "libs/*" + ] +} diff --git a/packages/one-version/__tests__/jsonc-parser.test.mjs b/packages/one-version/__tests__/jsonc-parser.test.mjs index e589cb7..43abeec 100644 --- a/packages/one-version/__tests__/jsonc-parser.test.mjs +++ b/packages/one-version/__tests__/jsonc-parser.test.mjs @@ -1,15 +1,43 @@ import assert from "node:assert"; -import { after, before, test } from "node:test"; +import { after, before, describe, test } from "node:test"; import { parse } from "../utils/jsonc-parser.mjs"; -test("parses plain old JSON fine", () => { - let sample = { - foo: { - bar: ["baz", 1, 2, false, true, null], - }, - }; +describe("jsonc-parser", () => { + test("parses plain old JSON fine", () => { + let sample = { + foo: { + bar: ["baz", 1, 2, false, true, null], + }, + }; - let result = parse(JSON.stringify(sample)); + let result = parse(JSON.stringify(sample)); - assert.deepEqual(result, sample); + assert.deepEqual(result, sample); + }); + + test("parses JSON with comments", () => { + let sample = { + foo: { + bar: ["baz", 1, 2, false, true, null], + }, + }; + + let result = parse(`{ +// comment + "foo": { + "bar": [ + // another comment + "baz", + /* block comments too */ + 1, + 2, + false, + true, + null + ] + } +}`); + + assert.deepEqual(result, sample); + }); }); diff --git a/packages/one-version/__tests__/one-version.test.mjs b/packages/one-version/__tests__/one-version.test.mjs index 0c15c4a..5386a33 100644 --- a/packages/one-version/__tests__/one-version.test.mjs +++ b/packages/one-version/__tests__/one-version.test.mjs @@ -1,15 +1,167 @@ import assert from "node:assert"; -import { after, before, test } from "node:test"; +import { exec as execCallback } from "node:child_process"; +import { promises as fsPromises } from "node:fs"; +import path from "node:path"; +import { after, before, describe, test } from "node:test"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; import { start } from "../one-version.mjs"; -test("supports help command", async () => { - let logs = []; - const logger = { - log: (...args) => { - logs.push(args.join(" ")); - }, - }; - await start({ rootDirectory: process.cwd(), logger, args: ["help"] }); +describe("one-version", () => { + test("supports help command", async () => { + let logs = []; + const logger = { + log: (...args) => { + logs.push(args.join(" ")); + }, + }; + await start({ rootDirectory: process.cwd(), logger, args: ["help"] }); - assert.match(logs[0], /one-version/); + assert.match(logs[0], /one-version/); + }); +}); + +let exec = promisify(execCallback); + +let __filename = fileURLToPath(import.meta.url); +let __dirname = path.dirname(__filename); + +describe("one-version integration tests", () => { + // setup + // copy the `__fixtures__/template-monorepo` into the following directories + // - `__fixtures__/npm` + // - `__fixtures__/pnpm` + // - `__fixtures__/yarn` + // - `__fixtures__/yarn-berry` + // - `__fixtures__/bun` + // - `__fixtures__/missing` + before(async () => { + let fixturesDir = path.join(__dirname, "..", "__fixtures__"); + let templateDir = path.join(fixturesDir, "template-monorepo"); + let targetDirs = [ + "npm", + "pnpm", + "yarn", + "yarn-berry", + "bun", + "missing", + ]; + + async function copyDirectory(src, dest) { + // Create the destination directory if it doesn't exist + await fsPromises.mkdir(dest, { recursive: true }); + + // Read all files and subdirectories from the source directory + let entries = await fsPromises.readdir(src, { withFileTypes: true }); + + // Copy each file and directory recursively + for (let entry of entries) { + let srcPath = path.join(src, entry.name); + let destPath = path.join(dest, entry.name); + + if (entry.isDirectory()) { + await copyDirectory(srcPath, destPath); + } else { + await fsPromises.copyFile(srcPath, destPath); + } + } + } + + let lockFile = { + npm: "package-lock.json", + pnpm: "pnpm-lock.yaml", + yarn: "yarn.lock", + "yarn-berry": "yarn.lock", + bun: "bun.lock", + }; + + for (const dir of targetDirs) { + const targetDir = path.join(fixturesDir, dir); + try { + await copyDirectory(templateDir, targetDir); + if (dir !== "missing") { + await fsPromises.writeFile(lockFile[dir], ""); + } + if (dir === "yarn-berry") { + await fsPromises.writeFile(".yarnrc.yml", "nodeLinker: node-modules"); + } + } catch (err) { + throw err; + } + } + }); + + after(async () => { + const fixturesDir = path.join(__dirname, "..", "__fixtures__"); + const targetDirs = [ + "npm", + "pnpm", + "yarn", + "yarn-berry", + "bun", + "missing", + ]; + + async function removeDirectory(dir) { + try { + await fsPromises.rm(dir, { recursive: true, force: true }); + } catch (err) { + throw err; + } + } + + for (const dir of targetDirs) { + const targetDir = path.join(fixturesDir, dir); + await removeDirectory(targetDir); + } + }); + + test("npm", async () => { + const targetDir = path.join(__dirname, "..", "__fixtures__", "npm"); + await start({ rootDirectory: targetDir, logger: console }); + // let { stdout } = await exec("npm run --silent --workspaces -- lerna version --json"); + // let versions = JSON.parse(stdout); + // assert.equal(versions.length, 1); + // assert.equal(versions[0].name, "one-version"); + }); + test("pnpm", async () => { + const targetDir = path.join(__dirname, "..", "__fixtures__", "npm"); + await start({ rootDirectory: targetDir, logger: console }); + // let { stdout } = await exec("npm run --silent --workspaces -- lerna version --json"); + // let versions = JSON.parse(stdout); + // assert.equal(versions.length, 1); + // assert.equal(versions[0].name, "one-version"); + }); + test("yarn", async () => { + const targetDir = path.join(__dirname, "..", "__fixtures__", "npm"); + await start({ rootDirectory: targetDir, logger: console }); + // let { stdout } = await exec("npm run --silent --workspaces -- lerna version --json"); + // let versions = JSON.parse(stdout); + // assert.equal(versions.length, 1); + // assert.equal(versions[0].name, "one-version"); + }); + test("yarn-berry", async () => { + const targetDir = path.join(__dirname, "..", "__fixtures__", "npm"); + await start({ rootDirectory: targetDir, logger: console }); + // let { stdout } = await exec("npm run --silent --workspaces -- lerna version --json"); + // let versions = JSON.parse(stdout); + // assert.equal(versions.length, 1); + // assert.equal(versions[0].name, "one-version"); + }); + test("bun", async () => { + const targetDir = path.join(__dirname, "..", "__fixtures__", "npm"); + await start({ rootDirectory: targetDir, logger: console }); + // let { stdout } = await exec("npm run --silent --workspaces -- lerna version --json"); + // let versions = JSON.parse(stdout); + // assert.equal(versions.length, 1); + // assert.equal(versions[0].name, "one-version"); + }); + test("missing", async () => { + const targetDir = path.join(__dirname, "..", "__fixtures__", "npm"); + await start({ rootDirectory: targetDir, logger: console }); + // let { stdout } = await exec("npm run --silent --workspaces -- lerna version --json"); + // let versions = JSON.parse(stdout); + // assert.equal(versions.length, 1); + // assert.equal(versions[0].name, "one-version"); + }); }); diff --git a/packages/one-version/biome.jsonc b/packages/one-version/biome.jsonc index dc20e31..4eb23c7 100644 --- a/packages/one-version/biome.jsonc +++ b/packages/one-version/biome.jsonc @@ -4,6 +4,7 @@ "enabled": true }, "linter": { + "ignore": ["jsonc-parser.mjs"], "enabled": true, "rules": { "recommended": true, diff --git a/packages/one-version/one-version.mjs b/packages/one-version/one-version.mjs index 76bede0..d9fa53f 100644 --- a/packages/one-version/one-version.mjs +++ b/packages/one-version/one-version.mjs @@ -1,21 +1,61 @@ +import { exec } from "node:child_process"; +import { existsSync, readFileSync } from "node:fs"; +import { createReadStream, readFileSync, rmSync, writeFileSync } from "node:fs"; +import path, { join as pathJoin } from "node:path"; +import { promisify } from "node:util"; +import { createDebug } from "./utils/create-debug.mjs"; import { parse } from "./utils/jsonc-parser.mjs"; -async function _loadConfig({ rootDirectory }) { +let debug = createDebug("one-version"); + +function loadConfig({ rootDirectory }) { + try { + // prefer jsonc first, then fallback to .json + if (existsSync(pathJoin(rootDirectory, "one-version.config.jsonc"))) { + return parse(readFileSync(pathJoin(rootDirectory, "one-version.config.jsonc"), "utf8")); + } + return parse(readFileSync(pathJoin(rootDirectory, "one-version.config.json"), "utf8")); + } catch (error) { + debug("Error loading config", error); + return {}; + } } -export async function start({ rootDirectory, logger, args, loadConfig = _loadConfig }) { - let [firstArg] = args; +function inferPackageManager({ rootDirectory }) { + if (existsSync(pathJoin(rootDirectory, "yarn.lock"))) { + if (existsSync(pathJoin(rootDirectory, ".yarnrc.yml"))) { + return "yarn-berry"; + } + return "yarn"; + } + if (existsSync(pathJoin(rootDirectory, "pnpm-lock.yaml"))) { + return "pnpm"; + } + if (existsSync(pathJoin(rootDirectory, "package-lock.json"))) { + return "npm"; + } + if (existsSync(pathJoin(rootDirectory, "bun.lockb"))) { + return "bun"; + } + throw new Error("Could not infer package manager! Please specify one in the config file."); +} - let usageLogs = [ - "", - `Usage:`, - ` one-version check - Check the repo to ensure all dependencies are match the expected versions`, - ` one-version help - Display this help message!`, - "", - ]; +let usageLogs = [ + "", + `Usage:`, + ` one-version check - Check the repo to ensure all dependencies are match the expected versions`, + ` one-version help - Display this help message!`, + "", +]; +export async function start({ rootDirectory, logger, args }) { + let [firstArg] = args; switch (firstArg) { case "check": { + let initialConfig = loadConfig({ rootDirectory }); + if (!initialConfig.packageManager) { + initialConfig.packageManager = inferPackageManager({ rootDirectory }); + } // @TODO return; } diff --git a/packages/one-version/utils/create-debug.mjs b/packages/one-version/utils/create-debug.mjs new file mode 100644 index 0000000..87a8ad2 --- /dev/null +++ b/packages/one-version/utils/create-debug.mjs @@ -0,0 +1,9 @@ +export function createDebug(namespace) { + let enabled = process.env.DEBUG?.includes(namespace); + + return function debug(...args) { + if (enabled) { + console.log(`[${namespace}]`, ...args); + } + }; +} diff --git a/packages/one-version/utils/jsonc-parser.mjs b/packages/one-version/utils/jsonc-parser.mjs index 2fc645a..67e0f04 100644 --- a/packages/one-version/utils/jsonc-parser.mjs +++ b/packages/one-version/utils/jsonc-parser.mjs @@ -1,6 +1,36 @@ // Mostly all of this is lifted from `jsonc-parser` package // See: https://github.com/microsoft/node-jsonc-parser/tree/b6b34ba39da3f5bee17d41004c03a86686dade4c +const SyntaxKind = { + OpenBraceToken: 1, + CloseBraceToken: 2, + OpenBracketToken: 3, + CloseBracketToken: 4, + CommaToken: 5, + ColonToken: 6, + NullKeyword: 7, + TrueKeyword: 8, + FalseKeyword: 9, + StringLiteral: 10, + NumericLiteral: 11, + LineCommentTrivia: 12, + BlockCommentTrivia: 13, + LineBreakTrivia: 14, + Trivia: 15, + Unknown: 16, + EOF: 17, +}; + +const ScanError = { + None: 0, + UnexpectedEndOfComment: 1, + UnexpectedEndOfString: 2, + UnexpectedEndOfNumber: 3, + InvalidUnicode: 4, + InvalidEscapeCharacter: 5, + InvalidCharacter: 6, +}; + /** * Parses the given text and returns the object the JSON content represents. On invalid input, the parser tries to be as fault tolerant as possible, but still return a result. * Therefore always check the errors list to find out if the input was valid. @@ -177,7 +207,6 @@ export function visit(text, visitor, options = { allowTrailingComma: false }) { if (skipUntilAfter.indexOf(token) !== -1) { scanNext(); break; - // biome-ignore lint/style/noUselessElse: don't feel confident that the recommended change is safe } else if (skipUntil.indexOf(token) !== -1) { break; } @@ -734,3 +763,104 @@ export function createScanner(text, ignoreTrivia = false) { getTokenError: () => scanError, }; } + +function isWhiteSpace(ch) { + return ch === CharacterCodes.space || ch === CharacterCodes.tab; +} + +function isLineBreak(ch) { + return ch === CharacterCodes.lineFeed || ch === CharacterCodes.carriageReturn; +} + +function isDigit(ch) { + return ch >= CharacterCodes._0 && ch <= CharacterCodes._9; +} + +const CharacterCodes = { + lineFeed: 0x0A, // \n + carriageReturn: 0x0D, // \r + + space: 0x0020, // " " + + _0: 0x30, + _1: 0x31, + _2: 0x32, + _3: 0x33, + _4: 0x34, + _5: 0x35, + _6: 0x36, + _7: 0x37, + _8: 0x38, + _9: 0x39, + + a: 0x61, + b: 0x62, + c: 0x63, + d: 0x64, + e: 0x65, + f: 0x66, + g: 0x67, + h: 0x68, + i: 0x69, + j: 0x6A, + k: 0x6B, + l: 0x6C, + m: 0x6D, + n: 0x6E, + o: 0x6F, + p: 0x70, + q: 0x71, + r: 0x72, + s: 0x73, + t: 0x74, + u: 0x75, + v: 0x76, + w: 0x77, + x: 0x78, + y: 0x79, + z: 0x7A, + + A: 0x41, + B: 0x42, + C: 0x43, + D: 0x44, + E: 0x45, + F: 0x46, + G: 0x47, + H: 0x48, + I: 0x49, + J: 0x4A, + K: 0x4B, + L: 0x4C, + M: 0x4D, + N: 0x4E, + O: 0x4F, + P: 0x50, + Q: 0x51, + R: 0x52, + S: 0x53, + T: 0x54, + U: 0x55, + V: 0x56, + W: 0x57, + X: 0x58, + Y: 0x59, + Z: 0x5a, + + asterisk: 0x2A, // * + backslash: 0x5C, // \ + closeBrace: 0x7D, // } + closeBracket: 0x5D, // ] + colon: 0x3A, // : + comma: 0x2C, // , + dot: 0x2E, // . + doubleQuote: 0x22, // " + minus: 0x2D, // - + openBrace: 0x7B, // { + openBracket: 0x5B, // [ + plus: 0x2B, // + + slash: 0x2F, // / + + formFeed: 0x0C, // \f + tab: 0x09, // \t +};