Skip to content

Commit

Permalink
Add IIFE build and browser/node testing framework
Browse files Browse the repository at this point in the history
This configures an IIFE build that can be used to pull in the SDK library using
a script tag. It also sets up AVA as a test runner with three different
configurations:

- `test:dev` - Run the `test/lib/` tests from `src/lib/` without an explicit
build step.
- `test:node` - Run the `test/lib/` tests from `dist/lib/` requiring an
explicit build.
- `test:browser` - Run the `test/browser/` tests in Chromium using
`dist/lib/browser/sindri.iife.js` requiring an explicit build.

I don't think there's really a way to run the same test suite in both the
browser and node environments, so my plan is to do most of the testing in node
and have a basic set of sanity check tests that run in puppeteer to make sure
that the browser builds don't accidentally depend on node-only libraries or
lack any polyfills.

Merges #41
  • Loading branch information
sangaline authored Jan 5, 2024
1 parent 71dc810 commit 135d917
Show file tree
Hide file tree
Showing 17 changed files with 1,799 additions and 33 deletions.
27 changes: 21 additions & 6 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
pull_request:

jobs:
lint:
lint-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
Expand All @@ -18,10 +18,10 @@ jobs:
id: setup-node
uses: actions/setup-node@v3
with:
node-version: 18
node-version: 18.18.2
cache: yarn

- name: Install Dependencies
- name: Install JavaScript Dependencies
run: |
yarn install --frozen-lockfile
Expand All @@ -40,24 +40,39 @@ jobs:
run: |
yarn build
- name: Test Node Build
if: success() || failure()
run: |
yarn test:node
- name: Install Chrome Dependencies
if: success() || failure()
run: |
sudo apt-get install -y libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgbm1 libasound2 libpangocairo-1.0-0 libxss1 libgtk-3-0
- name: Test Browser Build
if: success() || failure()
run: |
yarn test:browser
deploy:
runs-on: ubuntu-latest
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
needs: [lint]
needs: [lint-and-test]
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18
node-version: 18.18.2
registry-url: "https://registry.npmjs.org"

- name: Extract Version from Tag
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV

- name: Install Dependencies
- name: Install JavaScript Dependencies
run: yarn install --frozen-lockfile

- name: Update package.json Version
Expand Down
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nodejs v18.18.2
21 changes: 19 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:hydrogen-slim as development
FROM node:18.18.2-slim as development
ENV NODE_ENV=development

# Optionally convert the user to another UID/GUID to match host user file permissions.
Expand All @@ -15,10 +15,27 @@ RUN if [ "$UID" != "1000" ]; then \
; fi

RUN apt-get update
RUN apt-get install --yes git
RUN apt-get install --yes git \
`# Chromium installation dependencies` \
curl unzip \
`# Chromium runtime dependencies` \
libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgbm1 libasound2 libpangocairo-1.0-0 libxss1 libgtk-3-0

USER node

# Conditionally install an arm64 build of Chromium for Puppeteer if we're on an arm64 host.
# This prevents us from having to emulate x86_64 on arm Macs during development to run browser tests.
# See: https://github.com/puppeteer/puppeteer/issues/7740#issuecomment-1875162960
RUN if [ "$(uname -m)" = "aarch64" ]; then \
cd /home/node/ && \
curl 'https://playwright.azureedge.net/builds/chromium/1088/chromium-linux-arm64.zip' > chromium.zip && \
unzip chromium.zip && \
rm -f chromium.zip && \
echo 'export PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true' >> ~/.bashrc && \
echo 'export CHROME_PATH=/home/node/chrome-linux/chrome' >> ~/.bashrc && \
echo 'export PUPPETEER_EXECUTABLE_PATH=/home/node/chrome-linux/chrome' >> ~/.bashrc; \
fi

