Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ci: wip add script to install, build, and typecheck examples #104

Draft
wants to merge 29 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
153 changes: 153 additions & 0 deletions .github/workflow_scripts/changed.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#!/usr/bin/env node

import os from "node:os";
import path from "node:path";

import PackageJson from "@npmcli/package-json";
import { execa } from "execa";
import fse from "fs-extra";
import PQueue from "p-queue";

const concurrency = os.cpus().length;

console.log({ concurrency });

const installQueue = new PQueue({ concurrency, autoStart: false });
const buildQueue = new PQueue({ concurrency, autoStart: false });
const typecheckQueue = new PQueue({ concurrency, autoStart: false });

const TO_IGNORE = new Set([
".git",
".github",
".gitignore",
"package.json",
"prettier.config.js",
"yarn.lock",
]);

const yarnExamples = new Set(["yarn-pnp"]);
const pnpmExamples = new Set([]);

let examples = [];

if (process.env.CI) {
const { stderr, stdout, exitCode } = await execa(
"git",
["--no-pager", "diff", "--name-only", "HEAD~1"],
{ cwd: process.cwd() }
);

if (exitCode !== 0) {
console.error(stderr);
process.exit(exitCode);
}

const files = stdout.split("\n");
const dirs = files.map((f) => f.split("/").at(0));
examples = [...new Set(dirs)].filter((d) => !TO_IGNORE.has(d));
} else {
const entries = await fse.readdir(process.cwd(), { withFileTypes: true });
examples = entries
.filter((entry) => entry.isDirectory())
.filter((entry) => !TO_IGNORE.has(entry.name))
.map((entry) => entry.name)
.filter((entry) => fse.existsSync(path.join(entry, "package.json")));
}

const list = new Intl.ListFormat("en", { style: "long", type: "conjunction" });

console.log(`Testing changed examples: ${list.format(examples)}`);

for (const example of examples) {
const pkgJson = await PackageJson.load(example);

/** @type {import('execa').Options} */
const options = { cwd: example, reject: false };

const pm = pnpmExamples.has(example)
? "pnpm"
: yarnExamples.has(example)
? "yarn"
: "npm";

installQueue.add(async () => {
const hasSetup = !!pkgJson.content.scripts?.__setup;

if (hasSetup) {
console.log("🔧\u00A0Running setup script for", example);
const setupResult = await execa(pm, ["run", "__setup"], options);
if (setupResult.exitCode) {
console.error(setupResult.stderr);
throw new Error(`Error running setup script for ${example}`);
}
}

console.log(`📥\u00A0Installing ${example} with "${pm}"`);
const installResult = await execa(
pm,
pm === "npm"
? ["install", "--silent", "--legacy-peer-deps"]
: ["install", "--silent"],
options
);

if (installResult.exitCode) {
console.error(installResult.stderr);
throw new Error(`Error installing ${example}`);
}

const hasPrisma = fse.existsSync(
path.join(example, "prisma", "schema.prisma")
);

if (hasPrisma) {
console.log("Generating prisma types for", example);
const prismaGenerateCommand = await execa(
"npx",
["prisma", "generate"],
options
);

if (prismaGenerateCommand.exitCode) {
console.error(prismaGenerateCommand.stderr);
throw new Error(`Error generating prisma types for ${example}`);
}
}
});

buildQueue.add(async () => {
console.log(`📦\u00A0Building ${example}`);
const buildResult = await execa(pm, ["run", "build"], options);

if (buildResult.exitCode) {
console.error(buildResult.stderr);
throw new Error(`Error building ${example}`);
}
});

typecheckQueue.add(async () => {
console.log(`🕵️\u00A0Typechecking ${example}`);
const typecheckResult = await execa(pm, ["run", "typecheck"], options);

if (typecheckResult.exitCode) {
console.error(typecheckResult.stderr);
throw new Error(`Error typechecking ${example}`);
}
});
}

installQueue.start();

