Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
24 changes: 19 additions & 5 deletions .github/workflows/build-lint-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,37 @@ jobs:
build-lint-test:
name: Build, Lint, and Test
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
strategy:
matrix:
node-version: [20.x]
node-version: [24.22.2]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: ${{ matrix.node-version }}
- run: yarn --frozen-lockfile
- name: Install Yarn
run: npm install --global yarn@1.22.22
- name: Verify toolchain
run: |
test "$(node --version)" = "v${{ matrix.node-version }}"
test "$(yarn --version)" = "1.22.22"
- run: yarn install --frozen-lockfile
- run: yarn allow-scripts
- run: yarn build
- run: yarn lint
- name: Smoke-test production artifact
run: yarn test:smoke
- run: yarn test
all-jobs-pass:
name: All jobs pass
runs-on: ubuntu-latest
timeout-minutes: 2
permissions: {}
needs:
- build-lint-test
steps:
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v20.19.1
v24.22.2
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,18 @@ It can be used by navigating to `/request.html?method=${METHOD}&params=${PARAMS}

### Setup

- Install [Node.js](https://nodejs.org) version 20.19.0 or later
- If you are using [nvm](https://github.com/creationix/nvm#installation) (recommended) running `nvm use` will automatically choose the right node version for you.
- Install [Yarn v1](https://yarnpkg.com/en/docs/install)
- Install [Node.js](https://nodejs.org) 24.22.2 (the recommended development version)
- If you are using [nvm](https://github.com/creationix/nvm#installation) (recommended), running `nvm use` will automatically choose the version from `.nvmrc`.
- Node.js 24.22.1 or later is required, including when installing the published package. Node.js 20 and 22 are no longer supported. CI tests Node.js 24.22.2.
- Install [Yarn 1.22.22](https://classic.yarnpkg.com/en/docs/install)
- Run `yarn setup` to install dependencies and run any required post-install scripts
- **Warning:** Do not use the `yarn` / `yarn install` command directly. Use `yarn setup` instead. The normal install command will skip required post-install scripts, leaving your development environment in an invalid state.

### Testing and Linting

Run `yarn lint` to run the linter, or run `yarn lint:fix` to run the linter and fix any automatically fixable issues.

This package has no tests.
Run `yarn build && yarn test:smoke` to check that the production artifact contains both pages and their local assets. This does not execute the browser JavaScript, test wallet flows, or verify the deployed GitHub Pages site.

### Deploying

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "9.10.0",
"description": "A simple dapp used in MetaMask e2e tests.",
"engines": {
"node": ">=20.19.0"
"node": ">=24.22.1"
},
"scripts": {
"setup": "yarn install && yarn allow-scripts",
Expand All @@ -14,6 +14,7 @@
"lint:fix": "yarn lint:eslint --fix && yarn lint:misc --write",
"start": "webpack-dev-server",
"build": "webpack --mode production",
"test:smoke": "node scripts/smoke-test.js",
"test": "yarn lint",
"update-changelog": "auto-changelog update",
"prepublishOnly": "yarn test && yarn build"
Expand Down
212 changes: 212 additions & 0 deletions scripts/smoke-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
const fs = require('fs');
const http = require('http');
const path = require('path');

const fsPromises = fs.promises;
const artifactDirectory = path.resolve(process.argv[2] || 'dist');
const requestTimeout = 5000;
const testTimeout = 30000;
let activeServer;

const contentTypes = {
'.css': 'text/css',
'.html': 'text/html',
'.js': 'text/javascript',
'.json': 'application/json',
'.svg': 'image/svg+xml',
};

function fail(message) {
throw new Error(message);
}

function resolveArtifactPath(urlPath) {
const decodedPath = decodeURIComponent(urlPath.split('?')[0]);
const relativePath =
decodedPath === '/' ? 'index.html' : decodedPath.slice(1);
const resolvedPath = path.resolve(artifactDirectory, relativePath);

if (
resolvedPath !== artifactDirectory &&
!resolvedPath.startsWith(`${artifactDirectory}${path.sep}`)
) {
return null;
}

return resolvedPath;
}

function createServer() {
return http.createServer(async (incomingRequest, response) => {
let filePath;

try {
filePath = resolveArtifactPath(incomingRequest.url);
} catch {
response.writeHead(400);
response.end();
return;
}

let file;

try {
file = await fsPromises.readFile(filePath);
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
} catch {
response.writeHead(404);
response.end();
return;
}

const contentType =
contentTypes[path.extname(filePath)] || 'application/octet-stream';
response.writeHead(200, { 'Content-Type': contentType });
response.end(file);
});
}

function request(serverUrl, urlPath) {
return new Promise((resolve, reject) => {
const clientRequest = http.get(`${serverUrl}${urlPath}`, (response) => {
const chunks = [];

response.on('data', (chunk) => chunks.push(chunk));
response.on('end', () => {
resolve({
body: Buffer.concat(chunks),
contentType: response.headers['content-type'] || '',
statusCode: response.statusCode,
});
});
});

clientRequest.setTimeout(requestTimeout, () => {
clientRequest.destroy(new Error(`Request timed out: ${urlPath}`));
});
clientRequest.on('error', reject);
});
}

function localReferences(html) {
const references = new Set();
const referencePattern = /(?:src|href)="([^"]+)"/gu;
let match;

while ((match = referencePattern.exec(html))) {
const reference = match[1];
if (!/^(?:[a-z]+:|\/\/|#|data:)/iu.test(reference)) {
references.add(new URL(reference, 'http://localhost/').pathname);
}
}

return references;
}

async function runSmokeTest() {
try {
if (!(await fsPromises.stat(artifactDirectory)).isDirectory()) {
fail(`Artifact path is not a directory: ${artifactDirectory}`);
}
} catch (error) {
if (error.code === 'ENOENT') {
fail(`Artifact directory does not exist: ${artifactDirectory}`);
}
throw error;
}

const server = createServer();
activeServer = server;

await new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', resolve);
});

const address = server.address();
const serverUrl = `http://127.0.0.1:${address.port}`;

