{
13 | return (
14 |
15 | {label || ''}{children}
16 |
17 | );
18 | }
19 |
20 | export default React.memo(Child);
21 |
--------------------------------------------------------------------------------
/.github/workflows/pr-any.yml:
--------------------------------------------------------------------------------
1 | name: PR
2 | on: [pull_request]
3 |
4 | jobs:
5 | pr:
6 | continue-on-error: true
7 | strategy:
8 | matrix:
9 | step: ['lint', 'test', 'build', 'docs']
10 | runs-on: ubuntu-latest
11 | env:
12 | YARN_ENABLE_SCRIPTS: false
13 | steps:
14 | - uses: actions/checkout@v4
15 | - uses: actions/setup-node@v4
16 | with:
17 | node-version: 'lts/*'
18 | - name: ${{ matrix.step }}
19 | run: |
20 | yarn install --immutable
21 | yarn ${{ matrix.step }}
22 |
--------------------------------------------------------------------------------
/packages/dev/config/typedoc.cjs:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | module.exports = {
5 | exclude: '**/*+(index|e2e|spec|types).ts',
6 | excludeExternals: true,
7 | excludeNotExported: true,
8 | excludePrivate: true,
9 | excludeProtected: true,
10 | hideGenerator: true,
11 | includeDeclarations: false,
12 | module: 'commonjs',
13 | moduleResolution: 'node',
14 | name: 'polkadot{.js}',
15 | out: 'docs',
16 | stripInternal: 'true',
17 | theme: 'markdown'
18 | };
19 |
--------------------------------------------------------------------------------
/.github/workflows/auto-approve.yml:
--------------------------------------------------------------------------------
1 | name: bot
2 |
3 | on:
4 | pull_request:
5 | types: [labeled]
6 |
7 | jobs:
8 | approve:
9 | if: "! startsWith(github.event.head_commit.message, '[CI Skip]') && (!github.event.pull_request || github.event.pull_request.head.repo.full_name == github.repository)"
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: jacogr/action-approve@795afd1dd096a2071d7ec98740661af4e853b7da
13 | with:
14 | authors: jacogr, TarikGul
15 | labels: -auto
16 | token: ${{ secrets.GH_PAT_BOT }}
17 |
--------------------------------------------------------------------------------
/packages/dev/scripts/polkadot-dev-build-docs.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | // Copyright 2017-2025 @polkadot/dev authors & contributors
3 | // SPDX-License-Identifier: Apache-2.0
4 |
5 | import fs from 'node:fs';
6 | import path from 'node:path';
7 |
8 | import { copyDirSync, logBin, rimrafSync } from './util.mjs';
9 |
10 | logBin('polkadot-dev-build-docs');
11 |
12 | let docRoot = path.join(process.cwd(), 'docs');
13 |
14 | if (fs.existsSync(docRoot)) {
15 | docRoot = path.join(process.cwd(), 'build-docs');
16 |
17 | rimrafSync(docRoot);
18 | copyDirSync(path.join(process.cwd(), 'docs'), docRoot);
19 | }
20 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true
4 | },
5 | "files": [],
6 | "references": [
7 | { "path": "./packages/dev/tsconfig.build.json" },
8 | { "path": "./packages/dev/tsconfig.config.json" },
9 | { "path": "./packages/dev/tsconfig.scripts.json" },
10 | { "path": "./packages/dev/tsconfig.spec.json" },
11 | { "path": "./packages/dev-test/tsconfig.build.json" },
12 | { "path": "./packages/dev-test/tsconfig.spec.json" },
13 | { "path": "./packages/dev-ts/tsconfig.build.json" },
14 | { "path": "./packages/dev-ts/tsconfig.spec.json" }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/packages/dev/src/rootCjs.spec.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import type * as testRoot from './root.js';
5 |
6 | // NOTE We don't use ts-expect-error here since the build folder may or may
7 | // not exist (so the error may or may not be there)
8 | //
9 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
10 | // @ts-ignore This should only run against the compiled ouput, where this should exist
11 | import testRootBuild from '../build/cjs/root.js';
12 | import { runTests } from './rootTests.js';
13 |
14 | runTests(testRootBuild as unknown as typeof testRoot);
15 |
--------------------------------------------------------------------------------
/packages/dev/config/prettier.cjs:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | module.exports = {
5 | arrowParens: 'always',
6 | bracketSpacing: true,
7 | embeddedLanguageFormatting: 'off',
8 | endOfLine: 'lf',
9 | htmlWhitespaceSensitivity: 'ignore',
10 | jsxBracketSameLine: false,
11 | jsxSingleQuote: true,
12 | parser: 'typescript',
13 | printWidth: 2048,
14 | proseWrap: 'preserve',
15 | quoteProps: 'as-needed',
16 | requirePragma: true, // only on those files explicitly asked for
17 | semi: true,
18 | singleQuote: true,
19 | tabWidth: 2,
20 | trailingComma: 'none',
21 | useTabs: false
22 | };
23 |
--------------------------------------------------------------------------------
/packages/dev-test/src/env/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev-test authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { browser } from './browser.js';
5 | import { expect } from './expect.js';
6 | import { jest } from './jest.js';
7 | import { lifecycle } from './lifecycle.js';
8 | import { suite } from './suite.js';
9 |
10 | /**
11 | * Exposes the jest-y environment via globals.
12 | */
13 | export function exposeEnv (isBrowser: boolean): void {
14 | [expect, jest, lifecycle, suite, isBrowser && browser].forEach((env) => {
15 | env && Object
16 | .entries(env())
17 | .forEach(([key, fn]) => {
18 | globalThis[key as 'undefined'] ??= fn;
19 | });
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/packages/dev-test/src/env/browser.spec.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev-test authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { browser } from './browser.js';
5 |
6 | const all = browser();
7 |
8 | describe('browser', () => {
9 | it('contains window', () => {
10 | expect(all.window).toBeDefined();
11 | });
12 |
13 | it('contains a crypto implementation', () => {
14 | expect(all.crypto).toBeTruthy();
15 | expect(typeof all.crypto.getRandomValues).toBe('function');
16 | });
17 |
18 | it('contains the top-level objects', () => {
19 | expect(all.document).toBeDefined();
20 | expect(all.navigator).toBeDefined();
21 | });
22 |
23 | it('contains HTML*Element', () => {
24 | expect(typeof all.HTMLElement).toBe('function');
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/packages/dev/src/rootJs/Jsx.spec.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | ///
5 |
6 | import { fireEvent, render, screen } from '@testing-library/react';
7 | import { strict as assert } from 'node:assert';
8 | import React from 'react';
9 |
10 | import Jsx from './Jsx.js';
11 |
12 | describe('react testing', () => {
13 | it('shows the children when the checkbox is checked', () => {
14 | const testMessage = 'Test Message';
15 |
16 | render({testMessage});
17 |
18 | assert.equal(screen.queryByText(testMessage), null);
19 |
20 | fireEvent.click(screen.getByLabelText(/show/i));
21 |
22 | assert.notEqual(screen.getByText(testMessage), null);
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/packages/dev-ts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Jaco Greeff ",
3 | "bugs": "https://github.com/polkadot-js/dev/issues",
4 | "description": "An TS -> ESM loader for Node >= 16.12",
5 | "engines": {
6 | "node": ">=18"
7 | },
8 | "homepage": "https://github.com/polkadot-js/dev/tree/master/packages/dev-ts#readme",
9 | "license": "Apache-2.0",
10 | "name": "@polkadot/dev-ts",
11 | "repository": {
12 | "directory": "packages/dev-ts",
13 | "type": "git",
14 | "url": "https://github.com/polkadot-js/dev.git"
15 | },
16 | "sideEffects": false,
17 | "type": "module",
18 | "version": "0.84.2",
19 | "main": "./index.js",
20 | "exports": {
21 | "./globals.d.ts": "./src/globals.d.ts"
22 | },
23 | "dependencies": {
24 | "json5": "^2.2.3",
25 | "tslib": "^2.7.0",
26 | "typescript": "^5.5.4"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/packages/dev/src/rootStatic/kusama.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/workflows/push-master.yml:
--------------------------------------------------------------------------------
1 | name: Master
2 | on:
3 | push:
4 | branches:
5 | - master
6 |
7 | jobs:
8 | master:
9 | if: "! startsWith(github.event.head_commit.message, '[CI Skip]')"
10 | strategy:
11 | matrix:
12 | step: ['build:release']
13 | runs-on: ubuntu-latest
14 | env:
15 | YARN_ENABLE_SCRIPTS: false
16 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
17 | GH_PAT: ${{ secrets.GH_PAT_BOT }}
18 | GH_RELEASE_GITHUB_API_TOKEN: ${{ secrets.GH_PAT_BOT }}
19 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
20 | steps:
21 | - uses: actions/checkout@v4
22 | with:
23 | fetch-depth: 0
24 | token: ${{ secrets.GH_PAT_BOT }}
25 | - uses: actions/setup-node@v4
26 | with:
27 | node-version: 'lts/*'
28 | - name: ${{ matrix.step }}
29 | run: |
30 | yarn install --immutable
31 | yarn ${{ matrix.step }}
32 |
--------------------------------------------------------------------------------
/packages/dev-test/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Jaco Greeff ",
3 | "bugs": "https://github.com/polkadot-js/dev/issues",
4 | "description": "A basic test-functions-as-global library on top of node:test",
5 | "engines": {
6 | "node": ">=18.14"
7 | },
8 | "homepage": "https://github.com/polkadot-js/dev/tree/master/packages/dev-test#readme",
9 | "license": "Apache-2.0",
10 | "name": "@polkadot/dev-test",
11 | "repository": {
12 | "directory": "packages/dev-test",
13 | "type": "git",
14 | "url": "https://github.com/polkadot-js/dev.git"
15 | },
16 | "sideEffects": false,
17 | "type": "module",
18 | "version": "0.84.2",
19 | "main": "./index.js",
20 | "exports": {
21 | "./globals.d.ts": "./src/globals.d.ts"
22 | },
23 | "dependencies": {
24 | "jsdom": "^24.0.0",
25 | "tslib": "^2.7.0"
26 | },
27 | "devDependencies": {
28 | "@types/jsdom": "^21.1.6"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/.github/workflows/lock.yml:
--------------------------------------------------------------------------------
1 | name: 'Lock Threads'
2 |
3 | on:
4 | schedule:
5 | - cron: '30 1/3 * * *'
6 |
7 | jobs:
8 | lock:
9 | runs-on: ubuntu-latest
10 | env:
11 | YARN_ENABLE_SCRIPTS: false
12 | steps:
13 | - uses: dessant/lock-threads@c1b35aecc5cdb1a34539d14196df55838bb2f836
14 | with:
15 | github-token: ${{ secrets.GH_PAT_BOT }}
16 | issue-inactive-days: '7'
17 | issue-comment: >
18 | This thread has been automatically locked since there has not been
19 | any recent activity after it was closed. Please open a new issue
20 | if you think you have a related problem or query.
21 | pr-inactive-days: '2'
22 | pr-comment: >
23 | This pull request has been automatically locked since there
24 | has not been any recent activity after it was closed.
25 | Please open a new issue for related bugs.
26 |
--------------------------------------------------------------------------------
/packages/dev/scripts/polkadot-dev-circular.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | // Copyright 2017-2025 @polkadot/dev authors & contributors
3 | // SPDX-License-Identifier: Apache-2.0
4 |
5 | // @ts-expect-error For scripts we don't include @types/* definitions
6 | import madge from 'madge';
7 |
8 | import { exitFatal, logBin } from './util.mjs';
9 |
10 | logBin('polkadot-dev-circular');
11 |
12 | const res = await madge('./', { fileExtensions: ['ts', 'tsx'] });
13 |
14 | /** @type {string[][]} */
15 | const circular = res.circular();
16 |
17 | if (!circular.length) {
18 | process.stdout.write('No circular dependency found!\n');
19 | process.exit(0);
20 | }
21 |
22 | const err = `Failed with ${circular.length} circular dependencies`;
23 | const all = circular
24 | .map((files, idx) => `${(idx + 1).toString().padStart(4)}: ${files.join(' > ')}`)
25 | .join('\n');
26 |
27 | process.stdout.write(`\n${err}:\n\n${all}\n\n`);
28 |
29 | exitFatal(err);
30 |
--------------------------------------------------------------------------------
/packages/dev-test/src/types.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev-test authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
5 | export type AnyFn = (...args: any[]) => any;
6 |
7 | export type BaseObj = Record;
8 |
9 | // eslint-disable-next-line @typescript-eslint/ban-types
10 | export type BaseFn = Function;
11 |
12 | export type StubFn = (...args: unknown[]) => unknown;
13 |
14 | // These basically needs to align with the ReturnType
15 | // functions at least for the functionality that we are using: accessing calls &
16 | // managing the mock interface with resets and restores
17 | export type WithMock = F & {
18 | mock: {
19 | calls: {
20 | arguments: unknown[];
21 | }[];
22 |
23 | mockImplementation: (fn: AnyFn) => void;
24 | mockImplementationOnce: (fn: AnyFn) => void;
25 | resetCalls: () => void;
26 | restore: () => void;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/packages/dev/scripts/polkadot-ci-ghpages-force.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | // Copyright 2017-2025 @polkadot/dev authors & contributors
3 | // SPDX-License-Identifier: Apache-2.0
4 |
5 | import fs from 'node:fs';
6 |
7 | import { execGit, logBin } from './util.mjs';
8 |
9 | logBin('polkadot-ci-ghpages-force');
10 |
11 | // ensure we are on master
12 | execGit('checkout master');
13 |
14 | // checkout latest
15 | execGit('fetch');
16 | execGit('checkout gh-pages');
17 | execGit('pull');
18 | execGit('checkout --orphan gh-pages-temp');
19 |
20 | // ignore relevant files
21 | fs.writeFileSync('.gitignore', `
22 | .github/
23 | .vscode/
24 | .yarn/
25 | build/
26 | coverage/
27 | node_modules/
28 | packages/
29 | test/
30 | NOTES.md
31 | `);
32 |
33 | // add
34 | execGit('add -A');
35 | execGit('commit -am "refresh history"');
36 |
37 | // danger, force new
38 | execGit('branch -D gh-pages');
39 | execGit('branch -m gh-pages');
40 | execGit('push -f origin gh-pages');
41 |
42 | // switch to master
43 | execGit('checkout master');
44 |
--------------------------------------------------------------------------------
/packages/dev-test/src/globals.d.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev-test authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | /* eslint-disable no-var */
5 |
6 | import type { expect } from './env/expect.js';
7 | import type { jest } from './env/jest.js';
8 | import type { lifecycle } from './env/lifecycle.js';
9 | import type { suite } from './env/suite.js';
10 |
11 | type Expect = ReturnType;
12 |
13 | type Jest = ReturnType;
14 |
15 | type Lifecycle = ReturnType;
16 |
17 | type Suite = ReturnType;
18 |
19 | declare global {
20 | var after: Lifecycle['after'];
21 | var afterAll: Lifecycle['afterAll'];
22 | var afterEach: Lifecycle['afterEach'];
23 | var before: Lifecycle['before'];
24 | var beforeAll: Lifecycle['beforeAll'];
25 | var beforeEach: Lifecycle['beforeEach'];
26 | var describe: Suite['describe'];
27 | var expect: Expect['expect'];
28 | var it: Suite['it'];
29 | var jest: Jest['jest'];
30 | }
31 |
32 | export {};
33 |
--------------------------------------------------------------------------------
/scripts/all-bump-berry.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # Copyright 2017-2025 @polkadot/dev authors & contributors
3 | # SPDX-License-Identifier: Apache-2.0
4 |
5 | # This scripts updates and aligns the version of yarn berry used. It follows
6 | # the following approach -
7 | #
8 | # 1. Updates the version of yarn berry in the dev project
9 | # 2. Performs an install in dev to upgrade the locks/plugins
10 | # 3. Loops through each of the polkadot-js projects, copying the
11 | # config from dev
12 |
13 | DIRECTORIES=( "wasm" "common" "api" "docs" "ui" "phishing" "extension" "tools" "apps" )
14 |
15 | # update to latest inside dev
16 | cd dev
17 | echo "*** Updating yarn in dev"
18 | git pull
19 | yarn set version latest
20 | yarn
21 | cd ..
22 |
23 | # update all our existing polkadot-js projects
24 | for PKG in "${DIRECTORIES[@]}"; do
25 | echo "*** Updating yarn in $PKG"
26 | cd $PKG
27 | git pull
28 | rm -rf .yarn/plugins .yarn/releases
29 | cp -R ../dev/.yarn/plugins .yarn
30 | cp -R ../dev/.yarn/releases .yarn
31 | cat ../dev/.yarnrc.yml > .yarnrc.yml
32 | yarn
33 | cd ..
34 | done
35 |
--------------------------------------------------------------------------------
/CONTRIBUTORS:
--------------------------------------------------------------------------------
1 | 1382 Jaco Bump deps (#1134)
2 | 19 Tarik Gul Write the Buffer Import to necessary Deno build files (#1156)
3 | 2 Arjun Porwal Fix CI issue (#1159)
4 | 2 Nikos Kontakis Add rollup dynamic import variables plugin (#789)
5 | 1 Alex Saft Fix Vite build bundling error about EISDIR on `new URL('.', import.meta.url)` (#637)
6 | 1 Alex Wang support build when using lerna with on package (#214)
7 | 1 Amaury Martiny Allow .spec.ts files (#143)
8 | 1 Axel Chalon Include dotfiles when pushing to gh-pages (#268)
9 | 1 Ewa Kowalska Add fix flag to eslint script (#419)
10 | 1 Nazar Mokrynskyi Remove redundant babel plugins (#501)
11 | 1 rajk93 build(dev): ignore removing private fields (#1157)
12 | 1 Stefanie Doll Added react-hot-loader to 'dev-react' config (#191)
13 | 1 StefansArya Change bundle configuration to UMD (#557)
--------------------------------------------------------------------------------
/packages/dev/scripts/polkadot-dev-copy-dir.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | // Copyright 2017-2025 @polkadot/dev authors & contributors
3 | // SPDX-License-Identifier: Apache-2.0
4 |
5 | import { copyDirSync, exitFatal, logBin } from './util.mjs';
6 |
7 | const argv = process.argv.slice(2);
8 | const args = [];
9 | let cd = '';
10 | let flatten = false;
11 |
12 | for (let i = 0; i < argv.length; i++) {
13 | switch (argv[i]) {
14 | case '--cd':
15 | cd = argv[++i];
16 | break;
17 | case '--flatten':
18 | flatten = true;
19 | break;
20 | default:
21 | args.push(argv[i]);
22 | break;
23 | }
24 | }
25 |
26 | const sources = args.slice(0, args.length - 1);
27 | const dest = args[args.length - 1];
28 |
29 | logBin('polkadot-dev-copy-dir');
30 |
31 | if (!sources || !dest) {
32 | exitFatal('Expected at least one ... and one argument');
33 | }
34 |
35 | sources.forEach((src) =>
36 | copyDirSync(
37 | cd
38 | ? `${cd}/${src}`
39 | : src,
40 | cd
41 | ? `${cd}/${dest}${flatten ? '' : `/${src}`}`
42 | : `${dest}${flatten ? '' : `/${src}`}`
43 | )
44 | );
45 |
--------------------------------------------------------------------------------
/packages/dev/src/rootJs/Clazz.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | export class Clazz {
5 | #something = 123_456_789;
6 |
7 | readonly and: number;
8 |
9 | static staticProperty = 'foobar';
10 | static staticFunction = (): string|null => Clazz.staticProperty;
11 |
12 | /**
13 | * @param and the number we should and with
14 | */
15 | constructor (and: number) {
16 | this.and = and;
17 | this.#something = this.#something & and;
18 | }
19 |
20 | get something (): number {
21 | return this.#something;
22 | }
23 |
24 | async doAsync (): Promise {
25 | const res = await new Promise((resolve) => resolve(true));
26 |
27 | console.log(res);
28 |
29 | return res;
30 | }
31 |
32 | /**
33 | * @description Sets something to something
34 | * @param something The addition
35 | */
36 | setSomething = (something?: number): number => {
37 | this.#something = (something ?? 123_456) & this.and;
38 |
39 | return this.#something;
40 | };
41 |
42 | toString (): string {
43 | return `something=${this.#something}`;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/packages/dev-ts/src/common.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev-ts authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import type { LoaderOptions } from './types.js';
5 |
6 | import path from 'node:path';
7 | import process from 'node:process';
8 | import { pathToFileURL } from 'node:url';
9 |
10 | /** The path we are being executed from */
11 | export const CWD_PATH = process.cwd();
12 |
13 | /** The cwd path we are being executed from in URL form */
14 | export const CWD_URL = pathToFileURL(`${CWD_PATH}/`);
15 |
16 | /** The root path to node_modules (assuming it is in the root) */
17 | export const MOD_PATH = path.join(CWD_PATH, 'node_modules');
18 |
19 | /** List of allowed extensions for mappings */
20 | export const EXT_TS_ARRAY = ['.ts', '.tsx'];
21 |
22 | /** RegEx for files that we support via this loader */
23 | export const EXT_TS_REGEX = /\.tsx?$/;
24 |
25 | /** RegEx for matching JS files (imports map to TS) */
26 | export const EXT_JS_REGEX = /\.jsx?$/;
27 |
28 | /** RegEx for json files (as actually aliassed in polkadot-js) */
29 | export const EXT_JSON_REGEX = /\.json$/;
30 |
31 | /** Options for loader config */
32 | export const loaderOptions: LoaderOptions = {};
33 |
--------------------------------------------------------------------------------
/packages/dev/scripts/polkadot-dev-run-lint.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | // Copyright 2017-2025 @polkadot/dev authors & contributors
3 | // SPDX-License-Identifier: Apache-2.0
4 |
5 | import process from 'node:process';
6 | import yargs from 'yargs';
7 |
8 | import { __dirname, execPm, GITHUB_REPO, logBin } from './util.mjs';
9 |
10 | const TS_CONFIG_BUILD = true;
11 |
12 | logBin('polkadot-dev-run-lint');
13 |
14 | // Since yargs can also be a promise, we just relax the type here completely
15 | const argv = await yargs(process.argv.slice(2))
16 | .options({
17 | 'skip-eslint': {
18 | description: 'Skips running eslint',
19 | type: 'boolean'
20 | },
21 | 'skip-tsc': {
22 | description: 'Skips running tsc',
23 | type: 'boolean'
24 | }
25 | })
26 | .strict()
27 | .argv;
28 |
29 | if (!argv['skip-eslint']) {
30 | // We don't want to run with fix on CI
31 | const extra = GITHUB_REPO
32 | ? ''
33 | : '--fix';
34 |
35 | execPm(`polkadot-exec-eslint ${extra} ${process.cwd()}`);
36 | }
37 |
38 | if (!argv['skip-tsc']) {
39 | execPm(`polkadot-exec-tsc --noEmit --emitDeclarationOnly false --pretty${TS_CONFIG_BUILD ? ' --project tsconfig.build.json' : ''}`);
40 | }
41 |
--------------------------------------------------------------------------------
/packages/dev/config/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | /**
3 | * There uses the strictest configs as the base
4 | * https://github.com/tsconfig/bases/blob/f674fa6cbca17062ff02511b02872f8729a597ec/bases/strictest.json
5 | */
6 | "extends": "@tsconfig/strictest/tsconfig.json",
7 | "compilerOptions": {
8 | /**
9 | * Aligns with packages/dev/scripts/polkadot-dev-build-ts & packages/dev-ts/src/loader
10 | * (target here is specifically tied to the minimum supported Node version)
11 | */
12 | "module": "nodenext",
13 | "moduleResolution": "nodenext",
14 | "target": "es2022",
15 |
16 | /**
17 | * Specific compilation configs for polkadot-js projects as it is used
18 | * (we only compile *.d.ts via the tsc command-line)
19 | */
20 | "declaration": true,
21 | "emitDeclarationOnly": true,
22 | "jsx": "preserve",
23 | "verbatimModuleSyntax": true,
24 |
25 | /**
26 | * These appear in strictest, however we don't (yet) use them. For the most part it means
27 | * that we actually do have a large number of these lurking (especially on index checks)
28 | */
29 | "exactOptionalPropertyTypes": false,
30 | "noUncheckedIndexedAccess": false,
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/dev-test/README.md:
--------------------------------------------------------------------------------
1 | # @polkadot/dev-test
2 |
3 | This is a very basic Jest-compatible environment that could be used alongside tests. The need for this came from replacing Jest with `node --test` without rewriting all assertions.
4 |
5 | It provides the following -
6 |
7 | 1. Browser `window`, `document`, `navigator` (see usage for browser-specific path)
8 | 2. `jest` functions, specifically `spyOn` (not comprehensive, some will error, some witll noop)
9 | 3. `expect` functions (not comprehensive, caters for specific polkadot-js usage)
10 |
11 |
12 | ## Usage
13 |
14 | On thing to note is that `node:test` is still rapidly evolving - this includes the APIs and features. As such this requires at least Node 18.14, however 18.15+ is recommended.
15 |
16 | The entry points are different based on the environment you would like to operate in. For a browser-like environment,
17 |
18 | ```
19 | node --require @polkadot/dev-test/browser ...
20 | ```
21 |
22 | or for a basic describe/expect/jest-only global environment
23 |
24 | ```
25 | node --require @polkadot/dev-test/node ...
26 | ```
27 |
28 | The `...` above indicates any additional Node options, for instance a full command could be -
29 |
30 | ```
31 | node --require @polkadot/dev-test/node --test something.test.js
32 | ```
33 |
--------------------------------------------------------------------------------
/packages/dev/src/rootJs/Jsx.tsx:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | // Adapted from https://github.com/testing-library/react-testing-library#basic-example
5 |
6 | import type { Props } from './JsxChild.js';
7 |
8 | import React, { useCallback, useState } from 'react';
9 | import { styled } from 'styled-components';
10 |
11 | import Child from './JsxChild.js';
12 |
13 | function Hidden ({ children, className }: Props): React.ReactElement {
14 | const [isMessageVisible, setMessageVisibility] = useState(false);
15 |
16 | const onShow = useCallback(
17 | (e: React.ChangeEvent) =>
18 | setMessageVisibility(e.target.checked),
19 | []
20 | );
21 |
22 | return (
23 |
24 |
25 |
31 | {isMessageVisible && (
32 | <>
33 | {children}
34 |
35 | >
36 | )}
37 |
38 | );
39 | }
40 |
41 | const StyledDiv = styled.div`
42 | background: red;
43 | `;
44 |
45 | export default React.memo(Hidden);
46 |
--------------------------------------------------------------------------------
/packages/dev/scripts/polkadot-dev-deno-map.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | // Copyright 2017-2025 @polkadot/dev authors & contributors
3 | // SPDX-License-Identifier: Apache-2.0
4 |
5 | import fs from 'node:fs';
6 |
7 | import { DENO_POL_PRE } from './util.mjs';
8 |
9 | const [e, i] = fs
10 | .readdirSync('packages')
11 | .filter((p) => fs.existsSync(`packages/${p}/src/mod.ts`))
12 | .sort()
13 | .reduce((/** @type {[string[], Record]} */ [e, i], p) => {
14 | e.push(`export * as ${p.replace(/-/g, '_')} from '${DENO_POL_PRE}/${p}/mod.ts';`);
15 | i[`${DENO_POL_PRE}/${p}/`] = `./packages/${p}/build-deno/`;
16 |
17 | return [e, i];
18 | }, [[], {}]);
19 |
20 | if (!fs.existsSync('mod.ts')) {
21 | fs.writeFileSync('mod.ts', `// Copyright 2017-${new Date().getFullYear()} @polkadot/dev authors & contributors\n// SPDX-License-Identifier: Apache-2.0\n\n// auto-generated via polkadot-dev-deno-map, do not edit\n\n// This is a Deno file, so we can allow .ts imports
22 | /* eslint-disable import/extensions */\n\n${e.join('\n')}\n`);
23 | }
24 |
25 | if (fs.existsSync('import_map.in.json')) {
26 | const o = JSON.parse(fs.readFileSync('import_map.in.json', 'utf-8'));
27 |
28 | Object
29 | .entries(o.imports)
30 | .forEach(([k, v]) => {
31 | i[k] = v;
32 | });
33 | }
34 |
35 | fs.writeFileSync('import_map.json', JSON.stringify({ imports: i }, null, 2));
36 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Jaco Greeff ",
3 | "bugs": "https://github.com/polkadot-js/dev/issues",
4 | "engines": {
5 | "node": ">=18.14"
6 | },
7 | "homepage": "https://github.com/polkadot-js/dev#readme",
8 | "license": "Apache-2.0",
9 | "packageManager": "yarn@4.6.0",
10 | "private": true,
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/polkadot-js/dev.git"
14 | },
15 | "sideEffects": false,
16 | "type": "module",
17 | "version": "0.84.2",
18 | "versions": {
19 | "git": "0.84.2",
20 | "npm": "0.84.2"
21 | },
22 | "workspaces": [
23 | "packages/*"
24 | ],
25 | "scripts": {
26 | "build": "polkadot-dev-build-ts",
27 | "build:before": "polkadot-dev-copy-dir --cd packages/dev config scripts build",
28 | "build:release": "polkadot-ci-ghact-build --skip-beta",
29 | "clean": "polkadot-dev-clean-build",
30 | "docs": "polkadot-dev-build-docs",
31 | "lint": "polkadot-dev-run-lint",
32 | "postinstall": "./packages/dev/scripts/polkadot-dev-yarn-only.mjs",
33 | "test": "yarn build && polkadot-dev-run-test --dev-build --env browser"
34 | },
35 | "devDependencies": {
36 | "@polkadot/dev": "workspace:packages/dev",
37 | "@polkadot/dev-test": "workspace:packages/dev-test",
38 | "@polkadot/dev-ts": "workspace:packages/dev-ts"
39 | },
40 | "resolutions": {
41 | "typescript": "^5.5.4"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/packages/dev/scripts/polkadot-dev-clean-build.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | // Copyright 2017-2025 @polkadot/dev authors & contributors
3 | // SPDX-License-Identifier: Apache-2.0
4 |
5 | import fs from 'node:fs';
6 | import path from 'node:path';
7 |
8 | import { logBin, PATHS_BUILD, rimrafSync } from './util.mjs';
9 |
10 | const PKGS = path.join(process.cwd(), 'packages');
11 | const DIRS = PATHS_BUILD.map((d) => `build${d}`);
12 |
13 | logBin('polkadot-dev-clean-build');
14 |
15 | /**
16 | * @internal
17 | *
18 | * Retrieves all the files containing tsconfig.*.tsbuildinfo contained withing the directory
19 | *
20 | * @param {string} dir
21 | * @returns {string[]}
22 | */
23 | function getPaths (dir) {
24 | if (!fs.existsSync(dir)) {
25 | return [];
26 | }
27 |
28 | return fs
29 | .readdirSync(dir)
30 | .reduce((all, p) => {
31 | if (p.startsWith('tsconfig.') && p.endsWith('.tsbuildinfo')) {
32 | all.push(path.join(dir, p));
33 | }
34 |
35 | return all;
36 | }, DIRS.map((p) => path.join(dir, p)));
37 | }
38 |
39 | /**
40 | * @internal
41 | *
42 | * Removes all the specified directories
43 | *
44 | * @param {string[]} dirs
45 | */
46 | function cleanDirs (dirs) {
47 | dirs.forEach((d) => rimrafSync(d));
48 | }
49 |
50 | cleanDirs(getPaths(process.cwd()));
51 |
52 | if (fs.existsSync(PKGS)) {
53 | cleanDirs(getPaths(PKGS));
54 | cleanDirs(
55 | fs
56 | .readdirSync(PKGS)
57 | .map((f) => path.join(PKGS, f))
58 | .filter((f) => fs.statSync(f).isDirectory())
59 | .reduce((/** @type {string[]} */ res, d) => res.concat(getPaths(d)), [])
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/packages/dev/scripts/polkadot-dev-copy-to.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | // Copyright 2017-2025 @polkadot/dev authors & contributors
3 | // SPDX-License-Identifier: Apache-2.0
4 |
5 | import fs from 'node:fs';
6 | import path from 'node:path';
7 |
8 | import { copyDirSync, execPm, exitFatal, logBin, mkdirpSync, rimrafSync } from './util.mjs';
9 |
10 | const args = process.argv.slice(2);
11 |
12 | logBin('polkadot-dev-copy-to');
13 |
14 | if (args.length !== 1) {
15 | exitFatal('Expected one argument');
16 | }
17 |
18 | const dest = path.join(process.cwd(), '..', args[0], 'node_modules');
19 |
20 | if (!fs.existsSync(dest)) {
21 | exitFatal('Destination node_modules folder does not exist');
22 | }
23 |
24 | // build to ensure we actually have latest
25 | execPm('build');
26 |
27 | // map across what is available and copy it
28 | fs
29 | .readdirSync('packages')
30 | .map((dir) => {
31 | const pkgPath = path.join(process.cwd(), 'packages', dir);
32 |
33 | return [pkgPath, path.join(pkgPath, 'package.json')];
34 | })
35 | .filter(([, jsonPath]) => fs.existsSync(jsonPath))
36 | .map(([pkgPath, json]) => [JSON.parse(fs.readFileSync(json, 'utf8')).name, pkgPath])
37 | .forEach(([name, pkgPath]) => {
38 | console.log(`*** Copying ${name} to ${dest}`);
39 |
40 | const outDest = path.join(dest, name);
41 |
42 | // remove the destination
43 | rimrafSync(outDest);
44 |
45 | // create the root
46 | mkdirpSync(outDest);
47 |
48 | // copy the build output
49 | copyDirSync(path.join(pkgPath, 'build'), outDest);
50 |
51 | // copy node_modules, as available
52 | copyDirSync(path.join(pkgPath, 'node_modules'), path.join(outDest, 'node_modules'));
53 | });
54 |
--------------------------------------------------------------------------------
/packages/dev-test/src/env/suite.spec.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev-test authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | describe('describe()', () => {
5 | // eslint-disable-next-line jest/no-focused-tests
6 | describe.only('.only', () => {
7 | it('runs this one', () => {
8 | expect(true).toBe(true);
9 | });
10 | });
11 |
12 | describe('.skip', () => {
13 | // eslint-disable-next-line jest/no-disabled-tests
14 | describe.skip('.only (.skip)', () => {
15 | it('skips inside .only', () => {
16 | expect(true).toBe(true);
17 |
18 | throw new Error('FATAL: This should not run');
19 | });
20 | });
21 | });
22 | });
23 |
24 | describe('it()', () => {
25 | it('has been enhanced', () => {
26 | expect(it.todo).toBeDefined();
27 | });
28 |
29 | it('allows promises', async () => {
30 | expect(await Promise.resolve(true)).toBe(true);
31 | });
32 |
33 | describe('.only', () => {
34 | // eslint-disable-next-line jest/no-focused-tests
35 | it.only('runs this test when .only is used', () => {
36 | expect(true).toBe(true);
37 | });
38 |
39 | // eslint-disable-next-line jest/no-disabled-tests
40 | it.skip('skips when .skip is used', () => {
41 | expect(true).toBe(true);
42 |
43 | throw new Error('FATAL: This should not run');
44 | });
45 | });
46 |
47 | describe('.skip', () => {
48 | // eslint-disable-next-line jest/no-disabled-tests
49 | it.skip('skips when .skip is used', () => {
50 | expect(true).toBe(true);
51 |
52 | throw new Error('FATAL: This should not run');
53 | });
54 | });
55 |
56 | describe('.todo', () => {
57 | it.todo('marks as a todo when .todo is used', () => {
58 | expect(true).toBe(true);
59 | });
60 | });
61 | });
62 |
--------------------------------------------------------------------------------
/packages/dev-test/src/env/suite.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev-test authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { describe, it } from 'node:test';
5 |
6 | import { enhanceObj } from '../util.js';
7 |
8 | interface WrapOpts {
9 | only?: boolean;
10 | skip?: boolean;
11 | todo?: boolean;
12 | }
13 |
14 | type WrapFn = (name: string, options: { only?: boolean; skip?: boolean; timeout?: number; todo?: boolean; }, fn: () => void | Promise) => void | Promise;
15 |
16 | const MINUTE = 60 * 1000;
17 |
18 | /**
19 | * @internal
20 | *
21 | * Wraps either describe or it with relevant .only, .skip, .todo & .each helpers,
22 | * shimming it into a Jest-compatible environment.
23 | *
24 | * @param {} fn
25 | */
26 | function createWrapper (fn: T, defaultTimeout: number) {
27 | const wrap = (opts: WrapOpts) => (name: string, exec: () => void | Promise, timeout?: number) => fn(name, { ...opts, timeout: (timeout || defaultTimeout) }, exec) as unknown as void;
28 |
29 | // Ensure that we have consistent helpers on the function. These are not consistently
30 | // applied accross all node:test versions, latest has all, so always apply ours.
31 | // Instead of node:test options for e.g. timeout, we provide a Jest-compatible signature
32 | return enhanceObj(wrap({}), {
33 | only: wrap({ only: true }),
34 | skip: wrap({ skip: true }),
35 | todo: wrap({ todo: true })
36 | });
37 | }
38 |
39 | /**
40 | * This ensures that the describe and it functions match our actual usages.
41 | * This includes .only, .skip and .todo helpers (.each is not applied)
42 | **/
43 | export function suite () {
44 | return {
45 | describe: createWrapper(describe, 60 * MINUTE),
46 | it: createWrapper(it, 2 * MINUTE)
47 | };
48 | }
49 |
--------------------------------------------------------------------------------
/packages/dev/src/rootJs/index.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | /** This should appear as-is in the output with: 1. extension added, 2. augmented.d.ts correct */
5 | import './augmented.js';
6 |
7 | /** This import should appear as-in in the ouput (cjs without asserts) */
8 | import testJson from '@polkadot/dev/rootJs/testJson.json' assert { type: 'json' };
9 |
10 | /** Double double work, i.e. re-exports */
11 | export { Clazz } from './Clazz.js';
12 |
13 | /** Function to ensure that BigInt does not have the Babel Math.pow() transform */
14 | export function bigIntExp (): bigint {
15 | // 123_456n * 137_858_491_849n
16 | return 123_456_789n * (13n ** 10n);
17 | }
18 |
19 | /** Function to ensure that dynamic imports work */
20 | export async function dynamic (a: number, b: number): Promise {
21 | // NOTE we go via this path so it points to the same location in both ESM
22 | // and CJS output (a './dynamic' import would be different otherwise)
23 | const { sum } = await import('@polkadot/dev/rootJs/dynamic.mjs');
24 |
25 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return
26 | return sum(a, b);
27 | }
28 |
29 | /** Function to ensure we have json correctly imported */
30 | export function json (): string {
31 | return testJson.test.json;
32 | }
33 |
34 | /** Check support for the ?? operator */
35 | export function jsOpExp (a?: number): number {
36 | const defaults = {
37 | a: 42,
38 | b: 43,
39 | c: 44
40 | };
41 |
42 | return a ?? defaults.a;
43 | }
44 |
45 | /** This is an actual check to ensure PURE is all-happy */
46 | export const pureOpExp = /*#__PURE__*/ jsOpExp();
47 |
48 | const fooA = 1;
49 | const fooB = 2;
50 | const fooC = 3;
51 | const fooD = 4;
52 |
53 | export { fooA, fooB, fooC, fooD };
54 |
--------------------------------------------------------------------------------
/packages/dev/src/rootTests.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | ///
5 |
6 | /* global describe, expect, it */
7 |
8 | import type * as testRoot from './root.js';
9 |
10 | export function runTests ({ Clazz, TEST_PURE, bigIntExp, dynamic, jsOpExp, json }: typeof testRoot): void {
11 | describe('Clazz', (): void => {
12 | it('has staticProperty', (): void => {
13 | expect(Clazz.staticProperty).toBe('foobar');
14 | });
15 |
16 | it('creates an instance with get/set', (): void => {
17 | const c = new Clazz(456);
18 |
19 | expect(c.something).toBe(123_456_789 & 456);
20 |
21 | c.setSomething(123);
22 |
23 | expect(c.something).toBe(123 & 456);
24 | });
25 | });
26 |
27 | describe('TEST_PURE', (): void => {
28 | it('should have the correct value', (): void => {
29 | expect(TEST_PURE).toBe('testRoot');
30 | });
31 | });
32 |
33 | describe('dynamic()', (): void => {
34 | it('should allow dynamic import usage', async (): Promise => {
35 | expect(await dynamic(5, 37)).toBe(42);
36 | });
37 | });
38 |
39 | describe('bigIntExp()', (): void => {
40 | it('should return the correct value', (): void => {
41 | expect(bigIntExp()).toBe(123_456_789n * 137_858_491_849n);
42 | });
43 | });
44 |
45 | describe('jsOpExp', (): void => {
46 | it('handles 0 ?? 42 correctly', (): void => {
47 | expect(jsOpExp(0)).toBe(0);
48 | });
49 |
50 | it('handles undefined ?? 42 correctly', (): void => {
51 | expect(jsOpExp()).toBe(42);
52 | });
53 | });
54 |
55 | describe('json()', (): void => {
56 | it('should return the correct value', (): void => {
57 | expect(json()).toBe('works');
58 | });
59 | });
60 | }
61 |
--------------------------------------------------------------------------------
/packages/dev-test/src/util.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev-test authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import type { BaseFn, BaseObj, StubFn } from './types.js';
5 |
6 | /**
7 | * Extends an existing object with the additional function if they
8 | * are not already existing.
9 | */
10 | export function enhanceObj (obj: T, extra: X) {
11 | Object
12 | .entries(extra as Record)
13 | .forEach(([key, value]) => {
14 | (obj as Record)[key] ??= value;
15 | });
16 |
17 | return obj as T & Omit;
18 | }
19 |
20 | /**
21 | * @internal
22 | *
23 | * A helper to create a stub object based wite the stub creator supplied
24 | */
25 | function createStub (keys: N, creator: (key: string) => StubFn) {
26 | return keys.reduce>((obj, key) => {
27 | obj[key] ??= creator(key);
28 |
29 | return obj;
30 | }, {}) as unknown as { [K in N[number]]: StubFn };
31 | }
32 |
33 | /**
34 | * Extends a given object with the named functions if they do not
35 | * already exist on the object.
36 | *
37 | * @type {StubObjFn}
38 | */
39 | export function stubObj (objName: string, keys: N, alts?: Record) {
40 | return createStub(keys, (key) => () => {
41 | const alt = alts?.[key];
42 |
43 | throw new Error(`${objName}.${key} has not been implemented${alt ? ` (Use ${alt} instead)` : ''}`);
44 | });
45 | }
46 |
47 | /**
48 | * Extends a given object with the named functions if they do not
49 | * already exist on the object.
50 | *
51 | * @type {StubObjFn}
52 | */
53 | export function warnObj (objName: string, keys: N) {
54 | return createStub(keys, (key) => () => {
55 | console.warn(`${objName}.${key} has been implemented as a noop`);
56 | });
57 | }
58 |
--------------------------------------------------------------------------------
/scripts/all-update.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # Copyright 2017-2025 @polkadot/dev authors & contributors
3 | # SPDX-License-Identifier: Apache-2.0
4 |
5 | # This scripts updates all the inter polkadot-js dependencies. To do so it
6 | # creates a list of all the packages available and then loops through the
7 | # package.json files in the various repos, upgrading the dependecies as found.
8 | #
9 | # In this upgrading step is uses the local all-deps.js script
10 | #
11 | # In addition it also cleans old stale local branches, and performs an overall
12 | # dedupe - all maintenence operations.
13 |
14 | DIRECTORIES=( "dev" "wasm" "common" "api" "docs" "ui" "phishing" "extension" "tools" "apps" )
15 |
16 | for REPO in "${DIRECTORIES[@]}"; do
17 | if [ "$REPO" != "" ] && [ -d "./$REPO/.git" ]; then
18 | echo ""
19 | echo "*** Removing branches in $REPO"
20 |
21 | cd $REPO
22 |
23 | CURRENT=$(git rev-parse --abbrev-ref HEAD)
24 | BRANCHES=( $(git branch | awk -F ' +' '! /\(no branch\)/ {print $2}') )
25 |
26 | if [ "$CURRENT" = "master" ]; then
27 | git fetch
28 | git pull
29 | else
30 | echo "$CURRENT !== master"
31 | fi
32 |
33 | for BRANCH in "${BRANCHES[@]}"; do
34 | if [ "$BRANCH" != "$CURRENT" ] && [ "$BRANCH" != "master" ]; then
35 | git branch -d -f $BRANCH
36 | fi
37 | done
38 |
39 | git prune
40 | git gc
41 |
42 | cd ..
43 | fi
44 | done
45 |
46 | echo ""
47 | echo "*** Updating inter-package-deps"
48 |
49 | ./dev/scripts/all-deps.js
50 |
51 | echo ""
52 | echo "*** Installing updated packages"
53 |
54 | for REPO in "${DIRECTORIES[@]}"; do
55 | if [ "$REPO" != "" ] && [ -d "./$REPO/.git" ]; then
56 | echo ""
57 | cd $REPO
58 |
59 | DETACHED=$(git status | grep "HEAD detached")
60 |
61 | if [ "$DETACHED" != "" ]; then
62 | echo "*** Resetting $REPO"
63 |
64 | git reset --hard
65 | else
66 | echo "*** Installing $REPO"
67 |
68 | yarn install
69 | yarn dedupe
70 | fi
71 |
72 | cd ..
73 | fi
74 | done
75 |
--------------------------------------------------------------------------------
/packages/dev-test/src/util.spec.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev-test authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | ///
5 |
6 | import { enhanceObj, stubObj, warnObj } from './util.js';
7 |
8 | describe('enhanceObj', () => {
9 | it('extends objects with non-existing values', () => {
10 | const test = enhanceObj(
11 | enhanceObj(
12 | { a: () => 1 },
13 | { b: () => 2 }
14 | ),
15 | { c: () => 3 }
16 | );
17 |
18 | expect(test.a()).toBe(1);
19 | expect(test.b()).toBe(2);
20 | expect(test.c()).toBe(3);
21 | });
22 |
23 | it('does not override existing values', () => {
24 | const test = enhanceObj(
25 | enhanceObj(
26 | { a: 0, b: () => 1 },
27 | { a: () => 0, b: () => 2 }
28 | ),
29 | { c: () => 2 }
30 | );
31 |
32 | expect(test.a).toBe(0);
33 | expect(test.b()).toBe(1);
34 | expect(test.c()).toBe(2);
35 | });
36 | });
37 |
38 | describe('stubObj', () => {
39 | it('has entries throwing for unimplemented values', () => {
40 | const test = stubObj('obj', ['a', 'b'] as const);
41 |
42 | expect(
43 | () => test.b()
44 | ).toThrow('obj.b has not been implemented');
45 | });
46 |
47 | it('has entries throwing for unimplemented values (w/ alternatives)', () => {
48 | const test = stubObj('obj', ['a', 'b'] as const, { b: 'obj.a' });
49 |
50 | expect(
51 | () => test.b()
52 | ).toThrow('obj.b has not been implemented (Use obj.a instead)');
53 | });
54 | });
55 |
56 | describe('warnObj', () => {
57 | let spy: ReturnType;
58 |
59 | beforeEach(() => {
60 | spy = jest.spyOn(console, 'warn');
61 | });
62 |
63 | afterEach(() => {
64 | spy.mockRestore();
65 | });
66 |
67 | it('has entries warning on unimplemented', () => {
68 | const test = warnObj('obj', ['a', 'b'] as const);
69 |
70 | test.b();
71 |
72 | expect(spy).toHaveBeenCalledWith('obj.b has been implemented as a noop');
73 | });
74 | });
75 |
--------------------------------------------------------------------------------
/packages/dev/scripts/polkadot-dev-contrib.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | // Copyright 2017-2025 @polkadot/dev authors & contributors
3 | // SPDX-License-Identifier: Apache-2.0
4 |
5 | import fs from 'node:fs';
6 |
7 | import { execGit, logBin, mkdirpSync } from './util.mjs';
8 |
9 | const tmpDir = 'packages/build';
10 | const tmpFile = `${tmpDir}/CONTRIBUTORS`;
11 |
12 | logBin('polkadot-dev-contrib');
13 |
14 | mkdirpSync(tmpDir);
15 | execGit(`shortlog master -e -n -s > ${tmpFile}`);
16 |
17 | fs.writeFileSync(
18 | 'CONTRIBUTORS',
19 | Object
20 | .entries(
21 | fs
22 | .readFileSync(tmpFile, 'utf-8')
23 | .split('\n')
24 | .map((l) => l.trim())
25 | .filter((l) => !!l)
26 | .reduce((/** @type {Record} */ all, line) => {
27 | const [c, e] = line.split('\t');
28 | const count = parseInt(c, 10);
29 | const [name, rest] = e.split(' <');
30 | const isExcluded = (
31 | ['GitHub', 'Travis CI'].some((n) => name.startsWith(n)) ||
32 | ['>', 'action@github.com>'].some((e) => rest === e) ||
33 | [name, rest].some((n) => n.includes('[bot]'))
34 | );
35 |
36 | if (!isExcluded) {
37 | let [email] = rest.split('>');
38 |
39 | if (!all[email]) {
40 | email = Object.keys(all).find((k) =>
41 | name.includes(' ') &&
42 | all[k].name === name
43 | ) || email;
44 | }
45 |
46 | if (all[email]) {
47 | all[email].count += count;
48 | } else {
49 | all[email] = { count, name };
50 | }
51 | }
52 |
53 | return all;
54 | }, {})
55 | )
56 | .sort((a, b) => {
57 | const diff = b[1].count - a[1].count;
58 |
59 | return diff === 0
60 | ? a[1].name.localeCompare(b[1].name)
61 | : diff;
62 | })
63 | .map(([email, { count, name }], i) => {
64 | execGit(`log master -1 --author=${email} > ${tmpFile}-${i}`);
65 |
66 | const commit = fs
67 | .readFileSync(`${tmpFile}-${i}`, 'utf-8')
68 | .split('\n')[4]
69 | .trim();
70 |
71 | return `${`${count}`.padStart(8)}\t${name.padEnd(30)}\t${commit}`;
72 | })
73 | .join('\n')
74 | );
75 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | ## What?
4 |
5 | Individuals making significant and valuable contributions are given commit-access to a project to contribute as they see fit.
6 | A project is more like an open wiki than a standard guarded open source project.
7 |
8 | ## Rules
9 |
10 | There are a few basic ground-rules for contributors (including the maintainer(s) of the project):
11 |
12 | 1. **No `--force` pushes** or modifying the Git history in any way. If you need to rebase, ensure you do it in your own repo.
13 | 2. **Non-master branches**, prefixed with a short name moniker (e.g. `-`) must be used for ongoing work.
14 | 3. **All modifications** must be made in a **pull-request** to solicit feedback from other contributors.
15 | 4. A pull-request *must not be merged until CI* has finished successfully.
16 |
17 | #### Merging pull requests once CI is successful:
18 | - A pull request with no large change to logic that is an urgent fix may be merged after a non-author contributor has reviewed it well.
19 | - No PR should be merged until all reviews' comments are addressed.
20 |
21 | #### Reviewing pull requests:
22 | When reviewing a pull request, the end-goal is to suggest useful changes to the author. Reviews should finish with approval unless there are issues that would result in:
23 |
24 | - Buggy behaviour.
25 | - Undue maintenance burden.
26 | - Breaking with house coding style.
27 | - Pessimisation (i.e. reduction of speed as measured in the projects benchmarks).
28 | - Feature reduction (i.e. it removes some aspect of functionality that a significant minority of users rely on).
29 | - Uselessness (i.e. it does not strictly add a feature or fix a known issue).
30 |
31 | #### Reviews may not be used as an effective veto for a PR because:
32 | - There exists a somewhat cleaner/better/faster way of accomplishing the same feature/fix.
33 | - It does not fit well with some other contributors' longer-term vision for the project.
34 |
35 | ## Releases
36 |
37 | Declaring formal releases remains the prerogative of the project maintainer(s).
38 |
39 | ## Changes to this arrangement
40 |
41 | This is an experiment and feedback is welcome! This document may also be subject to pull-requests or changes by contributors where you believe you have something valuable to add or change.
42 |
43 | ## Heritage
44 |
45 | These contributing guidelines are modified from the "OPEN Open Source Project" guidelines for the Level project: [https://github.com/Level/community/blob/master/CONTRIBUTING.md](https://github.com/Level/community/blob/master/CONTRIBUTING.md)
46 |
--------------------------------------------------------------------------------
/packages/dev-ts/src/loader.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev-ts authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import crypto from 'node:crypto';
5 | import fs from 'node:fs';
6 | import path from 'node:path';
7 | import { fileURLToPath } from 'node:url';
8 | import ts from 'typescript';
9 |
10 | import { EXT_TS_REGEX, loaderOptions } from './common.js';
11 |
12 | interface Loaded {
13 | format: 'commonjs' | 'module';
14 | shortCircuit?: boolean;
15 | source: string;
16 | }
17 |
18 | type NexLoad = (url: string, context: Record) => Promise;
19 |
20 | /**
21 | * Load all TypeScript files, compile via tsc on-the-fly
22 | **/
23 | export async function load (url: string, context: Record, nextLoad: NexLoad): Promise {
24 | if (EXT_TS_REGEX.test(url)) {
25 | // used the chained loaders to retrieve
26 | const { source } = await nextLoad(url, {
27 | ...context,
28 | format: 'module'
29 | });
30 |
31 | // we use a hash of the source to determine caching
32 | const sourceHash = `//# sourceHash=${crypto.createHash('sha256').update(source).digest('hex')}`;
33 | const compiledFile = url.includes('/src/')
34 | ? fileURLToPath(
35 | url
36 | .replace(/\.tsx?$/, '.js')
37 | .replace('/src/', '/build-loader/')
38 | )
39 | : null;
40 |
41 | if (loaderOptions.isCached && compiledFile && fs.existsSync(compiledFile)) {
42 | const compiled = fs.readFileSync(compiledFile, 'utf-8');
43 |
44 | if (compiled.includes(sourceHash)) {
45 | return {
46 | format: 'module',
47 | source: compiled
48 | };
49 | }
50 | }
51 |
52 | // compile via typescript
53 | const { outputText } = ts.transpileModule(source.toString(), {
54 | compilerOptions: {
55 | ...(
56 | url.endsWith('.tsx')
57 | ? { jsx: ts.JsxEmit.ReactJSX }
58 | : {}
59 | ),
60 | esModuleInterop: true,
61 | importHelpers: true,
62 | inlineSourceMap: true,
63 | module: ts.ModuleKind.ESNext,
64 | moduleResolution: ts.ModuleResolutionKind.NodeNext,
65 | skipLibCheck: true,
66 | // Aligns with packages/dev/scripts/polkadot-dev-build-ts & packages/dev/config/tsconfig
67 | target: ts.ScriptTarget.ES2022
68 | },
69 | fileName: fileURLToPath(url)
70 | });
71 |
72 | if (loaderOptions.isCached && compiledFile) {
73 | const compiledDir = path.dirname(compiledFile);
74 |
75 | if (!fs.existsSync(compiledDir)) {
76 | fs.mkdirSync(compiledDir, { recursive: true });
77 | }
78 |
79 | fs.writeFileSync(compiledFile, `${outputText}\n${sourceHash}`, 'utf-8');
80 | }
81 |
82 | return {
83 | format: 'module',
84 | source: outputText
85 | };
86 | }
87 |
88 | return nextLoad(url, context);
89 | }
90 |
--------------------------------------------------------------------------------
/packages/dev-ts/src/testLoader.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev-ts authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import crypto from 'node:crypto';
5 | import fs from 'node:fs';
6 | import path from 'node:path';
7 | import { fileURLToPath } from 'node:url';
8 | import ts from 'typescript';
9 |
10 | import { EXT_TS_REGEX, loaderOptions } from './common.js';
11 |
12 | interface Loaded {
13 | format: 'commonjs' | 'module';
14 | shortCircuit?: boolean;
15 | source: string;
16 | }
17 |
18 | type NexLoad = (url: string, context: Record) => Promise;
19 |
20 | /**
21 | * Load all TypeScript files, compile via tsc on-the-fly
22 | **/
23 | export async function load (url: string, context: Record, nextLoad: NexLoad): Promise {
24 | if (EXT_TS_REGEX.test(url)) {
25 | // used the chained loaders to retrieve
26 | const { source } = await nextLoad(url, {
27 | ...context,
28 | format: 'module'
29 | });
30 |
31 | // This ensures there is support for Node v22 while also maintaining backwards compatibility for testing.
32 | const modifiedSrc = Buffer.from(source.toString().replace(/assert\s*\{\s*type:\s*'json'\s*\}/g, 'with { type: \'json\' }'), 'utf-8');
33 |
34 | // we use a hash of the source to determine caching
35 | const sourceHash = `//# sourceHash=${crypto.createHash('sha256').update(modifiedSrc as unknown as string).digest('hex')}`;
36 | const compiledFile = url.includes('/src/')
37 | ? fileURLToPath(
38 | url
39 | .replace(/\.tsx?$/, '.js')
40 | .replace('/src/', '/build-loader/')
41 | )
42 | : null;
43 |
44 | if (loaderOptions.isCached && compiledFile && fs.existsSync(compiledFile)) {
45 | const compiled = fs.readFileSync(compiledFile, 'utf-8');
46 |
47 | if (compiled.includes(sourceHash)) {
48 | return {
49 | format: 'module',
50 | source: compiled
51 | };
52 | }
53 | }
54 |
55 | // compile via typescript
56 | const { outputText } = ts.transpileModule(modifiedSrc.toString(), {
57 | compilerOptions: {
58 | ...(
59 | url.endsWith('.tsx')
60 | ? { jsx: ts.JsxEmit.ReactJSX }
61 | : {}
62 | ),
63 | esModuleInterop: true,
64 | importHelpers: true,
65 | inlineSourceMap: true,
66 | module: ts.ModuleKind.ESNext,
67 | moduleResolution: ts.ModuleResolutionKind.NodeNext,
68 | skipLibCheck: true,
69 | // Aligns with packages/dev/scripts/polkadot-dev-build-ts & packages/dev/config/tsconfig
70 | target: ts.ScriptTarget.ES2022
71 | },
72 | fileName: fileURLToPath(url)
73 | });
74 |
75 | if (loaderOptions.isCached && compiledFile) {
76 | const compiledDir = path.dirname(compiledFile);
77 |
78 | if (!fs.existsSync(compiledDir)) {
79 | fs.mkdirSync(compiledDir, { recursive: true });
80 | }
81 |
82 | fs.writeFileSync(compiledFile, `${outputText}\n${sourceHash}`, 'utf-8');
83 | }
84 |
85 | return {
86 | format: 'module',
87 | source: outputText
88 | };
89 | }
90 |
91 | return nextLoad(url, context);
92 | }
93 |
--------------------------------------------------------------------------------
/packages/dev-ts/README.md:
--------------------------------------------------------------------------------
1 | # @polkadot/dev-ts
2 |
3 | This is an Node TS loader, specifically written to cater for the polkadot-js needs, aka it is meant to be used inside polkadot-js projects. It doesn't aim to be a catch-all resolver, although it does cover quite a large spectrum of functionality.
4 |
5 | It caters for -
6 |
7 | 1. Pass through resolution and compiling of .ts & .tsx sources
8 | 2. Resolution of TS aliases
9 | 3. Resolution of .json files (alongside aliases)
10 | 4. Resolution of extensionless imports (basic, best-effort)
11 |
12 |
13 | ## Usage
14 |
15 | Just add the loader via the Node.js `--loader` option. The API supported here is only for Node 16.12+, so ensure a new-ish LTS version is used.
16 |
17 | ```
18 | node --loader @polkadot/dev-ts ...
19 | ```
20 |
21 | Internally to the polkadot-js libraries, loader caching is used. This means that compiled files are store on-disk alongside the `/src/` folder in `/build-loader/`. To enable caching behavior, the loader endpoint is changed slightly,
22 |
23 | ```
24 | node --loader @polkadot/dev-ts/cached ...
25 | ```
26 |
27 | This is generally the suggested default, but it is only exposed via a different loader endpoint to ensure that users explicitly opt-in and not be suprised by "random output folders" being created.
28 |
29 |
30 | ## Caveats
31 |
32 | The Node.js loader API could change in the future (as it has in the Node.js 16.12 version), so it _may_ break or stop working on newer versions, and obviously won't work at all on older versions. As of this writing (Node.js 18.14 being the most-recent LTS), using the `--loader` option will print a warning.
33 |
34 | With all that said, it is used as-is for the polkadot-js test infrastructure and currently operates without issues in _that_ environment.
35 |
36 | TL;DR Different configs could yield some issues.
37 |
38 |
39 | ## Why
40 |
41 | Yes, there are other options available - [@babel/register](https://babeljs.io/docs/babel-register), [@esbuild-kit/esm-loader](https://github.com/esbuild-kit/esm-loader), [@swc/register](https://github.com/swc-project/register), [@swc-node/loader](https://github.com/swc-project/swc-node/tree/master/packages/loader), [ts-node/esm](https://github.com/TypeStrong/ts-node), ...
42 |
43 | We started off with a basic `swc` loader (after swapping the infrastructure from Jest & Babel), just due to the fact that (at that time, and as of writing still) the base swc loader is still a WIP against the newer loader APIs. Since we didn't want to add more dependencies (and compile differently to our internal compiler infrastructure), we [adapted our own](https://nodejs.org/api/esm.html#esm_transpiler_loader).
44 |
45 | Since then we just swapped to using base `tsc` everywhere (for all builds) and may look at changing again (swc, esbuild. etc...) in the future. So effectively having a single loader, while re-inventing the wheel somewhat (since there seems to be a _lot_ of options available) allows us to just keep the loader compiling options fully aligned with what TS -> JS output approach we take.
46 |
47 | It meets our requirements: aligns fully with the overall configs we accross polkadot-js, compiles to ESM (no CJS used when testing/running) and has minimal dependencies that doesn't add bloat. In most cases you would probably be better off with one of the loaders/registration approaches linked in the first paragraph.
48 |
--------------------------------------------------------------------------------
/packages/dev-test/src/env/jest.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev-test authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import type { AnyFn, WithMock } from '../types.js';
5 |
6 | import { mock } from 'node:test';
7 |
8 | import { enhanceObj, stubObj, warnObj } from '../util.js';
9 |
10 | // logged via Object.keys(jest).sort()
11 | const JEST_KEYS_STUB = ['advanceTimersByTime', 'advanceTimersToNextTimer', 'autoMockOff', 'autoMockOn', 'clearAllMocks', 'clearAllTimers', 'createMockFromModule', 'deepUnmock', 'disableAutomock', 'doMock', 'dontMock', 'enableAutomock', 'fn', 'genMockFromModule', 'getRealSystemTime', 'getSeed', 'getTimerCount', 'isEnvironmentTornDown', 'isMockFunction', 'isolateModules', 'isolateModulesAsync', 'mock', 'mocked', 'now', 'replaceProperty', 'requireActual', 'requireMock', 'resetAllMocks', 'resetModules', 'restoreAllMocks', 'retryTimes', 'runAllImmediates', 'runAllTicks', 'runAllTimers', 'runOnlyPendingTimers', 'setMock', 'setSystemTime', 'setTimeout', 'spyOn', 'unmock', 'unstable_mockModule', 'useFakeTimers', 'useRealTimers'] as const;
12 |
13 | const JEST_KEYS_WARN = ['setTimeout'] as const;
14 |
15 | // logged via Object.keys(jest.fn()).sort()
16 | const MOCK_KEYS_STUB = ['_isMockFunction', 'getMockImplementation', 'getMockName', 'mock', 'mockClear', 'mockImplementation', 'mockImplementationOnce', 'mockName', 'mockRejectedValue', 'mockRejectedValueOnce', 'mockReset', 'mockResolvedValue', 'mockResolvedValueOnce', 'mockRestore', 'mockReturnThis', 'mockReturnValue', 'mockReturnValueOnce', 'withImplementation'] as const;
17 |
18 | const jestStub = stubObj('jest', JEST_KEYS_STUB);
19 | const jestWarn = warnObj('jest', JEST_KEYS_WARN);
20 | const mockStub = stubObj('jest.fn()', MOCK_KEYS_STUB);
21 |
22 | /**
23 | * @internal
24 | *
25 | * This adds the mockReset and mockRestore functionality to any
26 | * spy or mock function
27 | **/
28 | function extendMock (mocked: WithMock) {
29 | // We use the node:test mock here for casting below - however we
30 | // don't want this in any method signature since this is a private
31 | // types export, which could get us in "some" trouble
32 | //
33 | // Effectively the casts below ensure that our WithMock<*> aligns
34 | // on a high-level to what we use via private type...
35 | const spy = (mocked as unknown as ReturnType);
36 |
37 | return enhanceObj(enhanceObj(mocked, {
38 | mockImplementation: (fn: F): void => {
39 | spy.mock.mockImplementation(fn);
40 | },
41 | mockImplementationOnce: (fn: F): void => {
42 | spy.mock.mockImplementationOnce(fn);
43 | },
44 | mockReset: (): void => {
45 | spy.mock.resetCalls();
46 | },
47 | mockRestore: (): void => {
48 | spy.mock.restore();
49 | }
50 | }), mockStub);
51 | }
52 |
53 | /**
54 | * Sets up the jest object. This is certainly not extensive, and probably
55 | * not quite meant to be (never say never). Rather this adds the functionality
56 | * that we use in the polkadot-js projects.
57 | **/
58 | export function jest () {
59 | return {
60 | jest: enhanceObj(enhanceObj({
61 | fn: (fn?: F) => extendMock(mock.fn(fn)),
62 | restoreAllMocks: () => {
63 | mock.reset();
64 | },
65 | spyOn: (obj: object, key: string) => extendMock(mock.method(obj, key as keyof typeof obj))
66 | }, jestWarn), jestStub)
67 | };
68 | }
69 |
--------------------------------------------------------------------------------
/packages/dev-ts/src/resolver.spec.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev-ts authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | ///
5 |
6 | import path from 'node:path';
7 | import { pathToFileURL } from 'node:url';
8 |
9 | import { CWD_PATH } from './common.js';
10 | import { resolveAlias, resolveExtBare, resolveExtJs, resolveExtJson, resolveExtTs } from './resolver.js';
11 |
12 | const ROOT_URL = pathToFileURL(`${CWD_PATH}/`);
13 | const SRC_PATH = 'packages/dev/src';
14 | const SRC_URL = pathToFileURL(`${CWD_PATH}/${SRC_PATH}/`);
15 | const INDEX_PATH = `${SRC_PATH}/index.ts`;
16 | const INDEX_URL = pathToFileURL(INDEX_PATH);
17 |
18 | describe('resolveExtTs', () => {
19 | it('returns no value for a non .{ts, tsx} extension', () => {
20 | expect(
21 | resolveExtTs(`./${SRC_PATH}/cjs/sample.js`, ROOT_URL)
22 | ).not.toBeDefined();
23 | });
24 |
25 | it('returns a correct object for a .ts extension', () => {
26 | expect(
27 | resolveExtTs(INDEX_PATH, ROOT_URL)
28 | ).toEqual({
29 | format: 'module',
30 | shortCircuit: true,
31 | url: INDEX_URL.href
32 | });
33 | });
34 | });
35 |
36 | describe('resolveExtJs', () => {
37 | const modFound = {
38 | format: 'module',
39 | shortCircuit: true,
40 | url: pathToFileURL(`${CWD_PATH}/${SRC_PATH}/mod.ts`).href
41 | };
42 |
43 | it('returns the correct value for ./mod.js resolution', () => {
44 | expect(
45 | resolveExtJs('./mod.js', SRC_URL)
46 | ).toEqual(modFound);
47 | });
48 |
49 | it('returns the correct value for ../mod.js resolution', () => {
50 | expect(
51 | resolveExtJs('../mod.js', pathToFileURL(`${CWD_PATH}/${SRC_PATH}/rootJs/index.ts`))
52 | ).toEqual(modFound);
53 | });
54 |
55 | it('returns a correct object for a .jsx extension', () => {
56 | expect(
57 | resolveExtJs(`./${SRC_PATH}/rootJs/Jsx.jsx`, ROOT_URL)
58 | ).toEqual({
59 | format: 'module',
60 | shortCircuit: true,
61 | url: pathToFileURL(`${SRC_PATH}/rootJs/Jsx.tsx`).href
62 | });
63 | });
64 | });
65 |
66 | describe('resolveExtJson', () => {
67 | it('resolves .json files', () => {
68 | expect(
69 | resolveExtJson('../package.json', SRC_URL)
70 | ).toEqual({
71 | format: 'json',
72 | shortCircuit: true,
73 | url: pathToFileURL(path.join(SRC_PATH, '../package.json')).href
74 | });
75 | });
76 | });
77 |
78 | describe('resolveExtBare', () => {
79 | const indexFound = {
80 | format: 'module',
81 | shortCircuit: true,
82 | url: INDEX_URL.href
83 | };
84 |
85 | it('does not resolve non-relative paths', () => {
86 | expect(
87 | resolveExtBare(INDEX_PATH, ROOT_URL)
88 | ).not.toBeDefined();
89 | });
90 |
91 | it('resolves to the index via .', () => {
92 | expect(
93 | resolveExtBare('.', SRC_URL)
94 | ).toEqual(indexFound);
95 | });
96 |
97 | it('resolves to the index via ./index', () => {
98 | expect(
99 | resolveExtBare('./index', SRC_URL)
100 | ).toEqual(indexFound);
101 | });
102 |
103 | it('resolves to the sub-directory via ./rootJs', () => {
104 | expect(
105 | resolveExtBare('./rootJs', SRC_URL)
106 | ).toEqual({
107 | format: 'module',
108 | shortCircuit: true,
109 | url: pathToFileURL(`${SRC_PATH}/rootJs/index.ts`).href
110 | });
111 | });
112 |
113 | it('resolves to extensionless path', () => {
114 | expect(
115 | resolveExtBare('./packageInfo', SRC_URL)
116 | ).toEqual({
117 | format: 'module',
118 | shortCircuit: true,
119 | url: pathToFileURL(`${SRC_PATH}/packageInfo.ts`).href
120 | });
121 | });
122 | });
123 |
124 | describe('resolveAliases', () => {
125 | it('resolves packageInfo', () => {
126 | expect(
127 | resolveAlias('@polkadot/dev-ts/packageInfo', ROOT_URL)
128 | ).toEqual({
129 | format: 'module',
130 | shortCircuit: true,
131 | url: pathToFileURL('packages/dev-ts/src/packageInfo.ts').href
132 | });
133 | });
134 | });
135 |
--------------------------------------------------------------------------------
/packages/dev/config/rollup.js:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import pluginAlias from '@rollup/plugin-alias';
5 | import pluginCommonjs from '@rollup/plugin-commonjs';
6 | import pluginDynamicImportVars from '@rollup/plugin-dynamic-import-vars';
7 | import pluginInject from '@rollup/plugin-inject';
8 | import pluginJson from '@rollup/plugin-json';
9 | import { nodeResolve as pluginResolve } from '@rollup/plugin-node-resolve';
10 | import fs from 'node:fs';
11 | import path from 'node:path';
12 | import pluginCleanup from 'rollup-plugin-cleanup';
13 |
14 | /** @typedef {{ entries?: Record; external: string[]; globals?: Record; index?: string; inject?: Record; pkg: string; }} BundleDef */
15 | /** @typedef {{ file: string; format: 'umd'; generatedCode: Record; globals: Record; inlineDynamicImports: true; intro: string; name: string; }} BundleOutput */
16 | /** @typedef {{ context: 'global'; external: string[]; input: string; output: BundleOutput; plugins: any[]; }} Bundle */
17 |
18 | /**
19 | * @param {string} pkg
20 | * @returns {string}
21 | */
22 | function sanitizePkg (pkg) {
23 | return pkg.replace('@polkadot/', '');
24 | }
25 |
26 | /**
27 | * @param {string} input
28 | * @returns {string}
29 | */
30 | function createName (input) {
31 | return `polkadot-${sanitizePkg(input)}`
32 | .toLowerCase()
33 | .replace(/[^a-zA-Z0-9]+(.)/g, (_, c) => c.toUpperCase());
34 | }
35 |
36 | /**
37 | * @param {string} pkg
38 | * @param {string} [index]
39 | * @returns {string}
40 | */
41 | export function createInput (pkg, index) {
42 | const partialPath = `packages/${sanitizePkg(pkg)}/build`;
43 |
44 | return `${partialPath}/${
45 | index ||
46 | fs.existsSync(path.join(process.cwd(), partialPath, 'bundle.js'))
47 | ? 'bundle.js'
48 | : (
49 | JSON.parse(fs.readFileSync(path.join(process.cwd(), partialPath, 'package.json'), 'utf8')).browser ||
50 | 'index.js'
51 | )
52 | }`;
53 | }
54 |
55 | /**
56 | *
57 | * @param {string} pkg
58 | * @param {string[]} external
59 | * @param {Record} globals
60 | * @returns {BundleOutput}
61 | */
62 | export function createOutput (pkg, external, globals) {
63 | const name = sanitizePkg(pkg);
64 |
65 | return {
66 | file: `packages/${name}/build/bundle-polkadot-${name}.js`,
67 | format: 'umd',
68 | generatedCode: {
69 | constBindings: true
70 | },
71 | globals: external.reduce((all, p) => ({
72 | [p]: createName(p),
73 | ...all
74 | }), { ...globals }),
75 | // combine multi-chunk builds with dynamic imports
76 | inlineDynamicImports: true,
77 | // this is a mini x-global, determine where our context lies
78 | intro: 'const global = typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : window;',
79 | name: createName(pkg)
80 | };
81 | }
82 |
83 | /**
84 | *
85 | * @param {BundleDef} param0
86 | * @returns {Bundle}
87 | */
88 | export function createBundle ({ entries = {}, external, globals = {}, index, inject = {}, pkg }) {
89 | return {
90 | // specify this (we define global in the output intro as globalThis || self || window)
91 | context: 'global',
92 | external,
93 | input: createInput(pkg, index),
94 | output: createOutput(pkg, external, globals),
95 | // NOTE The expect-error directives are due to rollup plugins, see
96 | // - https://github.com/rollup/plugins/issues/1488
97 | // - https://github.com/rollup/plugins/issues/1329
98 | plugins: [
99 | // @ts-expect-error See the linked rollup issues above
100 | pluginAlias({ entries }),
101 | // @ts-expect-error See the linked rollup issues above
102 | pluginJson(),
103 | // @ts-expect-error See the linked rollup issues above
104 | pluginCommonjs(),
105 | // @ts-expect-error See the linked rollup issues above
106 | pluginDynamicImportVars(),
107 | // @ts-expect-error See the linked rollup issues above
108 | pluginInject(inject),
109 | pluginResolve({ browser: true }),
110 | pluginCleanup()
111 | ]
112 | };
113 | }
114 |
--------------------------------------------------------------------------------
/packages/dev-ts/src/tsconfig.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev-ts authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import type { TsAlias } from './types.js';
5 |
6 | import JSON5 from 'json5';
7 | import fs from 'node:fs';
8 | import path from 'node:path';
9 | import { pathToFileURL } from 'node:url';
10 |
11 | import { CWD_PATH, CWD_URL, MOD_PATH } from './common.js';
12 |
13 | interface JsonConfig {
14 | compilerOptions?: {
15 | baseUrl?: string;
16 | paths?: Record;
17 | };
18 | extends?: string | string[];
19 | }
20 |
21 | interface PartialConfig {
22 | paths: Record;
23 | url?: URL;
24 | }
25 |
26 | /**
27 | * @internal
28 | *
29 | * Extracts the (relevant) tsconfig info, also using extends
30 | **/
31 | function readConfigFile (currentPath = CWD_PATH, tsconfig = 'tsconfig.json', fromFile?: string): PartialConfig {
32 | const configFile = path.join(currentPath, tsconfig);
33 |
34 | if (!fs.existsSync(configFile)) {
35 | console.warn(`No ${configFile}${fromFile ? ` (extended from ${fromFile})` : ''} found, assuming defaults`);
36 |
37 | return { paths: {} };
38 | }
39 |
40 | try {
41 | const { compilerOptions, extends: parentConfig } = JSON5.parse(fs.readFileSync(configFile, 'utf8'));
42 | let url: URL | undefined;
43 |
44 | if (compilerOptions?.baseUrl) {
45 | const configDir = path.dirname(configFile);
46 |
47 | // the baseParentUrl is relative to the actual config file
48 | url = pathToFileURL(path.join(configDir, `${compilerOptions.baseUrl}/`));
49 | }
50 |
51 | // empty paths if none are found
52 | let paths = compilerOptions?.paths || {};
53 |
54 | if (parentConfig) {
55 | const allExtends = Array.isArray(parentConfig)
56 | ? parentConfig
57 | : [parentConfig];
58 |
59 | for (const extendsPath of allExtends) {
60 | const extRoot = extendsPath.startsWith('.')
61 | ? currentPath
62 | : MOD_PATH;
63 | const extSubs = extendsPath.split(/[\\/]/);
64 | const extPath = path.join(extRoot, ...extSubs.slice(0, -1));
65 | const extConfig = readConfigFile(extPath, extSubs.at(-1), configFile);
66 |
67 | // base configs are overridden by later configs, order here matters
68 | // FIXME The paths would be relative to the baseUrl at that point... for
69 | // now we don't care much since we define these 2 together in all @polkadot
70 | // configs, but it certainly _may_ create and issue at some point (for others)
71 | paths = { ...extConfig.paths, ...paths };
72 | url = url || extConfig.url;
73 | }
74 | }
75 |
76 | return url
77 | ? { paths, url }
78 | : { paths };
79 | } catch (error) {
80 | console.error(`FATAL: Error parsing ${configFile}:: ${(error as Error).message}`);
81 |
82 | throw error;
83 | }
84 | }
85 |
86 | /**
87 | * @internal
88 | *
89 | * Retrieves all TS aliases definitions
90 | **/
91 | function extractAliases (): TsAlias[] {
92 | const { paths, url = CWD_URL } = readConfigFile();
93 |
94 | return Object
95 | .entries(paths)
96 | .filter((kv): kv is [string, [string, ...string[]]] => !!kv[1].length)
97 | // TODO The path value is an array - we only handle the first entry in there,
98 | // this is a possible fix into the future if it is ever an issue... (may have
99 | // some impacts on the actual loader where only 1 alias is retrieved)
100 | .map(([key, [path]]) => {
101 | const filter = key.split(/[\\/]/);
102 | const isWildcard = filter.at(-1) === '*';
103 |
104 | // ensure that when we have wilcards specified, they always occur in the last position
105 | if (filter.filter((f) => f.includes('*')).length !== (isWildcard ? 1 : 0)) {
106 | throw new Error(`FATAL: Wildcards in tsconfig.json path entries are only supported in the last position. Invalid ${key}: ${path} mapping`);
107 | }
108 |
109 | return {
110 | filter: isWildcard
111 | ? filter.slice(0, -1)
112 | : filter,
113 | isWildcard,
114 | path,
115 | url
116 | };
117 | });
118 | }
119 |
120 | /** We only export the aliases from the config */
121 | export const tsAliases = extractAliases();
122 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | ## 0.84.2
4 |
5 | - Write the Buffer Import to necessary Deno build files
6 | - build(dev): ignore removing private fields
7 |
8 |
9 | ## 0.83.1
10 |
11 | - Parallelize the tests in `polkadot-exec-node-test`
12 |
13 |
14 | ## 0.82.1
15 |
16 | - Create seperate testLoader and testCached for Node v22 compatibility with JSON assertions
17 |
18 |
19 | ## 0.81.1
20 |
21 | - Duplicate .d.ts files into cjs
22 |
23 |
24 | ## 0.80.1
25 |
26 | - Typescript 5.5.4
27 |
28 |
29 | ## 0.79.4
30 |
31 | - fix: Add check for `deps` in topo sort
32 |
33 |
34 | ## 0.79.1
35 |
36 | - feat: add topological sorting to the packages when releasing
37 |
38 |
39 | ## 0.78.1
40 |
41 | - Update internal scripts to dedupe git & yarn commands
42 | - Update to latest yarn berry 4.0.2
43 |
44 |
45 | ## 0.77.1
46 |
47 | - Drop support for Node 16
48 |
49 |
50 | ## 0.76.1
51 |
52 | - Update to latest typescript-eslint (incl. new rulesets)
53 |
54 |
55 | ## 0.75.1
56 |
57 | - Swap eslint to flat config
58 |
59 |
60 | ## 0.74.1
61 |
62 | - Ensure correct structures for `tsconfig.*.json`
63 |
64 |
65 | ## 0.73.1
66 |
67 | - Drop support for Node 14
68 |
69 |
70 | ## 0.72.1
71 |
72 | - Split `@polkadot/dev-ts` & `@polkadot/dev-test` packages
73 |
74 |
75 | ## 0.71.1
76 |
77 | - Ensure all `src/*` has `.js` extensions (as per ESM, eslint rules, build updates)
78 |
79 |
80 | ## 0.70.1
81 |
82 | - Remove Babel (all compilation via tsc)
83 |
84 |
85 | ## 0.69.1
86 |
87 | - Remove Jest
88 |
89 |
90 | ## 0.68.1
91 |
92 | - 2023
93 | - Cleanup internally used script dependencies
94 |
95 |
96 | ## 0.67.1
97 |
98 | - Default to `esModuleInterop: false`
99 |
100 |
101 | ## 0.66.1
102 |
103 | - Output commonjs into `cjs/*`
104 |
105 |
106 | ## 0.65.1
107 |
108 | - 2022
109 | - Generate `detectPackage` template (with cjs `__dirname`)
110 |
111 |
112 | ## 0.64.1
113 |
114 | - Use tsconfig references and per-package TS build/lint
115 |
116 |
117 | ## 0.63.1
118 |
119 | - eslint 8
120 |
121 |
122 | ## 0.62.1
123 |
124 | - Swap default package build to esm with type: module
125 |
126 |
127 | ## 0.61.1
128 |
129 | - Build & publish both esm and cjs
130 |
131 |
132 | ## 0.60.1
133 |
134 | - Allow for both esm & cjs Babel config
135 |
136 |
137 | ## 0.59.1
138 |
139 | - Default to new React runtime preset (after React 16.14)
140 |
141 |
142 | ## 0.58.1
143 |
144 | - Drop vuepress dependency completely
145 |
146 |
147 | ## 0.57.1
148 |
149 | - Drop lerna dependency completely
150 |
151 |
152 | ## 0.56.1
153 |
154 | - Optional lerna in publish
155 |
156 |
157 | ## 0.55.3
158 |
159 | - Publish draft release
160 |
161 |
162 | ## 0.54.1
163 |
164 | - typescript-eslint 3
165 |
166 |
167 | ## 0.53.1
168 |
169 | - TypeScript 3.9
170 |
171 |
172 | ## 0.52.1
173 |
174 | - Stricter JSX rules
175 |
176 |
177 | ## 0.51.1
178 |
179 | - Arrow functions with ()
180 | - JSX sample tests
181 |
182 |
183 | ## 0.50.1
184 |
185 | - Yarn 2
186 |
187 |
188 | ## 0.41.1
189 |
190 | - TypeScript 3.8.2
191 | - Extend Babel plugins with latest TS features
192 |
193 |
194 | ## 0.40.1
195 |
196 | - Remove `@polkadot/dev-react`, combine into `@polkadot/dev`
197 | - Move all user-facing (non-CI scripts) to JS, which makes cross-platform easier
198 | - Add `polkadot-dev-circular` script to extract circular deps
199 |
200 |
201 | ## 0.34.1
202 |
203 | - Bump deps
204 |
205 |
206 | ## 0.33.1
207 |
208 | - Package scoping checks, build & pre-publish
209 | - Allow `.skip-{npm,build}` files to control build
210 | - Bump deps
211 |
212 |
213 | ## 0.32.1
214 |
215 | - GitHub workflows
216 | - Don't publish this package as beta
217 | - Bump deps
218 |
219 |
220 | ## 0.31.1
221 |
222 | - TypeScript eslint 2
223 | - Bump deps
224 |
225 |
226 | ## 0.30.1
227 |
228 | - Swap to TypeScript eslint
229 | - Bump deps
230 |
231 |
232 | ## 0.29.1
233 |
234 | - Split deploy & build steps
235 | - Rename `yarn run check` to `yarn lint`
236 | - Bump deps
237 |
238 |
239 | ## 0.28.1
240 |
241 | - Remove `useBuiltins` from babel config (corejs)
242 | - Bump deps
243 |
244 |
245 | ## 0.27.1
246 |
247 | - Beta versions now publish with a `beta` tag
248 |
249 |
250 | ## 0.26.1
251 |
252 | - Publish `..-beta.x` versions from CI. This helps a lot with the stream of versions that arrise from merging.
253 |
--------------------------------------------------------------------------------
/packages/dev/scripts/polkadot-dev-version.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | // Copyright 2017-2025 @polkadot/dev authors & contributors
3 | // SPDX-License-Identifier: Apache-2.0
4 |
5 | import fs from 'node:fs';
6 | import path from 'node:path';
7 | import yargs from 'yargs';
8 |
9 | import { execPm, exitFatal, logBin } from './util.mjs';
10 |
11 | /** @typedef {{ dependencies?: Record; devDependencies?: Record; peerDependencies?: Record; optionalDependencies?: Record; resolutions?: Record; name?: string; stableVersion?: string; version: string; }} PkgJson */
12 |
13 | const TYPES = ['major', 'minor', 'patch', 'pre'];
14 |
15 | const [type] = (
16 | await yargs(process.argv.slice(2))
17 | .demandCommand(1)
18 | .argv
19 | )._;
20 |
21 | if (typeof type !== 'string' || !TYPES.includes(type)) {
22 | exitFatal(`Invalid version bump "${type}", expected one of ${TYPES.join(', ')}`);
23 | }
24 |
25 | /**
26 | * @param {Record} dependencies
27 | * @param {string[]} others
28 | * @param {string} version
29 | * @returns {Record}
30 | */
31 | function updateDependencies (dependencies, others, version) {
32 | return Object
33 | .entries(dependencies)
34 | .sort((a, b) => a[0].localeCompare(b[0]))
35 | .reduce((/** @type {Record} */ result, [key, value]) => {
36 | result[key] = others.includes(key) && value !== '*'
37 | ? value.startsWith('^')
38 | ? `^${version}`
39 | : version
40 | : value;
41 |
42 | return result;
43 | }, {});
44 | }
45 |
46 | /**
47 | * @returns {[string, PkgJson]}
48 | */
49 | function readCurrentPkgJson () {
50 | const rootPath = path.join(process.cwd(), 'package.json');
51 | const rootJson = JSON.parse(fs.readFileSync(rootPath, 'utf8'));
52 |
53 | return [rootPath, rootJson];
54 | }
55 |
56 | /**
57 | * @param {string} path
58 | * @param {unknown} json
59 | */
60 | function writePkgJson (path, json) {
61 | fs.writeFileSync(path, `${JSON.stringify(json, null, 2)}\n`);
62 | }
63 |
64 | /**
65 | *
66 | * @param {string} version
67 | * @param {string[]} others
68 | * @param {string} pkgPath
69 | * @param {Record} json
70 | */
71 | function updatePackage (version, others, pkgPath, json) {
72 | const updated = Object
73 | .keys(json)
74 | .reduce((/** @type {Record} */ result, key) => {
75 | if (key === 'version') {
76 | result[key] = version;
77 | } else if (['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies', 'resolutions'].includes(key)) {
78 | result[key] = updateDependencies(json[key], others, version);
79 | } else if (key !== 'stableVersion') {
80 | result[key] = json[key];
81 | }
82 |
83 | return result;
84 | }, {});
85 |
86 | writePkgJson(pkgPath, updated);
87 | }
88 |
89 | function removeX () {
90 | const [rootPath, json] = readCurrentPkgJson();
91 |
92 | if (!json.version?.endsWith('-x')) {
93 | return false;
94 | }
95 |
96 | json.version = json.version.replace('-x', '');
97 | writePkgJson(rootPath, json);
98 |
99 | return true;
100 | }
101 |
102 | function addX () {
103 | const [rootPath, json] = readCurrentPkgJson();
104 |
105 | if (json.version.endsWith('-x')) {
106 | return false;
107 | }
108 |
109 | json.version = json.version + '-x';
110 | writePkgJson(rootPath, json);
111 |
112 | return true;
113 | }
114 |
115 | logBin('polkadot-dev-version');
116 |
117 | const isX = removeX();
118 |
119 | execPm(`version ${type === 'pre' ? 'prerelease' : type}`);
120 |
121 | if (isX && type === 'pre') {
122 | addX();
123 | }
124 |
125 | const [rootPath, rootJson] = readCurrentPkgJson();
126 |
127 | updatePackage(rootJson.version, [], rootPath, rootJson);
128 |
129 | // yarn workspaces does an OOM, manual looping takes ages
130 | if (fs.existsSync('packages')) {
131 | const packages = fs
132 | .readdirSync('packages')
133 | .map((dir) => path.join(process.cwd(), 'packages', dir, 'package.json'))
134 | .filter((pkgPath) => fs.existsSync(pkgPath))
135 | .map((pkgPath) => [pkgPath, JSON.parse(fs.readFileSync(pkgPath, 'utf8'))]);
136 | const others = packages.map(([, json]) => json.name);
137 |
138 | packages.forEach(([pkgPath, json]) => {
139 | updatePackage(rootJson.version, others, pkgPath, json);
140 | });
141 | }
142 |
143 | execPm('install');
144 |
--------------------------------------------------------------------------------
/packages/dev/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Jaco Greeff ",
3 | "bugs": "https://github.com/polkadot-js/dev/issues",
4 | "description": "A collection of shared CI scripts and development environment used by @polkadot projects",
5 | "engines": {
6 | "node": ">=18"
7 | },
8 | "homepage": "https://github.com/polkadot-js/dev/tree/master/packages/dev#readme",
9 | "license": "Apache-2.0",
10 | "name": "@polkadot/dev",
11 | "repository": {
12 | "directory": "packages/dev",
13 | "type": "git",
14 | "url": "https://github.com/polkadot-js/dev.git"
15 | },
16 | "sideEffects": false,
17 | "type": "module",
18 | "version": "0.84.2",
19 | "bin": {
20 | "polkadot-ci-ghact-build": "./scripts/polkadot-ci-ghact-build.mjs",
21 | "polkadot-ci-ghact-docs": "./scripts/polkadot-ci-ghact-docs.mjs",
22 | "polkadot-ci-ghpages-force": "./scripts/polkadot-ci-ghpages-force.mjs",
23 | "polkadot-dev-build-docs": "./scripts/polkadot-dev-build-docs.mjs",
24 | "polkadot-dev-build-ts": "./scripts/polkadot-dev-build-ts.mjs",
25 | "polkadot-dev-circular": "./scripts/polkadot-dev-circular.mjs",
26 | "polkadot-dev-clean-build": "./scripts/polkadot-dev-clean-build.mjs",
27 | "polkadot-dev-contrib": "./scripts/polkadot-dev-contrib.mjs",
28 | "polkadot-dev-copy-dir": "./scripts/polkadot-dev-copy-dir.mjs",
29 | "polkadot-dev-copy-to": "./scripts/polkadot-dev-copy-to.mjs",
30 | "polkadot-dev-deno-map": "./scripts/polkadot-dev-deno-map.mjs",
31 | "polkadot-dev-run-lint": "./scripts/polkadot-dev-run-lint.mjs",
32 | "polkadot-dev-run-node-ts": "./scripts/polkadot-dev-run-node-ts.mjs",
33 | "polkadot-dev-run-test": "./scripts/polkadot-dev-run-test.mjs",
34 | "polkadot-dev-version": "./scripts/polkadot-dev-version.mjs",
35 | "polkadot-dev-yarn-only": "./scripts/polkadot-dev-yarn-only.mjs",
36 | "polkadot-exec-eslint": "./scripts/polkadot-exec-eslint.mjs",
37 | "polkadot-exec-ghpages": "./scripts/polkadot-exec-ghpages.mjs",
38 | "polkadot-exec-ghrelease": "./scripts/polkadot-exec-ghrelease.mjs",
39 | "polkadot-exec-node-test": "./scripts/polkadot-exec-node-test.mjs",
40 | "polkadot-exec-rollup": "./scripts/polkadot-exec-rollup.mjs",
41 | "polkadot-exec-tsc": "./scripts/polkadot-exec-tsc.mjs",
42 | "polkadot-exec-webpack": "./scripts/polkadot-exec-webpack.mjs"
43 | },
44 | "exports": {
45 | "./config/eslint": "./config/eslint.js",
46 | "./config/prettier.cjs": "./config/prettier.cjs",
47 | "./config/tsconfig.json": "./config/tsconfig.json",
48 | "./rootJs/dynamic.mjs": "./src/rootJs/dynamic.mjs",
49 | "./rootJs/testJson.json": "./src/rootJs/testJson.json"
50 | },
51 | "dependencies": {
52 | "@eslint/js": "^8.56.0",
53 | "@polkadot/dev-test": "^0.84.2",
54 | "@polkadot/dev-ts": "^0.84.2",
55 | "@rollup/plugin-alias": "^5.1.1",
56 | "@rollup/plugin-commonjs": "^25.0.8",
57 | "@rollup/plugin-dynamic-import-vars": "^2.1.5",
58 | "@rollup/plugin-inject": "^5.0.5",
59 | "@rollup/plugin-json": "^6.1.0",
60 | "@rollup/plugin-node-resolve": "^15.3.1",
61 | "@tsconfig/strictest": "^2.0.2",
62 | "@typescript-eslint/eslint-plugin": "^6.19.1",
63 | "@typescript-eslint/parser": "^6.19.1",
64 | "eslint": "^8.56.0",
65 | "eslint-config-standard": "^17.1.0",
66 | "eslint-import-resolver-node": "^0.3.9",
67 | "eslint-import-resolver-typescript": "^3.6.1",
68 | "eslint-plugin-deprecation": "^2.0.0",
69 | "eslint-plugin-header": "^3.1.1",
70 | "eslint-plugin-import": "^2.29.1",
71 | "eslint-plugin-import-newlines": "^1.3.4",
72 | "eslint-plugin-jest": "^27.6.3",
73 | "eslint-plugin-n": "^16.6.2",
74 | "eslint-plugin-promise": "^6.1.1",
75 | "eslint-plugin-react": "^7.33.2",
76 | "eslint-plugin-react-hooks": "^4.6.0",
77 | "eslint-plugin-simple-import-sort": "^10.0.0",
78 | "eslint-plugin-sort-destructure-keys": "^1.5.0",
79 | "espree": "^9.6.1",
80 | "gh-pages": "^6.1.1",
81 | "gh-release": "^7.0.2",
82 | "globals": "^13.24.0",
83 | "json5": "^2.2.3",
84 | "madge": "^6.1.0",
85 | "rollup": "^4.9.6",
86 | "rollup-plugin-cleanup": "^3.2.1",
87 | "tslib": "^2.7.0",
88 | "typescript": "^5.5.4",
89 | "webpack": "^5.89.0",
90 | "webpack-cli": "^5.1.4",
91 | "webpack-dev-server": "^4.15.1",
92 | "webpack-merge": "^5.10.0",
93 | "webpack-subresource-integrity": "^5.2.0-rc.1",
94 | "yargs": "^17.7.2"
95 | },
96 | "devDependencies": {
97 | "@testing-library/react": "^14.1.2",
98 | "@types/node": "^20.11.5",
99 | "@types/react": "^18.2.48",
100 | "@types/react-dom": "^18.2.18",
101 | "@types/yargs": "^17.0.32",
102 | "react": "^18.2.0",
103 | "react-dom": "^18.2.0",
104 | "react-is": "^18.2.0",
105 | "styled-components": "^6.1.8"
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/packages/dev-test/src/env/browser.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev-test authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import { JSDOM } from 'jsdom';
5 |
6 | /**
7 | * Export a very basic JSDom environment - this is just enough so we have
8 | * @testing-environment/react tests passing in this repo
9 | *
10 | * FIXME: This approach is actually _explicitly_ discouraged by JSDOM - when
11 | * using window you should run the tests inside that context, instead of just
12 | * blindly relying on the globals as we do here
13 | */
14 | export function browser () {
15 | const { window } = new JSDOM('', { url: 'http://localhost' });
16 |
17 | return {
18 | // All HTML Elements that are defined on the JSDOM window object.
19 | // (we copied as-is from the types definition). We cannot get this
20 | // via Object.keys(window).filter(...) so we have to specify explicitly
21 | HTMLAnchorElement: window.HTMLAnchorElement,
22 | HTMLAreaElement: window.HTMLAreaElement,
23 | HTMLAudioElement: window.HTMLAudioElement,
24 | HTMLBRElement: window.HTMLBRElement,
25 | HTMLBaseElement: window.HTMLBaseElement,
26 | HTMLBodyElement: window.HTMLBodyElement,
27 | HTMLButtonElement: window.HTMLButtonElement,
28 | HTMLCanvasElement: window.HTMLCanvasElement,
29 | HTMLDListElement: window.HTMLDListElement,
30 | HTMLDataElement: window.HTMLDataElement,
31 | HTMLDataListElement: window.HTMLDataListElement,
32 | HTMLDetailsElement: window.HTMLDetailsElement,
33 | HTMLDialogElement: window.HTMLDialogElement,
34 | HTMLDirectoryElement: window.HTMLDirectoryElement,
35 | HTMLDivElement: window.HTMLDivElement,
36 | HTMLElement: window.HTMLElement,
37 | HTMLEmbedElement: window.HTMLEmbedElement,
38 | HTMLFieldSetElement: window.HTMLFieldSetElement,
39 | HTMLFontElement: window.HTMLFontElement,
40 | HTMLFormElement: window.HTMLFormElement,
41 | HTMLFrameElement: window.HTMLFrameElement,
42 | HTMLFrameSetElement: window.HTMLFrameSetElement,
43 | HTMLHRElement: window.HTMLHRElement,
44 | HTMLHeadElement: window.HTMLHeadElement,
45 | HTMLHeadingElement: window.HTMLHeadingElement,
46 | HTMLHtmlElement: window.HTMLHtmlElement,
47 | HTMLIFrameElement: window.HTMLIFrameElement,
48 | HTMLImageElement: window.HTMLImageElement,
49 | HTMLInputElement: window.HTMLInputElement,
50 | HTMLLIElement: window.HTMLLIElement,
51 | HTMLLabelElement: window.HTMLLabelElement,
52 | HTMLLegendElement: window.HTMLLegendElement,
53 | HTMLLinkElement: window.HTMLLinkElement,
54 | HTMLMapElement: window.HTMLMapElement,
55 | HTMLMarqueeElement: window.HTMLMarqueeElement,
56 | HTMLMediaElement: window.HTMLMediaElement,
57 | HTMLMenuElement: window.HTMLMenuElement,
58 | HTMLMetaElement: window.HTMLMetaElement,
59 | HTMLMeterElement: window.HTMLMeterElement,
60 | HTMLModElement: window.HTMLModElement,
61 | HTMLOListElement: window.HTMLOListElement,
62 | HTMLObjectElement: window.HTMLObjectElement,
63 | HTMLOptGroupElement: window.HTMLOptGroupElement,
64 | HTMLOptionElement: window.HTMLOptionElement,
65 | HTMLOutputElement: window.HTMLOutputElement,
66 | HTMLParagraphElement: window.HTMLParagraphElement,
67 | HTMLParamElement: window.HTMLParamElement,
68 | HTMLPictureElement: window.HTMLPictureElement,
69 | HTMLPreElement: window.HTMLPreElement,
70 | HTMLProgressElement: window.HTMLProgressElement,
71 | HTMLQuoteElement: window.HTMLQuoteElement,
72 | HTMLScriptElement: window.HTMLScriptElement,
73 | HTMLSelectElement: window.HTMLSelectElement,
74 | HTMLSlotElement: window.HTMLSlotElement,
75 | HTMLSourceElement: window.HTMLSourceElement,
76 | HTMLSpanElement: window.HTMLSpanElement,
77 | HTMLStyleElement: window.HTMLStyleElement,
78 | HTMLTableCaptionElement: window.HTMLTableCaptionElement,
79 | HTMLTableCellElement: window.HTMLTableCellElement,
80 | HTMLTableColElement: window.HTMLTableColElement,
81 | HTMLTableElement: window.HTMLTableElement,
82 | HTMLTableRowElement: window.HTMLTableRowElement,
83 | HTMLTableSectionElement: window.HTMLTableSectionElement,
84 | HTMLTemplateElement: window.HTMLTemplateElement,
85 | HTMLTextAreaElement: window.HTMLTextAreaElement,
86 | HTMLTimeElement: window.HTMLTimeElement,
87 | HTMLTitleElement: window.HTMLTitleElement,
88 | HTMLTrackElement: window.HTMLTrackElement,
89 | HTMLUListElement: window.HTMLUListElement,
90 | HTMLUnknownElement: window.HTMLUnknownElement,
91 | HTMLVideoElement: window.HTMLVideoElement,
92 | // normal service resumes, the base top-level names
93 | crypto: window.crypto,
94 | document: window.document,
95 | localStorage: window.localStorage,
96 | navigator: window.navigator,
97 | // window...
98 | window
99 | };
100 | }
101 |
--------------------------------------------------------------------------------
/packages/dev/config/eslint.js:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | // @ts-expect-error No definition for this one
5 | import eslintJs from '@eslint/js';
6 | import tsPlugin from '@typescript-eslint/eslint-plugin';
7 | import tsParser from '@typescript-eslint/parser';
8 | // @ts-expect-error No definition for this one
9 | import standardConfig from 'eslint-config-standard';
10 | import deprecationPlugin from 'eslint-plugin-deprecation';
11 | // @ts-expect-error No definition for this one
12 | import headerPlugin from 'eslint-plugin-header';
13 | // @ts-expect-error No definition for this one
14 | import importPlugin from 'eslint-plugin-import';
15 | // @ts-expect-error No definition for this one
16 | import importNewlinesPlugin from 'eslint-plugin-import-newlines';
17 | // @ts-expect-error No definition for this one
18 | import jestPlugin from 'eslint-plugin-jest';
19 | // @ts-expect-error No definition for this one
20 | import nPlugin from 'eslint-plugin-n';
21 | // @ts-expect-error No definition for this one
22 | import promisePlugin from 'eslint-plugin-promise';
23 | // @ts-expect-error No definition for this one
24 | import reactPlugin from 'eslint-plugin-react';
25 | // @ts-expect-error No definition for this one
26 | import reactHooksPlugin from 'eslint-plugin-react-hooks';
27 | // @ts-expect-error No definition for this one
28 | import simpleImportSortPlugin from 'eslint-plugin-simple-import-sort';
29 | // @ts-expect-error No definition for this one
30 | import sortDestructureKeysPlugin from 'eslint-plugin-sort-destructure-keys';
31 | import globals from 'globals';
32 |
33 | import { overrideAll, overrideJs, overrideJsx, overrideSpec } from './eslint.rules.js';
34 |
35 | const EXT_JS = ['.cjs', '.js', '.mjs'];
36 | const EXT_TS = ['.ts', '.tsx'];
37 | const EXT_ALL = [...EXT_JS, ...EXT_TS];
38 |
39 | /**
40 | * @internal
41 | * Converts a list of EXT_* defined above to globs
42 | * @param {string[]} exts
43 | * @returns {string[]}
44 | */
45 | function extsToGlobs (exts) {
46 | return exts.map((e) => `**/*${e}`);
47 | }
48 |
49 | export default [
50 | {
51 | ignores: [
52 | '**/.github/',
53 | '**/.vscode/',
54 | '**/.yarn/',
55 | '**/build/',
56 | '**/build-*/',
57 | '**/coverage/'
58 | ]
59 | },
60 | {
61 | languageOptions: {
62 | globals: {
63 | ...globals.browser,
64 | ...globals.node
65 | },
66 | parser: tsParser,
67 | parserOptions: {
68 | ecmaVersion: 'latest',
69 | project: './tsconfig.eslint.json',
70 | sourceType: 'module',
71 | warnOnUnsupportedTypeScriptVersion: false
72 | }
73 | },
74 | plugins: {
75 | '@typescript-eslint': tsPlugin,
76 | deprecation: deprecationPlugin,
77 | header: headerPlugin,
78 | import: importPlugin,
79 | 'import-newlines': importNewlinesPlugin,
80 | n: nPlugin,
81 | promise: promisePlugin,
82 | 'simple-import-sort': simpleImportSortPlugin,
83 | 'sort-destructure-keys': sortDestructureKeysPlugin
84 | },
85 | settings: {
86 | 'import/extensions': EXT_ALL,
87 | 'import/parsers': {
88 | '@typescript-eslint/parser': EXT_TS,
89 | espree: EXT_JS
90 | },
91 | 'import/resolver': {
92 | node: {
93 | extensions: EXT_ALL
94 | },
95 | typescript: {
96 | project: './tsconfig.eslint.json'
97 | }
98 | }
99 | }
100 | },
101 | {
102 | files: extsToGlobs(EXT_ALL),
103 | rules: {
104 | ...eslintJs.configs.recommended.rules,
105 | ...standardConfig.rules,
106 | ...tsPlugin.configs['recommended-type-checked'].rules,
107 | ...tsPlugin.configs['stylistic-type-checked'].rules,
108 | ...overrideAll
109 | }
110 | },
111 | {
112 | files: extsToGlobs(EXT_JS),
113 | rules: {
114 | ...overrideJs
115 | }
116 | },
117 | {
118 | files: [
119 | '**/*.tsx',
120 | '**/use*.ts'
121 | ],
122 | plugins: {
123 | react: reactPlugin,
124 | 'react-hooks': reactHooksPlugin
125 | },
126 | rules: {
127 | ...reactPlugin.configs.recommended.rules,
128 | ...reactHooksPlugin.configs.recommended.rules,
129 | ...overrideJsx
130 | },
131 | settings: {
132 | react: {
133 | version: 'detect'
134 | }
135 | }
136 | },
137 | {
138 | files: [
139 | '**/*.spec.ts',
140 | '**/*.spec.tsx'
141 | ],
142 | languageOptions: {
143 | globals: {
144 | ...globals.jest
145 | }
146 | },
147 | plugins: {
148 | jest: jestPlugin
149 | },
150 | rules: {
151 | ...jestPlugin.configs.recommended.rules,
152 | ...overrideSpec
153 | },
154 | settings: {
155 | jest: {
156 | version: 27
157 | }
158 | }
159 | }
160 | ];
161 |
--------------------------------------------------------------------------------
/packages/dev/scripts/polkadot-dev-run-test.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | // Copyright 2017-2025 @polkadot/dev authors & contributors
3 | // SPDX-License-Identifier: Apache-2.0
4 |
5 | import process from 'node:process';
6 |
7 | import { execNodeTs, exitFatal, exitFatalEngine, importPath, logBin, readdirSync } from './util.mjs';
8 |
9 | // A & B are just helpers here and in the errors below
10 | const EXT_A = ['spec', 'test'];
11 | const EXT_B = ['ts', 'tsx', 'js', 'jsx', 'cjs', 'mjs'];
12 |
13 | // The actual extensions we are looking for
14 | const EXTS = EXT_A.reduce((/** @type {string[]} */ exts, s) => exts.concat(...EXT_B.map((e) => `.${s}.${e}`)), []);
15 |
16 | logBin('polkadot-dev-run-test');
17 |
18 | exitFatalEngine();
19 |
20 | const cmd = [];
21 | const nodeFlags = [];
22 | const filters = [];
23 |
24 | /** @type {Record} */
25 | const filtersExcl = {};
26 | /** @type {Record} */
27 | const filtersIncl = {};
28 |
29 | const args = process.argv.slice(2);
30 | let testEnv = 'node';
31 | let isDev = false;
32 |
33 | for (let i = 0; i < args.length; i++) {
34 | switch (args[i]) {
35 | // when running inside a dev environment, specifically @polkadot/dev
36 | case '--dev-build':
37 | isDev = true;
38 | break;
39 |
40 | // environment, not passed-through
41 | case '--env':
42 | if (!['browser', 'node'].includes(args[++i])) {
43 | throw new Error(`Invalid --env ${args[i]}, expected 'browser' or 'node'`);
44 | }
45 |
46 | testEnv = args[i];
47 | break;
48 |
49 | // internal flags with no params
50 | case '--bail':
51 | case '--console':
52 | cmd.push(args[i]);
53 | break;
54 |
55 | // internal flags, with params
56 | case '--logfile':
57 | cmd.push(args[i]);
58 | cmd.push(args[++i]);
59 | break;
60 |
61 | // node flags that could have additional params
62 | case '--import':
63 | case '--loader':
64 | case '--require':
65 | nodeFlags.push(args[i]);
66 | nodeFlags.push(args[++i]);
67 | break;
68 |
69 | // any other non-flag arguments are passed-through
70 | default:
71 | if (args[i].startsWith('-')) {
72 | throw new Error(`Unknown flag ${args[i]} found`);
73 | }
74 |
75 | filters.push(args[i]);
76 |
77 | if (args[i].startsWith('^')) {
78 | const key = args[i].slice(1);
79 |
80 | if (filtersIncl[key]) {
81 | delete filtersIncl[key];
82 | } else {
83 | filtersExcl[key] = key.split(/[\\/]/);
84 | }
85 | } else {
86 | const key = args[i];
87 |
88 | if (filtersExcl[key]) {
89 | delete filtersExcl[key];
90 | } else {
91 | filtersIncl[key] = key.split(/[\\/]/);
92 | }
93 | }
94 |
95 | break;
96 | }
97 | }
98 |
99 | /**
100 | * @param {string[]} parts
101 | * @param {Record} filters
102 | * @returns {boolean}
103 | */
104 | function applyFilters (parts, filters) {
105 | return Object
106 | .values(filters)
107 | .some((filter) =>
108 | parts
109 | .map((_, i) => i)
110 | .filter((i) =>
111 | filter[0].startsWith(':')
112 | ? parts[i].includes(filter[0].slice(1))
113 | : filter.length === 1
114 | ? parts[i].startsWith(filter[0])
115 | : parts[i] === filter[0]
116 | )
117 | .some((start) =>
118 | filter.every((f, i) =>
119 | parts[start + i] && (
120 | f.startsWith(':')
121 | ? parts[start + i].includes(f.slice(1))
122 | : i === (filter.length - 1)
123 | ? parts[start + i].startsWith(f)
124 | : parts[start + i] === f
125 | )
126 | )
127 | )
128 | );
129 | }
130 |
131 | const files = readdirSync('packages', EXTS).filter((file) => {
132 | const parts = file.split(/[\\/]/);
133 | let isIncluded = true;
134 |
135 | if (Object.keys(filtersIncl).length) {
136 | isIncluded = applyFilters(parts, filtersIncl);
137 | }
138 |
139 | if (isIncluded && Object.keys(filtersExcl).length) {
140 | isIncluded = !applyFilters(parts, filtersExcl);
141 | }
142 |
143 | return isIncluded;
144 | });
145 |
146 | if (files.length === 0) {
147 | exitFatal(`No files matching *.{${EXT_A.join(', ')}}.{${EXT_B.join(', ')}} found${filters.length ? ` (filtering on ${filters.join(', ')})` : ''}`);
148 | }
149 |
150 | try {
151 | const allFlags = `${importPath('@polkadot/dev/scripts/polkadot-exec-node-test.mjs')} ${[...cmd, ...files].join(' ')}`;
152 |
153 | nodeFlags.push('--require');
154 | nodeFlags.push(
155 | isDev
156 | ? `./packages/dev-test/build/cjs/${testEnv}.js`
157 | : `@polkadot/dev-test/${testEnv}`
158 | );
159 |
160 | execNodeTs(allFlags, nodeFlags, false, isDev ? './packages/dev-ts/build/testCached.js' : '@polkadot/dev-ts/testCached');
161 | } catch {
162 | process.exit(1);
163 | }
164 |
--------------------------------------------------------------------------------
/packages/dev-test/src/env/jest.spec.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev-test authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | describe('jest', () => {
5 | it('has been enhanced', () => {
6 | expect(jest.setTimeout).toBeDefined();
7 | });
8 |
9 | describe('.fn', () => {
10 | it('works on .toHaveBeenCalled', () => {
11 | const mock = jest.fn(() => 3);
12 |
13 | expect(mock).not.toHaveBeenCalled();
14 | expect(mock()).toBe(3);
15 | expect(mock).toHaveBeenCalled();
16 | });
17 |
18 | it('works on .toHaveBeenCalledTimes', () => {
19 | const mock = jest.fn(() => 3);
20 |
21 | expect(mock()).toBe(3);
22 | expect(mock()).toBe(3);
23 |
24 | expect(mock).toHaveBeenCalledTimes(2);
25 | });
26 |
27 | it('works with .toHaveBeenCalledWith', () => {
28 | const sum = jest.fn((a: number, b: number) => a + b);
29 |
30 | expect(sum(1, 2)).toBe(3);
31 |
32 | expect(sum).toHaveBeenCalledWith(1, 2);
33 |
34 | expect(sum(2, 3)).toBe(5);
35 | expect(sum(4, 5)).toBe(9);
36 |
37 | expect(sum).toHaveBeenCalledWith(1, 2);
38 | expect(sum).toHaveBeenCalledWith(2, 3);
39 | expect(sum).toHaveBeenCalledWith(4, 5);
40 |
41 | expect(sum).toHaveBeenLastCalledWith(4, 5);
42 | });
43 |
44 | it('works with .toHaveBeenCalledWith & expect.objectContaining', () => {
45 | const test = jest.fn((a: unknown, b: unknown) => !!a && !!b);
46 |
47 | test({ a: 123, b: 'test' }, null);
48 |
49 | expect(test).toHaveBeenLastCalledWith({ a: 123, b: 'test' }, null);
50 | expect(test).toHaveBeenLastCalledWith(expect.objectContaining({}), null);
51 | expect(test).toHaveBeenLastCalledWith(expect.objectContaining({ a: 123 }), null);
52 | expect(test).toHaveBeenLastCalledWith(expect.objectContaining({ b: 'test' }), null);
53 | });
54 |
55 | it('allows .mockImplementation', () => {
56 | const mock = jest.fn(() => 3);
57 |
58 | expect(mock()).toBe(3);
59 |
60 | mock.mockImplementation(() => 4);
61 |
62 | expect(mock()).toBe(4);
63 | expect(mock()).toBe(4);
64 | });
65 |
66 | it('allows .mockImplementationOnce', () => {
67 | const mock = jest.fn(() => 3);
68 |
69 | expect(mock()).toBe(3);
70 |
71 | mock.mockImplementationOnce(() => 4);
72 |
73 | expect(mock()).toBe(4);
74 | expect(mock()).toBe(3);
75 | });
76 |
77 | it('allows resets', () => {
78 | const mock = jest.fn(() => 3);
79 |
80 | expect(mock).not.toHaveBeenCalled();
81 | expect(mock()).toBe(3);
82 | expect(mock).toHaveBeenCalled();
83 |
84 | mock.mockReset();
85 |
86 | expect(mock).not.toHaveBeenCalled();
87 | expect(mock()).toBe(3);
88 | expect(mock).toHaveBeenCalled();
89 | });
90 | });
91 |
92 | describe('.spyOn', () => {
93 | it('works on .toHaveBeenCalled', () => {
94 | const obj = {
95 | add: (a: number, b: number) => a + b
96 | };
97 | const spy = jest.spyOn(obj, 'add');
98 |
99 | expect(spy).not.toHaveBeenCalled();
100 | expect(obj.add(1, 2)).toBe(3);
101 | expect(spy).toHaveBeenCalledTimes(1);
102 | });
103 |
104 | it('allows .mockImplementation', () => {
105 | const obj = {
106 | add: (a: number, b: number) => a + b
107 | };
108 | const spy = jest.spyOn(obj, 'add');
109 |
110 | expect(obj.add(1, 2)).toBe(3);
111 | expect(spy).toHaveBeenCalledTimes(1);
112 |
113 | spy.mockImplementation(() => 4);
114 |
115 | expect(obj.add(1, 2)).toBe(4);
116 | expect(spy).toHaveBeenCalledTimes(2);
117 | expect(obj.add(1, 2)).toBe(4);
118 | expect(spy).toHaveBeenCalledTimes(3);
119 | });
120 |
121 | it('allows .mockImplementationOnce', () => {
122 | const obj = {
123 | add: (a: number, b: number) => a + b
124 | };
125 | const spy = jest.spyOn(obj, 'add');
126 |
127 | expect(obj.add(1, 2)).toBe(3);
128 | expect(spy).toHaveBeenCalledTimes(1);
129 |
130 | spy.mockImplementationOnce(() => 4);
131 |
132 | expect(obj.add(1, 2)).toBe(4);
133 | expect(spy).toHaveBeenCalledTimes(2);
134 | expect(obj.add(1, 2)).toBe(3);
135 | expect(spy).toHaveBeenCalledTimes(3);
136 | });
137 |
138 | it('allows resets', () => {
139 | const obj = {
140 | add: (a: number, b: number) => a + b
141 | };
142 | const spy = jest.spyOn(obj, 'add');
143 |
144 | expect(spy).not.toHaveBeenCalled();
145 | expect(obj.add(1, 2)).toBe(3);
146 | expect(spy).toHaveBeenCalledTimes(1);
147 |
148 | spy.mockReset();
149 |
150 | expect(spy).not.toHaveBeenCalled();
151 | expect(obj.add(1, 2)).toBe(3);
152 | expect(spy).toHaveBeenCalledTimes(1);
153 | });
154 |
155 | it('allows restores', () => {
156 | const obj = {
157 | add: (a: number, b: number) => a + b
158 | };
159 | const spy = jest.spyOn(obj, 'add');
160 |
161 | expect(spy).not.toHaveBeenCalled();
162 | expect(obj.add(1, 2)).toBe(3);
163 | expect(spy).toHaveBeenCalledTimes(1);
164 |
165 | spy.mockRestore();
166 | });
167 | });
168 | });
169 |
--------------------------------------------------------------------------------
/scripts/all-deps.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | // Copyright 2017-2025 @polkadot/dev authors & contributors
3 | // SPDX-License-Identifier: Apache-2.0
4 |
5 | import fs from 'node:fs';
6 | import path from 'node:path';
7 |
8 | /** @typedef {{ dependencies: Record; devDependencies: Record; peerDependencies: Record; optionalDependencies: Record; resolutions: Record; name: string; version: string; versions: { git: string; npm: string; } }} PkgJson */
9 |
10 | // The keys to look for in package.json
11 | const PKG_PATHS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies', 'resolutions'];
12 |
13 | const versions = {};
14 | const paths = [];
15 | let updated = 0;
16 |
17 | /**
18 | * Returns the path of the package.json inside the supplied directory
19 | *
20 | * @param {string} dir
21 | * @returns {string}
22 | */
23 | function packageJsonPath (dir) {
24 | return path.join(dir, 'package.json');
25 | }
26 |
27 | /**
28 | * Update the supplied dependency map with the latest (version map) versions
29 | *
30 | * @param {string} dir
31 | * @param {boolean} hasDirLogged
32 | * @param {string} key
33 | * @param {Record} dependencies
34 | * @returns {[number, Record]}
35 | */
36 | function updateDependencies (dir, hasDirLogged, key, dependencies) {
37 | let count = 0;
38 | const adjusted = Object
39 | .keys(dependencies)
40 | .sort()
41 | .reduce((result, name) => {
42 | const current = dependencies[name];
43 | const version = !current.startsWith('^') || current.endsWith('-x')
44 | ? current
45 | : versions[name] || current;
46 |
47 | if (version !== current) {
48 | if (count === 0) {
49 | if (!hasDirLogged) {
50 | console.log('\t', dir);
51 | }
52 |
53 | console.log('\t\t', key);
54 | }
55 |
56 | console.log('\t\t\t', name.padStart(30), '\t', current.padStart(8), '->', version);
57 | count++;
58 | updated++;
59 | }
60 |
61 | result[name] = version;
62 |
63 | return result;
64 | }, {});
65 |
66 | return [count, adjusted];
67 | }
68 |
69 | /**
70 | * Returns a parsed package.json
71 | *
72 | * @param {string} dir
73 | * @returns {PkgJson}
74 | */
75 | function parsePackage (dir) {
76 | return JSON.parse(
77 | fs.readFileSync(packageJsonPath(dir), 'utf-8')
78 | );
79 | }
80 |
81 | /**
82 | * Outputs the supplied package.json
83 | *
84 | * @param {string} dir
85 | * @param {Record} json
86 | */
87 | function writePackage (dir, json) {
88 | fs.writeFileSync(packageJsonPath(dir), `${JSON.stringify(json, null, 2)}\n`);
89 | }
90 |
91 | /**
92 | * Rerite the package.json with updated dependecies
93 | *
94 | * @param {string} dir
95 | */
96 | function updatePackage (dir) {
97 | const json = parsePackage(dir);
98 | let hasDirLogged = false;
99 |
100 | writePackage(dir, Object
101 | .keys(json)
102 | .reduce((result, key) => {
103 | if (PKG_PATHS.includes(key)) {
104 | const [count, adjusted] = updateDependencies(dir, hasDirLogged, key, json[key]);
105 |
106 | result[key] = adjusted;
107 |
108 | if (count) {
109 | hasDirLogged = true;
110 | }
111 | } else {
112 | result[key] = json[key];
113 | }
114 |
115 | return result;
116 | }, {})
117 | );
118 | }
119 |
120 | /**
121 | * Loop through package/*, extracting the package names and their versions
122 | *
123 | * @param {string} dir
124 | */
125 | function findPackages (dir) {
126 | const pkgsDir = path.join(dir, 'packages');
127 |
128 | paths.push(dir);
129 |
130 | if (!fs.existsSync(pkgsDir)) {
131 | return;
132 | }
133 |
134 | const { versions: { npm: lastVersion } } = parsePackage(dir);
135 |
136 | fs
137 | .readdirSync(pkgsDir)
138 | .filter((entry) => {
139 | const full = path.join(pkgsDir, entry);
140 |
141 | return !['.', '..'].includes(entry) &&
142 | fs.lstatSync(full).isDirectory() &&
143 | fs.existsSync(path.join(full, 'package.json'));
144 | })
145 | .forEach((dir) => {
146 | const full = path.join(pkgsDir, dir);
147 | const pkgJson = parsePackage(full);
148 |
149 | paths.push(full);
150 | versions[pkgJson.name] = `^${lastVersion}`;
151 |
152 | // for dev we want to pull through the additionals, i.e. we want to
153 | // align deps found in dev/others with those in dev as master
154 | if (pkgJson.name === '@polkadot/dev') {
155 | PKG_PATHS.forEach((depPath) => {
156 | Object
157 | .entries(pkgJson[depPath] || {})
158 | .filter(([, version]) => version.startsWith('^'))
159 | .forEach(([pkg, version]) => {
160 | versions[pkg] ??= version;
161 | });
162 | });
163 | }
164 | });
165 | }
166 |
167 | console.log('Extracting ...');
168 |
169 | fs
170 | .readdirSync('.')
171 | .filter((name) =>
172 | !['.', '..'].includes(name) &&
173 | fs.existsSync(packageJsonPath(name))
174 | )
175 | .sort()
176 | .forEach(findPackages);
177 |
178 | console.log('\t', Object.keys(versions).length, 'packages found');
179 |
180 | console.log('Updating ...');
181 |
182 | paths.forEach(updatePackage);
183 |
184 | console.log('\t', updated, 'versions adjusted');
185 |
--------------------------------------------------------------------------------
/packages/dev/src/rootEsm.spec.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | ///
5 |
6 | import fs from 'node:fs';
7 | import path from 'node:path';
8 |
9 | import * as testRoot from './root.js';
10 | import { runTests } from './rootTests.js';
11 |
12 | runTests(testRoot);
13 |
14 | describe('as-built output checks', (): void => {
15 | const buildRoot = path.join(process.cwd(), 'packages/dev/build');
16 | const buildFiles = fs.readdirSync(buildRoot);
17 |
18 | describe('build outputs', (): void => {
19 | it('does not contain the *.spec.ts/js files', (): void => {
20 | expect(
21 | buildFiles.filter((f) => f.includes('.spec.'))
22 | ).toEqual([]);
23 | });
24 |
25 | it('does not contain the rootRust folder', (): void => {
26 | expect(
27 | buildFiles.filter((f) => f.includes('rootRust'))
28 | ).toEqual([]);
29 | });
30 |
31 | it('has the static files copied (non-duplicated)', (): void => {
32 | expect(
33 | fs.existsSync(path.join(buildRoot, 'rootStatic/kusama.svg'))
34 | ).toBe(true);
35 | expect(
36 | fs.existsSync(path.join(buildRoot, 'cjs/rootStatic/kusama.svg'))
37 | ).toBe(false);
38 | });
39 |
40 | it('does not have stand-alone d.ts files copied', (): void => {
41 | expect(
42 | fs.existsSync(path.join(buildRoot, 'rootJs/test.json.d.ts'))
43 | ).toBe(false);
44 | });
45 |
46 | it('does have cjs + d.ts files copied', (): void => {
47 | expect(
48 | fs.existsSync(path.join(process.cwd(), 'packages/dev-test/build/globals.d.ts'))
49 | ).toBe(true);
50 | });
51 | });
52 |
53 | describe('code generation', (): void => {
54 | const jsIdx = {
55 | cjs: fs.readFileSync(path.join(buildRoot, 'cjs/rootJs/index.js'), { encoding: 'utf-8' }),
56 | esm: fs.readFileSync(path.join(buildRoot, 'rootJs/index.js'), { encoding: 'utf-8' })
57 | } as const;
58 | const idxTypes = Object.keys(jsIdx) as (keyof typeof jsIdx)[];
59 |
60 | describe('numeric seperators', (): void => {
61 | idxTypes.forEach((type) =>
62 | it(`does not conatin them & has the value in ${type}`, (): void => {
63 | expect(
64 | jsIdx[type].includes('123_456_789n')
65 | ).toBe(false);
66 | expect(
67 | jsIdx[type].includes('123456789n')
68 | ).toBe(true);
69 | })
70 | );
71 | });
72 |
73 | describe('dynamic imports', (): void => {
74 | idxTypes.forEach((type) =>
75 | it(`contains import(...) in ${type}`, (): void => {
76 | expect(
77 | jsIdx[type].includes("const { sum } = await import('@polkadot/dev/rootJs/dynamic.mjs');")
78 | ).toBe(true);
79 | })
80 | );
81 | });
82 |
83 | describe('type assertions', (): void => {
84 | idxTypes.forEach((type) =>
85 | it(`contains import(...) in ${type}`, (): void => {
86 | expect(
87 | jsIdx[type].includes(
88 | type === 'cjs'
89 | ? 'require("@polkadot/dev/rootJs/testJson.json")'
90 | // eslint-disable-next-line no-useless-escape
91 | : "import testJson from '@polkadot/dev/rootJs/testJson.json' assert { type: \'json\' };"
92 | )
93 | ).toBe(true);
94 | })
95 | );
96 | });
97 | });
98 |
99 | describe('commonjs', (): void => {
100 | const cjsRoot = path.join(buildRoot, 'cjs');
101 |
102 | it('contains commonjs package.js inside cjs', (): void => {
103 | expect(
104 | fs
105 | .readFileSync(path.join(cjsRoot, 'package.json'), { encoding: 'utf-8' })
106 | .includes('"type": "commonjs"')
107 | ).toBe(true);
108 | });
109 |
110 | it('contains cjs/sample.js', (): void => {
111 | expect(
112 | fs
113 | .readFileSync(path.join(cjsRoot, 'sample.js'), { encoding: 'utf-8' })
114 | .includes("module.exports = { foo: 'bar' };")
115 | ).toBe(true);
116 | });
117 | });
118 |
119 | describe('deno', (): void => {
120 | const denoRoot = path.join(process.cwd(), 'packages/dev/build-deno');
121 | const denoMod = fs.readFileSync(path.join(denoRoot, 'mod.ts'), 'utf-8');
122 |
123 | it('has *.ts imports', (): void => {
124 | expect(
125 | denoMod.includes("import './index.ts';")
126 | ).toBe(true);
127 | });
128 |
129 | it('has node: imports', (): void => {
130 | expect(
131 | denoMod.includes("import nodeCrypto from 'node:crypto';")
132 | ).toBe(true);
133 | });
134 |
135 | it('has deno.land/x imports', (): void => {
136 | expect(
137 | fs
138 | .readFileSync(path.join(denoRoot, 'rootJs/augmented.ts'))
139 | .includes("declare module 'https://deno.land/x/polkadot/dev/types.ts' {")
140 | ).toBe(true);
141 | });
142 |
143 | // See https://github.com/denoland/deno/issues/18557
144 | // NOTE: When available, the toBe(false) should be toBe(true)
145 | describe.todo('npm: prefixes', (): void => {
146 | it('has npm: imports', (): void => {
147 | expect(
148 | /import rollupAlias from 'npm:@rollup\/plugin-alias@\^\d\d?\.\d\d?\.\d\d?';/.test(denoMod)
149 | ).toBe(false); // true);
150 | });
151 |
152 | it('has npm: imports with paths', (): void => {
153 | expect(
154 | /import eslint from 'npm:eslint@\^\d\d?\.\d\d?\.\d\d?\/use-at-your-own-risk';/.test(denoMod)
155 | ).toBe(false); // true);
156 | });
157 | });
158 | });
159 | });
160 |
--------------------------------------------------------------------------------
/packages/dev-test/src/env/expect.spec.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev-test authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | describe('expect', () => {
5 | it('has been decorated', () => {
6 | expect(expect(true).not).toBeDefined();
7 | });
8 |
9 | it('throws on unimplemented', () => {
10 | expect(
11 | () => expect(true).not.toHaveReturnedWith()
12 | ).toThrow('expect(...).not.toHaveReturnedWith has not been implemented');
13 | });
14 |
15 | it('throws on unimplemented (with alternative)', () => {
16 | expect(
17 | () => expect(true).not.toBeFalsy()
18 | ).toThrow('expect(...).not.toBeFalsy has not been implemented (Use expect(...).toBeTruthy instead)');
19 | });
20 |
21 | describe('rejects', () => {
22 | it('matches a rejection via .toThrow', async () => {
23 | await expect(
24 | Promise.reject(new Error('this is a rejection message'))
25 | ).rejects.toThrow(/rejection/);
26 | });
27 | });
28 |
29 | describe('.toBeDefined', () => {
30 | it('does not throw on null', () => {
31 | expect(null).toBeDefined();
32 | });
33 |
34 | it('throws on undefined', () => {
35 | expect(
36 | () => expect(undefined).toBeDefined()
37 | ).toThrow();
38 | });
39 |
40 | it('.not does not throw on undefined', () => {
41 | expect(undefined).not.toBeDefined();
42 | });
43 | });
44 |
45 | describe('.toThrow', () => {
46 | const thrower = () => {
47 | throw new Error('some error');
48 | };
49 |
50 | it('matches error with empty throw', () => {
51 | expect(thrower).toThrow();
52 | });
53 |
54 | it('matches error with exact message', () => {
55 | expect(thrower).toThrow('some error');
56 | });
57 |
58 | it('matches error with regex message', () => {
59 | expect(thrower).toThrow(/me er/);
60 | });
61 |
62 | it('handles .not correctly (no throw, empty message)', () => {
63 | expect(() => undefined).not.toThrow();
64 | });
65 |
66 | it('handles .not correctly (no throw, regex match)', () => {
67 | expect(() => undefined).not.toThrow(/me er/);
68 | });
69 |
70 | it('handles .not correctly (throw, string match)', () => {
71 | expect(() => undefined).not.toThrow('no match');
72 | });
73 |
74 | it('handles .not correctly (throw, regex match)', () => {
75 | expect(() => undefined).not.toThrow(/no match/);
76 | });
77 | });
78 |
79 | describe('.toMatch', () => {
80 | it('fails matching when non-object passed in', () => {
81 | expect(
82 | () => expect(undefined).toMatch(/match/)
83 | ).toThrow(/Expected string/);
84 | });
85 |
86 | it('fails matching when non-matching string passed in', () => {
87 | expect(
88 | () => expect('some').toMatch(/match/)
89 | ).toThrow(/did not match/);
90 | });
91 |
92 | it('matches string passed', () => {
93 | expect(
94 | () => expect('matching').toMatch(/match/)
95 | ).not.toThrow();
96 | });
97 | });
98 |
99 | describe('.toMatchObject', () => {
100 | it('fails matching when non-object passed in', () => {
101 | expect(
102 | () => expect(undefined).toMatchObject({ foo: 'bar' })
103 | ).toThrow(/Expected object/);
104 | });
105 |
106 | it('matches empty object', () => {
107 | expect({
108 | a: 'foo',
109 | b: 'bar'
110 | }).toMatchObject({});
111 | });
112 |
113 | it('matches object with some fields', () => {
114 | expect({
115 | a: 'foo',
116 | b: 'bar',
117 | c: 123,
118 | d: [456, 789]
119 | }).toMatchObject({
120 | a: 'foo',
121 | c: 123,
122 | d: [456, 789]
123 | });
124 | });
125 |
126 | it('matches an object with some expect.stringMatching supplied', () => {
127 | expect({
128 | a: 'foo bar',
129 | b: 'baz',
130 | c: 'zaz'
131 | }).toMatchObject({
132 | a: expect.stringMatching(/o b/),
133 | b: expect.stringMatching('baz'),
134 | c: 'zaz'
135 | });
136 | });
137 |
138 | it('matches an object with expect.any supplied', () => {
139 | expect({
140 | a: 123,
141 | b: Boolean(true),
142 | c: 'foo'
143 | }).toMatchObject({
144 | a: expect.any(Number),
145 | b: expect.any(Boolean),
146 | c: 'foo'
147 | });
148 | });
149 |
150 | it('does not match an object with non instance value for expect.any', () => {
151 | expect(
152 | () => expect({
153 | a: true,
154 | b: 'foo'
155 | }).toMatchObject({
156 | a: expect.any(Number),
157 | b: 'foo'
158 | })
159 | ).toThrow(/not an instance of Number/);
160 | });
161 |
162 | it('matches an object with expect.anything supplied', () => {
163 | expect({
164 | a: 123,
165 | b: 'foo'
166 | }).toMatchObject({
167 | a: expect.anything(),
168 | b: 'foo'
169 | });
170 | });
171 |
172 | it('does not match an object with undefined value for expect.anything', () => {
173 | expect(
174 | () => expect({
175 | b: 'foo'
176 | }).toMatchObject({
177 | a: expect.anything(),
178 | b: 'foo'
179 | })
180 | ).toThrow(/non-nullish/);
181 | });
182 |
183 | it('does not match an object with non-array value', () => {
184 | expect(
185 | () => expect({
186 | a: 'foo',
187 | b: 'bar'
188 | }).toMatchObject({
189 | a: 'foo',
190 | b: [123, 456]
191 | })
192 | ).toThrow(/Expected array value/);
193 | });
194 |
195 | it('allows for deep matching', () => {
196 | expect({
197 | a: 123,
198 | b: {
199 | c: 456,
200 | d: {
201 | e: 'foo',
202 | f: 'bar',
203 | g: {
204 | h: [789, { z: 'baz' }]
205 | }
206 | }
207 | }
208 | }).toMatchObject({
209 | a: 123,
210 | b: {
211 | c: expect.any(Number),
212 | d: {
213 | f: 'bar',
214 | g: {
215 | h: [expect.any(Number), { z: 'baz' }]
216 | }
217 | }
218 | }
219 | });
220 | });
221 | });
222 | });
223 |
--------------------------------------------------------------------------------
/packages/dev-ts/src/resolver.ts:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev-ts authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import fs from 'node:fs';
5 | import path from 'node:path';
6 | import { fileURLToPath, pathToFileURL, URL } from 'node:url';
7 |
8 | import { CWD_URL, EXT_JS_REGEX, EXT_JSON_REGEX, EXT_TS_ARRAY, EXT_TS_REGEX } from './common.js';
9 | import { tsAliases } from './tsconfig.js';
10 |
11 | interface Resolved {
12 | format: 'commonjs' | 'json' | 'module';
13 | shortCircuit?: boolean;
14 | url: string;
15 | }
16 |
17 | interface ResolverContext {
18 | parentURL?: string;
19 | }
20 |
21 | type Resolver = (specifier: string, context: ResolverContext) => Resolved | undefined;
22 |
23 | /**
24 | * @internal
25 | *
26 | * From a specified URL, extract the actual full path as well as the
27 | * directory that this path reflects (either equivalent to path or the
28 | * root of the file being referenced)
29 | */
30 | function getParentPath (parentUrl: URL | string): { parentDir: string; parentPath: string; } {
31 | const parentPath = fileURLToPath(parentUrl);
32 |
33 | return {
34 | parentDir: fs.existsSync(parentPath) && fs.lstatSync(parentPath).isDirectory()
35 | ? parentPath
36 | : path.dirname(parentPath),
37 | parentPath
38 | };
39 | }
40 |
41 | /**
42 | * @internal
43 | *
44 | * Resolve fully-specified imports with extensions.
45 | **/
46 | export function resolveExtTs (specifier: string, parentUrl: URL | string): Resolved | void {
47 | // handle .ts extensions directly
48 | if (EXT_TS_REGEX.test(specifier)) {
49 | return {
50 | format: 'module',
51 | shortCircuit: true,
52 | url: new URL(specifier, parentUrl).href
53 | };
54 | }
55 | }
56 |
57 | /**
58 | * @internal
59 | *
60 | * Resolve fully-specified imports with extensions. Here we cater for the TS
61 | * mapping of import foo from './bar.js' where only './bar.ts' exists
62 | **/
63 | export function resolveExtJs (specifier: string, parentUrl: URL | string): Resolved | void {
64 | // handle ts imports where import *.js -> *.ts
65 | // (unlike the ts resolution, we only cater for relative paths)
66 | if (specifier.startsWith('.') && EXT_JS_REGEX.test(specifier)) {
67 | const full = fileURLToPath(new URL(specifier, parentUrl));
68 |
69 | // when it doesn't exist, we try and see if a source replacement helps
70 | if (!fs.existsSync(full)) {
71 | const found = EXT_TS_ARRAY
72 | .map((e) => full.replace(EXT_JS_REGEX, e))
73 | .find((f) => fs.existsSync(f) && fs.lstatSync(f).isFile());
74 |
75 | if (found) {
76 | return {
77 | format: 'module',
78 | shortCircuit: true,
79 | url: pathToFileURL(found).href
80 | };
81 | }
82 | }
83 | }
84 | }
85 |
86 | /**
87 | * @internal
88 | *
89 | * Resolution for Json files. Generally these would be via path aliasing.
90 | */
91 | export function resolveExtJson (specifier: string, parentUrl: URL | string): Resolved | void {
92 | if (specifier.startsWith('.') && EXT_JSON_REGEX.test(specifier)) {
93 | const { parentDir } = getParentPath(parentUrl);
94 | const jsonPath = path.join(parentDir, specifier);
95 |
96 | if (fs.existsSync(jsonPath)) {
97 | return {
98 | // .json needs to be in 'json' format for the loader, for the
99 | // the rest (it should only be TS) we use the 'module' format
100 | format: 'json',
101 | shortCircuit: true,
102 | url: pathToFileURL(jsonPath).href
103 | };
104 | }
105 | }
106 | }
107 |
108 | /**
109 | * @internal
110 | *
111 | * Resolve relative (extensionless) paths.
112 | *
113 | * At some point we probably might need to extend this to cater for the
114 | * ts (recommended) approach for using .js extensions inside the sources.
115 | * However, since we don't use this in the polkadot-js code, can kick this
116 | * down the line
117 | **/
118 | export function resolveExtBare (specifier: string, parentUrl: URL | string): Resolved | void {
119 | if (specifier.startsWith('.')) {
120 | const { parentDir, parentPath } = getParentPath(parentUrl);
121 | const found = specifier === '.'
122 | ? (
123 | // handle . imports for /index.ts
124 | EXT_TS_ARRAY
125 | .map((e) => path.join(parentDir, `index${e}`))
126 | .find((f) => fs.existsSync(f)) ||
127 | // handle the case where parentUrl needs an extension (generally via alias)
128 | EXT_TS_ARRAY
129 | .map((e) => `${parentPath}${e}`)
130 | .find((f) => fs.existsSync(f))
131 | )
132 | : (
133 | // tests to see if this is a file (without extension)
134 | EXT_TS_ARRAY
135 | .map((e) => path.join(parentDir, `${specifier}${e}`))
136 | .find((f) => fs.existsSync(f)) ||
137 | // test to see if this is a directory
138 | EXT_TS_ARRAY
139 | .map((e) => path.join(parentDir, `${specifier}/index${e}`))
140 | .find((f) => fs.existsSync(f))
141 | );
142 |
143 | if (found) {
144 | return {
145 | format: 'module',
146 | shortCircuit: true,
147 | url: pathToFileURL(found).href
148 | };
149 | }
150 | }
151 | }
152 |
153 | /**
154 | * @internal
155 | *
156 | * Resolve anything that is not an alias
157 | **/
158 | export function resolveNonAlias (specifier: string, parentUrl: URL | string): Resolved | void {
159 | return (
160 | resolveExtTs(specifier, parentUrl) ||
161 | resolveExtJs(specifier, parentUrl) ||
162 | resolveExtJson(specifier, parentUrl) ||
163 | resolveExtBare(specifier, parentUrl)
164 | );
165 | }
166 |
167 | /**
168 | * @internal
169 | *
170 | * Resolve TS alias mappings as defined in the tsconfig.json file
171 | **/
172 | export function resolveAlias (specifier: string, _parentUrl: URL | string, aliases = tsAliases): Resolved | void {
173 | const parts = specifier.split(/[\\/]/);
174 | const found = aliases
175 | // return a [filter, [...partIndex]] mapping
176 | .map((alias) => ({
177 | alias,
178 | indexes: parts
179 | .map((_, i) => i)
180 | .filter((start) =>
181 | (
182 | alias.isWildcard
183 | // parts should have more entries than the wildcard
184 | ? parts.length > alias.filter.length
185 | // or the same amount in case of a non-wildcard match
186 | : parts.length === alias.filter.length
187 | ) &&
188 | // match all parts of the alias
189 | alias.filter.every((f, i) =>
190 | parts[start + i] &&
191 | parts[start + i] === f
192 | )
193 | )
194 | }))
195 | // we only return the first
196 | .find(({ indexes }) => indexes.length);
197 |
198 | if (found) {
199 | // do the actual un-aliased resolution
200 | return resolveNonAlias(
201 | `./${found.alias.path.replace('*', path.join(...parts.slice(found.alias.filter.length)))}`,
202 | found.alias.url
203 | );
204 | }
205 | }
206 |
207 | /**
208 | * Resolves a path using our logic.
209 | *
210 | * 1. First we attempt to directly resolve if .ts/.tsx extension is found
211 | * 2. Then we do relative resolves (this is for extension-less .ts files)
212 | * 3. The we try to do resolution via TS aliases
213 | *
214 | * ... finally, try the next loader in the chain
215 | */
216 | export function resolve (specifier: string, context: ResolverContext, nextResolve: Resolver) {
217 | const parentUrl = context.parentURL || CWD_URL;
218 |
219 | return (
220 | resolveNonAlias(specifier, parentUrl) ||
221 | resolveAlias(specifier, parentUrl) ||
222 | nextResolve(specifier, context)
223 | );
224 | }
225 |
--------------------------------------------------------------------------------
/packages/dev/config/eslint.rules.js:
--------------------------------------------------------------------------------
1 | // Copyright 2017-2025 @polkadot/dev authors & contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import JSON5 from 'json5';
5 | import fs from 'node:fs';
6 | import path from 'node:path';
7 | import process from 'node:process';
8 |
9 | const FIXME = {
10 | // This is in the new 6.0.0 and we should switch this on
11 | // at some point. For a first iteration we keep as-is
12 | '@typescript-eslint/prefer-nullish-coalescing': 'off'
13 | };
14 |
15 | /**
16 | * Returns a copyright header pattern (using tsconfig.base.json)
17 | *
18 | * @returns {string}
19 | */
20 | function getHeaderPattern () {
21 | const tsPath = path.join(process.cwd(), 'tsconfig.base.json');
22 |
23 | if (!fs.existsSync(tsPath)) {
24 | throw new Error(`Unable to load ${tsPath}`);
25 | }
26 |
27 | const tsConfig = JSON5.parse(fs.readFileSync(tsPath, 'utf-8'));
28 |
29 | if (!tsConfig?.compilerOptions?.paths) {
30 | throw new Error(`Unable to extract compilerOptions.paths structure from ${tsPath}`);
31 | }
32 |
33 | const paths = Object.keys(tsConfig.compilerOptions.paths);
34 |
35 | if (!paths.length) {
36 | throw new Error(`No keys found in compilerOptions.paths from ${tsPath}`);
37 | }
38 |
39 | const packages = paths.reduce((packages, k) => {
40 | const [pd, pk] = k.split('/');
41 |
42 | if (pd !== '@polkadot' || !pk) {
43 | throw new Error(`Non @polkadot path in ${tsPath}`);
44 | }
45 |
46 | return packages.length
47 | ? `${packages}|${pk}`
48 | : pk;
49 | }, '');
50 | const fullyear = new Date().getFullYear();
51 | const years = [];
52 |
53 | for (let i = 17, last = fullyear - 2000; i < last; i++) {
54 | years.push(`${i}`);
55 | }
56 |
57 | return ` Copyright 20(${years.join('|')})(-${fullyear})? @polkadot/(${packages})`;
58 | }
59 |
60 | export const overrideAll = {
61 | ...FIXME,
62 | // the next 2 enforce isolatedModules & verbatimModuleSyntax
63 | '@typescript-eslint/consistent-type-exports': 'error',
64 | '@typescript-eslint/consistent-type-imports': 'error',
65 | '@typescript-eslint/dot-notation': 'error',
66 | '@typescript-eslint/indent': ['error', 2],
67 | '@typescript-eslint/no-non-null-assertion': 'error',
68 | // ts itself checks and ignores those starting with _, align the linting
69 | '@typescript-eslint/no-unused-vars': ['error', {
70 | args: 'all',
71 | argsIgnorePattern: '^_',
72 | caughtErrors: 'all',
73 | caughtErrorsIgnorePattern: '^_',
74 | destructuredArrayIgnorePattern: '^_',
75 | vars: 'all',
76 | varsIgnorePattern: '^_'
77 | }],
78 | '@typescript-eslint/type-annotation-spacing': 'error',
79 | 'arrow-parens': ['error', 'always'],
80 | 'brace-style': ['error', '1tbs'],
81 | curly: ['error', 'all'],
82 | 'default-param-last': 'off', // conflicts with TS version
83 | 'deprecation/deprecation': 'error',
84 | 'dot-notation': 'off', // conflicts with TS version
85 | 'func-style': ['error', 'declaration', {
86 | allowArrowFunctions: true
87 | }],
88 | // this does help with declarations, but also
89 | // applies to invocations, which is an issue...
90 | // 'function-paren-newline': ['error', 'never'],
91 | 'function-call-argument-newline': ['error', 'consistent'],
92 | 'header/header': ['error', 'line', [
93 | { pattern: getHeaderPattern() },
94 | ' SPDX-License-Identifier: Apache-2.0'
95 | ], 2],
96 | 'import-newlines/enforce': ['error', {
97 | forceSingleLine: true,
98 | items: 2048
99 | }],
100 | 'import/export': 'error',
101 | 'import/extensions': ['error', 'ignorePackages', {
102 | cjs: 'always',
103 | js: 'always',
104 | json: 'always',
105 | jsx: 'never',
106 | mjs: 'always',
107 | ts: 'never',
108 | tsx: 'never'
109 | }],
110 | 'import/first': 'error',
111 | 'import/newline-after-import': 'error',
112 | 'import/no-duplicates': 'error',
113 | 'import/order': 'off', // conflicts with simple-import-sort
114 | indent: 'off', // required as 'off' since typescript-eslint has own versions
115 | 'no-extra-semi': 'error',
116 | 'no-unused-vars': 'off',
117 | 'no-use-before-define': 'off',
118 | 'object-curly-newline': ['error', {
119 | ExportDeclaration: { minProperties: 2048 },
120 | ImportDeclaration: { minProperties: 2048 },
121 | ObjectPattern: { minProperties: 2048 }
122 | }],
123 | 'padding-line-between-statements': [
124 | 'error',
125 | { blankLine: 'always', next: '*', prev: ['const', 'let', 'var'] },
126 | { blankLine: 'any', next: ['const', 'let', 'var'], prev: ['const', 'let', 'var'] },
127 | { blankLine: 'always', next: 'block-like', prev: '*' },
128 | { blankLine: 'always', next: '*', prev: 'block-like' },
129 | { blankLine: 'always', next: 'function', prev: '*' },
130 | { blankLine: 'always', next: '*', prev: 'function' },
131 | { blankLine: 'always', next: 'try', prev: '*' },
132 | { blankLine: 'always', next: '*', prev: 'try' },
133 | { blankLine: 'always', next: 'return', prev: '*' },
134 | { blankLine: 'always', next: 'import', prev: '*' },
135 | { blankLine: 'always', next: '*', prev: 'import' },
136 | { blankLine: 'any', next: 'import', prev: 'import' }
137 | ],
138 | semi: ['error', 'always'],
139 | 'simple-import-sort/exports': 'error',
140 | 'simple-import-sort/imports': ['error', {
141 | groups: [
142 | ['^\u0000'], // all side-effects (0 at start)
143 | ['\u0000$', '^@polkadot.*\u0000$', '^\\..*\u0000$'], // types (0 at end)
144 | // ['^node:'], // node
145 | ['^[^/\\.]'], // non-polkadot
146 | ['^@polkadot'], // polkadot
147 | ['^\\.\\.(?!/?$)', '^\\.\\./?$', '^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'] // local (. last)
148 | ]
149 | }],
150 | 'sort-destructure-keys/sort-destructure-keys': ['error', {
151 | caseSensitive: true
152 | }],
153 | 'sort-keys': 'error',
154 | 'spaced-comment': ['error', 'always', {
155 | block: {
156 | // pure export helpers
157 | markers: ['#__PURE__']
158 | },
159 | line: {
160 | // TS reference types
161 | markers: ['/ void;
11 |
12 | type Mocked = Partial>;
13 |
14 | // logged via Object.keys(expect).sort()
15 | const EXPECT_KEYS = ['addEqualityTesters', 'addSnapshotSerializer', 'any', 'anything', 'arrayContaining', 'assertions', 'closeTo', 'extend', 'extractExpectedAssertionsErrors', 'getState', 'hasAssertions', 'not', 'objectContaining', 'setState', 'stringContaining', 'stringMatching', 'toMatchInlineSnapshot', 'toMatchSnapshot', 'toThrowErrorMatchingInlineSnapshot', 'toThrowErrorMatchingSnapshot'] as const;
16 |
17 | // logged via Object.keys(expect(0)).sort()
18 | const EXPECT_KEYS_FN = ['lastCalledWith', 'lastReturnedWith', 'not', 'nthCalledWith', 'nthReturnedWith', 'rejects', 'resolves', 'toBe', 'toBeCalled', 'toBeCalledTimes', 'toBeCalledWith', 'toBeCloseTo', 'toBeDefined', 'toBeFalsy', 'toBeGreaterThan', 'toBeGreaterThanOrEqual', 'toBeInstanceOf', 'toBeLessThan', 'toBeLessThanOrEqual', 'toBeNaN', 'toBeNull', 'toBeTruthy', 'toBeUndefined', 'toContain', 'toContainEqual', 'toEqual', 'toHaveBeenCalled', 'toHaveBeenCalledTimes', 'toHaveBeenCalledWith', 'toHaveBeenLastCalledWith', 'toHaveBeenNthCalledWith', 'toHaveLastReturnedWith', 'toHaveLength', 'toHaveNthReturnedWith', 'toHaveProperty', 'toHaveReturned', 'toHaveReturnedTimes', 'toHaveReturnedWith', 'toMatch', 'toMatchInlineSnapshot', 'toMatchObject', 'toMatchSnapshot', 'toReturn', 'toReturnTimes', 'toReturnWith', 'toStrictEqual', 'toThrow', 'toThrowError', 'toThrowErrorMatchingInlineSnapshot', 'toThrowErrorMatchingSnapshot'] as const;
19 |
20 | const stubExpect = stubObj('expect', EXPECT_KEYS);
21 | const stubExpectFn = stubObj('expect(...)', EXPECT_KEYS_FN, {
22 | toThrowError: 'expect(...).toThrow'
23 | });
24 | const stubExpectFnRejects = stubObj('expect(...).rejects', EXPECT_KEYS_FN, {
25 | toThrowError: 'expect(...).rejects.toThrow'
26 | });
27 | const stubExpectFnResolves = stubObj('expect(...).resolves', EXPECT_KEYS_FN);
28 | const stubExpectFnNot = stubObj('expect(...).not', EXPECT_KEYS_FN, {
29 | toBeFalsy: 'expect(...).toBeTruthy',
30 | toBeTruthy: 'expect(...).toBeFalsy',
31 | toThrowError: 'expect(...).not.toThrow'
32 | });
33 |
34 | /**
35 | * @internal
36 | *
37 | * A helper that wraps a matching function in an ExpectMatcher. This is (currently)
38 | * only used/checked for in the calledWith* helpers
39 | *
40 | * TODO We don't use it in polkadot-js, but a useful enhancement could be for
41 | * any of the to* expectations to detect and use those. An example of useful code
42 | * in that case:
43 | *
44 | * ```js
45 | * expect({
46 | * a: 'blah',
47 | * b: 3
48 | * }).toEqual(
49 | * expect.objectContaining({ b: 3 })
50 | * )
51 | * ```
52 | *
53 | * An example of matcher use can be seen in the isCalledWith loops
54 | */
55 | class Matcher {
56 | assertMatch: AssertMatchFn;
57 |
58 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
59 | constructor (assertFn: (value: any, check: any) => void, check?: unknown) {
60 | this.assertMatch = (value) => assertFn(value, check);
61 | }
62 | }
63 |
64 | /**
65 | * @internal
66 | *
67 | * Asserts that the input value is non-nullish
68 | */
69 | function assertNonNullish (value: unknown): void {
70 | assert.ok(value !== null && value !== undefined, `Expected non-nullish value, found ${value as string}`);
71 | }
72 |
73 | /**
74 | * @internal
75 | *
76 | * A helper that checks a single call arguments, which may include the
77 | * use of matchers. This is used in finding any call or checking a specific
78 | * call
79 | */
80 | function assertCallHasArgs (call: { arguments: unknown[] } | undefined, args: unknown[]): void {
81 | assert.ok(call && args.length === call.arguments?.length, 'Number of arguments does not match');
82 |
83 | args.forEach((arg, i) => assertMatch(call.arguments[i], arg));
84 | }
85 |
86 | /**
87 | * @internal
88 | *
89 | * A helper that checks for the first instance of a match on the actual call
90 | * arguments (this extracts the toHaveBeenCalledWith logic)
91 | */
92 | function assertSomeCallHasArgs (value: Mocked | undefined, args: unknown[]) {
93 | assert.ok(value?.mock?.calls.some((call) => {
94 | try {
95 | assertCallHasArgs(call, args);
96 |
97 | return true;
98 | } catch {
99 | return false;
100 | }
101 | }), 'No call found matching arguments');
102 | }
103 |
104 | /**
105 | * @internal
106 | *
107 | * Asserts that the value is either (equal deep) or matches the matcher (if supplied)
108 | */
109 | function assertMatch (value: unknown, check: unknown): void {
110 | check instanceof Matcher
111 | ? check.assertMatch(value)
112 | : Array.isArray(check)
113 | ? assertMatchArr(value, check)
114 | : check && typeof check === 'object'
115 | ? assertMatchObj(value, check)
116 | : assert.deepStrictEqual(value, check);
117 | }
118 |
119 | /**
120 | * @internal
121 | *
122 | * A helper to match the supplied array check against the resulting array
123 | *
124 | * @param {unknown} value
125 | * @param {unknown[]} check
126 | */
127 | function assertMatchArr (value: unknown, check: unknown[]): void {
128 | assert.ok(value && Array.isArray(value), `Expected array value, found ${typeof value}`);
129 | assert.ok(value.length === check.length, `Expected array with ${check.length} entries, found ${value.length}`);
130 |
131 | check.forEach((other, i) => assertMatch(value[i], other));
132 | }
133 |
134 | /**
135 | * @internal
136 | *
137 | * A helper to match the supplied fields against the resulting object
138 | */
139 | function assertMatchObj (value: unknown, check: object): void {
140 | assert.ok(value && typeof value === 'object', `Expected object value, found ${typeof value}`);
141 |
142 | Object
143 | .entries(check)
144 | .forEach(([key, other]) => assertMatch((value as Record)[key], other));
145 | }
146 |
147 | /**
148 | * @internal
149 | *
150 | * A helper to match a string value against another string or regex
151 | */
152 | function assertMatchStr (value: unknown, check: string | RegExp): void {
153 | assert.ok(typeof value === 'string', `Expected string value, found ${typeof value}`);
154 |
155 | typeof check === 'string'
156 | ? assert.strictEqual(value, check)
157 | : assert.match(value, check);
158 | }
159 |
160 | /**
161 | * @internal
162 | *
163 | * A helper to check the type of a specific value as used in the expect.any(Clazz) matcher
164 | *
165 | * @see https://github.com/facebook/jest/blob/a49c88610e49a3242576160740a32a2fe11161e1/packages/expect/src/asymmetricMatchers.ts#L103-L133
166 | */
167 | // eslint-disable-next-line @typescript-eslint/ban-types
168 | function assertInstanceOf (value: unknown, Clazz: Function): void {
169 | assert.ok(
170 | (Clazz === Array && Array.isArray(value)) ||
171 | (Clazz === BigInt && typeof value === 'bigint') ||
172 | (Clazz === Boolean && typeof value === 'boolean') ||
173 | (Clazz === Function && typeof value === 'function') ||
174 | (Clazz === Number && typeof value === 'number') ||
175 | (Clazz === Object && typeof value === 'object') ||
176 | (Clazz === String && typeof value === 'string') ||
177 | (Clazz === Symbol && typeof value === 'symbol') ||
178 | (value instanceof Clazz),
179 | `${value as string} is not an instance of ${Clazz.name}`
180 | );
181 | }
182 |
183 | /**
184 | * @internal
185 | *
186 | * A helper to ensure that the supplied string/array does include the checker string.
187 | *
188 | * @param {string | unknown[]} value
189 | * @param {string} check
190 | */
191 | // eslint-disable-next-line @typescript-eslint/ban-types
192 | function assertIncludes (value: string | unknown[], [check, Clazz]: [string, Function]): void {
193 | assertInstanceOf(value, Clazz);
194 | assert.ok(value?.includes(check), `${value as string} does not include ${check}`);
195 | }
196 |
197 | /**
198 | * Sets up the shimmed expect(...) function, including all .to* and .not.to*
199 | * functions. This is not comprehensive, rather is contains what we need to
200 | * make all polkadot-js usages pass
201 | **/
202 | export function expect () {
203 | const rootMatchers = {
204 | // eslint-disable-next-line @typescript-eslint/ban-types
205 | any: (Clazz: Function) => new Matcher(assertInstanceOf, Clazz),
206 | anything: () => new Matcher(assertNonNullish),
207 | arrayContaining: (check: string) => new Matcher(assertIncludes, [check, Array]),
208 | objectContaining: (check: object) => new Matcher(assertMatchObj, check),
209 | stringContaining: (check: string) => new Matcher(assertIncludes, [check, String]),
210 | stringMatching: (check: string | RegExp) => new Matcher(assertMatchStr, check)
211 | };
212 |
213 | return {
214 | expect: enhanceObj(enhanceObj((value: unknown) =>
215 | enhanceObj({
216 | not: enhanceObj({
217 | toBe: (other: unknown) => assert.notStrictEqual(value, other),
218 | toBeDefined: () => assert.ok(value === undefined),
219 | toBeNull: (value: unknown) => assert.ok(value !== null),
220 | toBeUndefined: () => assert.ok(value !== undefined),
221 | toEqual: (other: unknown) => assert.notDeepEqual(value, other),
222 | toHaveBeenCalled: () => assert.ok(!(value as Mocked | undefined)?.mock?.calls.length),
223 | toThrow: (message?: RegExp | Error | string) => assert.doesNotThrow(value as () => unknown, message && { message } as Error)
224 | }, stubExpectFnNot),
225 | rejects: enhanceObj({
226 | toThrow: (message?: RegExp | Error | string) => assert.rejects(value as Promise, message && { message } as Error)
227 | }, stubExpectFnRejects),
228 | resolves: enhanceObj({}, stubExpectFnResolves),
229 | toBe: (other: unknown) => assert.strictEqual(value, other),
230 | toBeDefined: () => assert.ok(value !== undefined),
231 | toBeFalsy: () => assert.ok(!value),
232 | // eslint-disable-next-line @typescript-eslint/ban-types
233 | toBeInstanceOf: (Clazz: Function) => assertInstanceOf(value, Clazz),
234 | toBeNull: (value: unknown) => assert.ok(value === null),
235 | toBeTruthy: () => assert.ok(value),
236 | toBeUndefined: () => assert.ok(value === undefined),
237 | toEqual: (other: unknown) => assert.deepEqual(value, other),
238 | toHaveBeenCalled: () => assert.ok((value as Mocked | undefined)?.mock?.calls.length),
239 | toHaveBeenCalledTimes: (count: number) => assert.equal((value as Mocked | undefined)?.mock?.calls.length, count),
240 | toHaveBeenCalledWith: (...args: unknown[]) => assertSomeCallHasArgs((value as Mocked | undefined), args),
241 | toHaveBeenLastCalledWith: (...args: unknown[]) => assertCallHasArgs((value as Mocked | undefined)?.mock?.calls.at(-1), args),
242 | toHaveLength: (length: number) => assert.equal((value as unknown[] | undefined)?.length, length),
243 | toMatch: (check: string | RegExp) => assertMatchStr(value, check),
244 | toMatchObject: (check: object) => assertMatchObj(value, check),
245 | toThrow: (message?: RegExp | Error | string) => assert.throws(value as () => unknown, message && { message } as Error)
246 | }, stubExpectFn), rootMatchers), stubExpect)
247 | };
248 | }
249 |
--------------------------------------------------------------------------------
/packages/dev/scripts/polkadot-exec-node-test.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | // Copyright 2017-2025 @polkadot/dev authors & contributors
3 | // SPDX-License-Identifier: Apache-2.0
4 |
5 | // For Node 18, earliest usable is 18.14:
6 | //
7 | // - node:test added in 18.0,
8 | // - run method exposed in 18.9,
9 | // - mock in 18.13,
10 | // - diagnostics changed in 18.14
11 | //
12 | // Node 16 is not supported:
13 | //
14 | // - node:test added is 16.17,
15 | // - run method exposed in 16.19,
16 | // - mock not available
17 |
18 | import fs from 'node:fs';
19 | import os from 'node:os';
20 | import path from 'node:path';
21 | import process from 'node:process';
22 | import { run } from 'node:test';
23 | import { isMainThread, parentPort, Worker, workerData } from 'node:worker_threads';
24 |
25 | // NOTE error should be defined as "Error", however the @types/node definitions doesn't include all
26 | /** @typedef {{ file?: string; message?: string; }} DiagStat */
27 | /** @typedef {{ details: { type: string; duration_ms: number; error: { message: string; failureType: unknown; stack: string; cause: { code: number; message: string; stack: string; generatedMessage?: any; }; code: number; } }; file?: string; name: string; testNumber: number; nesting: number; }} FailStat */
28 | /** @typedef {{ details: { duration_ms: number }; name: string; }} PassStat */
29 | /** @typedef {{ diag: DiagStat[]; fail: FailStat[]; pass: PassStat[]; skip: unknown[]; todo: unknown[]; total: number; [key: string]: any; }} Stats */
30 |
31 | console.time('\t elapsed :');
32 |
33 | const WITH_DEBUG = false;
34 |
35 | const args = process.argv.slice(2);
36 | /** @type {string[]} */
37 | const files = [];
38 |
39 | /** @type {Stats} */
40 | const stats = {
41 | diag: [],
42 | fail: [],
43 | pass: [],
44 | skip: [],
45 | todo: [],
46 | total: 0
47 | };
48 | /** @type {string | null} */
49 | let logFile = null;
50 | /** @type {number} */
51 | let startAt = 0;
52 | /** @type {boolean} */
53 | let bail = false;
54 | /** @type {boolean} */
55 | let toConsole = false;
56 | /** @type {number} */
57 | let progressRowCount = 0;
58 |
59 | for (let i = 0; i < args.length; i++) {
60 | if (args[i] === '--bail') {
61 | bail = true;
62 | } else if (args[i] === '--console') {
63 | toConsole = true;
64 | } else if (args[i] === '--logfile') {
65 | logFile = args[++i];
66 | } else {
67 | files.push(args[i]);
68 | }
69 | }
70 |
71 | /**
72 | * @internal
73 | *
74 | * Performs an indent of the line (and containing lines) with the specific count
75 | *
76 | * @param {number} count
77 | * @param {string} str
78 | * @param {string} start
79 | * @returns {string}
80 | */
81 | function indent (count, str = '', start = '') {
82 | let pre = '\n';
83 |
84 | switch (count) {
85 | case 0:
86 | break;
87 |
88 | case 1:
89 | pre += '\t';
90 | break;
91 |
92 | case 2:
93 | pre += '\t\t';
94 | break;
95 |
96 | default:
97 | pre += '\t\t\t';
98 | break;
99 | }
100 |
101 | pre += ' ';
102 |
103 | return `${pre}${start}${
104 | str
105 | .split('\n')
106 | .map((l) => l.trim())
107 | .join(`${pre}${start ? ' '.padStart(start.length, ' ') : ''}`)
108 | }\n`;
109 | }
110 |
111 | /**
112 | * @param {FailStat} r
113 | * @return {string | undefined}
114 | */
115 | function getFilename (r) {
116 | if (r.file?.includes('.spec.') || r.file?.includes('.test.')) {
117 | return r.file;
118 | }
119 |
120 | if (r.details.error.cause.stack) {
121 | const stack = r.details.error.cause.stack
122 | .split('\n')
123 | .map((l) => l.trim())
124 | .filter((l) => l.startsWith('at ') && (l.includes('.spec.') || l.includes('.test.')))
125 | .map((l) => l.match(/\(.*:\d\d?:\d\d?\)$/)?.[0])
126 | .map((l) => l?.replace('(', '')?.replace(')', ''));
127 |
128 | if (stack.length) {
129 | return stack[0];
130 | }
131 | }
132 |
133 | return r.file;
134 | }
135 |
136 | function complete () {
137 | process.stdout.write('\n');
138 |
139 | let logError = '';
140 |
141 | stats.fail.forEach((r) => {
142 | WITH_DEBUG && console.error(JSON.stringify(r, null, 2));
143 |
144 | let item = '';
145 |
146 | item += indent(1, [getFilename(r), r.name].filter((s) => !!s).join('\n'), 'x ');
147 | item += indent(2, `${r.details.error.failureType} / ${r.details.error.code}${r.details.error.cause.code && r.details.error.cause.code !== r.details.error.code ? ` / ${r.details.error.cause.code}` : ''}`);
148 |
149 | if (r.details.error.cause.message) {
150 | item += indent(2, r.details.error.cause.message);
151 | }
152 |
153 | logError += item;
154 |
155 | if (r.details.error.cause.stack) {
156 | item += indent(2, r.details.error.cause.stack);
157 | }
158 |
159 | process.stdout.write(item);
160 | });
161 |
162 | if (logFile && logError) {
163 | try {
164 | fs.appendFileSync(path.join(process.cwd(), logFile), logError);
165 | } catch (e) {
166 | console.error(e);
167 | }
168 | }
169 |
170 | console.log();
171 | console.log('\t passed ::', stats.pass.length);
172 | console.log('\t failed ::', stats.fail.length);
173 | console.log('\t skipped ::', stats.skip.length);
174 | console.log('\t todo ::', stats.todo.length);
175 | console.log('\t total ::', stats.total);
176 | console.timeEnd('\t elapsed :');
177 | console.log();
178 |
179 | // The full error information can be quite useful in the case of overall failures
180 | if ((stats.fail.length || toConsole) && stats.diag.length) {
181 | /** @type {string | undefined} */
182 | let lastFilename = '';
183 |
184 | stats.diag.forEach((r) => {
185 | WITH_DEBUG && console.error(JSON.stringify(r, null, 2));
186 |
187 | if (typeof r === 'string') {
188 | console.log(r); // Node.js <= 18.14
189 | } else if (r.file && r.file.includes('@polkadot/dev/scripts')) {
190 | // Ignore internal diagnostics
191 | } else {
192 | if (lastFilename !== r.file) {
193 | lastFilename = r.file;
194 |
195 | console.log(lastFilename ? `\n${lastFilename}::\n` : '\n');
196 | }
197 |
198 | // Edge case: We don't need additional noise that is not useful.
199 | if (!r.message?.split(' ').includes('tests')) {
200 | console.log(`\t${r.message?.split('\n').join('\n\t')}`);
201 | }
202 | }
203 | });
204 | }
205 |
206 | if (toConsole) {
207 | stats.pass.forEach((r) => {
208 | console.log(`pass ${r.name} ${r.details.duration_ms} ms`);
209 | });
210 |
211 | console.log();
212 |
213 | stats.fail.forEach((r) => {
214 | console.log(`fail ${r.name}`);
215 | });
216 |
217 | console.log();
218 | }
219 |
220 | if (stats.total === 0) {
221 | console.error('FATAL: No tests executed');
222 | console.error();
223 | process.exit(1);
224 | }
225 |
226 | process.exit(stats.fail.length);
227 | }
228 |
229 | /**
230 | * Prints the progress in real-time as data is passed from the worker.
231 | *
232 | * @param {string} symbol
233 | */
234 | function printProgress (symbol) {
235 | if (!progressRowCount) {
236 | progressRowCount = 0;
237 | }
238 |
239 | if (!startAt) {
240 | startAt = performance.now();
241 | }
242 |
243 | // If starting a new row, calculate and print the elapsed time
244 | if (progressRowCount === 0) {
245 | const now = performance.now();
246 | const elapsed = (now - startAt) / 1000;
247 | const minutes = Math.floor(elapsed / 60);
248 | const seconds = elapsed - minutes * 60;
249 |
250 | process.stdout.write(
251 | `${`${minutes}:${seconds.toFixed(3).padStart(6, '0')}`.padStart(11)} `
252 | );
253 | }
254 |
255 | // Print the symbol with formatting
256 | process.stdout.write(symbol);
257 |
258 | progressRowCount++;
259 |
260 | // Add spaces for readability
261 | if (progressRowCount % 10 === 0) {
262 | process.stdout.write(' '); // Double space every 10 symbols
263 | } else if (progressRowCount % 5 === 0) {
264 | process.stdout.write(' '); // Single space every 5 symbols
265 | }
266 |
267 | // If the row reaches 100 symbols, start a new row
268 | if (progressRowCount >= 100) {
269 | process.stdout.write('\n');
270 | progressRowCount = 0;
271 | }
272 | }
273 |
274 | async function runParallel () {
275 | const MAX_WORKERS = Math.min(os.cpus().length, files.length);
276 | const chunks = Math.ceil(files.length / MAX_WORKERS);
277 |
278 | try {
279 | // Create and manage worker threads
280 | const results = await Promise.all(
281 | Array.from({ length: MAX_WORKERS }, (_, i) => {
282 | const fileSubset = files.slice(i * chunks, (i + 1) * chunks);
283 |
284 | return new Promise((resolve, reject) => {
285 | const worker = new Worker(new URL(import.meta.url), {
286 | workerData: { files: fileSubset }
287 | });
288 |
289 | worker.on('message', (message) => {
290 | if (message.type === 'progress') {
291 | printProgress(message.data);
292 | } else if (message.type === 'result') {
293 | resolve(message.data);
294 | }
295 | });
296 |
297 | worker.on('error', reject);
298 | worker.on('exit', (code) => {
299 | if (code !== 0) {
300 | reject(new Error(`Worker stopped with exit code ${code}`));
301 | }
302 | });
303 | });
304 | })
305 | );
306 |
307 | // Aggregate results from workers
308 | results.forEach((result) => {
309 | Object.keys(stats).forEach((key) => {
310 | if (Array.isArray(stats[key])) {
311 | stats[key] = stats[key].concat(result[key]);
312 | } else if (typeof stats[key] === 'number') {
313 | stats[key] += result[key];
314 | }
315 | });
316 | });
317 |
318 | complete();
319 | } catch (err) {
320 | console.error('Error during parallel execution:', err);
321 | process.exit(1);
322 | }
323 | }
324 |
325 | if (isMainThread) {
326 | console.time('\tElapsed:');
327 | runParallel().catch((err) => console.error(err));
328 | } else {
329 | run({ files: workerData.files, timeout: 3_600_000 })
330 | .on('data', () => undefined)
331 | .on('end', () => parentPort && parentPort.postMessage(stats))
332 | .on('test:coverage', () => undefined)
333 | .on('test:diagnostic', (/** @type {DiagStat} */data) => {
334 | stats.diag.push(data);
335 | parentPort && parentPort.postMessage({ data: stats, type: 'result' });
336 | })
337 | .on('test:fail', (/** @type {FailStat} */ data) => {
338 | const statFail = structuredClone(data);
339 |
340 | if (data.details.error.cause?.stack) {
341 | statFail.details.error.cause.stack = data.details.error.cause.stack;
342 | }
343 |
344 | stats.fail.push(statFail);
345 | stats.total++;
346 | parentPort && parentPort.postMessage({ data: 'x', type: 'progress' });
347 |
348 | if (bail) {
349 | complete();
350 | }
351 | })
352 | .on('test:pass', (data) => {
353 | const symbol = typeof data.skip !== 'undefined' ? '>' : typeof data.todo !== 'undefined' ? '!' : '·';
354 |
355 | if (symbol === '>') {
356 | stats.skip.push(data);
357 | } else if (symbol === '!') {
358 | stats.todo.push(data);
359 | } else {
360 | stats.pass.push(data);
361 | }
362 |
363 | stats.total++;
364 | parentPort && parentPort.postMessage({ data: symbol, type: 'progress' });
365 | })
366 | .on('test:plan', () => undefined)
367 | .on('test:start', () => undefined);
368 | }
369 |
--------------------------------------------------------------------------------
/packages/dev/scripts/polkadot-ci-ghact-build.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | // Copyright 2017-2025 @polkadot/dev authors & contributors
3 | // SPDX-License-Identifier: Apache-2.0
4 |
5 | import fs from 'node:fs';
6 | import os from 'node:os';
7 | import path from 'node:path';
8 | import process from 'node:process';
9 | import yargs from 'yargs';
10 |
11 | import { copyDirSync, copyFileSync, denoCreateDir, execGit, execPm, execSync, exitFatal, GITHUB_REPO, GITHUB_TOKEN_URL, gitSetup, logBin, mkdirpSync, rimrafSync, topoSort } from './util.mjs';
12 |
13 | /** @typedef {Record} ChangelogMap */
14 |
15 | logBin('polkadot-ci-ghact-build');
16 |
17 | const DENO_REPO = 'polkadot-js/build-deno.land';
18 | const BUND_REPO = 'polkadot-js/build-bundle';
19 |
20 | const repo = `${GITHUB_TOKEN_URL}/${GITHUB_REPO}.git`;
21 | const denoRepo = `${GITHUB_TOKEN_URL}/${DENO_REPO}.git`;
22 | const bundRepo = `${GITHUB_TOKEN_URL}/${BUND_REPO}.git`;
23 | const bundClone = 'build-bundle-clone';
24 | const denoClone = 'build-deno-clone';
25 |
26 | let withDeno = false;
27 | let withBund = false;
28 | let withNpm = false;
29 |
30 | /** @type {string[]} */
31 | const shouldDeno = [];
32 | /** @type {string[]} */
33 | const shouldBund = [];
34 |
35 | const argv = await yargs(process.argv.slice(2))
36 | .options({
37 | 'skip-beta': {
38 | description: 'Do not increment as beta',
39 | type: 'boolean'
40 | }
41 | })
42 | .strict()
43 | .argv;
44 |
45 | /**
46 | * Removes a specific file, returning true if found, false otherwise
47 | *
48 | * @param {string} file
49 | * @returns {boolean}
50 | */
51 | function rmFile (file) {
52 | if (fs.existsSync(file)) {
53 | rimrafSync(file);
54 |
55 | return true;
56 | }
57 |
58 | return false;
59 | }
60 |
61 | /**
62 | * Retrieves the path of the root package.json
63 | *
64 | * @returns {string}
65 | */
66 | function npmGetJsonPath () {
67 | return path.resolve(process.cwd(), 'package.json');
68 | }
69 |
70 | /**
71 | * Retrieves the contents of the root package.json
72 | *
73 | * @returns {{ name: string; version: string; versions?: { npm?: string; git?: string } }}
74 | */
75 | function npmGetJson () {
76 | return JSON.parse(
77 | fs.readFileSync(npmGetJsonPath(), 'utf8')
78 | );
79 | }
80 |
81 | /**
82 | * Writes the contents of the root package.json
83 | *
84 | * @param {any} json
85 | */
86 | function npmSetJson (json) {
87 | fs.writeFileSync(npmGetJsonPath(), `${JSON.stringify(json, null, 2)}\n`);
88 | }
89 |
90 | /**
91 | * Retrieved the current version included in package.json
92 | *
93 | * @returns {string}
94 | */
95 | function npmGetVersion () {
96 | return npmGetJson().version;
97 | }
98 |
99 | /**
100 | * Sets the current to have an -x version specifier (aka beta)
101 | */
102 | function npmAddVersionX () {
103 | const json = npmGetJson();
104 |
105 | if (!json.version.endsWith('-x')) {
106 | json.version = json.version + '-x';
107 | npmSetJson(json);
108 | }
109 | }
110 |
111 | /**
112 | * Removes the current -x version specifier (aka beta)
113 | */
114 | function npmDelVersionX () {
115 | const json = npmGetJson();
116 |
117 | if (json.version.endsWith('-x')) {
118 | json.version = json.version.replace('-x', '');
119 | npmSetJson(json);
120 | }
121 | }
122 |
123 | /**
124 | * Sets the {versions: { npm, git } } fields in package.json
125 | */
126 | function npmSetVersionFields () {
127 | const json = npmGetJson();
128 |
129 | if (!json.versions) {
130 | json.versions = {};
131 | }
132 |
133 | json.versions.git = json.version;
134 |
135 | if (!json.version.endsWith('-x')) {
136 | json.versions.npm = json.version;
137 | }
138 |
139 | npmSetJson(json);
140 | rmFile('.123current');
141 | }
142 |
143 | /**
144 | * Sets the npm token in the home directory
145 | */
146 | function npmSetup () {
147 | const registry = 'registry.npmjs.org';
148 |
149 | fs.writeFileSync(path.join(os.homedir(), '.npmrc'), `//${registry}/:_authToken=${process.env['NPM_TOKEN']}`);
150 | }
151 |
152 | /**
153 | * Publishes the current package
154 | *
155 | * @returns {void}
156 | */
157 | function npmPublish () {
158 | if (fs.existsSync('.skip-npm') || !withNpm) {
159 | return;
160 | }
161 |
162 | ['LICENSE', 'package.json']
163 | .filter((file) => !fs.existsSync(path.join(process.cwd(), 'build', file)))
164 | .forEach((file) => copyFileSync(file, 'build'));
165 |
166 | process.chdir('build');
167 |
168 | const tag = npmGetVersion().includes('-') ? '--tag beta' : '';
169 | let count = 1;
170 |
171 | while (true) {
172 | try {
173 | execSync(`npm publish --quiet --access public ${tag}`);
174 |
175 | break;
176 | } catch {
177 | if (count < 5) {
178 | const end = Date.now() + 15000;
179 |
180 | console.error(`Publish failed on attempt ${count}/5. Retrying in 15s`);
181 | count++;
182 |
183 | while (Date.now() < end) {
184 | // just spin our wheels
185 | }
186 | }
187 | }
188 | }
189 |
190 | process.chdir('..');
191 | }
192 |
193 | /**
194 | * Creates a map of changelog entries
195 | *
196 | * @param {string[][]} parts
197 | * @param {ChangelogMap} result
198 | * @returns {ChangelogMap}
199 | */
200 | function createChangelogMap (parts, result = {}) {
201 | for (let i = 0, count = parts.length; i < count; i++) {
202 | const [n, ...e] = parts[i];
203 |
204 | if (!result[n]) {
205 | if (e.length) {
206 | result[n] = createChangelogMap([e]);
207 | } else {
208 | result[n] = { '': {} };
209 | }
210 | } else {
211 | if (e.length) {
212 | createChangelogMap([e], result[n]);
213 | } else {
214 | result[n][''] = {};
215 | }
216 | }
217 | }
218 |
219 | return result;
220 | }
221 |
222 | /**
223 | * Creates an array of changelog entries
224 | *
225 | * @param {ChangelogMap} map
226 | * @returns {string[]}
227 | */
228 | function createChangelogArr (map) {
229 | const result = [];
230 | const entries = Object.entries(map);
231 |
232 | for (let i = 0, count = entries.length; i < count; i++) {
233 | const [name, imap] = entries[i];
234 |
235 | if (name) {
236 | if (imap['']) {
237 | result.push(name);
238 | }
239 |
240 | const inner = createChangelogArr(imap);
241 |
242 | if (inner.length === 1) {
243 | result.push(`${name}-${inner[0]}`);
244 | } else if (inner.length) {
245 | result.push(`${name}-{${inner.join(', ')}}`);
246 | }
247 | }
248 | }
249 |
250 | return result;
251 | }
252 |
253 | /**
254 | * Adds changelog entries
255 | *
256 | * @param {string[]} changelog
257 | * @returns {string}
258 | */
259 | function addChangelog (changelog) {
260 | const [version, ...names] = changelog;
261 | const entry = `${
262 | createChangelogArr(
263 | createChangelogMap(
264 | names
265 | .sort()
266 | .map((n) => n.split('-'))
267 | )
268 | ).join(', ')
269 | } ${version}`;
270 | const newInfo = `## master\n\n- ${entry}\n`;
271 |
272 | if (!fs.existsSync('CHANGELOG.md')) {
273 | fs.writeFileSync('CHANGELOG.md', `# CHANGELOG\n\n${newInfo}`);
274 | } else {
275 | const md = fs.readFileSync('CHANGELOG.md', 'utf-8');
276 |
277 | fs.writeFileSync('CHANGELOG.md', md.includes('## master\n\n')
278 | ? md.replace('## master\n\n', newInfo)
279 | : md.replace('# CHANGELOG\n\n', `# CHANGELOG\n\n${newInfo}\n`)
280 | );
281 | }
282 |
283 | return entry;
284 | }
285 |
286 | /**
287 | *
288 | * @param {string} repo
289 | * @param {string} clone
290 | * @param {string[]} names
291 | */
292 | function commitClone (repo, clone, names) {
293 | if (names.length) {
294 | process.chdir(clone);
295 |
296 | const entry = addChangelog(names);
297 |
298 | gitSetup();
299 | execGit('add --all .');
300 | execGit(`commit --no-status --quiet -m "${entry}"`);
301 | execGit(`push ${repo}`, true);
302 |
303 | process.chdir('..');
304 | }
305 | }
306 |
307 | /**
308 | * Publishes a specific package to polkadot-js bundles
309 | *
310 | * @returns {void}
311 | */
312 | function bundlePublishPkg () {
313 | const { name, version } = npmGetJson();
314 | const dirName = name.split('/')[1];
315 | const bundName = `bundle-polkadot-${dirName}.js`;
316 | const srcPath = path.join('build', bundName);
317 | const dstDir = path.join('../..', bundClone);
318 |
319 | if (!fs.existsSync(srcPath)) {
320 | return;
321 | }
322 |
323 | console.log(`\n *** bundle ${name}`);
324 |
325 | if (shouldBund.length === 0) {
326 | shouldBund.push(version);
327 | }
328 |
329 | shouldBund.push(dirName);
330 |
331 | rimrafSync(path.join(dstDir, bundName));
332 | copyFileSync(srcPath, dstDir);
333 | }
334 |
335 | /**
336 | * Publishes all packages to polkadot-js bundles
337 | *
338 | * @returns {void}
339 | */
340 | function bundlePublish () {
341 | const { version } = npmGetJson();
342 |
343 | if (!withBund && version.includes('-')) {
344 | return;
345 | }
346 |
347 | execGit(`clone ${bundRepo} ${bundClone}`, true);
348 |
349 | loopFunc(bundlePublishPkg);
350 |
351 | commitClone(bundRepo, bundClone, shouldBund);
352 | }
353 |
354 | /**
355 | * Publishes a specific package to Deno
356 | *
357 | * @returns {void}
358 | */
359 | function denoPublishPkg () {
360 | const { name, version } = npmGetJson();
361 |
362 | if (fs.existsSync('.skip-deno') || !fs.existsSync('build-deno')) {
363 | return;
364 | }
365 |
366 | console.log(`\n *** deno ${name}`);
367 |
368 | const dirName = denoCreateDir(name);
369 | const denoPath = `../../${denoClone}/${dirName}`;
370 |
371 | if (shouldDeno.length === 0) {
372 | shouldDeno.push(version);
373 | }
374 |
375 | shouldDeno.push(dirName);
376 |
377 | rimrafSync(denoPath);
378 | mkdirpSync(denoPath);
379 |
380 | copyDirSync('build-deno', denoPath);
381 | }
382 |
383 | /**
384 | * Publishes all packages to Deno
385 | *
386 | * @returns {void}
387 | */
388 | function denoPublish () {
389 | const { version } = npmGetJson();
390 |
391 | if (!withDeno && version.includes('-')) {
392 | return;
393 | }
394 |
395 | execGit(`clone ${denoRepo} ${denoClone}`, true);
396 |
397 | loopFunc(denoPublishPkg);
398 |
399 | commitClone(denoRepo, denoClone, shouldDeno);
400 | }
401 |
402 | /**
403 | * Retrieves flags based on current specifications
404 | */
405 | function getFlags () {
406 | withDeno = rmFile('.123deno');
407 | withBund = rmFile('.123bundle');
408 | withNpm = rmFile('.123npm');
409 | }
410 |
411 | /**
412 | * Bumps the current version, also applying to all sub-packages
413 | */
414 | function verBump () {
415 | const { version: currentVersion, versions } = npmGetJson();
416 | const [version, tag] = currentVersion.split('-');
417 | const [,, patch] = version.split('.');
418 | const lastVersion = versions?.npm || currentVersion;
419 |
420 | if (argv['skip-beta'] || patch === '0') {
421 | // don't allow beta versions
422 | execPm('polkadot-dev-version patch');
423 | withNpm = true;
424 | } else if (tag || currentVersion === lastVersion) {
425 | // if we don't want to publish, add an X before passing
426 | if (!withNpm) {
427 | npmAddVersionX();
428 | } else {
429 | npmDelVersionX();
430 | }
431 |
432 | // beta version, just continue the stream of betas
433 | execPm('polkadot-dev-version pre');
434 | } else {
435 | // manually set, got for publish
436 | withNpm = true;
437 | }
438 |
439 | // always ensure we have made some changes, so we can commit
440 | npmSetVersionFields();
441 | rmFile('.123trigger');
442 |
443 | execPm('polkadot-dev-contrib');
444 | execGit('add --all .');
445 | }
446 |
447 | /**
448 | * Commits and pushes the current version on git
449 | */
450 | function gitPush () {
451 | const version = npmGetVersion();
452 | let doGHRelease = false;
453 |
454 | if (process.env['GH_RELEASE_GITHUB_API_TOKEN']) {
455 | const changes = fs.readFileSync('CHANGELOG.md', 'utf8');
456 |
457 | if (changes.includes(`## ${version}`)) {
458 | doGHRelease = true;
459 | } else if (version.endsWith('.1')) {
460 | exitFatal(`Unable to release, no CHANGELOG entry for ${version}`);
461 | }
462 | }
463 |
464 | execGit('add --all .');
465 |
466 | if (fs.existsSync('docs/README.md')) {
467 | execGit('add --all -f docs');
468 | }
469 |
470 | // add the skip checks for GitHub ...
471 | execGit(`commit --no-status --quiet -m "[CI Skip] ${version.includes('-x') ? 'bump' : 'release'}/${version.includes('-') ? 'beta' : 'stable'} ${version}
472 |
473 |
474 | skip-checks: true"`);
475 |
476 | // Make sure the release commit is on top of the latest master
477 | execGit(`pull --rebase ${repo} master`);
478 |
479 | // Now push normally
480 | execGit(`push ${repo} HEAD:${process.env['GITHUB_REF']}`, true);
481 |
482 | if (doGHRelease) {
483 | const files = process.env['GH_RELEASE_FILES']
484 | ? `--assets ${process.env['GH_RELEASE_FILES']}`
485 | : '';
486 |
487 | execPm(`polkadot-exec-ghrelease --draft ${files} --yes`);
488 | }
489 | }
490 |
491 | /**
492 | * Loops through the packages/* (or root), executing the supplied
493 | * function for each package found
494 | *
495 | * @param {() => unknown} fn
496 | */
497 | function loopFunc (fn) {
498 | if (fs.existsSync('packages')) {
499 | const dirs = fs
500 | .readdirSync('packages')
501 | .filter((dir) => {
502 | const pkgDir = path.join(process.cwd(), 'packages', dir);
503 |
504 | return fs.statSync(pkgDir).isDirectory() &&
505 | fs.existsSync(path.join(pkgDir, 'package.json')) &&
506 | fs.existsSync(path.join(pkgDir, 'build'));
507 | });
508 |
509 | topoSort(dirs)
510 | .forEach((dir) => {
511 | process.chdir(path.join('packages', dir));
512 | fn();
513 | process.chdir('../..');
514 | });
515 | } else {
516 | fn();
517 | }
518 | }
519 |
520 | // first do infrastructure setup
521 | gitSetup();
522 | npmSetup();
523 |
524 | // get flags immediate, then adjust
525 | getFlags();
526 | verBump();
527 |
528 | // perform the actual CI build
529 | execPm('polkadot-dev-clean-build');
530 | execPm('lint');
531 | execPm('test');
532 | execPm('build');
533 |
534 | // publish to all GH repos
535 | gitPush();
536 | denoPublish();
537 | bundlePublish();
538 |
539 | // publish to npm
540 | loopFunc(npmPublish);
541 |
--------------------------------------------------------------------------------