installQueue.on("empty", () => {
console.log(`installQueue is complete, moving on to buildQueue`);
return buildQueue.start();
});

buildQueue.on("empty", () => {
console.log(`buildQueue is complete, moving on to typecheckQueue`);
return typecheckQueue.start();
});

installQueue.on("error", (error) => console.error("🚨", error));
buildQueue.on("error", (error) => console.error("🚨", error));
typecheckQueue.on("error", (error) => console.error("🚨", error));
36 changes: 36 additions & 0 deletions .github/workflows/changed.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: 📦 Validate examples

on:
push:
branches:
- main
pull_request:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
typecheck:
name: 📦 Validate examples
if: github.repository == 'remix-run/examples'
runs-on: ubuntu-latest

steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
# we only need to compare the current commit with the previous one
fetch-depth: 2

- name: ⎔ Setup node
uses: actions/setup-node@v3
with:
node-version-file: ".nvmrc"
cache: "yarn"

- name: 📥 Install dependencies
run: yarn --frozen-lockfile

- name: 📦 Validate examples
run: node ./.github/workflow_scripts/changed.mjs
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ yarn.lock
pnpm-lock.yaml
pnpm-lock.yml

!./yarn.lock
!/yarn.lock
!yarn-pnp/yarn.lock
1 change: 1 addition & 0 deletions __template/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"paths": {
"~/*": ["./app/*"]
},
"skipLibCheck": true,

// Remix takes care of building everything in `remix build`.
"noEmit": true
Expand Down
2 changes: 2 additions & 0 deletions _official-blog-tutorial/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"exclude": ["./cypress", "./cypress.config.ts"],
"include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
"compilerOptions": {
"allowJs": true,
"forceConsistentCasingInFileNames": true,
"lib": ["DOM", "DOM.Iterable", "ES2019"],
"types": ["vitest/globals"],
"isolatedModules": true,
Expand Down
11 changes: 4 additions & 7 deletions _official-jokes/app/routes/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,10 @@ function validatePassword(password: string) {
}
}