try {
const pages = [
{ path: '/', title: 'E2E Test Dapp', script: 'main.js' },
{
path: '/request.html',
title: 'E2E Test Dapp - Send Individual Request',
script: 'request.js',
},
];
const references = new Set(['/constants.json']);

for (const page of pages) {
const response = await request(serverUrl, page.path);
const html = response.body.toString();

if (
response.statusCode !== 200 ||
!response.contentType.startsWith('text/html')
) {
fail(`Expected HTML page at ${page.path}`);
}
if (!html.includes(`<title>${page.title}</title>`)) {
fail(`Unexpected title at ${page.path}`);
}
if (!html.includes(`src="${page.script}"`)) {
fail(`Expected script ${page.script} in ${page.path}`);
}

for (const reference of localReferences(html)) {
references.add(reference);
}
}

for (const reference of references) {
const response = await request(serverUrl, reference);
if (response.statusCode !== 200 || response.body.length === 0) {
fail(`Expected a non-empty local asset at ${reference}`);
}

const extension = path.extname(reference);
const expectedContentType = contentTypes[extension];
if (
expectedContentType &&
!response.contentType.startsWith(expectedContentType)
) {
fail(
`Unexpected content type for ${reference}: ${response.contentType}`,
);
}
}

const constants = await request(serverUrl, '/constants.json');
JSON.parse(constants.body.toString());

const missingRoute = await request(serverUrl, '/does-not-exist');
if (missingRoute.statusCode !== 404) {
fail(
`Expected unknown route to return 404, got ${missingRoute.statusCode}`,
);
}
} finally {
await new Promise((resolve) => server.close(resolve));
activeServer = undefined;
}
}

const timeout = new Promise((_, reject) => {
const timer = setTimeout(() => {
if (activeServer) {
activeServer.closeAllConnections();
activeServer.close();
}
reject(new Error('Smoke test timed out'));
}, testTimeout);
timer.unref();
});

Promise.race([runSmokeTest(), timeout])
.then(() => {
console.log(`Production artifact smoke test passed: ${artifactDirectory}`);
})
.catch((error) => {
console.error(error.message);
process.exitCode = 1;
});
Loading