Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,23 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1
with:
persist-credentials: false

- uses: ./.github/actions/ci-setup
- name: Setup dependencies
uses: ./.github/actions/ci-setup

- name: Run format:check
run: yarn format:check

- name: Run lint
run: yarn lint:ci

- name: Run typecheck
run: yarn typecheck

ci-ok:
name: CI OK
runs-on: ubuntu-latest
Expand Down
41 changes: 41 additions & 0 deletions .oxlintrc.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript", "unicorn", "oxc", "vitest", "import", "promise"],
"categories": {
"correctness": "error",
"suspicious": "error",
"perf": "error",
"pedantic": "error",
},
Comment on lines +4 to +9
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven’t enabled the style and restrictions categories yet to keep the review scope lighter.

Let me know if you’d like me to include them.
I can either add them in this PR or follow up with a separate one, whichever you prefer.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what style would bring to the table over just relying on the formatter.

As to restrictions, I briefly looked through the list and some of those rules seem to be useful while some doesn't so perhaps we should try to enable that and turn off specific rules we don't like. I'll try to make a pass through this to assess which ones we want.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what style would bring to the table over just relying on the formatter.

Actually I find some of the style rules, like typescript/consistent-type-imports, pretty useful.
I would consider give some of them a go, but we we can do it later


As to restrictions, I briefly looked through the list and some of those rules seem to be useful while some doesn't so perhaps we should try to enable that and turn off specific rules we don't like. I'll try to make a pass through this to assess which ones we want.

Sure, I'll focus on addressing your other feedbacks for now.

"options": {
"typeAware": true,
"typeCheck": false,
},
"rules": {
"func-style": "off",
"max-lines": "off",
"max-lines-per-function": "off",
"no-implicit-coercion": "off",
"no-magic-numbers": "off",
"no-ternary": "off",
"no-warning-comments": "off",
"sort-keys": "off",

"jest/no-conditional-in-test": "off",

"typescript/array-type": ["error", { "default": "generic", "readonly": "generic" }],
"typescript/ban-types": "off", // deprecated, replaced by no-unsafe-function-type and no-wrapper-object-types
"typescript/consistent-type-imports": "error",
"typescript/explicit-function-return-type": "off",
"typescript/explicit-module-boundary-types": "off",
"typescript/no-unsafe-type-assertion": "off",
"typescript/no-unsafe-function-type": "error",
"typescript/no-wrapper-object-types": "error",
"typescript/prefer-readonly-parameter-types": "off",
"typescript/return-await": "off",
"typescript/strict-boolean-expressions": "off",
},
"env": {
"builtin": true,
},
}
139 changes: 87 additions & 52 deletions get-changed-packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,40 @@ import nodePath from "path";
import assembleReleasePlan from "@changesets/assemble-release-plan";
import { parse as parseConfig } from "@changesets/config";
import parseChangeset from "@changesets/parse";
import { PreState, NewChangeset } from "@changesets/types";
import { Packages, Tool } from "@manypkg/get-packages";
import type {
NewChangeset,
PreState,
WrittenConfig,
PackageJSON as ChangesetPackageJSON,
} from "@changesets/types";
import type { Packages, Tool } from "@manypkg/get-packages";
import { safeLoad } from "js-yaml";
import micromatch from "micromatch";
import fetch from "node-fetch";
import { ProbotOctokit } from "probot";