function validateUrl(url: string) {
function validateUrl(url: FormDataEntryValue | null) {
if (typeof url !== "string") return "/jokes";
const urls = ["/jokes", "/", "https://remix.run"];
if (urls.includes(url)) {
return url;
}
if (urls.includes(url)) return url;
return "/jokes";
}

Expand All @@ -49,9 +48,7 @@ export const action = async ({ request }: ActionArgs) => {
const loginType = form.get("loginType");
const password = form.get("password");
const username = form.get("username");
const redirectTo = validateUrl(
(form.get("redirectTo") as string) || "/jokes"
);
const redirectTo = validateUrl(form.get("redirectTo"));
if (
typeof loginType !== "string" ||
typeof password !== "string" ||
Expand Down
1 change: 1 addition & 0 deletions _official-jokes/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"paths": {
"~/*": ["./app/*"]
},
"skipLibCheck": true,

// Remix takes care of building everything in `remix build`.
"noEmit": true
Expand Down
1 change: 1 addition & 0 deletions _official-realtime-app/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"paths": {
"~/*": ["./app/*"]
},
"skipLibCheck": true,

// Remix takes care of building everything in `remix build`.
"noEmit": true
Expand Down
1 change: 0 additions & 1 deletion _official-tutorial/app/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
////////////////////////////////////////////////////////////////////////////////

import { matchSorter } from "match-sorter";
// @ts-ignore - no types, but it's a tiny function
import sortBy from "sort-by";
import invariant from "tiny-invariant";

Expand Down
2 changes: 0 additions & 2 deletions _official-tutorial/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,8 @@ function OptimisticFavorite({ contact }: { contact: ContactRecord }) {

// Now check if there are any pending fetchers that are changing this contact
for (const fetcher of fetchers) {
// @ts-expect-error https://github.com/remix-run/remix/pull/5476
if (fetcher.formAction === `/contacts/${contact.id}`) {
// Ask for the optimistic version of the data
// @ts-expect-error https://github.com/remix-run/remix/pull/5476
isFavorite = fetcher.formData.get("favorite") === "true";
}
}
Expand Down
2 changes: 0 additions & 2 deletions _official-tutorial/app/routes/contacts.$contactId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,7 @@ export default function Contact() {
function Favorite({ contact }: { contact: ContactRecord }) {
const fetcher = useFetcher<typeof action>();
let favorite = contact.favorite;
// @ts-expect-error
if (fetcher.formData) {
// @ts-expect-error
favorite = fetcher.formData.get("favorite") === "true";
}
return (
Expand Down
1 change: 1 addition & 0 deletions _official-tutorial/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@remix-run/eslint-config": "~1.14.2",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.8",
"@types/sort-by": "^1.2.0",
"eslint": "^8.27.0",
"typescript": "^4.8.4"
},
Expand Down
1 change: 1 addition & 0 deletions _official-tutorial/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"strict": true,
"allowJs": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"~/*": ["./app/*"]
Expand Down
1 change: 1 addition & 0 deletions basic/app/routes/demos/params/$id.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const loader = async ({ params }: LoaderArgs) => {
// Sometimes your code just blows up and you never anticipated it. Remix will
// automatically catch it and send the UI to the error boundary.
if (params.id === "kaboom") {
// @ts-expect-error - this is a deliberate error to test the error boundary
lol();
}

Expand Down
1 change: 1 addition & 0 deletions basic/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"paths": {
"~/*": ["./app/*"]
},
"skipLibCheck": true,

// Remix takes care of building everything in `remix build`.
"noEmit": true
Expand Down
1 change: 1 addition & 0 deletions bullmq-task-queue/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"paths": {
"~/*": ["./app/*"]
},
"skipLibCheck": true,

// Remix takes care of building everything in `remix build`.
"noEmit": true
Expand Down
1 change: 1 addition & 0 deletions catch-boundary/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"paths": {
"~/*": ["./app/*"]
},
"skipLibCheck": true,

// Remix takes care of building everything in `remix build`.
"noEmit": true
Expand Down
1 change: 1 addition & 0 deletions chakra-ui/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"paths": {
"~/*": ["./app/*"]
},
"skipLibCheck": true,

// Remix takes care of building everything in `remix build`.
"noEmit": true
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useState } from "react";

export function ComplexComponent() {
const [count, setCount] = useState(() => {
const [count, setCount] = useState<number>(() => {
const stored = localStorage.getItem("count");
if (!stored) return 0;
return JSON.parse(stored);
Expand Down
1 change: 1 addition & 0 deletions client-only-components/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"paths": {
"~/*": ["./app/*"]
},
"skipLibCheck": true,

// Remix takes care of building everything in `remix build`.
"noEmit": true
Expand Down
1 change: 1 addition & 0 deletions client-side-validation/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"paths": {
"~/*": ["./app/*"]
},
"skipLibCheck": true,

// Remix takes care of building everything in `remix build`.
"noEmit": true
Expand Down
1 change: 1 addition & 0 deletions collected-notes/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"paths": {
"~/*": ["./app/*"]
},
"skipLibCheck": true,

// Remix takes care of building everything in `remix build`.
"noEmit": true
Expand Down
2 changes: 1 addition & 1 deletion combobox-resource-route/app/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
ComboboxPopover,
} from "@reach/combobox";
import comboboxStyles from "@reach/combobox/styles.css";
import type { LinksFunction } from "@remix-run/react";
import type { LinksFunction } from "@remix-run/node";
import { Form, useFetcher, useSearchParams } from "@remix-run/react";

import type { Lang } from "~/models/langs";
Expand Down
2 changes: 1 addition & 1 deletion combobox-resource-route/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
"@remix-run/node": "~1.14.2",
"@remix-run/react": "~1.14.2",
"@remix-run/serve": "~1.14.2",
"match-sorter": "^6.3.1",
"isbot": "^3.6.5",
"match-sorter": "^6.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
Expand Down