# Skip installing any node dependencies because we're going to bind mount over `node_modules` anyway.
# We'll also do a volume mount to persist the yarn cache.
RUN mkdir -p ~/.cache/yarn
Expand Down
14 changes: 12 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"./dist/lib/index.js": "./dist/lib/browser/index.js",
"./dist/lib/index.mjs": "./dist/lib/browser/index.mjs"
},
"jsdelivr": "./dist/lib/browser/sindri.iife.js",
"exports": {
".": {
"import": "./dist/lib/index.mjs",
Expand All @@ -37,8 +38,8 @@
"./cli": "./dist/cli/index.js"
},
"scripts": {
"build": "NODE_ENV=production tsup --env.NODE_ENV $NODE_ENV",
"build:watch": "NODE_ENV=development tsup --watch --env.NODE_ENV $NODE_ENV",
"build": "rm -rf dist/* && NODE_ENV=production tsup",
"build:watch": "rm -rf dist/* && NODE_ENV=development tsup --watch",
"download-sindri-manifest-schema": "nwget https://sindri.app/api/v1/sindri-manifest-schema.json -O sindri-manifest.json && prettier --write sindri-manifest.json",
"download-sindri-manifest-schema:dev": "nwget http://localhost/api/v1/sindri-manifest-schema.json -O sindri-manifest.json && prettier --write sindri-manifest.json",
"download-sindri-manifest-schema:docker": "nwget http://host.docker.internal/api/v1/sindri-manifest-schema.json -O sindri-manifest.json && prettier --write sindri-manifest.json",
Expand All @@ -47,6 +48,9 @@
"generate-api:docker": "rm -rf src/lib/api/ && openapi --client axios --input http://host.docker.internal/api/openapi.json --output src/lib/api/ && prettier --write src/lib/api/**/*",
"lint": "eslint '**/*.{js,ts}'",
"format": "prettier --write '**/*.{js,json,md,ts}'",
"test:browser": "ava --config test/ava.config.browser.mjs",
"test:dev": "ava --config test/ava.config.dev.mjs",
"test:node": "ava --config test/ava.config.node.mjs",
"type-check": "tsc --noEmit"
},
"repository": {
Expand Down Expand Up @@ -75,6 +79,7 @@
"zod": "^3.22.4"
},
"devDependencies": {
"@ava/typescript": "^4.1.0",
"@commander-js/extra-typings": "^11.1.0",
"@tsconfig/node18": "^18.2.2",
"@types/ignore-walk": "^4.0.3",
Expand All @@ -84,12 +89,17 @@
"@types/tar": "^6.1.10",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"ava": "^6.0.1",
"esbuild": "^0.19.11",
"eslint": "^8.53.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1",
"jsdom": "^23.0.1",
"openapi-typescript-codegen": "^0.25.0",
"prettier": "^3.1.0",
"puppeteer": "^21.7.0",
"tsup": "^7.3.0",
"tsx": "^4.7.0",
"type-fest": "^4.8.2",
"typescript": "^5.2.2",
"wget-improved": "^3.4.0"
Expand Down
8 changes: 7 additions & 1 deletion src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
console.log("SDK");
import { InternalService } from "lib/api";

export default {
environment: process.env.NODE_ENV,
getSindriManifestSchema: async () =>
await InternalService.sindriManifestSchema(),
};
7 changes: 7 additions & 0 deletions test/ava.config.base.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default {
extensions: {
ts: "module",
},
files: ["test/**/*.test.ts"],
nodeArguments: ["--loader=tsx"],
};
15 changes: 15 additions & 0 deletions test/ava.config.browser.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import path from "path";
import process from "process";
import { fileURLToPath } from "url";

import baseConfig from "./ava.config.base.mjs";

// Use the browser tsconfig import paths.
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
process.env.TSX_TSCONFIG_PATH = path.join(__dirname, "tsconfig.browser.json");

export default {
...baseConfig,
files: ["test/browser/**/*.test.ts"],
};
6 changes: 6 additions & 0 deletions test/ava.config.dev.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import baseConfig from "./ava.config.base.mjs";

export default {
...baseConfig,
files: ["test/lib/**/*.test.ts"],
};
15 changes: 15 additions & 0 deletions test/ava.config.node.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import path from "path";
import process from "process";
import { fileURLToPath } from "url";

import baseConfig from "./ava.config.base.mjs";

// Use the node tsconfig import paths.
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
process.env.TSX_TSCONFIG_PATH = path.join(__dirname, "tsconfig.node.json");

export default {
...baseConfig,
files: ["test/lib/**/*.test.ts"],
};
15 changes: 15 additions & 0 deletions test/browser/main.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import test from "ava";
import withPage from "./withPage";

import sindriLibrary from "lib";

// The `sindri` library is injected in `withPage.ts`, but this tells TypeScript what the type is.
type SindriLibrary = typeof sindriLibrary;
declare const sindri: SindriLibrary;

test("should get Sindri manifest schema JSON", withPage, async (t, page) => {
const schema = await page.evaluate(async () =>
sindri.getSindriManifestSchema(),
);
t.true(schema?.title?.includes("Sindri"));
});
40 changes: 40 additions & 0 deletions test/browser/withPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";

import { ExecutionContext } from "ava";
import puppeteer, { Page } from "puppeteer";

type RunFunction = (t: ExecutionContext, page: Page) => Promise<void>;

const getSindriScriptPath = () => {
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const sindriScriptPath = path.join(
__dirname,
"..",
"..",
"dist",
"lib",
"browser",
"sindri.iife.js",
);
if (!fs.existsSync(sindriScriptPath)) {
throw new Error(`Expected IIFE build to exist at "${sindriScriptPath}".`);
}
return sindriScriptPath;
};

export default async (t: ExecutionContext, run: RunFunction) => {
const browser = await puppeteer.launch({ headless: "new" });
const page = await browser.newPage();
try {
await page.addScriptTag({
path: getSindriScriptPath(),
});
await run(t, page);
} finally {
await page.close();
await browser.close();
}
};
14 changes: 14 additions & 0 deletions test/lib/testConfiguration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import test from "ava";

import lib from "lib";

test("should get Sindri manifest schema JSON", async (t) => {
const schema = await lib.getSindriManifestSchema();
t.true(schema?.title?.includes("Sindri"));
});

test("library can be imported", (t) => {
t.true(
lib.environment && ["development", "production"].includes(lib.environment),
);
});
9 changes: 9 additions & 0 deletions test/tsconfig.browser.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"paths": {
"lib": ["../dist/lib/browser"],
"lib/*": ["../dist/lib/browser/*"]
}
}
}
9 changes: 9 additions & 0 deletions test/tsconfig.node.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"paths": {
"lib": ["../dist/lib"],
"lib/*": ["../dist/lib/*"]
}
}
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
"moduleResolution": "node",
"preserveConstEnums": true
},
"include": ["src/**/*"],
"include": ["src/**/*", "test/**/*"],
"exclude": ["dist/**/*", "node_modules/**/*"]
}
28 changes: 28 additions & 0 deletions tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import esbuild from "esbuild";
import { defineConfig } from "tsup";