export let getChangedPackages = async ({
import type { ProbotOctokit } from "probot";

interface PackageJSON extends ChangesetPackageJSON {
workspaces?: ReadonlyArray<string> | { packages: ReadonlyArray<string> };
bolt?: { workspaces: ReadonlyArray<string> };
}

interface PnpmWorkspace {
packages: ReadonlyArray<string>;
}

// TODO: it might be possible to remove this if improvements to `Array.isArray` ever land
// related thread: github.com/microsoft/TypeScript/issues/36554
function isArray<T>(
arg: T | {},
): arg is T extends ReadonlyArray<any>
? unknown extends T
? never
: ReadonlyArray<any>
: Array<any> {
return Array.isArray(arg);
}

export const getChangedPackages = async ({
owner,
repo,
ref,
Expand All @@ -21,12 +47,12 @@ export let getChangedPackages = async ({
owner: string;
repo: string;
ref: string;
changedFiles: string[] | Promise<string[]>;
changedFiles: ReadonlyArray<string> | Promise<ReadonlyArray<string>>;
octokit: InstanceType<typeof ProbotOctokit>;
installationToken: string;
}) => {
let hasErrored = false;
let encodedCredentials = Buffer.from(`x-access-token:${installationToken}`).toString("base64");
const encodedCredentials = Buffer.from(`x-access-token:${installationToken}`).toString("base64");

function fetchFile(path: string) {
return fetch(`https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${path}`, {
Expand All @@ -36,54 +62,58 @@ export let getChangedPackages = async ({
});
}

function fetchJsonFile(path: string) {
return fetchFile(path)
.then((x) => x.json())
.catch((err) => {
hasErrored = true;
console.error(err);
return {};
});
async function fetchJsonFile<T>(path: string): Promise<T> {
try {
const x = await fetchFile(path);
return x.json() as Promise<T>;
} catch (error) {
hasErrored = true;
console.error(error);
return {} as Promise<T>;
}
}

function fetchTextFile(path: string) {
return fetchFile(path)
.then((x) => x.text())
.catch((err) => {
hasErrored = true;
console.error(err);
return "";
});
async function fetchTextFile(path: string): Promise<string> {
try {
const x = await fetchFile(path);
return x.text();
} catch (err) {
hasErrored = true;
console.error(err);
return "";
}
}

async function getPackage(pkgPath: string) {
let jsonContent = await fetchJsonFile(pkgPath + "/package.json");
async function getPackage(pkgPath: string): Promise<{ dir: string; packageJson: PackageJSON }> {
const jsonContent = await fetchJsonFile(pkgPath + "/package.json");
return {
packageJson: jsonContent,
dir: pkgPath,
packageJson: jsonContent as PackageJSON,
};
}

let rootPackageJsonContentsPromise = fetchJsonFile("package.json");
let configPromise: Promise<any> = fetchJsonFile(".changeset/config.json");
const rootPackageJsonContentsPromise: Promise<PackageJSON> = fetchJsonFile("package.json");
const rawConfigPromise: Promise<WrittenConfig> = fetchJsonFile(".changeset/config.json");

let tree = await octokit.git.getTree({
const tree = await octokit.git.getTree({
owner,
repo,
recursive: "1",
tree_sha: ref,
});

let preStatePromise: Promise<PreState> | undefined;
let changesetPromises: Promise<NewChangeset>[] = [];
let potentialWorkspaceDirectories: string[] = [];
const changesetPromises: Array<Promise<NewChangeset>> = [];
const potentialWorkspaceDirectories: Array<string> = [];
let isPnpm = false;
let changedFiles = await changedFilesPromise;
const changedFiles = await changedFilesPromise;

for (let item of tree.data.tree) {
if (!item.path) continue;
for (const item of tree.data.tree) {
if (!item.path) {
continue;
}
if (item.path.endsWith("/package.json")) {
let dirPath = nodePath.dirname(item.path);
const dirPath = nodePath.dirname(item.path);
potentialWorkspaceDirectories.push(dirPath);
} else if (item.path === "pnpm-workspace.yaml") {
isPnpm = true;
Expand All @@ -95,43 +125,45 @@ export let getChangedPackages = async ({
item.path.endsWith(".md") &&
changedFiles.includes(item.path)
) {
let res = /\.changeset\/([^\.]+)\.md/.exec(item.path);
const res = /\.changeset\/([^.]+)\.md/.exec(item.path);
if (!res) {
throw new Error("could not get name from changeset filename");
}
let id = res[1];
const id = res[1];

changesetPromises.push(
fetchTextFile(item.path).then((text) => {
return { ...parseChangeset(text), id };
}),
fetchTextFile(item.path).then((text) => ({ ...parseChangeset(text), id })),
);
}
}
let tool:
| {
tool: Tool;
globs: string[];
globs: ReadonlyArray<string>;
}
| undefined;

if (isPnpm) {
const pnpmWorkspaceContent = await fetchTextFile("pnpm-workspace.yaml");
const pnpmWorkspace = safeLoad(pnpmWorkspaceContent) as PnpmWorkspace;

tool = {
tool: "pnpm",
globs: safeLoad(await fetchTextFile("pnpm-workspace.yaml")).packages,
globs: pnpmWorkspace.packages,
};
} else {
let rootPackageJsonContent = await rootPackageJsonContentsPromise;
const rootPackageJsonContent = await rootPackageJsonContentsPromise;

if (rootPackageJsonContent.workspaces) {
if (!Array.isArray(rootPackageJsonContent.workspaces)) {
if (isArray(rootPackageJsonContent.workspaces)) {
tool = {
tool: "yarn",
globs: rootPackageJsonContent.workspaces.packages,
globs: rootPackageJsonContent.workspaces,
};
} else {
tool = {
tool: "yarn",
globs: rootPackageJsonContent.workspaces,
globs: rootPackageJsonContent.workspaces.packages,
};
}
} else if (rootPackageJsonContent.bolt && rootPackageJsonContent.bolt.workspaces) {
Expand All @@ -142,9 +174,9 @@ export let getChangedPackages = async ({
}
}

let rootPackageJsonContent = await rootPackageJsonContentsPromise;
const rootPackageJsonContent = await rootPackageJsonContentsPromise;

let packages: Packages = {
const packages: Packages = {
root: {
dir: "/",
packageJson: rootPackageJsonContent,
Expand All @@ -154,10 +186,13 @@ export let getChangedPackages = async ({
};

if (tool) {
if (!Array.isArray(tool.globs) || !tool.globs.every((x) => typeof x === "string")) {
if (
!Array.isArray(tool.globs) ||
!tool.globs.every((glob: unknown) => typeof glob === "string")
) {
throw new Error("globs are not valid: " + JSON.stringify(tool.globs));
}
let matches = micromatch(potentialWorkspaceDirectories, tool.globs);
const matches = micromatch(potentialWorkspaceDirectories, tool.globs);

packages.packages = await Promise.all(matches.map((dir) => getPackage(dir)));
} else {
Expand All @@ -170,7 +205,7 @@ export let getChangedPackages = async ({
const releasePlan = assembleReleasePlan(
await Promise.all(changesetPromises),
packages,
await configPromise.then((rawConfig) => parseConfig(rawConfig, packages)),
parseConfig(await rawConfigPromise, packages),
await preStatePromise,
);

Expand All @@ -180,7 +215,7 @@ export let getChangedPackages = async ({
: packages.packages.filter((pkg) =>
changedFiles.some((changedFile) => changedFile.startsWith(`${pkg.dir}/`)),
)
).map((x) => x.packageJson.name),
).map((pkg) => pkg.packageJson.name),
releasePlan,
};
};
Loading