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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ jobs:
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
with:
build-command: pnpm build
build-command: pnpm build && pnpm lint:dist
lint-command: pnpm lint
test-command: pnpm test:coverage
5 changes: 4 additions & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@ jobs:
run: pnpm run lint
- name: Test
run: pnpm run test:coverage
- name: Build
run: pnpm run build
- name: Check Built Declarations
run: pnpm run lint:dist
- name: Upload Coverage
if: github.event_name != 'pull_request'
uses: codecov/codecov-action@v4
with:
directory: ./coverage/
token: ${{ secrets.CODECOV_TOKEN }}

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"format": "prettier --write src",
"format:translations": "find src/i18n/translations -name '*.json' -exec pnpm exec sort-json {} \\;",
"lint": "tsc && eslint --fix src",
"lint:dist": "node scripts/check-dist-types.js",
"prepare": "husky",
"storybook": "storybook dev --no-open -p 6006",
"storybook:build": "storybook build",
Expand Down
87 changes: 87 additions & 0 deletions scripts/check-dist-types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* Type-checks the built declarations in `dist` as a consumer without the global `React` namespace would see them.
*
* `@types/react` declares `export as namespace React`, which makes a bare `React.X` resolve in any program that
* includes it, so libui's own `tsc` cannot catch a declaration that uses `React` without importing it. Consumers
* whose React types lack that global (e.g., a vendored `react` package) get `TS2503: Cannot find namespace 'React'`,
* which `skipLibCheck` hides, and the affected props silently become `any`. To reproduce this, the compiler host
* strips the global from `@types/react` in memory before checking every `dist/**\/*.d.ts` file.
*
* Run after `pnpm build`.
*/

import path from 'node:path';
import process from 'node:process';

import ts from 'typescript';

const DIST_DIR = path.resolve('dist');
const TYPES_REACT_PATTERN = /[\\/]@types[\\/]react[\\/]/;
const UMD_GLOBAL_PATTERN = /^export as namespace React;$/m;

/** @type {ts.CompilerOptions} */
const options = {
jsx: ts.JsxEmit.ReactJSX,
lib: ['lib.dom.d.ts', 'lib.esnext.d.ts'],
module: ts.ModuleKind.NodeNext,
moduleResolution: ts.ModuleResolutionKind.NodeNext,
noEmit: true,
skipLibCheck: false,
strict: true,
target: ts.ScriptTarget.ES2022,
types: []
};

// guard against this check silently passing if the layout of @types/react changes
let didReadTypesReact = false;
let didStripGlobal = false;

const host = ts.createCompilerHost(options);
const readFile = host.readFile.bind(host);
/** @param {string} fileName */
host.readFile = (fileName) => {
const text = readFile(fileName);
if (text === undefined || !TYPES_REACT_PATTERN.test(fileName)) {
return text;
}
didReadTypesReact = true;
if (UMD_GLOBAL_PATTERN.test(text)) {
didStripGlobal = true;
return text.replace(UMD_GLOBAL_PATTERN, '');
}
return text;
};

const rootNames = ts.sys.readDirectory(DIST_DIR, ['.d.ts']);
if (rootNames.length === 0) {
console.error(`No declaration files found in '${DIST_DIR}'. Did you run 'pnpm build'?`);
process.exit(1);
}

const program = ts.createProgram({ host, options, rootNames });
const diagnostics = ts.getPreEmitDiagnostics(program).filter((d) => d.file?.fileName.startsWith(DIST_DIR));

if (!didReadTypesReact) {
console.error('No file from @types/react was read, so the check is not meaningful.');
process.exit(1);
} else if (!didStripGlobal) {
console.error("Could not find 'export as namespace React;' in @types/react, so the check is not meaningful.");
process.exit(1);
}

if (diagnostics.length > 0) {
const files = new Set(diagnostics.map((d) => d.file?.fileName));
console.error(
ts.formatDiagnosticsWithColorAndContext(diagnostics, {
getCanonicalFileName: (fileName) => fileName,
getCurrentDirectory: () => process.cwd(),
getNewLine: () => ts.sys.newLine
})
);
console.error(`Found ${diagnostics.length} error(s) in ${files.size} of ${rootNames.length} declaration file(s).`);
process.exit(1);
}

process.stdout.write(
`Checked ${rootNames.length} declaration file(s) without the global React namespace: no errors.${ts.sys.newLine}`
);
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"src/**/*",
"eslint.config.js",
"prettier.config.js",
"scripts/**/*",
"tailwind.config.cjs",
"tsup.config.mts",
"vite.config.mts",
Expand Down
Loading