process.env.NODE_ENV = process.env.NODE_ENV || "production";

export default defineConfig([
// SDK for NodeJS.
{
cjsInterop: true,
dts: true,
entry: ["src/lib/index.ts"],
env: {
NODE_ENV: process.env.NODE_ENV,
},
format: ["cjs", "esm"],
minify: process.env.NODE_ENV === "production",
outDir: "dist/lib",
Expand All @@ -20,20 +26,42 @@ export default defineConfig([
{
dts: true,
entry: ["src/lib/index.ts"],
env: {
NODE_ENV: process.env.NODE_ENV,
},
format: ["cjs", "esm"],
minify: process.env.NODE_ENV === "production",
outDir: "dist/lib/browser",
shims: true,
sourcemap: true,
splitting: true,
target: "esnext",
// Produce an IIFE bundle for use with a <script> tag in a browser.
onSuccess: async () => {
await esbuild.build({
bundle: true,
entryPoints: ["dist/lib/browser/index.mjs"],
format: "iife",
footer: {
js: "var sindri = sindriExports.default;",
},
globalName: "sindriExports",
minify: process.env.NODE_ENV === "production",
outfile: "dist/lib/browser/sindri.iife.js",
platform: "browser",
sourcemap: true,
});
},
// Additional browser-specific configuration will go here (e.g. polyfills).
},
// CLI Tool.
{
bundle: true,
dts: true,
entry: ["src/cli/index.ts"],
env: {
NODE_ENV: process.env.NODE_ENV,
},
format: ["cjs"],
minify: process.env.NODE_ENV === "production",
outDir: "dist/cli",
Expand Down
Loading

0 comments on commit 135d917

Please sign in to comment.