Files
nocobase/lint-staged.config.mjs
T
Zeke ZhangandClaude Opus 5.5 9e8fc3edd3 fix(ui-library): sync auth-ui, check items as plugins compile them, and document the library (#399)
* fix(ui-library): sync auth-ui, check items as plugins compile them, and document the library

Bring the auth-ui item up to date with the copies the templates ship.
Those gained translations in #264 while the registry source kept
hard-coded English, so `shadcn add @nocobase/auth-ui` installed an older
version than any template carried. The item now declares @nocobase/i18n.

- Relative imports inside items carry the .js extension. A plugin
  compiles installed source with NodeNext, which rejects an extensionless
  import; an application's bundler resolves either form.
- tsconfig.registry.json compiles the registry sources with NodeNext
  against the real workspace packages. tsconfig.json no longer maps the
  authentication plugin's actions to the preview's mock, so both programs
  check the items against the plugin's real exports and the mock is
  typed from them; only the Vite build swaps it in. A plugin change that
  breaks an item now fails typecheck in that plugin's pull request.
- The package is linted with the rules a plugin uses. The preview's
  shadcn primitives get the relaxations the Portal configuration gives an
  application's, and the two problems the rules found in the preview, a
  synchronous setState in an effect and a fetch that was never aborted,
  are fixed. The UI Library workflow lints before it publishes, and the
  pre-commit hook lints the directory with its own configuration.
- The preview mounts an i18n runtime, as an application does, so the
  translated items render without react-i18next warnings.
- MAINTAINING.md and USAGE.md document maintaining the library and
  installing its items into applications and plugins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui-library): ship auth-ui's translations with the item

The item's strings were translatable, but the translations lived only in
the templates' own locale files, so any other consumer had to copy the
keys by hand and could not tell when an upgrade added one.

- auth-ui ships locales/en-US.ts and locales/zh-CN.ts, listed in its
  files. en-US exports its shape as a type and zh-CN is annotated with
  it, so typecheck fails on a key either one lacks or adds.
- The item's docs, README and the usage guide describe merging the files
  into the consumer's locale resources, which installing does not do,
  and MAINTAINING.md makes shipping every key a rule for items.
- i18n:check also scans ui-library/registry/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* revert(ui-library): leave the i18n:check scan to #400

The change that adds ui-library/registry/ to i18n:check now lives in
#400, next to the parser fix that makes it effective, so the two pull
requests no longer edit the same lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui-library): address review findings

- The documented merge of an item's locales into a plugin's en-US.ts
  failed the plugin's declaration build with TS9015, because
  isolatedDeclarations cannot infer an object built with a spread. The
  item README now shows en-US annotated as
  `typeof authUi & typeof messages`, and USAGE.md says the merged file
  needs it.
- The preview's theme effect toggled only the `dark` class, leaving the
  inline color-scheme index.html sets on load stale after a toggle; it
  now updates both.
- ESLint relaxed only the first of dev-config's two shadcn registry
  blocks, so adding the chart primitive the documented way failed lint.
  dev-config now exports createShadcnRegistryConfig(root), which the
  Portal factory uses for client/ and ui-library uses for website/, so
  the list is no longer copied.
- tsconfig.registry.json compiled items with the ESNext library and
  without noUnusedLocals, unlike the plugins that install them; it now
  uses the create-plugin template's lib and noUnusedLocals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 21:37:41 +08:00

84 lines
3.7 KiB
JavaScript

import path from 'node:path';
const CODE = /\.(?:[cm]?[jt]sx?)$/u;
const FORMATTABLE =
/\.(?:[cm]?[jt]sx?|json5?|jsonc|mdx?|ya?ml|css|scss|less|html)$/u;
/**
* Directories that lint themselves rather than being covered by the root ESLint configuration, given as
* repository-relative prefixes. A directory belongs here when it has its own `eslint.config.*` AND its own ESLint
* dependency, because both halves matter: the root configuration enumerates the package roots it applies to and
* matches nothing under such a directory, so linting its files from the root silently succeeds without running a
* single rule, and `docs` is on ESLint 9 while the root is on ESLint 10, so the binary has to come from there too.
*
* Everything under `packages/` is covered by the root configuration and must not be listed.
*/
const SELF_LINTING_DIRECTORIES = ['docs', 'ui-library'];
/**
* ESLint runs once per package rather than once for every staged file.
*
* The type-aware rules build a TypeScript program per file and hold it for the length of the run, so a single process
* grows with the size of the commit. A repository-wide change — a refactor that moves every package, a dependency bump
* that rewrites every manifest — exhausts the heap, and the hook dies with an allocation failure rather than a lint
* result. Splitting by package bounds it: each process exits and frees what it held before the next starts.
*
* Grouping costs nothing for an ordinary commit, which touches one or two packages and so produces one or two
* commands, the same as before.
*
* Prettier still takes every staged file in one command. It formats a file at a time with no cross-file analysis, so
* its memory does not scale with the size of the commit the way ESLint's does. It also needs no per-directory
* treatment: `docs` inherits the same shared Prettier configuration as everything else, and the root
* `.prettierignore` — which the command below passes explicitly — is what decides which of its files are skipped.
*/
const groupByPackage = (files) => {
const groups = new Map();
for (const file of files) {
const relative = path.relative(import.meta.dirname, file);
const segments = relative.split(path.sep);
// `packages/<category>/<name>` is a package. A self-linting directory is its own group, keyed by its prefix so
// that the command can be run from inside it. Anything else is grouped as the repository root, which the root
// ESLint configuration covers.
const key =
segments[0] === 'packages' && segments.length > 3
? segments.slice(0, 3).join('/')
: (SELF_LINTING_DIRECTORIES.find(
(directory) => segments[0] === directory,
) ?? '');
const group = groups.get(key);
if (group) group.push(file);
else groups.set(key, [file]);
}
return [...groups.entries()];
};
const quote = (values) =>
values.map((value) => JSON.stringify(value)).join(' ');
export default {
'*': (files) => {
const code = files.filter((file) => CODE.test(file));
const formattable = files.filter((file) => FORMATTABLE.test(file));
const commands = groupByPackage(code).map(([key, group]) => {
const lint = `eslint --fix --no-warn-ignored --max-warnings 0 ${quote(group)}`;
// A self-linting directory needs its own ESLint, resolved from its own node_modules and reading its own
// configuration. `pnpm --dir` runs the binary there while the paths stay absolute, so they resolve either way.
return SELF_LINTING_DIRECTORIES.includes(key)
? `pnpm --dir ${JSON.stringify(key)} exec ${lint}`
: lint;
});
if (formattable.length > 0) {
commands.push(
`prettier --write --ignore-path .prettierignore ${quote(formattable)}`,
);
}
return commands;
},
};