diff --git a/.agents/skills/migrate-to-rstack-cli/SKILL.md b/.agents/skills/migrate-to-rstack-cli/SKILL.md index 917de99c..992cf06c 100644 --- a/.agents/skills/migrate-to-rstack-cli/SKILL.md +++ b/.agents/skills/migrate-to-rstack-cli/SKILL.md @@ -41,7 +41,7 @@ Use one of the default names: `rstack.config.ts`, `.js`, `.mts`, or `.mjs`. Use `rs -c ` or `rs --config ` only for a custom path. ```ts -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.app({ @@ -53,13 +53,21 @@ define.test({ }); ``` -Prefer async config functions and dynamic imports for runtime plugins and presets: +Use dynamic imports in async config functions only for external plugins, presets, and other dependencies: ```ts -define.lint(async () => { - const { js, ts } = await import('rstack/lint'); - return [js.configs.recommended, ts.configs.recommended]; +define.app(async () => { + const { pluginReact } = await import('@rsbuild/plugin-react'); + return { + plugins: [pluginReact()], + }; }); ``` +`define.lint` provides `@rslint/core` APIs to its config factory, so no manual import is needed: + +```ts +define.lint(({ js }) => [js.configs.recommended]); +``` + Rstack loads TypeScript configs as native ESM. Preserve runtime-resolvable file extensions, replace CommonJS globals such as `__dirname`. diff --git a/.agents/skills/migrate-to-rstack-cli/references/rslint.md b/.agents/skills/migrate-to-rstack-cli/references/rslint.md index 96b31d59..5e9b6d43 100644 --- a/.agents/skills/migrate-to-rstack-cli/references/rslint.md +++ b/.agents/skills/migrate-to-rstack-cli/references/rslint.md @@ -5,22 +5,23 @@ Read this reference when the project uses `@rslint/core`, `rslint.config.*`, `rs ## Steps 1. Replace the `rslint` executable prefix with `rs lint`. For example, replace `rslint --fix` with `rs lint --fix`. -2. Move the old config into `define.lint`, replacing Rslint's `defineConfig()` wrapper and import. Dynamically import presets from `rstack/lint` inside an async config function. -3. Replace direct config/API imports from `@rslint/core` with exports from `rstack/lint` where available. -4. Replace custom `--config` paths with the migrated `rstack.config.*` path. -5. Remove `@rslint/core` only when no uncovered direct runtime API remains. Delete `rslint.config.*`. +2. Move the old config into `define.lint`, replacing Rslint's `defineConfig()` wrapper and import. Receive `@rslint/core` exports from the factory parameter. +3. Replace custom `--config` paths with the migrated `rstack.config.*` path. +4. Remove `@rslint/core` only when no uncovered direct runtime API remains. Delete `rslint.config.*`. ## Config Pattern ```ts import { define } from 'rstack'; -define.lint(async () => { - const { js, ts } = await import('rstack/lint'); - return [js.configs.recommended, ts.configs.recommended]; -}); +define.lint(({ js, ts }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, +]); ``` +Preserve existing presets and rules during migration. + ## Script Pattern If a script also runs Prettier, migrate its formatting command as described in [prettier.md](prettier.md). diff --git a/.github/renovate.json b/.github/renovate.json index 7fb2dd94..eb178041 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,4 +1,18 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["github>rstackjs/renovate"] + "extends": ["github>rstackjs/renovate"], + "packageRules": [ + { + "description": "Disable TypeScript updates in Svelte and Vue templates until svelte-check, svelte2tsx, and vue-tsc support TypeScript 7", + "matchManagers": ["npm"], + "matchPackageNames": ["typescript"], + "matchFileNames": [ + "packages/create-rstack/template-app-svelte-ts/package.json", + "packages/create-rstack/template-app-vue-ts/package.json", + "packages/create-rstack/template-lib-svelte-ts/package.json", + "packages/create-rstack/template-lib-vue-ts/package.json" + ], + "enabled": false + } + ] } diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 795132ce..34138418 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -26,7 +26,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: - node-version: 24.18.1 + node-version: 24.19.0 package-manager-cache: false - name: Install Pnpm diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b0e43c23..80e75760 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -71,7 +71,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 24.18.1 + node-version: 24.19.0 package-manager-cache: false - name: Setup Pnpm diff --git a/.github/workflows/reusable-native-build.yml b/.github/workflows/reusable-native-build.yml index d06dbd48..4809a263 100644 --- a/.github/workflows/reusable-native-build.yml +++ b/.github/workflows/reusable-native-build.yml @@ -35,7 +35,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: - node-version: 24.18.1 + node-version: 24.19.0 package-manager-cache: false - name: Install Pnpm diff --git a/.github/workflows/reusable-native-release.yml b/.github/workflows/reusable-native-release.yml index 920a686e..f8643d3b 100644 --- a/.github/workflows/reusable-native-release.yml +++ b/.github/workflows/reusable-native-release.yml @@ -70,7 +70,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: - node-version: 24.18.1 + node-version: 24.19.0 package-manager-cache: false - name: Install Pnpm diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3062f9f7..6d0ae0dd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,6 +12,7 @@ on: permissions: contents: read + pull-requests: read # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: @@ -26,28 +27,52 @@ jobs: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 + id: changes + with: + predicate-quantifier: 'every' + filters: | + changed: + - "!**/*.md" + - "!**/*.mdx" + - "!**/_meta.json" + - "!**/_nav.json" + - "!**/dictionary.txt" + - name: Setup Node.js + if: steps.changes.outputs.changed == 'true' uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: - node-version: 24.18.1 + node-version: 24.19.0 package-manager-cache: false - name: Install Pnpm + if: steps.changes.outputs.changed == 'true' uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 with: run_install: true - name: Build Packages + if: steps.changes.outputs.changed == 'true' run: node --run build - name: Run Rust Tests + if: steps.changes.outputs.changed == 'true' run: cargo test --profile ci --workspace --locked - name: Build Native Binding + if: steps.changes.outputs.changed == 'true' run: pnpm --filter rstack build:native:ci - name: Check Generated Native Files + if: steps.changes.outputs.changed == 'true' run: git diff --exit-code -- packages/rstack/binding.cjs packages/rstack/binding.d.cts - name: Run Test + if: steps.changes.outputs.changed == 'true' && runner.os != 'Windows' run: node --run test + + # Run package tests serially on Windows to avoid resource contention between nested test workers. + - name: Run Test (Windows) + if: steps.changes.outputs.changed == 'true' && runner.os == 'Windows' + run: pnpm --workspace-concurrency=1 --filter "./packages/**" test diff --git a/Cargo.lock b/Cargo.lock index 345a8ab6..93e66a42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -190,6 +190,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + [[package]] name = "libloading" version = "0.9.0" @@ -214,13 +220,14 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "napi" -version = "3.12.0" +version = "3.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f71d6bc097c4a6eb853c3f24991ab8c9f50f57d1f719e305175541482217e36" +checksum = "459197f1592f4c3dbbf9c1b13f5a4599a343e4ef66b96bc340e2a518b36a6662" dependencies = [ "bitflags", "ctor", "futures", + "libc", "napi-build", "napi-sys", "nohash-hasher", @@ -229,15 +236,15 @@ dependencies = [ [[package]] name = "napi-build" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5282704fbe8d49b0cf8b08e3f33233416a528658f205c7e5ace63b582de0b11c" +checksum = "60fdf9b392c50e7c4170fa633bd909490ed7835cea4c046776d1a4dd8d2ae0ab" [[package]] name = "napi-derive" -version = "3.6.2" +version = "3.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9002b2940f0184444754546e0fcd15182f56948e6f381968b019d549387c42" +checksum = "0fa55ea69990c90b888e9e77044410e304ce7f35de599dc6d0b5c1923d2e59af" dependencies = [ "convert_case", "ctor", @@ -249,9 +256,9 @@ dependencies = [ [[package]] name = "napi-derive-backend" -version = "6.1.1" +version = "6.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d60b5d773ad46c698c8cc2cd9fde0b283d39cbb7f71c04bee633c7bdba4423bd" +checksum = "df4056ac7c18e4438ccf0edaed4340ca0d269278c8ec19284f7b23cb039fd0ae" dependencies = [ "convert_case", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 4ce85357..f7e5884d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,9 @@ rust-version = "1.88" [workspace.dependencies] ignore = { version = "0.4.33", default-features = false } -napi = { version = "3.12.0", default-features = false, features = ["napi9"] } -napi-build = "2.4.0" -napi-derive = "3.6.2" +napi = { version = "3.12.1", default-features = false, features = ["napi9"] } +napi-build = "2.4.1" +napi-derive = "3.6.3" pathdiff = "0.2.3" rstack-ignore = { path = "crates/rstack-ignore" } diff --git a/examples/app-react/rstack.config.ts b/examples/app-react/rstack.config.ts index 4d23b13c..d6c4a461 100644 --- a/examples/app-react/rstack.config.ts +++ b/examples/app-react/rstack.config.ts @@ -1,4 +1,4 @@ -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.app(async () => { @@ -12,12 +12,9 @@ define.test({ setupFiles: ['./tests/rstest.setup.ts'], }); -define.lint(async () => { - const { js, ts, reactPlugin, reactHooksPlugin } = await import('rstack/lint'); - return [ - js.configs.recommended, - ts.configs.recommended, - reactPlugin.configs.recommended, - reactHooksPlugin.configs.recommended, - ]; -}); +define.lint(({ js, ts, reactPlugin, reactHooksPlugin }) => [ + js.configs.recommended, + ts.configs.recommended, + reactPlugin.configs.recommended, + reactHooksPlugin.configs.recommended, +]); diff --git a/examples/app-vanilla/rstack.config.ts b/examples/app-vanilla/rstack.config.ts index 3504d640..6e3bb9a8 100644 --- a/examples/app-vanilla/rstack.config.ts +++ b/examples/app-vanilla/rstack.config.ts @@ -1,4 +1,4 @@ -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.test({ @@ -6,7 +6,4 @@ define.test({ setupFiles: ['./tests/rstest.setup.ts'], }); -define.lint(async () => { - const { js, ts } = await import('rstack/lint'); - return [js.configs.recommended, ts.configs.recommended]; -}); +define.lint(({ js, ts }) => [js.configs.recommended, ts.configs.recommended]); diff --git a/examples/documentation/rstack.config.ts b/examples/documentation/rstack.config.ts index f73f1863..8d99518f 100644 --- a/examples/documentation/rstack.config.ts +++ b/examples/documentation/rstack.config.ts @@ -1,4 +1,4 @@ -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; import path from 'node:path'; @@ -7,12 +7,9 @@ define.doc({ title: 'My Site', }); -define.lint(async () => { - const { js, ts, reactPlugin, reactHooksPlugin } = await import('rstack/lint'); - return [ - js.configs.recommended, - ts.configs.recommended, - reactPlugin.configs.recommended, - reactHooksPlugin.configs.recommended, - ]; -}); +define.lint(({ js, ts, reactPlugin, reactHooksPlugin }) => [ + js.configs.recommended, + ts.configs.recommended, + reactPlugin.configs.recommended, + reactHooksPlugin.configs.recommended, +]); diff --git a/examples/lib-node/rstack.config.ts b/examples/lib-node/rstack.config.ts index ccafc8da..104652eb 100644 --- a/examples/lib-node/rstack.config.ts +++ b/examples/lib-node/rstack.config.ts @@ -1,4 +1,4 @@ -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.lib({ @@ -6,7 +6,4 @@ define.lib({ syntax: ['node 22'], }); -define.lint(async () => { - const { js, ts } = await import('rstack/lint'); - return [js.configs.recommended, ts.configs.recommended]; -}); +define.lint(({ js, ts }) => [js.configs.recommended, ts.configs.recommended]); diff --git a/examples/lib-react/rstack.config.ts b/examples/lib-react/rstack.config.ts index 31913350..ec8669f4 100644 --- a/examples/lib-react/rstack.config.ts +++ b/examples/lib-react/rstack.config.ts @@ -1,4 +1,4 @@ -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.lib(async () => { @@ -22,12 +22,9 @@ define.test({ setupFiles: ['./tests/rstest.setup.ts'], }); -define.lint(async () => { - const { js, ts, reactPlugin, reactHooksPlugin } = await import('rstack/lint'); - return [ - js.configs.recommended, - ts.configs.recommended, - reactPlugin.configs.recommended, - reactHooksPlugin.configs.recommended, - ]; -}); +define.lint(({ js, ts, reactPlugin, reactHooksPlugin }) => [ + js.configs.recommended, + ts.configs.recommended, + reactPlugin.configs.recommended, + reactHooksPlugin.configs.recommended, +]); diff --git a/examples/rstest-inline-projects/rstack.config.ts b/examples/rstest-inline-projects/rstack.config.ts deleted file mode 100644 index 501912cb..00000000 --- a/examples/rstest-inline-projects/rstack.config.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Rstack configuration guide: https://rstack.rs/config -import { define } from 'rstack'; -import { defineInlineProject } from 'rstack/test'; - -define.app(async () => { - const { pluginReact } = await import('@rsbuild/plugin-react'); - - return { - plugins: [pluginReact()], - }; -}); - -define.test({ - projects: [ - defineInlineProject({ - name: 'ssr', - include: ['./tests/ssr.test.tsx'], - testEnvironment: 'node', - }), - defineInlineProject({ - name: 'dom', - include: ['./tests/dom.test.tsx'], - testEnvironment: 'happy-dom', - }), - ], -}); diff --git a/examples/rstest-inline-projects/package.json b/examples/test-inline-projects/package.json similarity index 92% rename from examples/rstest-inline-projects/package.json rename to examples/test-inline-projects/package.json index 4fea5d03..b1ddaad1 100644 --- a/examples/rstest-inline-projects/package.json +++ b/examples/test-inline-projects/package.json @@ -1,5 +1,5 @@ { - "name": "@examples/rstest-inline-projects", + "name": "@examples/test-inline-projects", "private": true, "type": "module", "scripts": { diff --git a/examples/test-inline-projects/rstack.config.ts b/examples/test-inline-projects/rstack.config.ts new file mode 100644 index 00000000..55383b03 --- /dev/null +++ b/examples/test-inline-projects/rstack.config.ts @@ -0,0 +1,27 @@ +// Configuration guide: https://rstack.rs/config +import { define } from 'rstack'; + +define.app(async () => { + const { pluginReact } = await import('@rsbuild/plugin-react'); + return { + plugins: [pluginReact()], + }; +}); + +define.test(async () => { + const { defineInlineProject } = await import('rstack/test'); + return { + projects: [ + defineInlineProject({ + name: 'ssr', + include: ['./tests/ssr.test.tsx'], + testEnvironment: 'node', + }), + defineInlineProject({ + name: 'dom', + include: ['./tests/dom.test.tsx'], + testEnvironment: 'happy-dom', + }), + ], + }; +}); diff --git a/examples/rstest-inline-projects/src/App.tsx b/examples/test-inline-projects/src/App.tsx similarity index 100% rename from examples/rstest-inline-projects/src/App.tsx rename to examples/test-inline-projects/src/App.tsx diff --git a/examples/rstest-inline-projects/src/index.tsx b/examples/test-inline-projects/src/index.tsx similarity index 100% rename from examples/rstest-inline-projects/src/index.tsx rename to examples/test-inline-projects/src/index.tsx diff --git a/examples/rstest-inline-projects/tests/dom.test.tsx b/examples/test-inline-projects/tests/dom.test.tsx similarity index 69% rename from examples/rstest-inline-projects/tests/dom.test.tsx rename to examples/test-inline-projects/tests/dom.test.tsx index ed6910fc..c096f3d2 100644 --- a/examples/rstest-inline-projects/tests/dom.test.tsx +++ b/examples/test-inline-projects/tests/dom.test.tsx @@ -5,5 +5,7 @@ import App from '../src/App'; test('renders the app in a DOM environment', () => { render(); - expect(screen.getByRole('heading', { name: 'Rstack React SSR' })).toBeTruthy(); + expect( + screen.getByRole('heading', { name: 'Rstack React SSR' }), + ).toBeTruthy(); }); diff --git a/examples/rstest-inline-projects/tests/ssr.test.tsx b/examples/test-inline-projects/tests/ssr.test.tsx similarity index 100% rename from examples/rstest-inline-projects/tests/ssr.test.tsx rename to examples/test-inline-projects/tests/ssr.test.tsx diff --git a/examples/rstest-inline-projects/tsconfig.json b/examples/test-inline-projects/tsconfig.json similarity index 100% rename from examples/rstest-inline-projects/tsconfig.json rename to examples/test-inline-projects/tsconfig.json diff --git a/packages/create-rstack/README.md b/packages/create-rstack/README.md index fcac4ec4..42c07e2c 100644 --- a/packages/create-rstack/README.md +++ b/packages/create-rstack/README.md @@ -53,7 +53,7 @@ npx create-rstack --dir my-project --template app-vanilla-ts --no-git ## Documentation -See the [Rstack documentation](https://rstack.rs). +See the [Rstack CLI documentation](https://rstack.rs). ## License diff --git a/packages/create-rstack/package.json b/packages/create-rstack/package.json index 31f78bd1..84e08d10 100644 --- a/packages/create-rstack/package.json +++ b/packages/create-rstack/package.json @@ -1,6 +1,6 @@ { "name": "create-rstack", - "version": "3.1.2", + "version": "3.2.1", "description": "Create a new Rstack project", "homepage": "https://rstack.rs", "bugs": { diff --git a/packages/create-rstack/rstack.config.ts b/packages/create-rstack/rstack.config.ts index 923fc17d..7daaf593 100644 --- a/packages/create-rstack/rstack.config.ts +++ b/packages/create-rstack/rstack.config.ts @@ -1,4 +1,4 @@ -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.lib({ diff --git a/packages/create-rstack/src/index.ts b/packages/create-rstack/src/index.ts index f6f32f78..1429da78 100644 --- a/packages/create-rstack/src/index.ts +++ b/packages/create-rstack/src/index.ts @@ -5,7 +5,13 @@ import { create, select, } from '@rstackjs/create-toolkit'; -import { access, appendFile, mkdir, readFile, writeFile } from 'node:fs/promises'; +import { + access, + appendFile, + mkdir, + readFile, + writeFile, +} from 'node:fs/promises'; import path from 'node:path'; const packageRoot = path.join(import.meta.dirname, '..'); @@ -87,12 +93,15 @@ const getTemplateName = async ({ template }: Argv): Promise => { }), ); - return resolveTemplateName(documentationType === 'basic' ? 'doc' : 'doc-i18n'); + return resolveTemplateName( + documentationType === 'basic' ? 'doc' : 'doc-i18n', + ); } const templateType = checkCancel( await select({ - message: projectType === 'app' ? 'Select framework' : 'Select library type', + message: + projectType === 'app' ? 'Select framework' : 'Select library type', options: projectType === 'app' ? [ @@ -129,12 +138,32 @@ const getTemplateName = async ({ template }: Argv): Promise => { }; const getStagedConfig = (templateName: string): string => { - const scriptExtensions = ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs', 'mts', 'cts']; - const formatExtensions = ['json', 'jsonc', 'md', 'mdx', 'css', 'html', 'yml', 'yaml']; + const scriptExtensions = [ + 'js', + 'jsx', + 'ts', + 'tsx', + 'mjs', + 'cjs', + 'mts', + 'cts', + ]; + const formatExtensions = [ + 'json', + 'jsonc', + 'md', + 'mdx', + 'css', + 'html', + 'yml', + 'yaml', + ]; const componentExtensions = ['svelte', 'vue']; const templateFormatExtensions = [ ...formatExtensions, - ...componentExtensions.filter((extension) => templateName.includes(extension)), + ...componentExtensions.filter((extension) => + templateName.includes(extension), + ), ]; return [ @@ -157,7 +186,9 @@ const injectStagedSetup = async ({ return; } - const configExtension = await access(path.join(distFolder, 'rstack.config.ts')).then( + const configExtension = await access( + path.join(distFolder, 'rstack.config.ts'), + ).then( () => 'ts', () => 'js', ); @@ -167,8 +198,8 @@ const injectStagedSetup = async ({ }; packageJson.scripts = Object.fromEntries( - Object.entries({ ...packageJson.scripts, prepare: 'rs setup' }).sort(([left], [right]) => - left.localeCompare(right), + Object.entries({ ...packageJson.scripts, prepare: 'rs setup' }).sort( + ([left], [right]) => left.localeCompare(right), ), ); diff --git a/packages/create-rstack/template-app-lit-ts/package.json b/packages/create-rstack/template-app-lit-ts/package.json index aba33ac0..df58ecf0 100644 --- a/packages/create-rstack/template-app-lit-ts/package.json +++ b/packages/create-rstack/template-app-lit-ts/package.json @@ -19,7 +19,7 @@ "devDependencies": { "@types/node": "^24.13.3", "happy-dom": "^20.11.2", - "rstack": "^0.5.2", + "rstack": "^0.6.1", "typescript": "^7.0.2" } } diff --git a/packages/create-rstack/template-app-lit-ts/rstack.config.ts b/packages/create-rstack/template-app-lit-ts/rstack.config.ts index cdfe0ac0..434eac7d 100644 --- a/packages/create-rstack/template-app-lit-ts/rstack.config.ts +++ b/packages/create-rstack/template-app-lit-ts/rstack.config.ts @@ -1,4 +1,4 @@ -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.app({ @@ -12,15 +12,10 @@ define.app({ }, }); -define.test({ - testEnvironment: 'happy-dom', -}); - -define.lint(async () => { - const { js, ts } = await import('rstack/lint'); - - return [js.configs.recommended, ts.configs.recommended]; -}); +define.lint(({ js, ts }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, +]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-app-lit/package.json b/packages/create-rstack/template-app-lit/package.json index 6d260528..3e3f3b90 100644 --- a/packages/create-rstack/template-app-lit/package.json +++ b/packages/create-rstack/template-app-lit/package.json @@ -18,6 +18,6 @@ }, "devDependencies": { "happy-dom": "^20.11.2", - "rstack": "^0.5.2" + "rstack": "^0.6.1" } } diff --git a/packages/create-rstack/template-app-lit/rstack.config.js b/packages/create-rstack/template-app-lit/rstack.config.js index d09e1329..e5a6fd11 100644 --- a/packages/create-rstack/template-app-lit/rstack.config.js +++ b/packages/create-rstack/template-app-lit/rstack.config.js @@ -1,5 +1,5 @@ // @ts-check -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.app({ @@ -13,15 +13,7 @@ define.app({ }, }); -define.test({ - testEnvironment: 'happy-dom', -}); - -define.lint(async () => { - const { js } = await import('rstack/lint'); - - return [js.configs.recommended]; -}); +define.lint(({ js }) => [js.configs.recommended]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-app-preact-ts/package.json b/packages/create-rstack/template-app-preact-ts/package.json index 6c1c71c9..0764ec7f 100644 --- a/packages/create-rstack/template-app-preact-ts/package.json +++ b/packages/create-rstack/template-app-preact-ts/package.json @@ -18,11 +18,11 @@ }, "devDependencies": { "@rsbuild/plugin-preact": "^2.0.0", - "@testing-library/jest-dom": "^7.0.0", + "@testing-library/jest-dom": "^7.0.1", "@testing-library/preact": "^3.2.4", "@types/node": "^24.13.3", "happy-dom": "^20.11.2", - "rstack": "^0.5.2", + "rstack": "^0.6.1", "typescript": "^7.0.2" } } diff --git a/packages/create-rstack/template-app-preact-ts/rstack.config.ts b/packages/create-rstack/template-app-preact-ts/rstack.config.ts index 9ee763c8..9fdeba76 100644 --- a/packages/create-rstack/template-app-preact-ts/rstack.config.ts +++ b/packages/create-rstack/template-app-preact-ts/rstack.config.ts @@ -1,9 +1,8 @@ -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.app(async () => { const { pluginPreact } = await import('@rsbuild/plugin-preact'); - return { plugins: [pluginPreact()], }; @@ -13,16 +12,12 @@ define.test({ setupFiles: ['./tests/rstest.setup.ts'], }); -define.lint(async () => { - const { js, ts, reactHooksPlugin, reactPlugin } = await import('rstack/lint'); - - return [ - js.configs.recommended, - ts.configs.recommended, - reactPlugin.configs.recommended, - reactHooksPlugin.configs.recommended, - ]; -}); +define.lint(({ js, ts, reactHooksPlugin, reactPlugin }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, + reactPlugin.configs.recommended, + reactHooksPlugin.configs.recommended, +]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-app-preact/package.json b/packages/create-rstack/template-app-preact/package.json index c21f8931..5a72ed4b 100644 --- a/packages/create-rstack/template-app-preact/package.json +++ b/packages/create-rstack/template-app-preact/package.json @@ -18,9 +18,9 @@ }, "devDependencies": { "@rsbuild/plugin-preact": "^2.0.0", - "@testing-library/jest-dom": "^7.0.0", + "@testing-library/jest-dom": "^7.0.1", "@testing-library/preact": "^3.2.4", "happy-dom": "^20.11.2", - "rstack": "^0.5.2" + "rstack": "^0.6.1" } } diff --git a/packages/create-rstack/template-app-preact/rstack.config.js b/packages/create-rstack/template-app-preact/rstack.config.js index e2b64e47..912d57af 100644 --- a/packages/create-rstack/template-app-preact/rstack.config.js +++ b/packages/create-rstack/template-app-preact/rstack.config.js @@ -1,10 +1,9 @@ // @ts-check -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.app(async () => { const { pluginPreact } = await import('@rsbuild/plugin-preact'); - return { plugins: [pluginPreact()], }; @@ -14,15 +13,11 @@ define.test({ setupFiles: ['./tests/rstest.setup.js'], }); -define.lint(async () => { - const { js, reactHooksPlugin, reactPlugin } = await import('rstack/lint'); - - return [ - js.configs.recommended, - reactPlugin.configs.recommended, - reactHooksPlugin.configs.recommended, - ]; -}); +define.lint(({ js, reactHooksPlugin, reactPlugin }) => [ + js.configs.recommended, + reactPlugin.configs.recommended, + reactHooksPlugin.configs.recommended, +]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-app-react-ts/package.json b/packages/create-rstack/template-app-react-ts/package.json index dcabfd86..dd7ea8c4 100644 --- a/packages/create-rstack/template-app-react-ts/package.json +++ b/packages/create-rstack/template-app-react-ts/package.json @@ -20,13 +20,13 @@ "devDependencies": { "@rsbuild/plugin-react": "^2.1.0", "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "^7.0.0", + "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.2", "@types/node": "^24.13.3", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", "happy-dom": "^20.11.2", - "rstack": "^0.5.2", + "rstack": "^0.6.1", "typescript": "^7.0.2" } } diff --git a/packages/create-rstack/template-app-react-ts/rstack.config.ts b/packages/create-rstack/template-app-react-ts/rstack.config.ts index 356c98e1..cb508e42 100644 --- a/packages/create-rstack/template-app-react-ts/rstack.config.ts +++ b/packages/create-rstack/template-app-react-ts/rstack.config.ts @@ -1,9 +1,8 @@ -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.app(async () => { const { pluginReact } = await import('@rsbuild/plugin-react'); - return { plugins: [pluginReact()], }; @@ -13,16 +12,12 @@ define.test({ setupFiles: ['./tests/rstest.setup.ts'], }); -define.lint(async () => { - const { js, ts, reactPlugin, reactHooksPlugin } = await import('rstack/lint'); - - return [ - js.configs.recommended, - ts.configs.recommended, - reactPlugin.configs.recommended, - reactHooksPlugin.configs.recommended, - ]; -}); +define.lint(({ js, ts, reactPlugin, reactHooksPlugin }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, + reactPlugin.configs.recommended, + reactHooksPlugin.configs.recommended, +]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-app-react/package.json b/packages/create-rstack/template-app-react/package.json index b724a854..803c4abb 100644 --- a/packages/create-rstack/template-app-react/package.json +++ b/packages/create-rstack/template-app-react/package.json @@ -20,9 +20,9 @@ "devDependencies": { "@rsbuild/plugin-react": "^2.1.0", "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "^7.0.0", + "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.2", "happy-dom": "^20.11.2", - "rstack": "^0.5.2" + "rstack": "^0.6.1" } } diff --git a/packages/create-rstack/template-app-react/rstack.config.js b/packages/create-rstack/template-app-react/rstack.config.js index ddd9f056..886b005a 100644 --- a/packages/create-rstack/template-app-react/rstack.config.js +++ b/packages/create-rstack/template-app-react/rstack.config.js @@ -1,10 +1,9 @@ // @ts-check -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.app(async () => { const { pluginReact } = await import('@rsbuild/plugin-react'); - return { plugins: [pluginReact()], }; @@ -14,15 +13,11 @@ define.test({ setupFiles: ['./tests/rstest.setup.js'], }); -define.lint(async () => { - const { js, reactHooksPlugin, reactPlugin } = await import('rstack/lint'); - - return [ - js.configs.recommended, - reactPlugin.configs.recommended, - reactHooksPlugin.configs.recommended, - ]; -}); +define.lint(({ js, reactHooksPlugin, reactPlugin }) => [ + js.configs.recommended, + reactPlugin.configs.recommended, + reactHooksPlugin.configs.recommended, +]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-app-solid-ts/package.json b/packages/create-rstack/template-app-solid-ts/package.json index 5c38f341..b223ec74 100644 --- a/packages/create-rstack/template-app-solid-ts/package.json +++ b/packages/create-rstack/template-app-solid-ts/package.json @@ -20,10 +20,10 @@ "@rsbuild/plugin-babel": "^2.0.1", "@rsbuild/plugin-solid": "^1.2.2", "@solidjs/testing-library": "^0.8.10", - "@testing-library/jest-dom": "^7.0.0", + "@testing-library/jest-dom": "^7.0.1", "@types/node": "^24.13.3", "happy-dom": "^20.11.2", - "rstack": "^0.5.2", + "rstack": "^0.6.1", "typescript": "^7.0.2" } } diff --git a/packages/create-rstack/template-app-solid-ts/rstack.config.ts b/packages/create-rstack/template-app-solid-ts/rstack.config.ts index 355d1b46..d364bf54 100644 --- a/packages/create-rstack/template-app-solid-ts/rstack.config.ts +++ b/packages/create-rstack/template-app-solid-ts/rstack.config.ts @@ -1,10 +1,9 @@ -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.app(async () => { const { pluginBabel } = await import('@rsbuild/plugin-babel'); const { pluginSolid } = await import('@rsbuild/plugin-solid'); - return { plugins: [ pluginBabel({ @@ -19,11 +18,10 @@ define.test({ setupFiles: ['./tests/rstest.setup.ts'], }); -define.lint(async () => { - const { js, ts } = await import('rstack/lint'); - - return [js.configs.recommended, ts.configs.recommended]; -}); +define.lint(({ js, ts }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, +]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-app-solid/package.json b/packages/create-rstack/template-app-solid/package.json index c092fd73..ff10813d 100644 --- a/packages/create-rstack/template-app-solid/package.json +++ b/packages/create-rstack/template-app-solid/package.json @@ -20,8 +20,8 @@ "@rsbuild/plugin-babel": "^2.0.1", "@rsbuild/plugin-solid": "^1.2.2", "@solidjs/testing-library": "^0.8.10", - "@testing-library/jest-dom": "^7.0.0", + "@testing-library/jest-dom": "^7.0.1", "happy-dom": "^20.11.2", - "rstack": "^0.5.2" + "rstack": "^0.6.1" } } diff --git a/packages/create-rstack/template-app-solid/rstack.config.js b/packages/create-rstack/template-app-solid/rstack.config.js index 80b7b6f7..da3845a4 100644 --- a/packages/create-rstack/template-app-solid/rstack.config.js +++ b/packages/create-rstack/template-app-solid/rstack.config.js @@ -1,11 +1,10 @@ // @ts-check -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.app(async () => { const { pluginBabel } = await import('@rsbuild/plugin-babel'); const { pluginSolid } = await import('@rsbuild/plugin-solid'); - return { plugins: [ pluginBabel({ @@ -20,11 +19,7 @@ define.test({ setupFiles: ['./tests/rstest.setup.js'], }); -define.lint(async () => { - const { js } = await import('rstack/lint'); - - return [js.configs.recommended]; -}); +define.lint(({ js }) => [js.configs.recommended]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-app-svelte-ts/package.json b/packages/create-rstack/template-app-svelte-ts/package.json index 1a00fa3b..eeafeb60 100644 --- a/packages/create-rstack/template-app-svelte-ts/package.json +++ b/packages/create-rstack/template-app-svelte-ts/package.json @@ -14,17 +14,17 @@ "test:watch": "rs test --watch" }, "dependencies": { - "svelte": "^5.56.8" + "svelte": "^5.56.9" }, "devDependencies": { "@rsbuild/plugin-svelte": "^2.0.1", - "@testing-library/jest-dom": "^7.0.0", + "@testing-library/jest-dom": "^7.0.1", "@testing-library/svelte": "^5.4.2", "@types/node": "^24.13.3", "happy-dom": "^20.11.2", "prettier-plugin-svelte": "^4.1.1", - "rstack": "^0.5.2", - "svelte-check": "^4.7.5", + "rstack": "^0.6.1", + "svelte-check": "^4.7.6", "typescript": "^6.0.3" } } diff --git a/packages/create-rstack/template-app-svelte-ts/rstack.config.ts b/packages/create-rstack/template-app-svelte-ts/rstack.config.ts index 66320b37..f2dec78f 100644 --- a/packages/create-rstack/template-app-svelte-ts/rstack.config.ts +++ b/packages/create-rstack/template-app-svelte-ts/rstack.config.ts @@ -1,9 +1,8 @@ -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.app(async () => { const { pluginSvelte } = await import('@rsbuild/plugin-svelte'); - return { plugins: [pluginSvelte()], }; @@ -13,11 +12,10 @@ define.test({ setupFiles: ['./tests/rstest.setup.ts'], }); -define.lint(async () => { - const { js, ts } = await import('rstack/lint'); - - return [js.configs.recommended, ts.configs.recommended]; -}); +define.lint(({ js, ts }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, +]); define.fmt({ plugins: ['prettier-plugin-svelte'], diff --git a/packages/create-rstack/template-app-svelte/package.json b/packages/create-rstack/template-app-svelte/package.json index d3ef1e7c..a0ba16f1 100644 --- a/packages/create-rstack/template-app-svelte/package.json +++ b/packages/create-rstack/template-app-svelte/package.json @@ -14,14 +14,14 @@ "test:watch": "rs test --watch" }, "dependencies": { - "svelte": "^5.56.8" + "svelte": "^5.56.9" }, "devDependencies": { "@rsbuild/plugin-svelte": "^2.0.1", - "@testing-library/jest-dom": "^7.0.0", + "@testing-library/jest-dom": "^7.0.1", "@testing-library/svelte": "^5.4.2", "happy-dom": "^20.11.2", "prettier-plugin-svelte": "^4.1.1", - "rstack": "^0.5.2" + "rstack": "^0.6.1" } } diff --git a/packages/create-rstack/template-app-svelte/rstack.config.js b/packages/create-rstack/template-app-svelte/rstack.config.js index 742edde8..0fb4fac1 100644 --- a/packages/create-rstack/template-app-svelte/rstack.config.js +++ b/packages/create-rstack/template-app-svelte/rstack.config.js @@ -1,10 +1,9 @@ // @ts-check -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.app(async () => { const { pluginSvelte } = await import('@rsbuild/plugin-svelte'); - return { plugins: [pluginSvelte()], }; @@ -14,11 +13,7 @@ define.test({ setupFiles: ['./tests/rstest.setup.js'], }); -define.lint(async () => { - const { js } = await import('rstack/lint'); - - return [js.configs.recommended]; -}); +define.lint(({ js }) => [js.configs.recommended]); define.fmt({ plugins: ['prettier-plugin-svelte'], diff --git a/packages/create-rstack/template-app-vanilla-ts/package.json b/packages/create-rstack/template-app-vanilla-ts/package.json index 6736f404..1992286f 100644 --- a/packages/create-rstack/template-app-vanilla-ts/package.json +++ b/packages/create-rstack/template-app-vanilla-ts/package.json @@ -15,10 +15,10 @@ }, "devDependencies": { "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "^7.0.0", + "@testing-library/jest-dom": "^7.0.1", "@types/node": "^24.13.3", "happy-dom": "^20.11.2", - "rstack": "^0.5.2", + "rstack": "^0.6.1", "typescript": "^7.0.2" } } diff --git a/packages/create-rstack/template-app-vanilla-ts/rstack.config.ts b/packages/create-rstack/template-app-vanilla-ts/rstack.config.ts index 349fcfae..dfe1edc1 100644 --- a/packages/create-rstack/template-app-vanilla-ts/rstack.config.ts +++ b/packages/create-rstack/template-app-vanilla-ts/rstack.config.ts @@ -1,4 +1,4 @@ -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.app({ @@ -9,11 +9,10 @@ define.test({ setupFiles: ['./tests/rstest.setup.ts'], }); -define.lint(async () => { - const { js, ts } = await import('rstack/lint'); - - return [js.configs.recommended, ts.configs.recommended]; -}); +define.lint(({ js, ts }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, +]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-app-vanilla/package.json b/packages/create-rstack/template-app-vanilla/package.json index 32376dc9..ad5a99fb 100644 --- a/packages/create-rstack/template-app-vanilla/package.json +++ b/packages/create-rstack/template-app-vanilla/package.json @@ -15,8 +15,8 @@ }, "devDependencies": { "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "^7.0.0", + "@testing-library/jest-dom": "^7.0.1", "happy-dom": "^20.11.2", - "rstack": "^0.5.2" + "rstack": "^0.6.1" } } diff --git a/packages/create-rstack/template-app-vanilla/rstack.config.js b/packages/create-rstack/template-app-vanilla/rstack.config.js index 23e75fdf..cf825efc 100644 --- a/packages/create-rstack/template-app-vanilla/rstack.config.js +++ b/packages/create-rstack/template-app-vanilla/rstack.config.js @@ -1,5 +1,5 @@ // @ts-check -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.app({ @@ -10,11 +10,7 @@ define.test({ setupFiles: ['./tests/rstest.setup.js'], }); -define.lint(async () => { - const { js } = await import('rstack/lint'); - - return [js.configs.recommended]; -}); +define.lint(({ js }) => [js.configs.recommended]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-app-vue-ts/package.json b/packages/create-rstack/template-app-vue-ts/package.json index f7103c91..2ca2e7a4 100644 --- a/packages/create-rstack/template-app-vue-ts/package.json +++ b/packages/create-rstack/template-app-vue-ts/package.json @@ -18,11 +18,11 @@ }, "devDependencies": { "@rsbuild/plugin-vue": "^2.0.1", - "@testing-library/jest-dom": "^7.0.0", + "@testing-library/jest-dom": "^7.0.1", "@types/node": "^24.13.3", "@vue/test-utils": "^2.4.11", "happy-dom": "^20.11.2", - "rstack": "^0.5.2", + "rstack": "^0.6.1", "typescript": "^6.0.3", "vue-tsc": "^3.3.9" } diff --git a/packages/create-rstack/template-app-vue-ts/rstack.config.ts b/packages/create-rstack/template-app-vue-ts/rstack.config.ts index f2224449..0c8fe28e 100644 --- a/packages/create-rstack/template-app-vue-ts/rstack.config.ts +++ b/packages/create-rstack/template-app-vue-ts/rstack.config.ts @@ -1,9 +1,8 @@ -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.app(async () => { const { pluginVue } = await import('@rsbuild/plugin-vue'); - return { plugins: [pluginVue()], }; @@ -13,11 +12,10 @@ define.test({ setupFiles: ['./tests/rstest.setup.ts'], }); -define.lint(async () => { - const { js, ts } = await import('rstack/lint'); - - return [js.configs.recommended, ts.configs.recommended]; -}); +define.lint(({ js, ts }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, +]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-app-vue-ts/src/env.d.ts b/packages/create-rstack/template-app-vue-ts/src/env.d.ts new file mode 100644 index 00000000..8afcdfbb --- /dev/null +++ b/packages/create-rstack/template-app-vue-ts/src/env.d.ts @@ -0,0 +1,6 @@ +declare module '*.vue' { + import type { DefineComponent } from 'vue'; + + const component: DefineComponent; + export default component; +} diff --git a/packages/create-rstack/template-app-vue/package.json b/packages/create-rstack/template-app-vue/package.json index 03d5df80..508b72b5 100644 --- a/packages/create-rstack/template-app-vue/package.json +++ b/packages/create-rstack/template-app-vue/package.json @@ -18,9 +18,9 @@ }, "devDependencies": { "@rsbuild/plugin-vue": "^2.0.1", - "@testing-library/jest-dom": "^7.0.0", + "@testing-library/jest-dom": "^7.0.1", "@vue/test-utils": "^2.4.11", "happy-dom": "^20.11.2", - "rstack": "^0.5.2" + "rstack": "^0.6.1" } } diff --git a/packages/create-rstack/template-app-vue/rstack.config.js b/packages/create-rstack/template-app-vue/rstack.config.js index b67b85e1..1cf8b2e1 100644 --- a/packages/create-rstack/template-app-vue/rstack.config.js +++ b/packages/create-rstack/template-app-vue/rstack.config.js @@ -1,10 +1,9 @@ // @ts-check -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.app(async () => { const { pluginVue } = await import('@rsbuild/plugin-vue'); - return { plugins: [pluginVue()], }; @@ -14,11 +13,7 @@ define.test({ setupFiles: ['./tests/rstest.setup.js'], }); -define.lint(async () => { - const { js } = await import('rstack/lint'); - - return [js.configs.recommended]; -}); +define.lint(({ js }) => [js.configs.recommended]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-common/README.md b/packages/create-rstack/template-common/README.md index abd777a3..f1777fb0 100644 --- a/packages/create-rstack/template-common/README.md +++ b/packages/create-rstack/template-common/README.md @@ -21,5 +21,5 @@ Install the dependencies: ## Learn more -- [Rstack documentation](https://rstack.rs) -- [Rstack GitHub repository](https://github.com/rstackjs/rstack-cli) +- [Rstack CLI documentation](https://rstack.rs) +- [Rstack CLI GitHub repository](https://github.com/rstackjs/rstack-cli) diff --git a/packages/create-rstack/template-doc-i18n/README.md b/packages/create-rstack/template-doc-i18n/README.md index ba4906ab..8e0f4ae9 100644 --- a/packages/create-rstack/template-doc-i18n/README.md +++ b/packages/create-rstack/template-doc-i18n/README.md @@ -19,5 +19,5 @@ Install the dependencies: ## Learn more -- [Rstack documentation](https://rstack.rs) +- [Rstack CLI documentation](https://rstack.rs) - [Rspress documentation](https://rspress.rs) diff --git a/packages/create-rstack/template-doc-i18n/package.json b/packages/create-rstack/template-doc-i18n/package.json index 921e526a..99653b4d 100644 --- a/packages/create-rstack/template-doc-i18n/package.json +++ b/packages/create-rstack/template-doc-i18n/package.json @@ -18,7 +18,7 @@ "@types/react-dom": "^19.2.4", "react": "^19.2.8", "react-dom": "^19.2.8", - "rstack": "^0.5.2", + "rstack": "^0.6.1", "typescript": "^7.0.2" } } diff --git a/packages/create-rstack/template-doc-i18n/rstack.config.ts b/packages/create-rstack/template-doc-i18n/rstack.config.ts index 2ab17eff..87a0190d 100644 --- a/packages/create-rstack/template-doc-i18n/rstack.config.ts +++ b/packages/create-rstack/template-doc-i18n/rstack.config.ts @@ -1,4 +1,4 @@ -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import path from 'node:path'; import { define } from 'rstack'; @@ -23,16 +23,12 @@ define.doc({ ], }); -define.lint(async () => { - const { js, ts, reactPlugin, reactHooksPlugin } = await import('rstack/lint'); - - return [ - js.configs.recommended, - ts.configs.recommended, - reactPlugin.configs.recommended, - reactHooksPlugin.configs.recommended, - ]; -}); +define.lint(({ js, ts, reactPlugin, reactHooksPlugin }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, + reactPlugin.configs.recommended, + reactHooksPlugin.configs.recommended, +]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-doc/README.md b/packages/create-rstack/template-doc/README.md index cd9ac929..84ca63bc 100644 --- a/packages/create-rstack/template-doc/README.md +++ b/packages/create-rstack/template-doc/README.md @@ -19,5 +19,5 @@ Install the dependencies: ## Learn more -- [Rstack documentation](https://rstack.rs) +- [Rstack CLI documentation](https://rstack.rs) - [Rspress documentation](https://rspress.rs) diff --git a/packages/create-rstack/template-doc/package.json b/packages/create-rstack/template-doc/package.json index 4b86ecd8..72785a93 100644 --- a/packages/create-rstack/template-doc/package.json +++ b/packages/create-rstack/template-doc/package.json @@ -18,7 +18,7 @@ "@types/react-dom": "^19.2.4", "react": "^19.2.8", "react-dom": "^19.2.8", - "rstack": "^0.5.2", + "rstack": "^0.6.1", "typescript": "^7.0.2" } } diff --git a/packages/create-rstack/template-doc/rstack.config.ts b/packages/create-rstack/template-doc/rstack.config.ts index 466b9d5c..4a65ede6 100644 --- a/packages/create-rstack/template-doc/rstack.config.ts +++ b/packages/create-rstack/template-doc/rstack.config.ts @@ -1,4 +1,4 @@ -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import path from 'node:path'; import { define } from 'rstack'; @@ -7,16 +7,12 @@ define.doc({ title: 'My Site', }); -define.lint(async () => { - const { js, ts, reactPlugin, reactHooksPlugin } = await import('rstack/lint'); - - return [ - js.configs.recommended, - ts.configs.recommended, - reactPlugin.configs.recommended, - reactHooksPlugin.configs.recommended, - ]; -}); +define.lint(({ js, ts, reactPlugin, reactHooksPlugin }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, + reactPlugin.configs.recommended, + reactHooksPlugin.configs.recommended, +]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-lib-node-ts/README.md b/packages/create-rstack/template-lib-node-ts/README.md index 93e8a06e..5d6dbbef 100644 --- a/packages/create-rstack/template-lib-node-ts/README.md +++ b/packages/create-rstack/template-lib-node-ts/README.md @@ -20,5 +20,5 @@ Install the dependencies: ## Learn more -- [Rstack documentation](https://rstack.rs) +- [Rstack CLI documentation](https://rstack.rs) - [Rslib documentation](https://rslib.rs) diff --git a/packages/create-rstack/template-lib-node-ts/package.json b/packages/create-rstack/template-lib-node-ts/package.json index 9f2c7a60..95e1ff92 100644 --- a/packages/create-rstack/template-lib-node-ts/package.json +++ b/packages/create-rstack/template-lib-node-ts/package.json @@ -25,7 +25,7 @@ }, "devDependencies": { "@types/node": "^24.13.3", - "rstack": "^0.5.2", + "rstack": "^0.6.1", "typescript": "^7.0.2" }, "engines": { diff --git a/packages/create-rstack/template-lib-node-ts/rstack.config.ts b/packages/create-rstack/template-lib-node-ts/rstack.config.ts index d0f94060..09769776 100644 --- a/packages/create-rstack/template-lib-node-ts/rstack.config.ts +++ b/packages/create-rstack/template-lib-node-ts/rstack.config.ts @@ -1,4 +1,4 @@ -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.lib({ @@ -6,15 +6,10 @@ define.lib({ dts: true, }); -define.test({ - // Configure Rstest -}); - -define.lint(async () => { - const { js, ts } = await import('rstack/lint'); - - return [js.configs.recommended, ts.configs.recommended]; -}); +define.lint(({ js, ts }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, +]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-lib-node/README.md b/packages/create-rstack/template-lib-node/README.md index 93e8a06e..5d6dbbef 100644 --- a/packages/create-rstack/template-lib-node/README.md +++ b/packages/create-rstack/template-lib-node/README.md @@ -20,5 +20,5 @@ Install the dependencies: ## Learn more -- [Rstack documentation](https://rstack.rs) +- [Rstack CLI documentation](https://rstack.rs) - [Rslib documentation](https://rslib.rs) diff --git a/packages/create-rstack/template-lib-node/package.json b/packages/create-rstack/template-lib-node/package.json index d80ee62b..34fc835d 100644 --- a/packages/create-rstack/template-lib-node/package.json +++ b/packages/create-rstack/template-lib-node/package.json @@ -22,7 +22,7 @@ "test:watch": "rs test --watch" }, "devDependencies": { - "rstack": "^0.5.2" + "rstack": "^0.6.1" }, "engines": { "node": ">=22.12.0" diff --git a/packages/create-rstack/template-lib-node/rstack.config.js b/packages/create-rstack/template-lib-node/rstack.config.js index 3f62051d..6080e25d 100644 --- a/packages/create-rstack/template-lib-node/rstack.config.js +++ b/packages/create-rstack/template-lib-node/rstack.config.js @@ -1,20 +1,12 @@ // @ts-check -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.lib({ syntax: ['node 22'], }); -define.test({ - // Configure Rstest -}); - -define.lint(async () => { - const { js } = await import('rstack/lint'); - - return [js.configs.recommended]; -}); +define.lint(({ js }) => [js.configs.recommended]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-lib-react-ts/README.md b/packages/create-rstack/template-lib-react-ts/README.md index 93e8a06e..5d6dbbef 100644 --- a/packages/create-rstack/template-lib-react-ts/README.md +++ b/packages/create-rstack/template-lib-react-ts/README.md @@ -20,5 +20,5 @@ Install the dependencies: ## Learn more -- [Rstack documentation](https://rstack.rs) +- [Rstack CLI documentation](https://rstack.rs) - [Rslib documentation](https://rslib.rs) diff --git a/packages/create-rstack/template-lib-react-ts/package.json b/packages/create-rstack/template-lib-react-ts/package.json index 98d6ae9c..2ad075e2 100644 --- a/packages/create-rstack/template-lib-react-ts/package.json +++ b/packages/create-rstack/template-lib-react-ts/package.json @@ -25,7 +25,7 @@ "devDependencies": { "@rsbuild/plugin-react": "^2.1.0", "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "^7.0.0", + "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.2", "@types/node": "^24.13.3", "@types/react": "^19.2.18", @@ -33,7 +33,7 @@ "happy-dom": "^20.11.2", "react": "^19.2.8", "react-dom": "^19.2.8", - "rstack": "^0.5.2", + "rstack": "^0.6.1", "typescript": "^7.0.2" }, "peerDependencies": { diff --git a/packages/create-rstack/template-lib-react-ts/rstack.config.ts b/packages/create-rstack/template-lib-react-ts/rstack.config.ts index 89cb5d03..1aeb4946 100644 --- a/packages/create-rstack/template-lib-react-ts/rstack.config.ts +++ b/packages/create-rstack/template-lib-react-ts/rstack.config.ts @@ -1,9 +1,8 @@ -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.lib(async () => { const { pluginReact } = await import('@rsbuild/plugin-react'); - return { bundle: false, dts: true, @@ -23,16 +22,12 @@ define.test({ setupFiles: ['./tests/rstest.setup.ts'], }); -define.lint(async () => { - const { js, ts, reactPlugin, reactHooksPlugin } = await import('rstack/lint'); - - return [ - js.configs.recommended, - ts.configs.recommended, - reactPlugin.configs.recommended, - reactHooksPlugin.configs.recommended, - ]; -}); +define.lint(({ js, ts, reactPlugin, reactHooksPlugin }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, + reactPlugin.configs.recommended, + reactHooksPlugin.configs.recommended, +]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-lib-react/README.md b/packages/create-rstack/template-lib-react/README.md index 93e8a06e..5d6dbbef 100644 --- a/packages/create-rstack/template-lib-react/README.md +++ b/packages/create-rstack/template-lib-react/README.md @@ -20,5 +20,5 @@ Install the dependencies: ## Learn more -- [Rstack documentation](https://rstack.rs) +- [Rstack CLI documentation](https://rstack.rs) - [Rslib documentation](https://rslib.rs) diff --git a/packages/create-rstack/template-lib-react/package.json b/packages/create-rstack/template-lib-react/package.json index f9b3c3dc..f91d1744 100644 --- a/packages/create-rstack/template-lib-react/package.json +++ b/packages/create-rstack/template-lib-react/package.json @@ -23,13 +23,13 @@ "devDependencies": { "@rsbuild/plugin-react": "^2.1.0", "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "^7.0.0", + "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.2", "@types/react-dom": "^19.2.4", "happy-dom": "^20.11.2", "react": "^19.2.8", "react-dom": "^19.2.8", - "rstack": "^0.5.2" + "rstack": "^0.6.1" }, "peerDependencies": { "react": ">=18.0.0", diff --git a/packages/create-rstack/template-lib-react/rstack.config.js b/packages/create-rstack/template-lib-react/rstack.config.js index 3ef9c799..76fdfda3 100644 --- a/packages/create-rstack/template-lib-react/rstack.config.js +++ b/packages/create-rstack/template-lib-react/rstack.config.js @@ -1,10 +1,9 @@ // @ts-check -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.lib(async () => { const { pluginReact } = await import('@rsbuild/plugin-react'); - return { bundle: false, source: { @@ -23,15 +22,11 @@ define.test({ setupFiles: ['./tests/rstest.setup.js'], }); -define.lint(async () => { - const { js, reactHooksPlugin, reactPlugin } = await import('rstack/lint'); - - return [ - js.configs.recommended, - reactPlugin.configs.recommended, - reactHooksPlugin.configs.recommended, - ]; -}); +define.lint(({ js, reactHooksPlugin, reactPlugin }) => [ + js.configs.recommended, + reactPlugin.configs.recommended, + reactHooksPlugin.configs.recommended, +]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-lib-solid-ts/README.md b/packages/create-rstack/template-lib-solid-ts/README.md index 93e8a06e..5d6dbbef 100644 --- a/packages/create-rstack/template-lib-solid-ts/README.md +++ b/packages/create-rstack/template-lib-solid-ts/README.md @@ -20,5 +20,5 @@ Install the dependencies: ## Learn more -- [Rstack documentation](https://rstack.rs) +- [Rstack CLI documentation](https://rstack.rs) - [Rslib documentation](https://rslib.rs) diff --git a/packages/create-rstack/template-lib-solid-ts/package.json b/packages/create-rstack/template-lib-solid-ts/package.json index 17b3132c..fa98fcbb 100644 --- a/packages/create-rstack/template-lib-solid-ts/package.json +++ b/packages/create-rstack/template-lib-solid-ts/package.json @@ -27,10 +27,10 @@ "@rsbuild/plugin-babel": "^2.0.1", "@rsbuild/plugin-solid": "^1.2.2", "@solidjs/testing-library": "^0.8.10", - "@testing-library/jest-dom": "^7.0.0", + "@testing-library/jest-dom": "^7.0.1", "@types/node": "^24.13.3", "happy-dom": "^20.11.2", - "rstack": "^0.5.2", + "rstack": "^0.6.1", "solid-js": "^1.9.14", "typescript": "^7.0.2" }, diff --git a/packages/create-rstack/template-lib-solid-ts/rstack.config.ts b/packages/create-rstack/template-lib-solid-ts/rstack.config.ts index 25c187d5..c61c28e5 100644 --- a/packages/create-rstack/template-lib-solid-ts/rstack.config.ts +++ b/packages/create-rstack/template-lib-solid-ts/rstack.config.ts @@ -1,10 +1,9 @@ -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.lib(async () => { const { pluginBabel } = await import('@rsbuild/plugin-babel'); const { pluginSolid } = await import('@rsbuild/plugin-solid'); - return { lib: [ { @@ -63,7 +62,6 @@ define.lib(async () => { define.test(async () => { const { pluginBabel } = await import('@rsbuild/plugin-babel'); const { pluginSolid } = await import('@rsbuild/plugin-solid'); - return { setupFiles: ['./tests/rstest.setup.ts'], plugins: [ @@ -75,11 +73,10 @@ define.test(async () => { }; }); -define.lint(async () => { - const { js, ts } = await import('rstack/lint'); - - return [js.configs.recommended, ts.configs.recommended]; -}); +define.lint(({ js, ts }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, +]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-lib-solid/README.md b/packages/create-rstack/template-lib-solid/README.md index 93e8a06e..5d6dbbef 100644 --- a/packages/create-rstack/template-lib-solid/README.md +++ b/packages/create-rstack/template-lib-solid/README.md @@ -20,5 +20,5 @@ Install the dependencies: ## Learn more -- [Rstack documentation](https://rstack.rs) +- [Rstack CLI documentation](https://rstack.rs) - [Rslib documentation](https://rslib.rs) diff --git a/packages/create-rstack/template-lib-solid/package.json b/packages/create-rstack/template-lib-solid/package.json index 90567af4..836fec38 100644 --- a/packages/create-rstack/template-lib-solid/package.json +++ b/packages/create-rstack/template-lib-solid/package.json @@ -25,9 +25,9 @@ "@rsbuild/plugin-babel": "^2.0.1", "@rsbuild/plugin-solid": "^1.2.2", "@solidjs/testing-library": "^0.8.10", - "@testing-library/jest-dom": "^7.0.0", + "@testing-library/jest-dom": "^7.0.1", "happy-dom": "^20.11.2", - "rstack": "^0.5.2", + "rstack": "^0.6.1", "solid-js": "^1.9.14" }, "peerDependencies": { diff --git a/packages/create-rstack/template-lib-solid/rstack.config.js b/packages/create-rstack/template-lib-solid/rstack.config.js index 75f1d03e..974cf85d 100644 --- a/packages/create-rstack/template-lib-solid/rstack.config.js +++ b/packages/create-rstack/template-lib-solid/rstack.config.js @@ -1,11 +1,10 @@ // @ts-check -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.lib(async () => { const { pluginBabel } = await import('@rsbuild/plugin-babel'); const { pluginSolid } = await import('@rsbuild/plugin-solid'); - return { lib: [ { @@ -63,7 +62,6 @@ define.lib(async () => { define.test(async () => { const { pluginBabel } = await import('@rsbuild/plugin-babel'); const { pluginSolid } = await import('@rsbuild/plugin-solid'); - return { setupFiles: ['./tests/rstest.setup.js'], plugins: [ @@ -75,11 +73,7 @@ define.test(async () => { }; }); -define.lint(async () => { - const { js } = await import('rstack/lint'); - - return [js.configs.recommended]; -}); +define.lint(({ js }) => [js.configs.recommended]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-lib-svelte-ts/README.md b/packages/create-rstack/template-lib-svelte-ts/README.md index 93e8a06e..5d6dbbef 100644 --- a/packages/create-rstack/template-lib-svelte-ts/README.md +++ b/packages/create-rstack/template-lib-svelte-ts/README.md @@ -20,5 +20,5 @@ Install the dependencies: ## Learn more -- [Rstack documentation](https://rstack.rs) +- [Rstack CLI documentation](https://rstack.rs) - [Rslib documentation](https://rslib.rs) diff --git a/packages/create-rstack/template-lib-svelte-ts/package.json b/packages/create-rstack/template-lib-svelte-ts/package.json index 956e0de2..15ad5f7f 100644 --- a/packages/create-rstack/template-lib-svelte-ts/package.json +++ b/packages/create-rstack/template-lib-svelte-ts/package.json @@ -27,10 +27,10 @@ "@types/node": "^24.13.3", "happy-dom": "^20.11.2", "prettier-plugin-svelte": "^4.1.1", - "rstack": "^0.5.2", - "svelte": "^5.56.8", - "svelte-check": "^4.7.5", - "svelte2tsx": "^0.7.59", + "rstack": "^0.6.1", + "svelte": "^5.56.9", + "svelte-check": "^4.7.6", + "svelte2tsx": "^0.7.61", "typescript": "^6.0.3" }, "peerDependencies": { diff --git a/packages/create-rstack/template-lib-svelte-ts/rstack.config.ts b/packages/create-rstack/template-lib-svelte-ts/rstack.config.ts index 3ac5f03d..6aeaf38c 100644 --- a/packages/create-rstack/template-lib-svelte-ts/rstack.config.ts +++ b/packages/create-rstack/template-lib-svelte-ts/rstack.config.ts @@ -1,10 +1,9 @@ -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; import { svelteDtsPlugin } from './scripts/rslib-plugin-svelte-dts.ts'; define.lib(async () => { const { pluginSvelte } = await import('@rsbuild/plugin-svelte'); - return { bundle: false, source: { @@ -19,15 +18,10 @@ define.lib(async () => { }; }); -define.test({ - testEnvironment: 'happy-dom', -}); - -define.lint(async () => { - const { js, ts } = await import('rstack/lint'); - - return [js.configs.recommended, ts.configs.recommended]; -}); +define.lint(({ js, ts }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, +]); define.fmt({ plugins: ['prettier-plugin-svelte'], diff --git a/packages/create-rstack/template-lib-svelte/README.md b/packages/create-rstack/template-lib-svelte/README.md index 93e8a06e..5d6dbbef 100644 --- a/packages/create-rstack/template-lib-svelte/README.md +++ b/packages/create-rstack/template-lib-svelte/README.md @@ -20,5 +20,5 @@ Install the dependencies: ## Learn more -- [Rstack documentation](https://rstack.rs) +- [Rstack CLI documentation](https://rstack.rs) - [Rslib documentation](https://rslib.rs) diff --git a/packages/create-rstack/template-lib-svelte/package.json b/packages/create-rstack/template-lib-svelte/package.json index d0c867a9..5756c287 100644 --- a/packages/create-rstack/template-lib-svelte/package.json +++ b/packages/create-rstack/template-lib-svelte/package.json @@ -24,8 +24,8 @@ "@rsbuild/plugin-svelte": "^2.0.1", "happy-dom": "^20.11.2", "prettier-plugin-svelte": "^4.1.1", - "rstack": "^0.5.2", - "svelte": "^5.56.8" + "rstack": "^0.6.1", + "svelte": "^5.56.9" }, "peerDependencies": { "svelte": "^5.0.0" diff --git a/packages/create-rstack/template-lib-svelte/rstack.config.js b/packages/create-rstack/template-lib-svelte/rstack.config.js index b7d7ba0f..7a952e06 100644 --- a/packages/create-rstack/template-lib-svelte/rstack.config.js +++ b/packages/create-rstack/template-lib-svelte/rstack.config.js @@ -1,10 +1,9 @@ // @ts-check -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.lib(async () => { const { pluginSvelte } = await import('@rsbuild/plugin-svelte'); - return { bundle: false, source: { @@ -19,15 +18,7 @@ define.lib(async () => { }; }); -define.test({ - testEnvironment: 'happy-dom', -}); - -define.lint(async () => { - const { js } = await import('rstack/lint'); - - return [js.configs.recommended]; -}); +define.lint(({ js }) => [js.configs.recommended]); define.fmt({ plugins: ['prettier-plugin-svelte'], diff --git a/packages/create-rstack/template-lib-vue-ts/README.md b/packages/create-rstack/template-lib-vue-ts/README.md index 93e8a06e..5d6dbbef 100644 --- a/packages/create-rstack/template-lib-vue-ts/README.md +++ b/packages/create-rstack/template-lib-vue-ts/README.md @@ -20,5 +20,5 @@ Install the dependencies: ## Learn more -- [Rstack documentation](https://rstack.rs) +- [Rstack CLI documentation](https://rstack.rs) - [Rslib documentation](https://rslib.rs) diff --git a/packages/create-rstack/template-lib-vue-ts/package.json b/packages/create-rstack/template-lib-vue-ts/package.json index 1c5146c1..3b5f7930 100644 --- a/packages/create-rstack/template-lib-vue-ts/package.json +++ b/packages/create-rstack/template-lib-vue-ts/package.json @@ -24,11 +24,11 @@ }, "devDependencies": { "@rsbuild/plugin-vue": "^2.0.1", - "@testing-library/jest-dom": "^7.0.0", + "@testing-library/jest-dom": "^7.0.1", "@types/node": "^24.13.3", "@vue/test-utils": "^2.4.11", "happy-dom": "^20.11.2", - "rstack": "^0.5.2", + "rstack": "^0.6.1", "typescript": "^6.0.3", "vue": "^3.5.41", "vue-tsc": "^3.3.9" diff --git a/packages/create-rstack/template-lib-vue-ts/rstack.config.ts b/packages/create-rstack/template-lib-vue-ts/rstack.config.ts index e9c37a60..c30a51b2 100644 --- a/packages/create-rstack/template-lib-vue-ts/rstack.config.ts +++ b/packages/create-rstack/template-lib-vue-ts/rstack.config.ts @@ -1,9 +1,8 @@ -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.lib(async () => { const { pluginVue } = await import('@rsbuild/plugin-vue'); - return { bundle: false, source: { @@ -22,11 +21,10 @@ define.test({ setupFiles: ['./tests/rstest.setup.ts'], }); -define.lint(async () => { - const { js, ts } = await import('rstack/lint'); - - return [js.configs.recommended, ts.configs.recommended]; -}); +define.lint(({ js, ts }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, +]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-lib-vue/README.md b/packages/create-rstack/template-lib-vue/README.md index 93e8a06e..5d6dbbef 100644 --- a/packages/create-rstack/template-lib-vue/README.md +++ b/packages/create-rstack/template-lib-vue/README.md @@ -20,5 +20,5 @@ Install the dependencies: ## Learn more -- [Rstack documentation](https://rstack.rs) +- [Rstack CLI documentation](https://rstack.rs) - [Rslib documentation](https://rslib.rs) diff --git a/packages/create-rstack/template-lib-vue/package.json b/packages/create-rstack/template-lib-vue/package.json index 9ed02556..d22c4278 100644 --- a/packages/create-rstack/template-lib-vue/package.json +++ b/packages/create-rstack/template-lib-vue/package.json @@ -22,10 +22,10 @@ }, "devDependencies": { "@rsbuild/plugin-vue": "^2.0.1", - "@testing-library/jest-dom": "^7.0.0", + "@testing-library/jest-dom": "^7.0.1", "@vue/test-utils": "^2.4.11", "happy-dom": "^20.11.2", - "rstack": "^0.5.2", + "rstack": "^0.6.1", "vue": "^3.5.41" }, "peerDependencies": { diff --git a/packages/create-rstack/template-lib-vue/rstack.config.js b/packages/create-rstack/template-lib-vue/rstack.config.js index 09a3de03..42ec7988 100644 --- a/packages/create-rstack/template-lib-vue/rstack.config.js +++ b/packages/create-rstack/template-lib-vue/rstack.config.js @@ -1,10 +1,9 @@ // @ts-check -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.lib(async () => { const { pluginVue } = await import('@rsbuild/plugin-vue'); - return { bundle: false, source: { @@ -23,11 +22,7 @@ define.test({ setupFiles: ['./tests/rstest.setup.js'], }); -define.lint(async () => { - const { js } = await import('rstack/lint'); - - return [js.configs.recommended]; -}); +define.lint(({ js }) => [js.configs.recommended]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/tests/create.test.ts b/packages/create-rstack/tests/create.test.ts index 32576411..63a44649 100644 --- a/packages/create-rstack/tests/create.test.ts +++ b/packages/create-rstack/tests/create.test.ts @@ -31,29 +31,65 @@ type SourceTemplate = { const sourceTemplates: SourceTemplate[] = [ { template: 'app-vanilla', sourceExtension: 'js', testFile: 'dom.test.js' }, - { template: 'app-vanilla-ts', sourceExtension: 'ts', testFile: 'dom.test.ts' }, + { + template: 'app-vanilla-ts', + sourceExtension: 'ts', + testFile: 'dom.test.ts', + }, { template: 'app-react', sourceExtension: 'jsx', testFile: 'index.test.jsx' }, - { template: 'app-react-ts', sourceExtension: 'tsx', testFile: 'index.test.tsx' }, - { template: 'app-preact', sourceExtension: 'jsx', testFile: 'index.test.jsx' }, - { template: 'app-preact-ts', sourceExtension: 'tsx', testFile: 'index.test.tsx' }, + { + template: 'app-react-ts', + sourceExtension: 'tsx', + testFile: 'index.test.tsx', + }, + { + template: 'app-preact', + sourceExtension: 'jsx', + testFile: 'index.test.jsx', + }, + { + template: 'app-preact-ts', + sourceExtension: 'tsx', + testFile: 'index.test.tsx', + }, { template: 'app-vue', sourceExtension: 'js', testFile: 'index.test.js' }, { template: 'app-vue-ts', sourceExtension: 'ts', testFile: 'index.test.ts' }, { template: 'app-lit', sourceExtension: 'js', testFile: 'index.test.js' }, { template: 'app-lit-ts', sourceExtension: 'ts', testFile: 'index.test.ts' }, { template: 'app-svelte', sourceExtension: 'js', testFile: 'index.test.js' }, - { template: 'app-svelte-ts', sourceExtension: 'ts', testFile: 'index.test.ts' }, + { + template: 'app-svelte-ts', + sourceExtension: 'ts', + testFile: 'index.test.ts', + }, { template: 'app-solid', sourceExtension: 'jsx', testFile: 'index.test.jsx' }, - { template: 'app-solid-ts', sourceExtension: 'tsx', testFile: 'index.test.tsx' }, + { + template: 'app-solid-ts', + sourceExtension: 'tsx', + testFile: 'index.test.tsx', + }, { template: 'lib-node', sourceExtension: 'js', testFile: 'index.test.js' }, { template: 'lib-node-ts', sourceExtension: 'ts', testFile: 'index.test.ts' }, { template: 'lib-react', sourceExtension: 'jsx', testFile: 'index.test.jsx' }, - { template: 'lib-react-ts', sourceExtension: 'tsx', testFile: 'index.test.tsx' }, + { + template: 'lib-react-ts', + sourceExtension: 'tsx', + testFile: 'index.test.tsx', + }, { template: 'lib-vue', sourceExtension: 'js', testFile: 'index.test.js' }, { template: 'lib-vue-ts', sourceExtension: 'ts', testFile: 'index.test.ts' }, { template: 'lib-svelte', sourceExtension: 'js', testFile: 'index.test.js' }, - { template: 'lib-svelte-ts', sourceExtension: 'ts', testFile: 'index.test.ts' }, + { + template: 'lib-svelte-ts', + sourceExtension: 'ts', + testFile: 'index.test.ts', + }, { template: 'lib-solid', sourceExtension: 'jsx', testFile: 'index.test.jsx' }, - { template: 'lib-solid-ts', sourceExtension: 'tsx', testFile: 'index.test.tsx' }, + { + template: 'lib-solid-ts', + sourceExtension: 'tsx', + testFile: 'index.test.tsx', + }, ]; const docTemplates = [ @@ -74,14 +110,25 @@ const docTemplates = [ ]; const getCheckScript = (template: string, hasTypeScript: boolean): string => - hasTypeScript && !templatesWithoutTypeCheck.has(template) ? typeCheckScript : checkScript; + hasTypeScript && !templatesWithoutTypeCheck.has(template) + ? typeCheckScript + : checkScript; -const readProjectPackage = async (projectDirectory: string): Promise => - JSON.parse(await readFile(path.join(projectDirectory, 'package.json'), 'utf8')) as ProjectPackage; +const readProjectPackage = async ( + projectDirectory: string, +): Promise => + JSON.parse( + await readFile(path.join(projectDirectory, 'package.json'), 'utf8'), + ) as ProjectPackage; -const expectFiles = async (projectDirectory: string, files: string[]): Promise => { +const expectFiles = async ( + projectDirectory: string, + files: string[], +): Promise => { for (const file of files) { - await expect(access(path.join(projectDirectory, file))).resolves.toBeUndefined(); + await expect( + access(path.join(projectDirectory, file)), + ).resolves.toBeUndefined(); } }; @@ -92,10 +139,16 @@ const expectStagedSetup = async ( ): Promise => { expect(scripts.prepare).toBe('rs setup'); expect( - await readFile(path.join(projectDirectory, '.rstack', 'hooks', 'pre-commit'), 'utf8'), + await readFile( + path.join(projectDirectory, '.rstack', 'hooks', 'pre-commit'), + 'utf8', + ), ).toBe('rs staged\n'); expect( - await readFile(path.join(projectDirectory, `rstack.config.${configExtension}`), 'utf8'), + await readFile( + path.join(projectDirectory, `rstack.config.${configExtension}`), + 'utf8', + ), ).toContain('define.staged({'); }; @@ -109,7 +162,10 @@ const expectNoStagedSetup = async ( access(path.join(projectDirectory, '.rstack', 'hooks', 'pre-commit')), ).rejects.toThrow(); expect( - await readFile(path.join(projectDirectory, `rstack.config.${configExtension}`), 'utf8'), + await readFile( + path.join(projectDirectory, `rstack.config.${configExtension}`), + 'utf8', + ), ).not.toContain('define.staged({'); }; @@ -122,8 +178,14 @@ const expectProjectSetup = async ( const packageJson = await readProjectPackage(projectDirectory); expect(packageJson.name).toBe('my-app'); - expect(packageJson.scripts.check).toBe(getCheckScript(template, hasTypeScript)); - await expectStagedSetup(projectDirectory, configExtension, packageJson.scripts); + expect(packageJson.scripts.check).toBe( + getCheckScript(template, hasTypeScript), + ); + await expectStagedSetup( + projectDirectory, + configExtension, + packageJson.scripts, + ); const tsconfig = access(path.join(projectDirectory, 'tsconfig.json')); if (hasTypeScript) { @@ -135,7 +197,9 @@ const expectProjectSetup = async ( afterEach(async () => { await Promise.all( - tempDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })), + tempDirectories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })), ); }); @@ -154,7 +218,8 @@ const createProject = async ( tempDirectories.push(tempDirectory); if (initializeGitIn) { - const gitDirectory = initializeGitIn === 'project' ? projectDirectory : tempDirectory; + const gitDirectory = + initializeGitIn === 'project' ? projectDirectory : tempDirectory; await mkdir(gitDirectory, { recursive: true }); await execFileAsync('git', ['init', '--quiet'], { cwd: gitDirectory }); } @@ -205,15 +270,26 @@ test.each(sourceTemplates)( if (template.startsWith('app-')) { files.push('README.md', '.gitignore'); } + if (template === 'app-vue-ts') { + files.push('src/env.d.ts'); + } - await expectProjectSetup(projectDirectory, template, configExtension, hasTypeScript); + await expectProjectSetup( + projectDirectory, + template, + configExtension, + hasTypeScript, + ); await expectFiles(projectDirectory, files); }, ); -test.each(docTemplates)('creates the $template template', async ({ template, files }) => { - const projectDirectory = await createProject(template); +test.each(docTemplates)( + 'creates the $template template', + async ({ template, files }) => { + const projectDirectory = await createProject(template); - await expectProjectSetup(projectDirectory, template, 'ts', true); - await expectFiles(projectDirectory, files); -}); + await expectProjectSetup(projectDirectory, template, 'ts', true); + await expectFiles(projectDirectory, files); + }, +); diff --git a/packages/rstack/binding.cjs b/packages/rstack/binding.cjs index 29f79bb3..6c7528a3 100644 --- a/packages/rstack/binding.cjs +++ b/packages/rstack/binding.cjs @@ -77,8 +77,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-android-arm64') const bindingPackageVersion = require('@rstackjs/cli-android-arm64/package.json').version - if (bindingPackageVersion !== '0.5.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -93,8 +93,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-android-arm-eabi') const bindingPackageVersion = require('@rstackjs/cli-android-arm-eabi/package.json').version - if (bindingPackageVersion !== '0.5.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -114,8 +114,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-win32-x64-gnu') const bindingPackageVersion = require('@rstackjs/cli-win32-x64-gnu/package.json').version - if (bindingPackageVersion !== '0.5.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -130,8 +130,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-win32-x64-msvc') const bindingPackageVersion = require('@rstackjs/cli-win32-x64-msvc/package.json').version - if (bindingPackageVersion !== '0.5.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -147,8 +147,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-win32-ia32-msvc') const bindingPackageVersion = require('@rstackjs/cli-win32-ia32-msvc/package.json').version - if (bindingPackageVersion !== '0.5.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -163,8 +163,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-win32-arm64-msvc') const bindingPackageVersion = require('@rstackjs/cli-win32-arm64-msvc/package.json').version - if (bindingPackageVersion !== '0.5.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -182,8 +182,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-darwin-universal') const bindingPackageVersion = require('@rstackjs/cli-darwin-universal/package.json').version - if (bindingPackageVersion !== '0.5.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -198,8 +198,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-darwin-x64') const bindingPackageVersion = require('@rstackjs/cli-darwin-x64/package.json').version - if (bindingPackageVersion !== '0.5.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -214,8 +214,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-darwin-arm64') const bindingPackageVersion = require('@rstackjs/cli-darwin-arm64/package.json').version - if (bindingPackageVersion !== '0.5.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -234,8 +234,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-freebsd-x64') const bindingPackageVersion = require('@rstackjs/cli-freebsd-x64/package.json').version - if (bindingPackageVersion !== '0.5.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -250,8 +250,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-freebsd-arm64') const bindingPackageVersion = require('@rstackjs/cli-freebsd-arm64/package.json').version - if (bindingPackageVersion !== '0.5.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -271,8 +271,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-linux-x64-musl') const bindingPackageVersion = require('@rstackjs/cli-linux-x64-musl/package.json').version - if (bindingPackageVersion !== '0.5.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -287,8 +287,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-linux-x64-gnu') const bindingPackageVersion = require('@rstackjs/cli-linux-x64-gnu/package.json').version - if (bindingPackageVersion !== '0.5.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -305,8 +305,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-linux-arm64-musl') const bindingPackageVersion = require('@rstackjs/cli-linux-arm64-musl/package.json').version - if (bindingPackageVersion !== '0.5.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -321,8 +321,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-linux-arm64-gnu') const bindingPackageVersion = require('@rstackjs/cli-linux-arm64-gnu/package.json').version - if (bindingPackageVersion !== '0.5.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -339,8 +339,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-linux-arm-musleabihf') const bindingPackageVersion = require('@rstackjs/cli-linux-arm-musleabihf/package.json').version - if (bindingPackageVersion !== '0.5.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -355,8 +355,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-linux-arm-gnueabihf') const bindingPackageVersion = require('@rstackjs/cli-linux-arm-gnueabihf/package.json').version - if (bindingPackageVersion !== '0.5.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -373,8 +373,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-linux-loong64-musl') const bindingPackageVersion = require('@rstackjs/cli-linux-loong64-musl/package.json').version - if (bindingPackageVersion !== '0.5.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -389,8 +389,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-linux-loong64-gnu') const bindingPackageVersion = require('@rstackjs/cli-linux-loong64-gnu/package.json').version - if (bindingPackageVersion !== '0.5.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -407,8 +407,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-linux-riscv64-musl') const bindingPackageVersion = require('@rstackjs/cli-linux-riscv64-musl/package.json').version - if (bindingPackageVersion !== '0.5.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -423,8 +423,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-linux-riscv64-gnu') const bindingPackageVersion = require('@rstackjs/cli-linux-riscv64-gnu/package.json').version - if (bindingPackageVersion !== '0.5.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -440,8 +440,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-linux-ppc64-gnu') const bindingPackageVersion = require('@rstackjs/cli-linux-ppc64-gnu/package.json').version - if (bindingPackageVersion !== '0.5.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -456,8 +456,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-linux-s390x-gnu') const bindingPackageVersion = require('@rstackjs/cli-linux-s390x-gnu/package.json').version - if (bindingPackageVersion !== '0.5.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -476,8 +476,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-openharmony-arm64') const bindingPackageVersion = require('@rstackjs/cli-openharmony-arm64/package.json').version - if (bindingPackageVersion !== '0.5.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -492,8 +492,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-openharmony-x64') const bindingPackageVersion = require('@rstackjs/cli-openharmony-x64/package.json').version - if (bindingPackageVersion !== '0.5.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -508,8 +508,8 @@ function requireNative() { try { const binding = require('@rstackjs/cli-openharmony-arm') const bindingPackageVersion = require('@rstackjs/cli-openharmony-arm/package.json').version - if (bindingPackageVersion !== '0.5.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -648,8 +648,8 @@ if (!nativeBinding || forceWasi) { if (!candidateFailed) { if (process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { const bindingPackageVersion = require('@rstackjs/cli-wasm32-wasi/package.json').version - if (bindingPackageVersion !== '0.5.2') { - throw new Error(`WASI binding package version mismatch, expected 0.5.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.6.1') { + throw new Error(`WASI binding package version mismatch, expected 0.6.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } } wasiBinding = require('@rstackjs/cli-wasm32-wasi') diff --git a/packages/rstack/package.json b/packages/rstack/package.json index a30788f7..df58a421 100644 --- a/packages/rstack/package.json +++ b/packages/rstack/package.json @@ -1,6 +1,6 @@ { "name": "rstack", - "version": "0.5.2", + "version": "0.6.1", "description": "One CLI for JavaScript development, powered by Rstack.", "homepage": "https://rstack.rs", "bugs": { diff --git a/packages/rstack/rslib.config.ts b/packages/rstack/rslib.config.ts index a2662b13..2f26a5fc 100644 --- a/packages/rstack/rslib.config.ts +++ b/packages/rstack/rslib.config.ts @@ -2,7 +2,8 @@ import { defineConfig } from '@rslib/core'; import prettierPkgJson from 'prettier/package.json' with { type: 'json' }; import pkgJson from './package.json' with { type: 'json' }; -const fullyMinifiedChunks = /(?:fmt(?:Lsp|Plugins)?|sortPackageJsonPlugin|staged)\.js$/; +const fullyMinifiedChunks = + /(?:fmt(?:Lsp|Plugins)?|sortPackageJsonPlugin|staged)\.js$/; export default defineConfig({ dts: true, @@ -54,16 +55,4 @@ export default defineConfig({ ], }, }, - tools: { - rspack: { - module: { - parser: { - javascript: { - // @rstest/adapter-rslib resolves extended tsconfig paths from a runtime base. - createRequire: false, - }, - }, - }, - }, - }, }); diff --git a/packages/rstack/rstack.config.ts b/packages/rstack/rstack.config.ts index 650c9379..8abe863e 100644 --- a/packages/rstack/rstack.config.ts +++ b/packages/rstack/rstack.config.ts @@ -1,4 +1,4 @@ -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.test(async () => { @@ -11,6 +11,7 @@ define.test(async () => { // Temporary projects may contain files that match Rstest's test glob. exclude: ['**/test-temp-*/**'], extends: withRslibConfig(), + testTimeout: 30_000, source: { tsconfigPath: './tests/tsconfig.json', }, diff --git a/packages/rstack/src/cli/args.ts b/packages/rstack/src/cli/args.ts index cf55aa88..29d13f52 100644 --- a/packages/rstack/src/cli/args.ts +++ b/packages/rstack/src/cli/args.ts @@ -5,7 +5,10 @@ import { type ParseArgsOptionsConfig, } from 'node:util'; -type ParseArgsOptionDescriptor = Omit & { +type ParseArgsOptionDescriptor = Omit< + NodeParseArgsOptionDescriptor, + 'default' +> & { default?: never; }; @@ -13,11 +16,14 @@ type ParseArgsConfig = Omit & { options?: Record; }; -type CamelCase = Value extends `${infer Head}-${infer Tail}` - ? `${Head}${Capitalize>}` - : Value; +type CamelCase = + Value extends `${infer Head}-${infer Tail}` + ? `${Head}${Capitalize>}` + : Value; -type NodeParseArgsResult = ReturnType>; +type NodeParseArgsResult = ReturnType< + typeof nodeParseArgs +>; type ParseArgsResult = Omit< NodeParseArgsResult, @@ -25,7 +31,9 @@ type ParseArgsResult = Omit< > & { values: { [ - Name in keyof NodeParseArgsResult['values'] as CamelCase + Name in keyof NodeParseArgsResult['values'] as CamelCase< + Name & string + > ]: NodeParseArgsResult['values'][Name]; }; }; @@ -34,16 +42,20 @@ const KEBAB_CASE_REGEXP = /-([a-z])/g; const toCamelCase = (value: string): string => value.includes('-') - ? value.replace(KEBAB_CASE_REGEXP, (_, character: string) => character.toUpperCase()) + ? value.replace(KEBAB_CASE_REGEXP, (_, character: string) => + character.toUpperCase(), + ) : value; -export function parseArgs( - config?: Config, -): ParseArgsResult { +export function parseArgs< + const Config extends ParseArgsConfig = ParseArgsConfig, +>(config?: Config): ParseArgsResult { const options: ParseArgsOptionsConfig = {}; const optionNames: [originalName: string, camelName: string][] = []; - for (const [originalName, descriptor] of Object.entries(config?.options ?? {})) { + for (const [originalName, descriptor] of Object.entries( + config?.options ?? {}, + )) { const camelName = toCamelCase(originalName); optionNames.push([originalName, camelName]); options[originalName] = descriptor; @@ -61,7 +73,8 @@ export function parseArgs', 'Specify Rstack config file path']; +const CONFIG_OPTION: HelpItem = [ + '-c, --config ', + 'Specify Rstack config file path', +]; const HELP_OPTION: HelpItem = ['-h, --help', 'Display this help message']; const VERSION_OPTION: HelpItem = ['-v, --version', 'Display version number']; const CONFIG_HELP_OPTIONS = [CONFIG_OPTION, HELP_OPTION]; -const OPEN_OPTION: HelpItem = ['-o, --open [url]', 'Open the page in browser on startup']; -const PORT_OPTION: HelpItem = ['--port ', 'Set the port number for the server']; +const OPEN_OPTION: HelpItem = [ + '-o, --open [url]', + 'Open the page in browser on startup', +]; +const PORT_OPTION: HelpItem = [ + '--port ', + 'Set the port number for the server', +]; const STRICT_PORT_OPTION: HelpItem = [ '--strict-port', 'Exit if the specified port is already in use', ]; -const HOST_OPTION: HelpItem = ['--host [host]', 'Set the host that the server listens to']; -const BASE_OPTION: HelpItem = ['--base ', 'Set the base path and override config.base']; -const SERVER_OPTIONS = [OPEN_OPTION, PORT_OPTION, STRICT_PORT_OPTION, HOST_OPTION]; +const HOST_OPTION: HelpItem = [ + '--host [host]', + 'Set the host that the server listens to', +]; +const BASE_OPTION: HelpItem = [ + '--base ', + 'Set the base path and override config.base', +]; +const SERVER_OPTIONS = [ + OPEN_OPTION, + PORT_OPTION, + STRICT_PORT_OPTION, + HOST_OPTION, +]; const TEST_UPDATE_OPTION: HelpItem = ['-u, --update', 'Update snapshot files']; const TEST_COVERAGE_OPTION: HelpItem = ['--coverage', 'Enable code coverage']; -const TEST_PROJECT_OPTION: HelpItem = ['--project ', 'Filter test projects by name']; +const TEST_PROJECT_OPTION: HelpItem = [ + '--project ', + 'Filter test projects by name', +]; const TEST_NAME_OPTION: HelpItem = [ '-t, --test-name-pattern ', 'Run tests with names matching the pattern', @@ -76,8 +99,14 @@ const TEST_OPTIONS = [ TEST_NAME_OPTION, ]; -const LIB_WATCH_OPTION: HelpItem = ['-w, --watch', 'Enable watch mode and rebuild on changes']; -const LIB_DTS_OPTION: HelpItem = ['--dts', 'Emit declaration files (use --no-dts to disable)']; +const LIB_WATCH_OPTION: HelpItem = [ + '-w, --watch', + 'Enable watch mode and rebuild on changes', +]; +const LIB_DTS_OPTION: HelpItem = [ + '--dts', + 'Emit declaration files (use --no-dts to disable)', +]; const LIB_BUILD_OPTIONS = [LIB_WATCH_OPTION, LIB_DTS_OPTION]; const commandHint = (command: string): HelpSection => ({ @@ -123,7 +152,10 @@ const HELP_DEFINITIONS = { sections: [ { title: 'Options', - items: [['--type-check', 'Enable TypeScript type checking'], ...CONFIG_HELP_OPTIONS], + items: [ + ['--type-check', 'Enable TypeScript type checking'], + ...CONFIG_HELP_OPTIONS, + ], }, ], }, @@ -144,7 +176,10 @@ const HELP_DEFINITIONS = { { title: 'Options', items: [ - ['-w, --watch', 'Enable watch mode to automatically rebuild on file changes'], + [ + '-w, --watch', + 'Enable watch mode to automatically rebuild on file changes', + ], ['--dist-path ', 'Set the root directory of output files'], ['--source-map', 'Enable source map'], ...CONFIG_HELP_OPTIONS, @@ -228,7 +263,11 @@ const HELP_DEFINITIONS = { commandHint('test'), { title: 'Options', - items: [['-w, --watch', 'Enable watch mode'], ...TEST_OPTIONS, ...CONFIG_HELP_OPTIONS], + items: [ + ['-w, --watch', 'Enable watch mode'], + ...TEST_OPTIONS, + ...CONFIG_HELP_OPTIONS, + ], }, ], }, @@ -273,7 +312,10 @@ const HELP_DEFINITIONS = { ['--print-location', 'Print test locations'], ['--summary', 'Print a summary'], TEST_PROJECT_OPTION, - ['-t, --test-name-pattern ', 'List tests with names matching the pattern'], + [ + '-t, --test-name-pattern ', + 'List tests with names matching the pattern', + ], ...CONFIG_HELP_OPTIONS, ], }, @@ -339,7 +381,10 @@ const HELP_DEFINITIONS = { { title: 'Options', items: [ - ['--output ', 'Set the output path for inspection results (default: .rsbuild)'], + [ + '--output ', + 'Set the output path for inspection results (default: .rsbuild)', + ], ['--verbose', 'Show complete function definitions in output'], ...CONFIG_HELP_OPTIONS, ], @@ -366,9 +411,15 @@ const HELP_DEFINITIONS = { ['--fix', 'Automatically fix problems'], ['--type-check', 'Enable TypeScript type checking'], ['--type-check-only', 'Run only TypeScript type checking'], - ['--format ', 'Set output format (default | jsonline | github | gitlab)'], + [ + '--format ', + 'Set output format (default | jsonline | github | gitlab)', + ], ['--quiet', 'Report errors only'], - ['--timing [all|N]', 'Print a per-rule timing table (all rules or top N)'], + [ + '--timing [all|N]', + 'Print a per-rule timing table (all rules or top N)', + ], ['--max-warnings ', 'Set the maximum number of warnings'], ['--rule ', 'Override a rule (repeatable)'], ['--no-color', 'Disable colored output'], @@ -388,14 +439,23 @@ const HELP_DEFINITIONS = { ['-w, --write', 'Write formatted files in place (default)'], ['--check', 'Check whether files are formatted'], ['-l, --list-different', 'Print paths of unformatted files'], - ['--ignore-path ', 'Path to an additional ignore file (repeatable)'], + [ + '--ignore-path ', + 'Path to an additional ignore file (repeatable)', + ], ['-u, --ignore-unknown', 'Ignore unknown files'], ['--no-cache', 'Disable the formatting cache'], ['--cache-location ', 'Path to the formatting cache directory'], - ['--no-error-on-unmatched-pattern', 'Do not error when no files match'], + [ + '--no-error-on-unmatched-pattern', + 'Do not error when no files match', + ], ['--with-node-modules', 'Process files inside node_modules'], ['--parallel-workers ', 'Number of parallel workers'], - ['--stdin-filepath ', 'Format stdin as if it were saved at '], + [ + '--stdin-filepath ', + 'Format stdin as if it were saved at ', + ], ['--lsp', 'Run a language server on stdio'], ...CONFIG_HELP_OPTIONS, ], @@ -409,7 +469,10 @@ const HELP_DEFINITIONS = { { title: 'Options', items: [ - ['--allow-empty', 'Allow empty commits when tasks revert all staged changes'], + [ + '--allow-empty', + 'Allow empty commits when tasks revert all staged changes', + ], [ '-p, --concurrent ', 'The number of tasks to run concurrently, or false for serial', @@ -435,7 +498,10 @@ const HELP_DEFINITIONS = { { title: 'Options', items: [ - ['--hooks-dir ', 'Specify hooks directory relative to the Git repository root'], + [ + '--hooks-dir ', + 'Specify hooks directory relative to the Git repository root', + ], HELP_OPTION, ], }, @@ -444,10 +510,15 @@ const HELP_DEFINITIONS = { } satisfies Record; const renderItems = (items: readonly HelpItem[]): string => { - const labelWidth = items.reduce((width, [label]) => Math.max(width, label.length), 0); + const labelWidth = items.reduce( + (width, [label]) => Math.max(width, label.length), + 0, + ); return items - .map(([label, description]) => ` ${label.padEnd(labelWidth)} ${description}`) + .map( + ([label, description]) => ` ${label.padEnd(labelWidth)} ${description}`, + ) .join('\n'); }; @@ -459,7 +530,11 @@ const renderSection = (section: HelpSection): string => { return section.dim ? color.dim(section.content) : section.content; }; -const renderHelp = ({ usage, description, sections = [] }: HelpDefinition): string => { +const renderHelp = ({ + usage, + description, + sections = [], +}: HelpDefinition): string => { const blocks = [ color.bold(`Rstack v${RSTACK_VERSION}`), `${color.cyan('Usage')}:\n${color.yellow(` $ ${usage}`)}`, @@ -474,4 +549,5 @@ const renderHelp = ({ usage, description, sections = [] }: HelpDefinition): stri return blocks.join('\n\n'); }; -export const renderCommandHelp = (topic: HelpTopic): string => renderHelp(HELP_DEFINITIONS[topic]); +export const renderCommandHelp = (topic: HelpTopic): string => + renderHelp(HELP_DEFINITIONS[topic]); diff --git a/packages/rstack/src/cli/commands.ts b/packages/rstack/src/cli/commands.ts index 07ed732e..4368dbea 100644 --- a/packages/rstack/src/cli/commands.ts +++ b/packages/rstack/src/cli/commands.ts @@ -18,7 +18,11 @@ async function runRsbuildCLI(args: string[]): Promise { const argv = [ process.execPath, 'rsbuild', - ...insertConfigArg(args, '--config', join(import.meta.dirname, 'rsbuildConfig.js')), + ...insertConfigArg( + args, + '--config', + join(import.meta.dirname, 'rsbuildConfig.js'), + ), ]; const { runCLI } = await import('@rsbuild/core'); @@ -46,7 +50,11 @@ async function runRstestCLI(args: string[]): Promise { const argv = [ process.execPath, 'rstest', - ...insertConfigArg(args, '--config', join(import.meta.dirname, 'rstestConfig.js')), + ...insertConfigArg( + args, + '--config', + join(import.meta.dirname, 'rstestConfig.js'), + ), ]; const { runCLI } = await import('@rstest/core'); @@ -70,7 +78,11 @@ async function runRslibCLI(args: string[]): Promise { const argv = [ process.execPath, 'rslib', - ...insertConfigArg(args, '--config', join(import.meta.dirname, 'rslibConfig.js')), + ...insertConfigArg( + args, + '--config', + join(import.meta.dirname, 'rslibConfig.js'), + ), ]; const { runCLI } = await import('@rslib/core'); @@ -83,7 +95,9 @@ const isMissingRspressCoreError = (error: unknown): boolean => { } const code = 'code' in error ? error.code : undefined; - return code === 'ERR_MODULE_NOT_FOUND' && error.message.includes('@rspress/core'); + return ( + code === 'ERR_MODULE_NOT_FOUND' && error.message.includes('@rspress/core') + ); }; async function runRspressCLI(args: string[]): Promise { @@ -103,7 +117,11 @@ async function runRspressCLI(args: string[]): Promise { const argv = [ process.execPath, 'rspress', - ...insertConfigArg(args, '--config', join(import.meta.dirname, 'rspressConfig.js')), + ...insertConfigArg( + args, + '--config', + join(import.meta.dirname, 'rspressConfig.js'), + ), ]; try { @@ -128,7 +146,11 @@ async function runRslintCLI(args: string[]): Promise { const argv = [ process.execPath, 'rslint', - ...insertConfigArg(args, '--config', join(import.meta.dirname, 'rslintConfig.js')), + ...insertConfigArg( + args, + '--config', + join(import.meta.dirname, 'rslintConfig.js'), + ), ]; const { runCLI } = await import('@rslint/core'); @@ -171,7 +193,8 @@ export async function setupCommands(): Promise { // when the config is later loaded from another directory. The motivating case // is `rs fmt --lsp`, which loads the config from the LSP workspace root the // client reports, and that root need not be the process working directory. - getConfigState().configPath = configPath === undefined ? undefined : resolve(configPath); + getConfigState().configPath = + configPath === undefined ? undefined : resolve(configPath); if (!command || command === '-h' || command === '--help') { return printCommandHelp('root'); diff --git a/packages/rstack/src/config.ts b/packages/rstack/src/config.ts index f977f6fc..ecc3983b 100644 --- a/packages/rstack/src/config.ts +++ b/packages/rstack/src/config.ts @@ -8,9 +8,14 @@ import type { RstestConfigExport } from '@rstest/core'; import type { FmtConfigDefinition } from './fmt/types.ts'; import type { StagedConfig } from './staged.ts'; -export type RslintConfigDefinition = RslintConfig | (() => Promise); +export type RslintConfigDefinition = + RslintConfig | (() => Promise); export type RspressConfigDefinition = UserConfig | UserConfigAsyncFn; +type RslintConfigFactory = ( + lint: typeof import('@rslint/core'), +) => RslintConfig | Promise; + export type Configs = { app?: RsbuildConfigDefinition; lib?: RslibConfigDefinition; @@ -58,7 +63,8 @@ type ConfigState = { declare global { // rslint-disable-next-line no-var - var __rstackConfigSessionStorage: AsyncLocalStorage | undefined; + var __rstackConfigSessionStorage: + AsyncLocalStorage | undefined; // rslint-disable-next-line no-var var __rstackCliState: ConfigState | undefined; } @@ -68,7 +74,8 @@ const getConfigSessionStorage = (): AsyncLocalStorage => { // imports the internal Rstack config. Keep the storage on globalThis so // every module instance reads and writes the same active session. if (!globalThis.__rstackConfigSessionStorage) { - globalThis.__rstackConfigSessionStorage = new AsyncLocalStorage(); + globalThis.__rstackConfigSessionStorage = + new AsyncLocalStorage(); } return globalThis.__rstackConfigSessionStorage; @@ -90,7 +97,7 @@ type Define = { * * This config is used by the `rs dev`, `rs build`, and `rs preview` commands. * - * @see {@link https://rstack.rs/config | Rstack configuration guide} + * @see {@link https://rstack.rs/config | Configuration guide} */ app: (config: RsbuildConfigDefinition) => void; /** @@ -98,7 +105,7 @@ type Define = { * * This config is used by the `rs lib` command. * - * @see {@link https://rstack.rs/config | Rstack configuration guide} + * @see {@link https://rstack.rs/config | Configuration guide} */ lib: (config: RslibConfigDefinition) => void; /** @@ -106,7 +113,7 @@ type Define = { * * This config is used by the `rs doc` command. * - * @see {@link https://rstack.rs/config | Rstack configuration guide} + * @see {@link https://rstack.rs/config | Configuration guide} */ doc: (config: RspressConfigDefinition) => void; /** @@ -118,23 +125,24 @@ type Define = { * falls back to `define.lib`. For multi-project configs, this applies to every inline * project without an explicit `extends`. The app config takes precedence when both are defined. * - * @see {@link https://rstack.rs/config | Rstack configuration guide} + * @see {@link https://rstack.rs/config | Configuration guide} */ test: (config: RstestConfigExport) => void; /** * Defines the Rslint config for linting. * * This config is used by the `rs lint` command. + * A config factory receives the exports from `rstack/lint`. * - * @see {@link https://rstack.rs/config | Rstack configuration guide} + * @see {@link https://rstack.rs/config | Configuration guide} */ - lint: (config: RslintConfig | (() => Promise)) => void; + lint: (config: RslintConfig | RslintConfigFactory) => void; /** * Defines the Prettier config for formatting. * * This config will be used by the `rs fmt` command. * - * @see {@link https://rstack.rs/config | Rstack configuration guide} + * @see {@link https://rstack.rs/config | Configuration guide} */ fmt: (config: FmtConfigDefinition) => void; /** @@ -142,16 +150,21 @@ type Define = { * * This config is used by the `rs staged` command. * - * @see {@link https://rstack.rs/config | Rstack configuration guide} + * @see {@link https://rstack.rs/config | Configuration guide} */ staged: (config: StagedConfig) => void; }; -const setConfig = (type: T, config: Configs[T]): void => { +const setConfig = ( + type: T, + config: Configs[T], +): void => { const session = getConfigSessionStorage().getStore(); if (!session?.active) { - throw new Error(`The "${type}" config must be defined while loading an Rstack config.`); + throw new Error( + `The "${type}" config must be defined while loading an Rstack config.`, + ); } if (type in session.configs) { @@ -165,7 +178,13 @@ export const define: Define = { lib: (config) => setConfig('lib', config), doc: (config) => setConfig('doc', config), test: (config) => setConfig('test', config), - lint: (config) => setConfig('lint', config), + lint: (config) => + setConfig( + 'lint', + typeof config === 'function' + ? async () => config(await import('@rslint/core')) + : config, + ), fmt: (config) => setConfig('fmt', config), staged: (config) => setConfig('staged', config), }; diff --git a/packages/rstack/src/fmt/cacheIdentity.ts b/packages/rstack/src/fmt/cacheIdentity.ts index 0bbdbbe1..81d7bdd6 100644 --- a/packages/rstack/src/fmt/cacheIdentity.ts +++ b/packages/rstack/src/fmt/cacheIdentity.ts @@ -1,4 +1,4 @@ -import { hash } from 'node:crypto'; +import { hash as createDigest } from 'node:crypto'; import { isAbsolute } from 'node:path'; import stableStringify from 'fast-json-stable-stringify'; import { fmtCacheVersion } from './cacheStore.ts'; @@ -12,10 +12,16 @@ type CacheKeyResolver = (filePath: string) => string | undefined; type OptionsHasher = (options: ResolvedFmtOptions) => string | undefined; type PluginFingerprints = ReadonlyMap; -const sha256 = (content: string | Uint8Array): string => hash('sha256', content, 'hex'); +const cacheHashLength = 16; +const createCacheHash = (content: string | Uint8Array): string => + createDigest('sha256', content, 'base64url').slice(0, cacheHashLength); /** Identifies formatter behavior shared by all cache entries in this process. */ -const cacheNamespace: string = JSON.stringify([fmtCacheVersion, RSTACK_VERSION, PRETTIER_VERSION]); +const cacheNamespace: string = JSON.stringify([ + fmtCacheVersion, + RSTACK_VERSION, + PRETTIER_VERSION, +]); /** Creates project-relative POSIX cache keys without repeating path setup. */ const createCacheKeyResolver = (rootPath: string): CacheKeyResolver => { @@ -28,7 +34,9 @@ const createCacheKeyResolver = (rootPath: string): CacheKeyResolver => { }; /** Hashes final per-file options and memoizes option objects shared by many files. */ -const createOptionsHasher = (pluginFingerprints?: PluginFingerprints): OptionsHasher => { +const createOptionsHasher = ( + pluginFingerprints?: PluginFingerprints, +): OptionsHasher => { const hashes = new WeakMap(); return (options) => { @@ -45,8 +53,13 @@ const createOptionsHasher = (pluginFingerprints?: PluginFingerprints): OptionsHa const fingerprints: string[] = []; for (const plugin of plugins) { const key = - plugin instanceof URL ? plugin.href : typeof plugin === 'string' ? plugin : undefined; - const fingerprint = key === undefined ? undefined : pluginFingerprints?.get(key); + plugin instanceof URL + ? plugin.href + : typeof plugin === 'string' + ? plugin + : undefined; + const fingerprint = + key === undefined ? undefined : pluginFingerprints?.get(key); if (fingerprint === undefined) { hashes.set(options, null); return undefined; @@ -55,7 +68,7 @@ const createOptionsHasher = (pluginFingerprints?: PluginFingerprints): OptionsHa } value = { ...options, plugins: fingerprints }; } - hash = sha256(stableStringify(value)); + hash = createCacheHash(stableStringify(value)); } catch { // Circular or unreadable options cannot be cached. } @@ -65,4 +78,10 @@ const createOptionsHasher = (pluginFingerprints?: PluginFingerprints): OptionsHa }; }; -export { cacheNamespace, createCacheKeyResolver, createOptionsHasher, sha256 }; +export { + cacheHashLength, + cacheNamespace, + createCacheHash, + createCacheKeyResolver, + createOptionsHasher, +}; diff --git a/packages/rstack/src/fmt/cacheStore.ts b/packages/rstack/src/fmt/cacheStore.ts index dfc47b60..abecddae 100644 --- a/packages/rstack/src/fmt/cacheStore.ts +++ b/packages/rstack/src/fmt/cacheStore.ts @@ -2,18 +2,44 @@ import { randomUUID } from 'node:crypto'; import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'; import path from 'node:path'; -const fmtCacheFileName = 'v1.json'; -const fmtCacheVersion = 1; +const fmtCacheFileName = 'cache.json'; +const fmtCacheVersion = 2; -type FmtCacheState = 'clean' | 'dirty' | 'unsupported'; -type FmtCacheEntry = - | readonly [contentHash: string, optionsHash: string, state: 'clean' | 'dirty'] - | readonly [contentHash: string | null, optionsHash: string, state: 'unsupported']; +const fileEntryWidth = 4; +const contentHashOffset = 1; +const optionsIndexOffset = 2; +const stateOffset = 3; + +const fmtCacheStates = ['clean', 'dirty', 'unsupported'] as const; +type FmtCacheState = (typeof fmtCacheStates)[number]; +type FmtCacheStateId = 0 | 1 | 2; + +const fmtCacheStateIds = { + clean: 0, + dirty: 1, + unsupported: 2, +} as const satisfies Record; + +type FmtCacheFileValue = string | number; +type FmtCacheEntry = readonly [ + contentHash: string, + optionsHash: string, + state: FmtCacheState, +]; interface FmtCacheFile { version: typeof fmtCacheVersion; namespace: string; - files: Record; + options: string[]; + /** Repeated tuples of file path, content hash, options index, and numeric state. */ + files: FmtCacheFileValue[]; +} + +interface ParsedFmtCacheFile { + cache: FmtCacheFile; + fileOffsets: Map; + optionsIndexes: Map; + optionsUseCounts: number[]; } interface FmtCacheStore { @@ -23,30 +49,22 @@ interface FmtCacheStore { save(): Promise; } -const createEmptyCache = (namespace: string): FmtCacheFile => ({ - version: fmtCacheVersion, - namespace, - files: Object.create(null) as Record, +const createEmptyCache = (namespace: string): ParsedFmtCacheFile => ({ + cache: { + version: fmtCacheVersion, + namespace, + options: [], + files: [], + }, + fileOffsets: new Map(), + optionsIndexes: new Map(), + optionsUseCounts: [], }); -const parseCacheEntry = (value: unknown): FmtCacheEntry | undefined => { - if (!Array.isArray(value) || value.length !== 3 || typeof value[1] !== 'string') { - return; - } - - if (value[2] === 'unsupported') { - return value[0] === null || typeof value[0] === 'string' - ? [value[0], value[1], value[2]] - : undefined; - } - if (typeof value[0] !== 'string' || (value[2] !== 'clean' && value[2] !== 'dirty')) { - return; - } - - return [value[0], value[1], value[2]]; -}; - -const parseCacheFile = (content: string): FmtCacheFile | undefined => { +const parseCacheFile = ( + content: string, + expectedNamespace: string, +): ParsedFmtCacheFile | undefined => { let value: unknown; try { value = JSON.parse(content); @@ -54,39 +72,46 @@ const parseCacheFile = (content: string): FmtCacheFile | undefined => { return; } + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + return; + } + + const cache = value as FmtCacheFile; + const { version, namespace, options, files } = cache; if ( - typeof value !== 'object' || - value === null || - Array.isArray(value) || - !('version' in value) || - value.version !== fmtCacheVersion || - !('namespace' in value) || - typeof value.namespace !== 'string' || - !('files' in value) || - typeof value.files !== 'object' || - value.files === null || - Array.isArray(value.files) + version !== fmtCacheVersion || + namespace !== expectedNamespace || + !Array.isArray(options) || + !Array.isArray(files) || + files.length % fileEntryWidth !== 0 ) { return; } - const files = Object.create(null) as Record; - for (const [filePath, rawEntry] of Object.entries(value.files)) { - const entry = parseCacheEntry(rawEntry); - if (!entry) { - return; - } - files[filePath] = entry; + const optionsIndexes = new Map(); + for (let index = 0; index < options.length; index++) { + optionsIndexes.set(options[index], index); + } + + const fileOffsets = new Map(); + const optionsUseCounts = new Array(options.length).fill(0); + for (let offset = 0; offset < files.length; offset += fileEntryWidth) { + const filePath = files[offset] as string; + const optionsIndex = files[offset + optionsIndexOffset] as number; + fileOffsets.set(filePath, offset); + optionsUseCounts[optionsIndex]++; } return { - version: fmtCacheVersion, - namespace: value.namespace, - files, + cache, + fileOffsets, + optionsIndexes, + optionsUseCounts, }; }; -const serializeCache = (cache: FmtCacheFile): string => `${JSON.stringify(cache)}\n`; +const serializeCache = (cache: FmtCacheFile): string => + `${JSON.stringify(cache)}\n`; const isFileNotFoundError = (error: unknown): error is NodeJS.ErrnoException => error instanceof Error && 'code' in error && error.code === 'ENOENT'; @@ -100,43 +125,126 @@ const getTemporaryPath = (filePath: string): string => class FmtCacheStoreImpl implements FmtCacheStore { readonly #filePath: string; readonly #cache: FmtCacheFile; + readonly #fileOffsets: Map; + readonly #optionsIndexes: Map; + readonly #optionsUseCounts: number[]; #savedContent: string | undefined; #changed: boolean; constructor( filePath: string, - cache: FmtCacheFile, + parsed: ParsedFmtCacheFile, savedContent: string | undefined, changed: boolean, ) { this.#filePath = filePath; - this.#cache = cache; + this.#cache = parsed.cache; + this.#fileOffsets = parsed.fileOffsets; + this.#optionsIndexes = parsed.optionsIndexes; + this.#optionsUseCounts = parsed.optionsUseCounts; this.#savedContent = savedContent; this.#changed = changed; } get(filePath: string): FmtCacheEntry | undefined { - return this.#cache.files[filePath]; + const offset = this.#fileOffsets.get(filePath); + if (offset === undefined) { + return; + } + + const { files, options } = this.#cache; + const contentHash = files[offset + contentHashOffset] as string; + const optionsHash = options[files[offset + optionsIndexOffset] as number]; + const state = + fmtCacheStates[files[offset + stateOffset] as FmtCacheStateId]; + return [contentHash, optionsHash, state]; } set(filePath: string, entry: FmtCacheEntry): void { - const current = this.#cache.files[filePath]; - if (current?.[0] === entry[0] && current[1] === entry[1] && current[2] === entry[2]) { - return; + const { files, options } = this.#cache; + const [contentHash, optionsHash, state] = entry; + const stateId = fmtCacheStateIds[state]; + const offset = this.#fileOffsets.get(filePath); + + if (offset !== undefined) { + const currentOptionsIndex = files[offset + optionsIndexOffset] as number; + if ( + files[offset + contentHashOffset] === contentHash && + options[currentOptionsIndex] === optionsHash && + files[offset + stateOffset] === stateId + ) { + return; + } + + const optionsIndex = this.#getOrCreateOptionsIndex(optionsHash); + if (currentOptionsIndex !== optionsIndex) { + this.#optionsUseCounts[currentOptionsIndex]--; + this.#optionsUseCounts[optionsIndex]++; + files[offset + optionsIndexOffset] = optionsIndex; + } + files[offset + contentHashOffset] = contentHash; + files[offset + stateOffset] = stateId; + } else { + const optionsIndex = this.#getOrCreateOptionsIndex(optionsHash); + const nextOffset = files.length; + files.push(filePath, contentHash, optionsIndex, stateId); + this.#fileOffsets.set(filePath, nextOffset); + this.#optionsUseCounts[optionsIndex]++; } - this.#cache.files[filePath] = - entry[2] === 'unsupported' - ? [entry[0], entry[1], 'unsupported'] - : [entry[0], entry[1], entry[2]]; this.#changed = true; } + #getOrCreateOptionsIndex(optionsHash: string): number { + const current = this.#optionsIndexes.get(optionsHash); + if (current !== undefined) { + return current; + } + + const index = this.#cache.options.length; + this.#cache.options.push(optionsHash); + this.#optionsIndexes.set(optionsHash, index); + this.#optionsUseCounts.push(0); + return index; + } + + /** Removes unreferenced option hashes and remaps file entries to the compacted indexes. */ + #compactUnusedOptions(): void { + if (!this.#optionsUseCounts.includes(0)) { + return; + } + + const { files, options } = this.#cache; + const counts = this.#optionsUseCounts; + const remap = new Int32Array(options.length).fill(-1); + let nextIndex = 0; + this.#optionsIndexes.clear(); + for (let index = 0; index < options.length; index++) { + const count = counts[index]; + if (count > 0) { + const option = options[index]; + remap[index] = nextIndex; + options[nextIndex] = option; + counts[nextIndex] = count; + this.#optionsIndexes.set(option, nextIndex); + nextIndex++; + } + } + options.length = nextIndex; + counts.length = nextIndex; + + for (let offset = 0; offset < files.length; offset += fileEntryWidth) { + const index = files[offset + optionsIndexOffset] as number; + files[offset + optionsIndexOffset] = remap[index]; + } + } + async save(): Promise { if (!this.#changed) { return false; } + this.#compactUnusedOptions(); const content = serializeCache(this.#cache); if (content === this.#savedContent) { this.#changed = false; @@ -159,19 +267,20 @@ class FmtCacheStoreImpl implements FmtCacheStore { } } -const loadFmtCacheStore = async (filePath: string, namespace: string): Promise => { +const loadFmtCacheStore = async ( + filePath: string, + namespace: string, +): Promise => { const emptyCache = createEmptyCache(namespace); try { const content = await readFile(filePath, 'utf8'); - const cache = parseCacheFile(content); - if (!cache) { + const parsed = parseCacheFile(content, namespace); + if (!parsed) { return new FmtCacheStoreImpl(filePath, emptyCache, undefined, true); } - return cache.namespace === namespace - ? new FmtCacheStoreImpl(filePath, cache, content, false) - : new FmtCacheStoreImpl(filePath, emptyCache, undefined, true); + return new FmtCacheStoreImpl(filePath, parsed, content, false); } catch (error) { const missing = isFileNotFoundError(error); return new FmtCacheStoreImpl(filePath, emptyCache, undefined, !missing); diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index 6bd314f2..7b13f498 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -35,15 +35,25 @@ const parseMaxWorkers = (value: string | undefined): number | undefined => { } const maxWorkers = Number(value); - if (!/^\d+$/.test(value) || !Number.isSafeInteger(maxWorkers) || maxWorkers < 1) { - throw new Error('The --parallel-workers option must be a positive integer.'); + if ( + !/^\d+$/.test(value) || + !Number.isSafeInteger(maxWorkers) || + maxWorkers < 1 + ) { + throw new Error( + 'The --parallel-workers option must be a positive integer.', + ); } return maxWorkers; }; /** Rejects the mode flags and file arguments that a server-like option replaces. */ -const assertExclusiveMode = (option: string, hasMode: boolean, positionals: string[]): void => { +const assertExclusiveMode = ( + option: string, + hasMode: boolean, + positionals: string[], +): void => { if (hasMode) { throw new Error( `The ${option} option cannot be used with --write, --check, or --list-different.`, @@ -82,7 +92,9 @@ const parseFmtArgs = (args: string[]): ParsedFmtCLIArgs => { const listDifferent = values.listDifferent; const modes = [write, check, listDifferent].filter(Boolean); if (modes.length > 1) { - throw new Error('The --write, --check, and --list-different options cannot be used together.'); + throw new Error( + 'The --write, --check, and --list-different options cannot be used together.', + ); } const mode = check ? 'check' : listDifferent ? 'list-different' : 'write'; @@ -130,14 +142,17 @@ const parseFmtArgs = (args: string[]): ParsedFmtCLIArgs => { }; }; -const createDisplayPathResolver = (cwd: string): ((filePath: string) => string) => { +const createDisplayPathResolver = ( + cwd: string, +): ((filePath: string) => string) => { const resolveRelativePath = createRelativePathResolver(cwd); return (filePath) => toPosixPath(resolveRelativePath(filePath)); }; const prettyTime = (seconds: number): string => { - const format = (time: string, unit: 'm' | 's') => color.bold(`${time}${unit}`); + const format = (time: string, unit: 'm' | 's') => + color.bold(`${time}${unit}`); if (seconds < 10) { const digits = seconds >= 0.01 ? 2 : 3; @@ -156,7 +171,10 @@ const prettyTime = (seconds: number): string => { return minutesLabel; } - const secondsLabel = format(remainingSeconds.toFixed(remainingSeconds % 1 === 0 ? 0 : 1), 's'); + const secondsLabel = format( + remainingSeconds.toFixed(remainingSeconds % 1 === 0 ? 0 : 1), + 's', + ); return `${minutesLabel} ${secondsLabel}`; }; @@ -171,7 +189,9 @@ const reportNoSupportedFiles = (patterns: string[]): void => { const targets = (patterns.length ? patterns : ['.']) .map((pattern) => color.cyan(JSON.stringify(pattern))) .join(', '); - logger.error(`No supported files matched ${targets}, or all matching files were ignored.`); + logger.error( + `No supported files matched ${targets}, or all matching files were ignored.`, + ); process.exitCode = 2; }; @@ -303,7 +323,9 @@ const runFmtCLI = async (args: string[]): Promise => { return; } - const cacheDirPath = cacheLocation ? path.resolve(cwd, cacheLocation) : undefined; + const cacheDirPath = cacheLocation + ? path.resolve(cwd, cacheLocation) + : undefined; if (cacheDirPath) { const cacheDirPrefix = cacheDirPath.endsWith(path.sep) ? cacheDirPath @@ -327,7 +349,8 @@ const runFmtCLI = async (args: string[]): Promise => { if (files.length === 0) { // Staged tasks may pass only paths excluded by formatter ignore rules. - const allowUnmatched = noErrorOnUnmatchedPattern || process.env.RSTACK_STAGED === '1'; + const allowUnmatched = + noErrorOnUnmatchedPattern || process.env.RSTACK_STAGED === '1'; if (allowUnmatched) { return; } diff --git a/packages/rstack/src/fmt/config.ts b/packages/rstack/src/fmt/config.ts index 362a4527..5e4c543d 100644 --- a/packages/rstack/src/fmt/config.ts +++ b/packages/rstack/src/fmt/config.ts @@ -17,6 +17,22 @@ type ResolveFmtConfigOptions = { type PathMatcher = (filePath: string) => boolean; type FmtOptionsResolver = (filePath: string) => ResolvedFmtOptions; +/** + * Each path from the root represents an ordered sequence of matching overrides. + * A node stores the options merged along that path. + */ +type OptionsCacheNode = { + children: WeakMap; + options: ResolvedFmtOptions; +}; + +const createOptionsCacheNode = ( + options: ResolvedFmtOptions, +): OptionsCacheNode => ({ + children: new WeakMap(), + options, +}); + const neverMatches: PathMatcher = () => false; const compileMatchers = ( @@ -38,7 +54,9 @@ const compileMatchers = ( return micromatch.matcher(patterns[0], options); } - const matchers = patterns.map((pattern) => micromatch.matcher(pattern, options)); + const matchers = patterns.map((pattern) => + micromatch.matcher(pattern, options), + ); return (filePath) => { for (const matches of matchers) { @@ -65,7 +83,11 @@ const createPathMatcher = ( } } - const basenameMatcher = compileMatchers(basenamePatterns, excludedPatterns, true); + const basenameMatcher = compileMatchers( + basenamePatterns, + excludedPatterns, + true, + ); const pathMatcher = compileMatchers(pathPatterns, excludedPatterns, false); if (!basenameMatcher || !pathMatcher) { @@ -75,7 +97,10 @@ const createPathMatcher = ( }; /** Splits a flat config into project-level formatting options and rules. */ -const normalizeFmtConfig = (config: FmtConfig | undefined, rootPath: string): ResolvedFmtConfig => { +const normalizeFmtConfig = ( + config: FmtConfig | undefined, + rootPath: string, +): ResolvedFmtConfig => { const { ignorePatterns = [], overrides = [], ...baseOptions } = config ?? {}; return { @@ -90,28 +115,38 @@ const normalizeFmtConfig = (config: FmtConfig | undefined, rootPath: string): Re }; /** Creates a reusable resolver for applying per-file formatter overrides. */ -const createOptionsResolver = (config: ResolvedFmtConfig): FmtOptionsResolver => { +const createOptionsResolver = ( + config: ResolvedFmtConfig, +): FmtOptionsResolver => { if (config.overrides.length === 0) { return () => config.baseOptions; } const resolveRelativePath = createRelativePathResolver(config.rootPath); + const rootCacheNode = createOptionsCacheNode(config.baseOptions); return (filePath) => { - let options = config.baseOptions; + let cacheNode = rootCacheNode; const relativeFilePath = resolveRelativePath(filePath); for (const override of config.overrides) { if (!override.options || !override.matches(relativeFilePath)) { continue; } - if (options === config.baseOptions) { - options = { ...options }; + + // Reuse the merged result for this override after the current matched sequence. + let nextCacheNode = cacheNode.children.get(override.options); + if (!nextCacheNode) { + nextCacheNode = createOptionsCacheNode({ + ...cacheNode.options, + ...override.options, + }); + cacheNode.children.set(override.options, nextCacheNode); } - Object.assign(options, override.options); + cacheNode = nextCacheNode; } - return options; + return cacheNode.options; }; }; @@ -121,7 +156,8 @@ const resolveFmtConfig = async ({ configFilePath, cwd, }: ResolveFmtConfigOptions): Promise => { - const config = typeof definition === 'function' ? await definition() : definition; + const config = + typeof definition === 'function' ? await definition() : definition; const rootPath = configFilePath ? dirname(configFilePath) : cwd; return normalizeFmtConfig(config, rootPath); diff --git a/packages/rstack/src/fmt/discoverPaths.ts b/packages/rstack/src/fmt/discoverPaths.ts index 0a213400..32e94431 100644 --- a/packages/rstack/src/fmt/discoverPaths.ts +++ b/packages/rstack/src/fmt/discoverPaths.ts @@ -142,7 +142,10 @@ class GitIgnoreFiles { } /** Matches one directory's entries in a single native call. */ - matchDirents(parentPath: string, dirents: Dirent[]): boolean | number | Uint8Array | undefined { + matchDirents( + parentPath: string, + dirents: Dirent[], + ): boolean | number | Uint8Array | undefined { if (!this.#hasRules || dirents.length === 0) { return; } @@ -156,7 +159,11 @@ class GitIgnoreFiles { if (dirents.length === 1) { const dirent = dirents[0]; - return this.#matcher!.isIgnoredChild(relativeParent, dirent.name, dirent.isDirectory()); + return this.#matcher!.isIgnoredChild( + relativeParent, + dirent.name, + dirent.isDirectory(), + ); } const names = new Array(dirents.length); @@ -168,7 +175,11 @@ class GitIgnoreFiles { names[index] = dirent.name; directoryMask |= Number(dirent.isDirectory()) << index; } - return this.#matcher!.isIgnoredBatchMask(relativeParent, names, directoryMask >>> 0); + return this.#matcher!.isIgnoredBatchMask( + relativeParent, + names, + directoryMask >>> 0, + ); } const directoryFlags = new Uint8Array(dirents.length); @@ -188,9 +199,14 @@ class GitIgnoreFiles { } // Ignore files may disappear or become unreadable during traversal. - const loading = readFile(path.join(directoryPath, '.gitignore'), 'utf8').then( + const loading = readFile( + path.join(directoryPath, '.gitignore'), + 'utf8', + ).then( (content) => { - const relativePath = toPosixPath(this.#resolveRelativePath(directoryPath)); + const relativePath = toPosixPath( + this.#resolveRelativePath(directoryPath), + ); this.#matcher ??= new (loadNativeBinding().GitIgnoreMatcher)(); this.#hasRules = this.#matcher.addSource(relativePath, content); }, @@ -222,7 +238,8 @@ const createTraversalOptions = ( if (dirent.isDirectory()) { return ( - (dirent as GitIgnoreDirent)[gitIgnored] === true || isIgnored?.(targetPath, true) === true + (dirent as GitIgnoreDirent)[gitIgnored] === true || + isIgnored?.(targetPath, true) === true ); } @@ -298,7 +315,14 @@ const discoverDirectoryFiles = async ( const result = await readdir( rootPath, - createTraversalOptions(gitIgnore, ignoredDirNames, signal, onError, isIncluded, isIgnored), + createTraversalOptions( + gitIgnore, + ignoredDirNames, + signal, + onError, + isIncluded, + isIgnored, + ), ); // tiny-readdir only handles fulfilled onDirents promises, so rethrow after its counter settles. @@ -310,7 +334,9 @@ const discoverDirectoryFiles = async ( }; const normalizeGlob = (cwd: string, pattern: string): string => { - const relativePattern = path.isAbsolute(pattern) ? path.relative(cwd, pattern) : pattern; + const relativePattern = path.isAbsolute(pattern) + ? path.relative(cwd, pattern) + : pattern; return toPosixPath(relativePattern); }; @@ -334,7 +360,10 @@ const classifyPatterns = async ( const entries = await Promise.all( patterns.map(async (pattern): Promise => { if (pattern.startsWith('!')) { - return { kind: 'negative-glob', value: normalizeGlob(cwd, pattern.slice(1)) }; + return { + kind: 'negative-glob', + value: normalizeGlob(cwd, pattern.slice(1)), + }; } const filePath = path.resolve(cwd, pattern); @@ -344,7 +373,9 @@ const classifyPatterns = async ( const stats = await lstatSafe(filePath); if (stats?.isFile()) { - return isBinaryPath(filePath) ? undefined : { kind: 'file', value: filePath }; + return isBinaryPath(filePath) + ? undefined + : { kind: 'file', value: filePath }; } if (stats?.isDirectory()) { return { kind: 'directory', value: filePath }; @@ -389,11 +420,15 @@ const classifyPatterns = async ( }; const getOutermostPaths = (paths: string[]): string[] => { - const sortedPaths = [...new Set(paths)].sort((left, right) => left.length - right.length); + const sortedPaths = [...new Set(paths)].sort( + (left, right) => left.length - right.length, + ); const outermostPaths: string[] = []; for (const filePath of sortedPaths) { - if (!outermostPaths.some((parentPath) => isPathInside(parentPath, filePath))) { + if ( + !outermostPaths.some((parentPath) => isPathInside(parentPath, filePath)) + ) { outermostPaths.push(filePath); } } @@ -402,8 +437,14 @@ const getOutermostPaths = (paths: string[]): string[] => { }; /** Merges overlapping roots; micromatch remains responsible for glob syntax. */ -const getTraversalRoots = (cwd: string, directories: string[], globs: string[]): string[] => { - const globRoots = globs.map((pattern) => path.resolve(cwd, micromatch.scan(pattern).base || '.')); +const getTraversalRoots = ( + cwd: string, + directories: string[], + globs: string[], +): string[] => { + const globRoots = globs.map((pattern) => + path.resolve(cwd, micromatch.scan(pattern).base || '.'), + ); return getOutermostPaths([...directories, ...globRoots]); }; @@ -431,9 +472,13 @@ const discoverFmtPaths = async ({ negativeGlobs, } = await classifyPatterns(cwd, patterns, ignoredDirNames); const directoryRoots = getOutermostPaths(directories); - const globMatchers = globs.map((pattern) => micromatch.matcher(pattern, { dot: true })); + const globMatchers = globs.map((pattern) => + micromatch.matcher(pattern, { dot: true }), + ); const candidates = new Set( - isIgnored ? explicitFiles.filter((filePath) => !isIgnored(filePath, false)) : explicitFiles, + isIgnored + ? explicitFiles.filter((filePath) => !isIgnored(filePath, false)) + : explicitFiles, ); const traversalRoots = getTraversalRoots(cwd, directoryRoots, globs); @@ -447,7 +492,10 @@ const discoverFmtPaths = async ({ } await gitIgnore.loadThrough(rootPath); - if (gitIgnore.isIgnored(rootPath, true) || isIgnored?.(rootPath, true) === true) { + if ( + gitIgnore.isIgnored(rootPath, true) || + isIgnored?.(rootPath, true) === true + ) { return []; } @@ -457,7 +505,11 @@ const discoverFmtPaths = async ({ const isIncluded = includesAll ? undefined : (filePath: string): boolean => { - if (directoryRoots.some((directoryPath) => isPathInside(directoryPath, filePath))) { + if ( + directoryRoots.some((directoryPath) => + isPathInside(directoryPath, filePath), + ) + ) { return true; } @@ -465,7 +517,13 @@ const discoverFmtPaths = async ({ return globMatchers.some((matches) => matches(relativePath)); }; - return discoverDirectoryFiles(rootPath, gitIgnore, ignoredDirNames, isIncluded, isIgnored); + return discoverDirectoryFiles( + rootPath, + gitIgnore, + ignoredDirNames, + isIncluded, + isIgnored, + ); }), ); diff --git a/packages/rstack/src/fmt/discovery.ts b/packages/rstack/src/fmt/discovery.ts index 6eca1d83..8e238de1 100644 --- a/packages/rstack/src/fmt/discovery.ts +++ b/packages/rstack/src/fmt/discovery.ts @@ -1,38 +1,9 @@ import path from 'node:path'; -import { createOptionsResolver, type FmtOptionsResolver } from './config.ts'; import { discoverFmtPaths } from './discoverPaths.ts'; +import { createFmtFileResolver } from './fileResolver.ts'; import { createIgnoreMatcher } from './ignore.ts'; -import type { FmtPluginResolver } from './plugins.ts'; import type { DiscoverFmtFilesOptions, FmtFileRequest } from './types.ts'; -const createFileRequest = ( - filePath: string, - resolveOptions: FmtOptionsResolver, -): FmtFileRequest => ({ - path: filePath, - options: resolveOptions(filePath), -}); - -/** Imports the plugin chunk on first use and shares the resolver across calls. */ -const createLazyPluginResolver = (rootPath: string): (() => Promise) => { - let resolver: Promise | undefined; - - return () => - (resolver ??= import( - /* rspackChunkName: 'fmtPlugins' */ - './plugins.ts' - ).then(({ createPluginResolver }) => createPluginResolver(rootPath))); -}; - -/** Resolves the plugin specifiers of a request whose options configure plugins. */ -const resolveFileRequestPlugins = async ( - file: FmtFileRequest, - getPluginResolver: () => Promise, -): Promise => - file.options.plugins?.length - ? { ...file, options: (await getPluginResolver())(file.options) } - : file; - const createDirMatcher = (dirPath: string): ((filePath: string) => boolean) => { const prefix = dirPath.endsWith(path.sep) ? dirPath : `${dirPath}${path.sep}`; return (filePath) => filePath === dirPath || filePath.startsWith(prefix); @@ -48,7 +19,9 @@ const discoverFmtFiles = async ({ config, }: DiscoverFmtFilesOptions): Promise => { const isIgnored = await createIgnoreMatcher({ config, cwd, ignorePaths }); - const isExcluded = excludedDirPath ? createDirMatcher(excludedDirPath) : undefined; + const isExcluded = excludedDirPath + ? createDirMatcher(excludedDirPath) + : undefined; const shouldIgnore = isExcluded ? (filePath: string, isDirectory = false) => isExcluded(filePath) || isIgnored(filePath, isDirectory) @@ -63,14 +36,9 @@ const discoverFmtFiles = async ({ return []; } - const resolveOptions = createOptionsResolver(config); - const getPluginResolver = createLazyPluginResolver(config.rootPath); + const resolveFile = createFmtFileResolver(config); - return Promise.all( - filePaths.map((filePath) => - resolveFileRequestPlugins(createFileRequest(filePath, resolveOptions), getPluginResolver), - ), - ); + return Promise.all(filePaths.map((filePath) => resolveFile(filePath))); }; -export { createFileRequest, createLazyPluginResolver, discoverFmtFiles, resolveFileRequestPlugins }; +export { discoverFmtFiles }; diff --git a/packages/rstack/src/fmt/fileResolver.ts b/packages/rstack/src/fmt/fileResolver.ts new file mode 100644 index 00000000..aa74151c --- /dev/null +++ b/packages/rstack/src/fmt/fileResolver.ts @@ -0,0 +1,30 @@ +import { createOptionsResolver } from './config.ts'; +import type { FmtPluginResolver } from './plugins.ts'; +import type { FmtFileRequest, ResolvedFmtConfig } from './types.ts'; + +type FmtFileResolver = (filePath: string) => Promise; + +/** Applies per-file overrides and resolves configured plugin specifiers. */ +const createFmtFileResolver = (config: ResolvedFmtConfig): FmtFileResolver => { + const resolveOptions = createOptionsResolver(config); + let pluginResolver: Promise | undefined; + + return async (filePath) => { + let options = resolveOptions(filePath); + + if (options.plugins?.length) { + pluginResolver ??= import( + /* rspackChunkName: 'fmtPlugins' */ + './plugins.ts' + ).then(({ createPluginResolver }) => + createPluginResolver(config.rootPath), + ); + options = (await pluginResolver)(options); + } + + return { path: filePath, options }; + }; +}; + +export { createFmtFileResolver }; +export type { FmtFileResolver }; diff --git a/packages/rstack/src/fmt/format.ts b/packages/rstack/src/fmt/format.ts index 0aa653af..d2aac91c 100644 --- a/packages/rstack/src/fmt/format.ts +++ b/packages/rstack/src/fmt/format.ts @@ -12,7 +12,8 @@ import type { FmtFileRequest } from './types.ts'; type PrettierPlugins = NonNullable; type FormatFmtSourceResult = - { status: 'unsupported' } | { status: 'formatted'; source: string; formatted: string }; + | { status: 'unsupported' } + | { status: 'formatted'; source: string; formatted: string }; const fileInfoOptions = { ignorePath: [], diff --git a/packages/rstack/src/fmt/ignore.ts b/packages/rstack/src/fmt/ignore.ts index 6874394c..48e52105 100644 --- a/packages/rstack/src/fmt/ignore.ts +++ b/packages/rstack/src/fmt/ignore.ts @@ -29,10 +29,14 @@ const createDefaultMatcher = (): IgnorePredicate => { const createSourceMatcher = (sources: IgnoreSource[]): IgnorePredicate => { const matcher = new (loadNativeBinding().IgnoreMatcher)(sources); - return (filePath, isDirectory = false) => matcher.isIgnored(filePath, isDirectory); + return (filePath, isDirectory = false) => + matcher.isIgnored(filePath, isDirectory); }; -const loadIgnoreSource = async (cwd: string, ignorePath: string): Promise => { +const loadIgnoreSource = async ( + cwd: string, + ignorePath: string, +): Promise => { const filePath = path.resolve(cwd, ignorePath); let patterns: string; diff --git a/packages/rstack/src/fmt/lsp/minimalEdit.ts b/packages/rstack/src/fmt/lsp/minimalEdit.ts index d391d955..97cfba7f 100644 --- a/packages/rstack/src/fmt/lsp/minimalEdit.ts +++ b/packages/rstack/src/fmt/lsp/minimalEdit.ts @@ -8,8 +8,10 @@ interface MinimalEdit { const CARRIAGE_RETURN = 0x0d; const LINE_FEED = 0x0a; -const isHighSurrogate = (code: number): boolean => code >= 0xd800 && code <= 0xdbff; -const isLowSurrogate = (code: number): boolean => code >= 0xdc00 && code <= 0xdfff; +const isHighSurrogate = (code: number): boolean => + code >= 0xd800 && code <= 0xdbff; +const isLowSurrogate = (code: number): boolean => + code >= 0xdc00 && code <= 0xdfff; /** * True when `index` splits a unit that occupies a single position: a surrogate @@ -33,7 +35,10 @@ const splitsIndivisibleUnit = (text: string, index: number): boolean => { * ends instead of replacing the whole document, which keeps selections, folds, * and undo history intact. Offsets are converted to positions by the caller. */ -const computeMinimalEdit = (source: string, formatted: string): MinimalEdit | undefined => { +const computeMinimalEdit = ( + source: string, + formatted: string, +): MinimalEdit | undefined => { if (source === formatted) { return undefined; } @@ -109,7 +114,10 @@ interface MinimalTextEdit { * `\r\n`, or a lone `\r`, like the protocol's. `computeMinimalEdit` keeping * boundaries out of surrogate pairs and `\r\n` is what makes the mapping exact. */ -const computeMinimalTextEdit = (source: string, formatted: string): MinimalTextEdit | undefined => { +const computeMinimalTextEdit = ( + source: string, + formatted: string, +): MinimalTextEdit | undefined => { const edit = computeMinimalEdit(source, formatted); if (!edit) { return undefined; diff --git a/packages/rstack/src/fmt/lsp/server.ts b/packages/rstack/src/fmt/lsp/server.ts index d1e1348c..f2a5742f 100644 --- a/packages/rstack/src/fmt/lsp/server.ts +++ b/packages/rstack/src/fmt/lsp/server.ts @@ -9,15 +9,12 @@ import { type InitializeParams, type TextEdit, } from 'vscode-languageserver/node'; -import { createOptionsResolver, type FmtOptionsResolver } from '../config.ts'; import { - createFileRequest, - createLazyPluginResolver, - resolveFileRequestPlugins, -} from '../discovery.ts'; + createFmtFileResolver, + type FmtFileResolver, +} from '../fileResolver.ts'; import { formatFmtSource } from '../format.ts'; import { createIgnoreMatcher, type IgnorePredicate } from '../ignore.ts'; -import type { FmtPluginResolver } from '../plugins.ts'; import type { ResolvedFmtConfig } from '../types.ts'; import { computeMinimalTextEdit } from './minimalEdit.ts'; @@ -35,9 +32,7 @@ type FmtLspSessionOptions = RunFmtLspOptions & { root: string }; interface FmtLspSession { isIgnored: IgnorePredicate; - resolveOptions: FmtOptionsResolver; - /** Resolves plugin specifiers through the file system; cached per session. */ - getPluginResolver: () => Promise; + resolveFile: FmtFileResolver; } const toFilePath = (uri: string): string | undefined => { @@ -75,7 +70,8 @@ const redirectConsoleToConnection = (connection: Connection): void => { connection.console.log(serializeConsoleArguments(args)); console.trace = (...args: unknown[]): void => { const stack = new Error().stack?.replace(/(.+\n){2}/, '') ?? ''; - const message = args.length === 0 ? 'Trace' : `Trace: ${serializeConsoleArguments(args)}`; + const message = + args.length === 0 ? 'Trace' : `Trace: ${serializeConsoleArguments(args)}`; connection.console.log(`${message}\n${stack}`); }; console.assert = (assertion?: unknown, ...args: unknown[]): void => { @@ -95,7 +91,7 @@ const redirectConsoleToConnection = (connection: Connection): void => { counters.set(key, count); connection.console.log(`${key}: ${count}`); }; - console.countReset = (label?: unknown): void => { + console.countReset = (label?: string): void => { if (label === undefined) { counters.clear(); } else { @@ -107,7 +103,9 @@ const redirectConsoleToConnection = (connection: Connection): void => { const resolveWorkspaceRoot = (params: InitializeParams): string | undefined => { const rootUri = params.workspaceFolders?.[0]?.uri ?? params.rootUri; - return (rootUri ? toFilePath(rootUri) : undefined) ?? params.rootPath ?? undefined; + return ( + (rootUri ? toFilePath(rootUri) : undefined) ?? params.rootPath ?? undefined + ); }; /** Loads everything a formatting request needs, once per server lifetime. */ @@ -122,8 +120,7 @@ const createFmtLspSession = async ({ return { isIgnored, - resolveOptions: createOptionsResolver(config), - getPluginResolver: createLazyPluginResolver(config.rootPath), + resolveFile: createFmtFileResolver(config), }; }; @@ -137,10 +134,7 @@ const formatDocumentSource = async ( return undefined; } - const file = await resolveFileRequestPlugins( - createFileRequest(filePath, session.resolveOptions), - session.getPluginResolver, - ); + const file = await session.resolveFile(filePath); const result = await formatFmtSource(file, () => source); return result.status === 'formatted' ? result.formatted : undefined; @@ -167,7 +161,10 @@ const createDocumentEdits = async ( return []; } - const edit = formatted === undefined ? undefined : computeMinimalTextEdit(source, formatted); + const edit = + formatted === undefined + ? undefined + : computeMinimalTextEdit(source, formatted); return edit ? [edit] : []; }; @@ -210,25 +207,27 @@ const startFmtLsp = (options: RunFmtLspOptions, onExit: () => void): void => { // TODO: watch the config file and reset the session when it changes. const getSession = (): Promise => - (sessionPromise ??= createFmtLspSession({ ...options, root }).catch((error: unknown) => { - // Retry on the next request rather than caching the failure forever. - sessionPromise = undefined; - // A workspace that cannot be set up returns no edits for every document, - // which looks like "nothing to format" in editors that hide the server - // log, so it is shown to the user instead of only being logged. Repeats - // of the same failure stay silent so saving a file cannot spam the editor. - const message = `rs fmt cannot format this workspace: ${String(error)}`; - if (reportedSessionError !== message) { - reportedSessionError = message; - // A notification rather than `window.showErrorMessage`, which sends a - // request the server would then wait on for a response it does not need. - void connection.sendNotification(ShowMessageNotification.type, { - type: MessageType.Error, - message, - }); - } - throw error; - })); + (sessionPromise ??= createFmtLspSession({ ...options, root }).catch( + (error: unknown) => { + // Retry on the next request rather than caching the failure forever. + sessionPromise = undefined; + // A workspace that cannot be set up returns no edits for every document, + // which looks like "nothing to format" in editors that hide the server + // log, so it is shown to the user instead of only being logged. Repeats + // of the same failure stay silent so saving a file cannot spam the editor. + const message = `rs fmt cannot format this workspace: ${String(error)}`; + if (reportedSessionError !== message) { + reportedSessionError = message; + // A notification rather than `window.showErrorMessage`, which sends a + // request the server would then wait on for a response it does not need. + void connection.sendNotification(ShowMessageNotification.type, { + type: MessageType.Error, + message, + }); + } + throw error; + }, + )); connection.onExit(onExit); @@ -246,26 +245,30 @@ const startFmtLsp = (options: RunFmtLspOptions, onExit: () => void): void => { }; }); - connection.onDocumentFormatting(async ({ textDocument }): Promise => { - const filePath = toFilePath(textDocument.uri); - if (!filePath) { - return []; - } + connection.onDocumentFormatting( + async ({ textDocument }): Promise => { + const filePath = toFilePath(textDocument.uri); + if (!filePath) { + return []; + } - // A formatting failure must never disrupt editing; unsupported, ignored, - // and unparsable documents all resolve to "no edits". - try { - const session = await getSession(); + // A formatting failure must never disrupt editing; unsupported, ignored, + // and unparsable documents all resolve to "no edits". + try { + const session = await getSession(); - return await createDocumentEdits( - () => documents.get(textDocument.uri), - (source) => formatDocumentSource(session, filePath, source), - ); - } catch (error) { - connection.console.error(`Failed to format "${filePath}": ${String(error)}`); - return []; - } - }); + return await createDocumentEdits( + () => documents.get(textDocument.uri), + (source) => formatDocumentSource(session, filePath, source), + ); + } catch (error) { + connection.console.error( + `Failed to format "${filePath}": ${String(error)}`, + ); + return []; + } + }, + ); connection.listen(); }; diff --git a/packages/rstack/src/fmt/pathHelpers.ts b/packages/rstack/src/fmt/pathHelpers.ts index 1ad48566..a8f72f9e 100644 --- a/packages/rstack/src/fmt/pathHelpers.ts +++ b/packages/rstack/src/fmt/pathHelpers.ts @@ -3,10 +3,14 @@ import path from 'node:path'; type RelativePathResolver = (filePath: string) => string; const toPosixPath: (filePath: string) => string = - path.sep === '\\' ? (filePath) => filePath.replaceAll('\\', '/') : (filePath) => filePath; + path.sep === '\\' + ? (filePath) => filePath.replaceAll('\\', '/') + : (filePath) => filePath; const createRelativePathResolver = (rootPath: string): RelativePathResolver => { - const rootPrefix = rootPath.endsWith(path.sep) ? rootPath : `${rootPath}${path.sep}`; + const rootPrefix = rootPath.endsWith(path.sep) + ? rootPath + : `${rootPath}${path.sep}`; return (filePath) => filePath === rootPath @@ -17,7 +21,8 @@ const createRelativePathResolver = (rootPath: string): RelativePathResolver => { }; /** Prettier only inspects a file's shebang when its basename contains no dot. */ -const hasDottedBasename = (filePath: string): boolean => path.basename(filePath).includes('.'); +const hasDottedBasename = (filePath: string): boolean => + path.basename(filePath).includes('.'); export { createRelativePathResolver, hasDottedBasename, toPosixPath }; export type { RelativePathResolver }; diff --git a/packages/rstack/src/fmt/plugins.ts b/packages/rstack/src/fmt/plugins.ts index 312dfbb1..263bcb32 100644 --- a/packages/rstack/src/fmt/plugins.ts +++ b/packages/rstack/src/fmt/plugins.ts @@ -1,5 +1,11 @@ import { readFile, realpath } from 'node:fs/promises'; -import { isAbsolute, join, relative, resolve as resolvePath, sep } from 'node:path'; +import { + isAbsolute, + join, + relative, + resolve as resolvePath, + sep, +} from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { moduleResolve } from 'import-meta-resolve'; import type { Options as PrettierOptions } from 'prettier'; @@ -8,12 +14,16 @@ import type { FmtPluginSpecifier, ResolvedFmtOptions } from './types.ts'; type FmtPlugin = NonNullable[number]; type FmtPluginResolver = (options: ResolvedFmtOptions) => ResolvedFmtOptions; -type FingerprintResolver = (plugin: FmtPluginSpecifier) => Promise; +type FingerprintResolver = ( + plugin: FmtPluginSpecifier, +) => Promise; const resolveModuleUrl = (specifier: string, parentUrl: URL): string => moduleResolve(specifier, parentUrl).href; -const isFmtPluginSpecifier = (plugin: FmtPlugin): plugin is FmtPluginSpecifier => +const isFmtPluginSpecifier = ( + plugin: FmtPlugin, +): plugin is FmtPluginSpecifier => typeof plugin === 'string' || plugin instanceof URL; const getPackageRoot = (entryPath: string): string | undefined => { @@ -38,7 +48,9 @@ const getPackageRoot = (entryPath: string): string | undefined => { return entryPath.slice(0, end); }; -const fingerprintPlugin = async (pluginUrl: string): Promise => { +const fingerprintPlugin = async ( + pluginUrl: string, +): Promise => { try { const url = new URL(pluginUrl); if (url.protocol !== 'file:') { @@ -53,7 +65,9 @@ const fingerprintPlugin = async (pluginUrl: string): Promise return undefined; } - const pkg: unknown = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8')); + const pkg: unknown = JSON.parse( + await readFile(join(packageRoot, 'package.json'), 'utf8'), + ); if ( typeof pkg !== 'object' || pkg === null || @@ -94,11 +108,12 @@ const createFingerprintResolver = (): FingerprintResolver => { /** Creates a project-root resolver for plugins in final per-file options. */ const createPluginResolver = (rootPath: string): FmtPluginResolver => { const parentUrl = pathToFileURL(join(rootPath, 'index.js')); - const cache = new Map(); + const pluginCache = new Map(); + const optionsCache = new WeakMap(); const resolvePlugin = (plugin: FmtPluginSpecifier): string => { const specifier = plugin instanceof URL ? plugin.href : plugin; - const cached = cache.get(specifier); + const cached = pluginCache.get(specifier); if (cached !== undefined) { return cached; } @@ -119,11 +134,16 @@ const createPluginResolver = (rootPath: string): FmtPluginResolver => { } } - cache.set(specifier, resolved); + pluginCache.set(specifier, resolved); return resolved; }; return (options) => { + const cached = optionsCache.get(options); + if (cached !== undefined) { + return cached; + } + const { plugins } = options; if (!plugins?.length) { return options; @@ -136,10 +156,13 @@ const createPluginResolver = (rootPath: string): FmtPluginResolver => { } const resolvedPlugins = plugins.map(resolvePlugin); - - return resolvedPlugins.every((plugin, index) => plugin === plugins[index]) + const resolvedOptions = resolvedPlugins.every( + (plugin, index) => plugin === plugins[index], + ) ? options : { ...options, plugins: resolvedPlugins }; + optionsCache.set(options, resolvedOptions); + return resolvedOptions; }; }; diff --git a/packages/rstack/src/fmt/prettierPlugins.ts b/packages/rstack/src/fmt/prettierPlugins.ts index 829d7e6d..a88a600e 100644 --- a/packages/rstack/src/fmt/prettierPlugins.ts +++ b/packages/rstack/src/fmt/prettierPlugins.ts @@ -24,7 +24,10 @@ const getPrettierPlugins = async ( ): Promise => { const plugins = options.sortPackageJson === true && /(^|[/\\])package\.json$/.test(filePath) - ? [...defaultFmtPlugins, (await import('./sortPackageJsonPlugin.ts')).sortPackageJsonPlugin] + ? [ + ...defaultFmtPlugins, + (await import('./sortPackageJsonPlugin.ts')).sortPackageJsonPlugin, + ] : defaultFmtPlugins; return options.plugins?.length ? [...plugins, ...options.plugins] : plugins; diff --git a/packages/rstack/src/fmt/runner.ts b/packages/rstack/src/fmt/runner.ts index c594530c..89ad3a9a 100644 --- a/packages/rstack/src/fmt/runner.ts +++ b/packages/rstack/src/fmt/runner.ts @@ -1,4 +1,8 @@ -import { cacheNamespace, createCacheKeyResolver, createOptionsHasher } from './cacheIdentity.ts'; +import { + cacheNamespace, + createCacheKeyResolver, + createOptionsHasher, +} from './cacheIdentity.ts'; import { loadFmtCacheStore } from './cacheStore.ts'; import type { FmtCacheEntry, FmtCacheStore } from './cacheStore.ts'; import { hasDottedBasename } from './pathHelpers.ts'; @@ -77,7 +81,10 @@ const loadPluginFingerprints = async ( ); const resolveFingerprint = createFingerprintResolver(); const entries = await Promise.all( - Array.from(plugins, async ([key, plugin]) => [key, await resolveFingerprint(plugin)] as const), + Array.from( + plugins, + async ([key, plugin]) => [key, await resolveFingerprint(plugin)] as const, + ), ); const fingerprints = new Map(); for (const [key, fingerprint] of entries) { @@ -89,7 +96,10 @@ const loadPluginFingerprints = async ( }; /** Resolves the portable cache identity before work is dispatched. */ -const createRunTask = (file: FmtFileRequest, cache?: RunCache): FmtFileRunTask => { +const createRunTask = ( + file: FmtFileRequest, + cache?: RunCache, +): FmtFileRunTask => { let key: string | undefined; let fileCache: FmtFileCache | undefined; @@ -116,7 +126,7 @@ const isCachedUnsupported = ({ file, cache }: FmtFileRunTask): boolean => { return false; } return ( - cache.entry[0] === null && + cache.entry[0] === '' && cache.entry[1] === cache.optionsHash && cache.entry[2] === 'unsupported' && hasDottedBasename(file.path) @@ -207,7 +217,9 @@ const runWithWorkers = async ( workerPool.workerCount >= minPriorityWorkers ? await runPriorityTasks(tasks, shouldWrite, workerPool.formatFile) : await Promise.all( - tasks.map((task) => runFmtFile(task, shouldWrite, workerPool.formatFile)), + tasks.map((task) => + runFmtFile(task, shouldWrite, workerPool.formatFile), + ), ); const processedFiles: FmtFileResult[] = []; let processedFileCount = 0; @@ -277,7 +289,10 @@ const runFmtFiles = async ({ return { ...result, - exitCode: files.length > 0 && result.processedFileCount === 0 ? 2 : getExitCode(result.files), + exitCode: + files.length > 0 && result.processedFileCount === 0 + ? 2 + : getExitCode(result.files), }; }; diff --git a/packages/rstack/src/fmt/stdin.ts b/packages/rstack/src/fmt/stdin.ts index 90062709..6501ba73 100644 --- a/packages/rstack/src/fmt/stdin.ts +++ b/packages/rstack/src/fmt/stdin.ts @@ -1,10 +1,5 @@ import { resolve } from 'node:path'; -import { createOptionsResolver } from './config.ts'; -import { - createFileRequest, - createLazyPluginResolver, - resolveFileRequestPlugins, -} from './discovery.ts'; +import { createFmtFileResolver } from './fileResolver.ts'; import { formatFmtSource } from './format.ts'; import { createIgnoreMatcher } from './ignore.ts'; import type { ResolvedFmtConfig } from './types.ts'; @@ -83,10 +78,7 @@ const runFmtStdin = async ({ return; } - const file = await resolveFileRequestPlugins( - createFileRequest(absolutePath, createOptionsResolver(config)), - createLazyPluginResolver(config.rootPath), - ); + const file = await createFmtFileResolver(config)(absolutePath); const result = await formatFmtSource(file, () => source); if (result.status === 'unsupported') { diff --git a/packages/rstack/src/fmt/types.ts b/packages/rstack/src/fmt/types.ts index 23e2dd6f..90d50a31 100644 --- a/packages/rstack/src/fmt/types.ts +++ b/packages/rstack/src/fmt/types.ts @@ -1,4 +1,7 @@ -import type { Config as PrettierConfig, Options as PrettierOptions } from 'prettier'; +import type { + Config as PrettierConfig, + Options as PrettierOptions, +} from 'prettier'; import type { FmtCacheEntry } from './cacheStore.ts'; /** Plugin objects cannot cross worker boundaries and are not planned for support. */ @@ -25,7 +28,8 @@ type FmtOverride = Omit & { options?: FmtOptions; }; -interface FmtConfig extends Omit, FmtBuiltinOptions { +interface FmtConfig + extends Omit, FmtBuiltinOptions { plugins?: FmtPluginSpecifier[]; overrides?: FmtOverride[]; /** Gitignore-compatible patterns relative to the Rstack config root. */ diff --git a/packages/rstack/src/fmt/worker.ts b/packages/rstack/src/fmt/worker.ts index 1befd4ce..35d47f49 100644 --- a/packages/rstack/src/fmt/worker.ts +++ b/packages/rstack/src/fmt/worker.ts @@ -12,7 +12,8 @@ interface FormatFileTask { cache?: FmtFileCache; } -const hashContent = (content: string | Uint8Array): string => hash('sha256', content, 'hex'); +const hashContent = (content: string | Uint8Array): string => + hash('sha256', content, 'base64url').slice(0, 16); /** * Use synchronous direct I/O inside the dedicated worker to avoid libuv @@ -44,7 +45,7 @@ const formatFile = async ({ if (cache?.entry && cache.entry[1] === cache.optionsHash) { const { entry } = cache; if (entry[2] === 'unsupported') { - if (entry[0] === null) { + if (entry[0] === '') { if (hasDottedBasename(file.path)) { return { status: 'unsupported' }; } @@ -72,8 +73,9 @@ const formatFile = async ({ status: 'unsupported', cacheEntry: [ hasDottedBasename(file.path) - ? null - : (contentHash ?? hashContent(sourceBuffer ?? readFileSync(file.path))), + ? '' + : (contentHash ?? + hashContent(sourceBuffer ?? readFileSync(file.path))), cache.optionsHash, 'unsupported', ], diff --git a/packages/rstack/src/fmt/workerPool.ts b/packages/rstack/src/fmt/workerPool.ts index 41bded06..800e4768 100644 --- a/packages/rstack/src/fmt/workerPool.ts +++ b/packages/rstack/src/fmt/workerPool.ts @@ -22,7 +22,10 @@ interface FmtWorkerPool { * scheduling and memory pressure. */ const getWorkerCount = (fileCount: number, maxWorkers?: number): number => - Math.min(fileCount, maxWorkers ?? Math.min(8, Math.max(1, availableParallelism() - 1))); + Math.min( + fileCount, + maxWorkers ?? Math.min(8, Math.max(1, availableParallelism() - 1)), + ); const getWorkerUrl = (): URL => { // Source tests run after build and exercise the same worker artifact as the CLI. @@ -33,7 +36,10 @@ const getWorkerUrl = (): URL => { }; /** Creates and starts every worker before formatting can begin. */ -const createWorkerPool = async (fileCount: number, maxWorkers?: number): Promise => { +const createWorkerPool = async ( + fileCount: number, + maxWorkers?: number, +): Promise => { const workerCount = getWorkerCount(fileCount, maxWorkers); const pool = new Tinypool({ filename: getWorkerUrl().href, diff --git a/packages/rstack/src/fmt/yukuPlugin.ts b/packages/rstack/src/fmt/yukuPlugin.ts index 1fa2f646..824af9fb 100644 --- a/packages/rstack/src/fmt/yukuPlugin.ts +++ b/packages/rstack/src/fmt/yukuPlugin.ts @@ -70,7 +70,8 @@ const locStart = (node: Locatable): number => { return firstDecorator ? Math.min(locStart(firstDecorator), start) : start; }; -const locEndWithFullText = (node: Locatable): number => (node.range?.[1] ?? node.end) as number; +const locEndWithFullText = (node: Locatable): number => + (node.range?.[1] ?? node.end) as number; const locEnd = (node: Locatable): number => { switch (node.type) { @@ -89,7 +90,9 @@ const locEnd = (node: Locatable): number => { return node.label ? locEnd(node.label) : locStart(node) + 'break'.length; case 'ContinueStatement': - return node.label ? locEnd(node.label) : locStart(node) + 'continue'.length; + return node.label + ? locEnd(node.label) + : locStart(node) + 'continue'.length; case 'DebuggerStatement': return locStart(node) + 'debugger'.length; @@ -136,10 +139,13 @@ const hasPragmaFrom = (originalText: string, pragmas: Set): boolean => { return false; }; -const hasPragma = (text: string): boolean => hasPragmaFrom(text, FORMAT_PRAGMAS); -const hasIgnorePragma = (text: string): boolean => hasPragmaFrom(text, FORMAT_IGNORE_PRAGMAS); +const hasPragma = (text: string): boolean => + hasPragmaFrom(text, FORMAT_PRAGMAS); +const hasIgnorePragma = (text: string): boolean => + hasPragmaFrom(text, FORMAT_IGNORE_PRAGMAS); -const getVisitorKeys = estreePrinter.getVisitorKeys as ((node: AstNode) => string[]) | undefined; +const getVisitorKeys = estreePrinter.getVisitorKeys as + ((node: AstNode) => string[]) | undefined; if (!getVisitorKeys) { throw new Error('The Prettier ESTree printer does not expose visitor keys.'); @@ -158,7 +164,10 @@ const asAstNode = (value: unknown): AstNode => { return value; }; -const withExtra = (node: AstNode, extra: Record): Record => ({ +const withExtra = ( + node: AstNode, + extra: Record, +): Record => ({ ...(node.extra !== null && typeof node.extra === 'object' ? (node.extra as Record) : undefined), @@ -201,7 +210,10 @@ const mergeNestedJsdocComments = (comments: PrettierComment[]): void => { } }; -const stripComments = (originalText: string, comments: PrettierComment[]): string => { +const stripComments = ( + originalText: string, + comments: PrettierComment[], +): string => { if (comments.length === 0) { return originalText; } @@ -287,7 +299,10 @@ const isUnbalancedLogicalTree = (node: AstNode): boolean => { return false; } - return node.right.type === 'LogicalExpression' && node.operator === node.right.operator; + return ( + node.right.type === 'LogicalExpression' && + node.operator === node.right.operator + ); }; const rebalanceLogicalTree = (node: AstNode): AstNode => { @@ -351,7 +366,9 @@ const postprocess = ( .filter(isTypeCastComment) .map((comment) => locEnd(comment)); - const previousCommentEnd = typeCastCommentEnds.findLast((end) => end <= start); + const previousCommentEnd = typeCastCommentEnds.findLast( + (end) => end <= start, + ); const shouldKeepParentheses = previousCommentEnd !== undefined && text.slice(previousCommentEnd, start).trim().length === 0; @@ -402,12 +419,17 @@ const postprocess = ( return undefined; }, onLeave(node) { - return isUnbalancedLogicalTree(node) ? rebalanceLogicalTree(node) : undefined; + return isUnbalancedLogicalTree(node) + ? rebalanceLogicalTree(node) + : undefined; }, }) as AstNode; }; -const indexToPosition = (text: string, index: number): { column: number; line: number } => { +const indexToPosition = ( + text: string, + index: number, +): { column: number; line: number } => { const lineBreakBefore = index === 0 ? -1 : text.lastIndexOf('\n', index - 1); let line = 1; @@ -423,18 +445,17 @@ const indexToPosition = (text: string, index: number): { column: number; line: n }; }; -const createParseError = (error: Diagnostic, text: string): Diagnostic | SyntaxError => { - if (typeof error?.start !== 'number' || typeof error?.end !== 'number') { - return error; - } - +const createParseError = (error: Diagnostic, text: string): SyntaxError => { const start = indexToPosition(text, error.start); const end = indexToPosition(text, error.end); - return Object.assign(new SyntaxError(`${error.message} (${start.line}:${start.column})`), { - cause: error, - loc: { start, end }, - }); + return Object.assign( + new SyntaxError(`${error.message} (${start.line}:${start.column})`), + { + cause: error, + loc: { start, end }, + }, + ); }; const parseWithOptions = (text: string, options: ParseOptions): ParseResult => { @@ -464,7 +485,10 @@ const getSourceType = (filepath: string): SourceType | undefined => { return undefined; }; -const getLanguageCombinations = (text: string, filepath: string): SourceLang[] => { +const getLanguageCombinations = ( + text: string, + filepath: string, +): SourceLang[] => { const normalizedPath = filepath.toLowerCase(); if (JS_TS_FILE_REGEXP.test(normalizedPath)) { @@ -497,25 +521,48 @@ const tryCombinations = (combinations: (() => ParseResult)[]): ParseResult => { throw new Error('No Yuku parser combinations were provided.'); }; -const parseJavaScript = (text: string, options: ParserOptions): AstNode => { +const parseJavaScript = ( + text: string, + options: ParserOptions, +): AstNode => { const sourceType = getSourceType(options.filepath); - const combinations = (sourceType ? [sourceType] : SOURCE_TYPE_COMBINATIONS).map( - (candidate) => () => parseWithOptions(text, { sourceType: candidate, lang: 'jsx' }), + const combinations = ( + sourceType ? [sourceType] : SOURCE_TYPE_COMBINATIONS + ).map( + (candidate) => () => + parseWithOptions(text, { sourceType: candidate, lang: 'jsx' }), ); const { program, comments } = tryCombinations(combinations); - return postprocess(program as unknown as AstNode, comments as PrettierComment[], text, 'yuku-js'); + return postprocess( + program as unknown as AstNode, + comments as PrettierComment[], + text, + 'yuku-js', + ); }; -const parseTypeScript = (text: string, options: ParserOptions): AstNode => { +const parseTypeScript = ( + text: string, + options: ParserOptions, +): AstNode => { const sourceType = getSourceType(options.filepath); const languages = getLanguageCombinations(text, options.filepath); - const combinations = (sourceType ? [sourceType] : SOURCE_TYPE_COMBINATIONS).flatMap((candidate) => - languages.map((lang) => () => parseWithOptions(text, { sourceType: candidate, lang })), + const combinations = ( + sourceType ? [sourceType] : SOURCE_TYPE_COMBINATIONS + ).flatMap((candidate) => + languages.map( + (lang) => () => parseWithOptions(text, { sourceType: candidate, lang }), + ), ); const { program, comments } = tryCombinations(combinations); - return postprocess(program as unknown as AstNode, comments as PrettierComment[], text, 'yuku-ts'); + return postprocess( + program as unknown as AstNode, + comments as PrettierComment[], + text, + 'yuku-ts', + ); }; const createParser = ( @@ -534,17 +581,19 @@ const parserNames = new Map([ ['typescript', 'yuku-ts'], ]); -const languages: SupportLanguage[] = estreePlugin.languages.flatMap((language) => { - const parsers = [ - ...new Set( - language.parsers - .map((parser) => parserNames.get(parser)) - .filter((parser): parser is string => parser !== undefined), - ), - ]; - - return parsers.length > 0 ? [{ ...language, parsers }] : []; -}); +const languages: SupportLanguage[] = estreePlugin.languages.flatMap( + (language) => { + const parsers = [ + ...new Set( + language.parsers + .map((parser) => parserNames.get(parser)) + .filter((parser): parser is string => parser !== undefined), + ), + ]; + + return parsers.length > 0 ? [{ ...language, parsers }] : []; + }, +); const yukuPlugin: Plugin = { languages, diff --git a/packages/rstack/src/native/index.ts b/packages/rstack/src/native/index.ts index bf682324..c760309e 100644 --- a/packages/rstack/src/native/index.ts +++ b/packages/rstack/src/native/index.ts @@ -6,5 +6,7 @@ export type NativeBinding = typeof import('../../binding.cjs'); const require = createRequire(import.meta.url); export const loadNativeBinding = (): NativeBinding => { const packageJsonPath = require.resolve('rstack/package.json'); - return require(path.join(path.dirname(packageJsonPath), 'binding.cjs')) as NativeBinding; + return require( + path.join(path.dirname(packageJsonPath), 'binding.cjs'), + ) as NativeBinding; }; diff --git a/packages/rstack/src/projectCache.ts b/packages/rstack/src/projectCache.ts index 86d0ffcb..39894d22 100644 --- a/packages/rstack/src/projectCache.ts +++ b/packages/rstack/src/projectCache.ts @@ -4,13 +4,17 @@ import path from 'node:path'; const cacheGitignore = '*\n'; type ProjectCacheResult = - { status: 'available'; path: string } | { status: 'unavailable'; path: string; error: unknown }; + | { status: 'available'; path: string } + | { status: 'unavailable'; path: string; error: unknown }; /** Returns the disposable cache directory for a resolved Rstack project root. */ -const getProjectCacheDir = (rootPath: string): string => path.join(rootPath, '.rstack', 'cache'); +const getProjectCacheDir = (rootPath: string): string => + path.join(rootPath, '.rstack', 'cache'); /** Creates the project cache directory without making cache failures fatal. */ -const ensureProjectCacheDir = async (rootPath: string): Promise => { +const ensureProjectCacheDir = async ( + rootPath: string, +): Promise => { const cachePath = getProjectCacheDir(rootPath); const ignorePath = path.join(cachePath, '.gitignore'); diff --git a/packages/rstack/src/rsbuildConfig.ts b/packages/rstack/src/rsbuildConfig.ts index 01385c44..02935da8 100644 --- a/packages/rstack/src/rsbuildConfig.ts +++ b/packages/rstack/src/rsbuildConfig.ts @@ -1,4 +1,8 @@ -import type { ConfigParams, RsbuildConfigDefinition, WatchFiles } from '@rsbuild/core'; +import type { + ConfigParams, + RsbuildConfigDefinition, + WatchFiles, +} from '@rsbuild/core'; import { loadRstackConfig, type Configs } from './config.ts'; const resolveRsbuildConfig = async (configs: Configs, params: ConfigParams) => { @@ -31,7 +35,11 @@ const loadRsbuildConfig: RsbuildConfigDefinition = async (params) => { dev: { ...config.dev, watchFiles: [ - ...(watchFiles ? (Array.isArray(watchFiles) ? watchFiles : [watchFiles]) : []), + ...(watchFiles + ? Array.isArray(watchFiles) + ? watchFiles + : [watchFiles] + : []), watchConfig, ], }, diff --git a/packages/rstack/src/rslibConfig.ts b/packages/rstack/src/rslibConfig.ts index 6f0011ed..a3159c63 100644 --- a/packages/rstack/src/rslibConfig.ts +++ b/packages/rstack/src/rslibConfig.ts @@ -1,7 +1,15 @@ -import type { ConfigParams, RslibConfig, RslibConfigDefinition } from '@rslib/core'; +import type { WatchFiles } from '@rsbuild/core'; +import type { + ConfigParams, + RslibConfig, + RslibConfigDefinition, +} from '@rslib/core'; import { loadRstackConfig, type Configs } from './config.ts'; -const resolveRslibConfig = async (configs: Configs, params: ConfigParams): Promise => { +const resolveRslibConfig = async ( + configs: Configs, + params: ConfigParams, +): Promise => { const libConfig = configs.lib; if (!libConfig) { return {}; @@ -13,8 +21,33 @@ const resolveRslibConfig = async (configs: Configs, params: ConfigParams): Promi }; const loadRslibConfig = (async (params: ConfigParams) => { - const { configs } = await loadRstackConfig(); - return resolveRslibConfig(configs, params); + const { configs, filePath, dependencies } = await loadRstackConfig(); + const config = await resolveRslibConfig(configs, params); + + if (!filePath) { + return config; + } + + const watchFiles = config.dev?.watchFiles; + const watchConfig: WatchFiles = { + paths: [filePath, ...dependencies], + type: 'restart', + }; + + return { + ...config, + dev: { + ...config.dev, + watchFiles: [ + ...(watchFiles + ? Array.isArray(watchFiles) + ? watchFiles + : [watchFiles] + : []), + watchConfig, + ], + }, + }; }) as RslibConfigDefinition; export default loadRslibConfig; diff --git a/packages/rstack/src/rslintConfig.ts b/packages/rstack/src/rslintConfig.ts index c0e953c3..50f13c20 100644 --- a/packages/rstack/src/rslintConfig.ts +++ b/packages/rstack/src/rslintConfig.ts @@ -2,15 +2,15 @@ import { loadRstackConfig } from './config.ts'; import type { RslintConfig } from '@rslint/core'; const { configs } = await loadRstackConfig(); -const lintExports = configs.lint ?? []; +const lintDefinition = configs.lint ?? []; let lintConfig: RslintConfig; // TODO: support function in Rslint core -if (typeof lintExports === 'function') { - lintConfig = await lintExports(); +if (typeof lintDefinition === 'function') { + lintConfig = await lintDefinition(); } else { - lintConfig = lintExports; + lintConfig = lintDefinition; } export default lintConfig; diff --git a/packages/rstack/src/rspressConfig.ts b/packages/rstack/src/rspressConfig.ts index ed5efd59..68bb41c5 100644 --- a/packages/rstack/src/rspressConfig.ts +++ b/packages/rstack/src/rspressConfig.ts @@ -1,3 +1,4 @@ +import type { WatchFiles } from '@rsbuild/core'; import type { UserConfig } from '@rspress/core'; import { loadRstackConfig, type Configs } from './config.ts'; @@ -13,6 +14,34 @@ const resolveRspressConfig = async (configs: Configs): Promise => { }; export default async (): Promise => { - const { configs } = await loadRstackConfig(); - return resolveRspressConfig(configs); + const { configs, filePath, dependencies } = await loadRstackConfig(); + const config = await resolveRspressConfig(configs); + + if (!filePath) { + return config; + } + + const watchFiles = config.builderConfig?.dev?.watchFiles; + const watchConfig: WatchFiles = { + paths: [filePath, ...dependencies], + type: 'restart', + }; + + return { + ...config, + builderConfig: { + ...config.builderConfig, + dev: { + ...config.builderConfig?.dev, + watchFiles: [ + ...(watchFiles + ? Array.isArray(watchFiles) + ? watchFiles + : [watchFiles] + : []), + watchConfig, + ], + }, + }, + }; }; diff --git a/packages/rstack/src/rstestConfig.ts b/packages/rstack/src/rstestConfig.ts index b28d61d2..119c7112 100644 --- a/packages/rstack/src/rstestConfig.ts +++ b/packages/rstack/src/rstestConfig.ts @@ -14,7 +14,8 @@ const resolveAutomaticExtends = async ( /* rspackChunkName: 'adapterRsbuild' */ '@rstest/adapter-rsbuild' ); - const config = typeof appConfig === 'function' ? await appConfig(params) : appConfig; + const config = + typeof appConfig === 'function' ? await appConfig(params) : appConfig; return withRsbuildConfig({ config, @@ -27,7 +28,8 @@ const resolveAutomaticExtends = async ( /* rspackChunkName: 'adapterRslib' */ '@rstest/adapter-rslib' ); - const config = typeof libConfig === 'function' ? await libConfig(params) : libConfig; + const config = + typeof libConfig === 'function' ? await libConfig(params) : libConfig; return withRslibConfig({ config, @@ -51,7 +53,11 @@ const injectExtends = ( }; }; -const extendsConfig = async (configs: Configs, testConfig: RstestConfig, params: ConfigParams) => { +const extendsConfig = async ( + configs: Configs, + testConfig: RstestConfig, + params: ConfigParams, +) => { if ('extends' in testConfig) { return testConfig; } @@ -73,7 +79,9 @@ const extendsConfig = async (configs: Configs, testConfig: RstestConfig, params: return { ...testConfig, projects: testConfig.projects.map((project) => - typeof project === 'string' ? project : injectExtends(project, automaticExtends), + typeof project === 'string' + ? project + : injectExtends(project, automaticExtends), ), }; }; diff --git a/packages/rstack/src/setup/hooks.ts b/packages/rstack/src/setup/hooks.ts index 676da734..7a6e523c 100644 --- a/packages/rstack/src/setup/hooks.ts +++ b/packages/rstack/src/setup/hooks.ts @@ -24,7 +24,10 @@ const quoteShellPath = (value: string): string => { process.platform === 'win32' ? value .replaceAll('\\', '/') - .replace(/^([A-Za-z]):\//u, (_, drive: string) => `/${drive.toLowerCase()}/`) + .replace( + /^([A-Za-z]):\//u, + (_, drive: string) => `/${drive.toLowerCase()}/`, + ) : value; return `'${shellPath.replaceAll("'", `'"'"'`)}'`; @@ -78,7 +81,8 @@ rs_run "$@" export const createHookFiles = ( nodeExecutable: string = process.execPath, ): Record => { - const messageShim = createShim(`# Keep the message file valid after changing directories. + const messageShim = + createShim(`# Keep the message file valid after changing directories. [ -n "\${1-}" ] || exit 1 case "$1" in /*|[A-Za-z]:/*) ;; @@ -90,7 +94,8 @@ case "$1" in esac `); - const prePushShim = createShim(`# Keep a local remote path valid after changing directories. + const prePushShim = + createShim(`# Keep a local remote path valid after changing directories. rs_remote_name=\${1-} rs_remote_location=\${2-} [ -n "$rs_remote_name" ] && [ -n "$rs_remote_location" ] || exit 1 @@ -109,7 +114,9 @@ set -- "$rs_remote_name" "$rs_remote_location" "$@" `); const defaultShim = createShim(); - const files: Record = { runner: createRunner(nodeExecutable) }; + const files: Record = { + runner: createRunner(nodeExecutable), + }; for (const name of hookNames) { files[name] = name.endsWith('-msg') diff --git a/packages/rstack/src/setup/index.ts b/packages/rstack/src/setup/index.ts index c090404f..ef597323 100644 --- a/packages/rstack/src/setup/index.ts +++ b/packages/rstack/src/setup/index.ts @@ -16,7 +16,9 @@ export const runSetupCLI = async (args: string[]): Promise => { const hooksDirs = values.hooksDir; if (hooksDirs && hooksDirs.length > 1) { - throw new Error('The --hooks-dir option cannot be specified more than once.'); + throw new Error( + 'The --hooks-dir option cannot be specified more than once.', + ); } const hooksDir = hooksDirs?.[0]; @@ -39,7 +41,9 @@ export const runSetupCLI = async (args: string[]): Promise => { } const reason = - result.reason === 'disabled' ? 'disabled by RSTACK_HOOKS' : 'not a Git repository'; + result.reason === 'disabled' + ? 'disabled by RSTACK_HOOKS' + : 'not a Git repository'; logger.info(`Git hooks setup skipped: ${color.yellow(reason)}.`); return; } diff --git a/packages/rstack/src/setup/install.ts b/packages/rstack/src/setup/install.ts index 81d4198f..eae7c186 100644 --- a/packages/rstack/src/setup/install.ts +++ b/packages/rstack/src/setup/install.ts @@ -1,5 +1,12 @@ import { spawnSync } from 'node:child_process'; -import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'; +import { + chmodSync, + existsSync, + mkdirSync, + readFileSync, + statSync, + writeFileSync, +} from 'node:fs'; import path from 'node:path'; import { createHookFiles, hookNames } from './hooks.ts'; @@ -54,7 +61,10 @@ const resolveHooksDir = (hooksDir: string): string | FailedInstallResult => { const resolvedDir = hooksDir.replaceAll('\\', '/'); if (resolvedDir.length === 0) { - return fail('invalid-hooks-directory', 'Git hooks directory must not be empty.'); + return fail( + 'invalid-hooks-directory', + 'Git hooks directory must not be empty.', + ); } if (path.isAbsolute(resolvedDir)) { @@ -65,15 +75,20 @@ const resolveHooksDir = (hooksDir: string): string | FailedInstallResult => { } if (resolvedDir.includes('..')) { - return fail('invalid-hooks-directory', 'Git hooks directory must not contain "..".'); + return fail( + 'invalid-hooks-directory', + 'Git hooks directory must not contain "..".', + ); } return resolvedDir; }; -const runGit = (cwd: string, args: string[]) => spawnSync('git', args, { cwd, encoding: 'utf8' }); +const runGit = (cwd: string, args: string[]) => + spawnSync('git', args, { cwd, encoding: 'utf8' }); -const removeLineEnding = (value: string): string => value.replace(/\r?\n$/u, ''); +const removeLineEnding = (value: string): string => + value.replace(/\r?\n$/u, ''); const gitFailure = ( error: NodeJS.ErrnoException | undefined, @@ -83,7 +98,10 @@ const gitFailure = ( return fail('git-not-found', 'Git command not found.'); } - return fail('git-command-failed', `Failed to run Git: ${error?.message || stderr.trim()}`); + return fail( + 'git-command-failed', + `Failed to run Git: ${error?.message || stderr.trim()}`, + ); }; const resolveGitContext = (cwd: string): GitContext | InstallResult => { @@ -123,23 +141,33 @@ const resolveGitContext = (cwd: string): GitContext | InstallResult => { } if (!gitRoot || !gitCommonDirectory || !effectiveHooksDirectory) { - return fail('git-command-failed', 'Failed to resolve the Git repository paths.'); + return fail( + 'git-command-failed', + 'Failed to resolve the Git repository paths.', + ); } return { defaultHooksDirectory: path.join(gitCommonDirectory, 'hooks'), effectiveHooksDirectory, gitRoot, - projectPath: repositoryPrefix.replaceAll('\\', '/').replace(/\/$/u, '') || '.', + projectPath: + repositoryPrefix.replaceAll('\\', '/').replace(/\/$/u, '') || '.', }; }; -const isCurrentFile = (filePath: string, content: string, executable = false): boolean => { +const isCurrentFile = ( + filePath: string, + content: string, + executable = false, +): boolean => { try { // Windows does not expose POSIX executable bits, but Git for Windows still runs hook shims. return ( readFileSync(filePath, 'utf8') === content && - (!executable || process.platform === 'win32' || (statSync(filePath).mode & 0o777) === 0o755) + (!executable || + process.platform === 'win32' || + (statSync(filePath).mode & 0o777) === 0o755) ); } catch { return false; @@ -153,7 +181,9 @@ const readOwner = (directory: string): string | undefined => { try { const content = readFileSync(path.join(directory, ownerFileName), 'utf8'); const owner = removeLineEnding(content); - return content === `${owner}\n` && owner.length > 0 && !/[\r\n]/u.test(owner) + return content === `${owner}\n` && + owner.length > 0 && + !/[\r\n]/u.test(owner) ? owner : undefined; } catch { @@ -163,13 +193,21 @@ const readOwner = (directory: string): string | undefined => { const displayPath = (gitRoot: string, filePath: string): string => { const relativePath = path.relative(gitRoot, filePath).replaceAll('\\', '/'); - return relativePath.length > 0 && !relativePath.startsWith('../') ? relativePath : filePath; + return relativePath.length > 0 && !relativePath.startsWith('../') + ? relativePath + : filePath; }; const ownerConflict = (project: string): SkippedInstallResult => - skip('owned-by-another-project', `Git hooks are already managed by Rstack project "${project}"`); + skip( + 'owned-by-another-project', + `Git hooks are already managed by Rstack project "${project}"`, + ); -const directoryConflict = (gitRoot: string, directory: string): SkippedInstallResult => +const directoryConflict = ( + gitRoot: string, + directory: string, +): SkippedInstallResult => skip( 'hooks-directory-conflict', `the hooks directory "${displayPath(gitRoot, directory)}" is not managed by Rstack`, @@ -191,7 +229,8 @@ const claimOwner = ( // Exclusive creation makes concurrent prepare scripts agree on one owner. writeFileSync(ownerPath, `${project}\n`, { flag: 'wx' }); } catch (error) { - const code = error instanceof Error && 'code' in error ? error.code : undefined; + const code = + error instanceof Error && 'code' in error ? error.code : undefined; if (code !== 'EEXIST') { throw error; } @@ -200,7 +239,9 @@ const claimOwner = ( if (!concurrentOwner) { return directoryConflict(gitRoot, directory); } - return concurrentOwner === project ? undefined : ownerConflict(concurrentOwner); + return concurrentOwner === project + ? undefined + : ownerConflict(concurrentOwner); } return undefined; @@ -228,11 +269,19 @@ export const installHooks = ({ return context; } - const { defaultHooksDirectory, effectiveHooksDirectory, gitRoot, projectPath } = context; + const { + defaultHooksDirectory, + effectiveHooksDirectory, + gitRoot, + projectPath, + } = context; const hooksPath = `${resolvedDir}/${generatedDirectoryName}`; const directory = path.join(gitRoot, resolvedDir, generatedDirectoryName); const hooksPathMatches = isSamePath(effectiveHooksDirectory, directory); - const usesDefaultHooks = isSamePath(effectiveHooksDirectory, defaultHooksDirectory); + const usesDefaultHooks = isSamePath( + effectiveHooksDirectory, + defaultHooksDirectory, + ); if (!hooksPathMatches && !usesDefaultHooks) { const activeOwner = readOwner(effectiveHooksDirectory); @@ -269,7 +318,9 @@ export const installHooks = ({ const unchanged = hooksPathMatches && isCurrentFile(path.join(directory, '.gitignore'), gitignore) && - files.every(([name, content]) => isCurrentFile(path.join(directory, name), content, true)); + files.every(([name, content]) => + isCurrentFile(path.join(directory, name), content, true), + ); if (unchanged) { return { status: 'unchanged', hooksPath }; } @@ -293,7 +344,12 @@ export const installHooks = ({ } // Point Git at the generated directory only after every runtime file is ready. - const configured = runGit(cwd, ['config', '--local', 'core.hooksPath', hooksPath]); + const configured = runGit(cwd, [ + 'config', + '--local', + 'core.hooksPath', + hooksPath, + ]); if (configured.error || configured.status === null) { return gitFailure(configured.error, configured.stderr); } diff --git a/packages/rstack/src/staged.ts b/packages/rstack/src/staged.ts index b1a8e6dc..e364c490 100644 --- a/packages/rstack/src/staged.ts +++ b/packages/rstack/src/staged.ts @@ -3,13 +3,16 @@ import { parseArgs } from './cli/args.ts'; import { printCommandHelp } from './cli/help.ts'; import { loadRstackConfig } from './config.ts'; -export type StagedSyncTaskGenerator = (stagedFileNames: readonly string[]) => string | string[]; +export type StagedSyncTaskGenerator = ( + stagedFileNames: readonly string[], +) => string | string[]; export type StagedAsyncTaskGenerator = ( stagedFileNames: readonly string[], ) => Promise; -export type StagedTaskGenerator = StagedSyncTaskGenerator | StagedAsyncTaskGenerator; +export type StagedTaskGenerator = + StagedSyncTaskGenerator | StagedAsyncTaskGenerator; export type StagedFunctionTask = { title: string; @@ -17,7 +20,10 @@ export type StagedFunctionTask = { }; export type StagedTask = - string | StagedFunctionTask | StagedTaskGenerator | (string | StagedTaskGenerator)[]; + | string + | StagedFunctionTask + | StagedTaskGenerator + | (string | StagedTaskGenerator)[]; export type StagedConfig = Record | StagedTaskGenerator; @@ -57,7 +63,10 @@ export async function runStagedCLI(args: string[]): Promise { const success = await lintStaged({ allowEmpty: values.allowEmpty, - concurrent: values.concurrent === undefined ? undefined : JSON.parse(values.concurrent), + concurrent: + values.concurrent === undefined + ? undefined + : (JSON.parse(values.concurrent) as boolean | number), config: stagedConfig, cwd: values.cwd, debug: values.debug, diff --git a/packages/rstack/tests/cli/args.test.ts b/packages/rstack/tests/cli/args.test.ts index 2063dce9..93c4db5b 100644 --- a/packages/rstack/tests/cli/args.test.ts +++ b/packages/rstack/tests/cli/args.test.ts @@ -4,17 +4,20 @@ import { parseArgs } from '../../src/cli/args.ts'; test.each([ ['--long-option', 'kebab'], ['--longOption', 'camel'], -] as const)('accepts %s and returns only a camel-case value', (option, value) => { - const { values } = parseArgs({ - args: [option, value], - options: { - 'long-option': { type: 'string' }, - }, - }); +] as const)( + 'accepts %s and returns only a camel-case value', + (option, value) => { + const { values } = parseArgs({ + args: [option, value], + options: { + 'long-option': { type: 'string' }, + }, + }); - expect(values).toEqual({ longOption: value }); - expect('long-option' in values).toBe(false); -}); + expect(values).toEqual({ longOption: value }); + expect('long-option' in values).toBe(false); + }, +); test('combines repeated kebab-case and camel-case values', () => { const { values } = parseArgs({ diff --git a/packages/rstack/tests/cli/check.test.ts b/packages/rstack/tests/cli/check.test.ts index 87b92845..a2b975a9 100644 --- a/packages/rstack/tests/cli/check.test.ts +++ b/packages/rstack/tests/cli/check.test.ts @@ -65,7 +65,9 @@ test('enables type checking only with --type-check', () => { expect(withoutTypeCheck.status).toBe(0); expect(withTypeCheck.status).toBe(1); - expect(`${withTypeCheck.stdout}\n${withTypeCheck.stderr}`).toContain('TS2322'); + expect(`${withTypeCheck.stdout}\n${withTypeCheck.stderr}`).toContain( + 'TS2322', + ); }); test('does not run the formatting check when lint fails', () => { @@ -75,6 +77,8 @@ test('does not run the formatting check when lint fails', () => { const result = runCheck(); expect(result.status).toBe(1); - expect(`${result.stdout}\n${result.stderr}`).toContain("Unexpected 'debugger' statement"); + expect(`${result.stdout}\n${result.stderr}`).toContain( + "Unexpected 'debugger' statement", + ); expect(result.stdout).not.toContain('Checking formatting...'); }); diff --git a/packages/rstack/tests/cli/fmt/cache.test.ts b/packages/rstack/tests/cli/fmt/cache.test.ts index 15e51dea..d5f98a3c 100644 --- a/packages/rstack/tests/cli/fmt/cache.test.ts +++ b/packages/rstack/tests/cli/fmt/cache.test.ts @@ -1,8 +1,41 @@ import { expect, test } from 'rstack/test'; -import { expectWriteSummary, normalizeDuration, setupFmtTest } from './helpers.ts'; - -const { projectFileExists, readProjectFile, resolveProjectPath, runFmt, writeProjectFile } = - setupFmtTest(); +import { + expectWriteSummary, + normalizeDuration, + setupFmtTest, +} from './helpers.ts'; + +const { + projectFileExists, + readProjectFile, + resolveProjectPath, + runFmt, + writeProjectFile, +} = setupFmtTest(); + +interface SerializedFmtCache { + version: number; + namespace: string; + options: string[]; + files: (string | number)[]; +} + +const readFmtCache = (filePath: string): SerializedFmtCache => + JSON.parse(readProjectFile(filePath)) as SerializedFmtCache; + +const expectSingleCleanEntry = ( + cache: SerializedFmtCache, + filePath: string, +): void => { + expect(cache.version).toBe(2); + expect(typeof cache.namespace).toBe('string'); + expect(cache.options).toHaveLength(1); + expect(cache.options[0]).toHaveLength(16); + expect(cache.files).toHaveLength(4); + expect(cache.files[0]).toBe(filePath); + expect(cache.files[1]).toEqual(expect.any(String)); + expect(cache.files.slice(2)).toEqual([0, 0]); +}; test.each([ ['write', []], @@ -16,12 +49,10 @@ test.each([ expect(result.status).toBe(0); expect(readProjectFile('.rstack/cache/.gitignore')).toBe('*\n'); - expect(JSON.parse(readProjectFile('.rstack/cache/fmt/v1.json'))).toMatchObject({ - version: 1, - files: { - 'index.ts': [expect.any(String), expect.any(String), 'clean'], - }, - }); + expectSingleCleanEntry( + readFmtCache('.rstack/cache/fmt/cache.json'), + 'index.ts', + ); expect(readProjectFile('.rstack/cache/fmt-v1.json')).toBe('legacy'); }); @@ -51,74 +82,86 @@ test('--no-cache bypasses cache reads and writes', () => { expect(projectFileExists('.rstack/cache/.gitignore')).toBe(false); }); -test.each(['relative', 'absolute'] as const)('uses a %s custom cache location', (kind) => { - const cacheLocation = kind === 'relative' ? 'custom-cache' : resolveProjectPath('custom-cache'); - writeProjectFile('index.ts', 'const value = 1;\n'); - - const result = runFmt(['--cache-location', cacheLocation, 'index.ts']); - - expect(result.status).toBe(0); - expect(JSON.parse(readProjectFile('custom-cache/v1.json'))).toMatchObject({ - version: 1, - files: { - 'index.ts': [expect.any(String), expect.any(String), 'clean'], - }, - }); - expect(projectFileExists('custom-cache/.gitignore')).toBe(false); - expect(projectFileExists('.rstack')).toBe(false); -}); - -test.each(['.', '..'])('rejects a custom cache location at %s', (cacheLocation) => { - const result = runFmt(['--cache-location', cacheLocation, '.']); - - expect(result.status).toBe(2); - expect(result.stdout).toBe(''); - expect(result.stderr).toContain( - 'The --cache-location directory cannot be the current working directory or an ancestor.', - ); -}); +test.each(['relative', 'absolute'] as const)( + 'uses a %s custom cache location', + (kind) => { + const cacheLocation = + kind === 'relative' ? 'custom-cache' : resolveProjectPath('custom-cache'); + writeProjectFile('index.ts', 'const value = 1;\n'); + + const result = runFmt(['--cache-location', cacheLocation, 'index.ts']); + + expect(result.status).toBe(0); + expectSingleCleanEntry(readFmtCache('custom-cache/cache.json'), 'index.ts'); + expect(projectFileExists('custom-cache/.gitignore')).toBe(false); + expect(projectFileExists('.rstack')).toBe(false); + }, +); + +test.each(['.', '..'])( + 'rejects a custom cache location at %s', + (cacheLocation) => { + const result = runFmt(['--cache-location', cacheLocation, '.']); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain( + 'The --cache-location directory cannot be the current working directory or an ancestor.', + ); + }, +); test('excludes the custom cache directory from formatting', () => { const cacheLocation = 'custom-cache'; writeProjectFile('index.ts', 'const value = 1;\n'); writeProjectFile('custom-cache/nested/ignored.ts', 'const value=2'); - expect(runFmt(['--cache-location', cacheLocation, 'index.ts']).status).toBe(0); + expect(runFmt(['--cache-location', cacheLocation, 'index.ts']).status).toBe( + 0, + ); const result = runFmt(['--cache-location', cacheLocation, '.']); expect(result.status).toBe(0); expectWriteSummary(result.stdout, 2, 0); - expect(readProjectFile('custom-cache/nested/ignored.ts')).toBe('const value=2'); + expect(readProjectFile('custom-cache/nested/ignored.ts')).toBe( + 'const value=2', + ); }); test('uses an explicit config root cache from a subdirectory', () => { const appPath = resolveProjectPath('packages/app'); writeProjectFile('packages/app/index.ts', 'const value=1'); - const result = runFmt(['index.ts', '--config', '../../rstack.config.ts'], appPath); + const result = runFmt( + ['index.ts', '--config', '../../rstack.config.ts'], + appPath, + ); expect(result.status).toBe(0); expect(readProjectFile('packages/app/index.ts')).toBe('const value = 1;\n'); - expect(projectFileExists('.rstack/cache/fmt/v1.json')).toBe(true); + expect(projectFileExists('.rstack/cache/fmt/cache.json')).toBe(true); expect(projectFileExists('packages/app/.rstack')).toBe(false); - expect(JSON.parse(readProjectFile('.rstack/cache/fmt/v1.json'))).toMatchObject({ - files: { - 'packages/app/index.ts': [expect.any(String), expect.any(String), 'clean'], - }, - }); + expectSingleCleanEntry( + readFmtCache('.rstack/cache/fmt/cache.json'), + 'packages/app/index.ts', + ); }); test('recovers from a corrupted cache', () => { writeProjectFile('index.ts', 'const value = 1;\n'); const first = runFmt(['--check', 'index.ts']); - writeProjectFile('.rstack/cache/fmt/v1.json', '{'); + writeProjectFile('.rstack/cache/fmt/cache.json', '{'); const second = runFmt(['--check', 'index.ts']); expect(second.status).toBe(0); - expect(normalizeDuration(second.stdout)).toBe(normalizeDuration(first.stdout)); + expect(normalizeDuration(second.stdout)).toBe( + normalizeDuration(first.stdout), + ); expect(second.stderr).toBe(first.stderr); - expect(JSON.parse(readProjectFile('.rstack/cache/fmt/v1.json'))).toMatchObject({ version: 1 }); + expect( + JSON.parse(readProjectFile('.rstack/cache/fmt/cache.json')), + ).toMatchObject({ version: 2 }); }); test('formats without a writable cache directory', () => { diff --git a/packages/rstack/tests/cli/fmt/config.test.ts b/packages/rstack/tests/cli/fmt/config.test.ts index d5947aae..5cda6c72 100644 --- a/packages/rstack/tests/cli/fmt/config.test.ts +++ b/packages/rstack/tests/cli/fmt/config.test.ts @@ -6,7 +6,8 @@ import { sortedPackageJson, } from './helpers.ts'; -const { readProjectFile, runFmt, writeFixturePlugin, writeProjectFile } = setupFmtTest(); +const { readProjectFile, runFmt, writeFixturePlugin, writeProjectFile } = + setupFmtTest(); test('does not sort package.json by default', () => { writeProjectFile('package.json', packageJsonSource); @@ -35,7 +36,9 @@ define.fmt({ sortPackageJson: true }); expect(result.status).toBe(0); expect(result.stderr).toBe(''); expect(readProjectFile('package.json')).toBe(sortedPackageJson); - expect(readProjectFile('packages/example/package.json')).toBe(sortedPackageJson); + expect(readProjectFile('packages/example/package.json')).toBe( + sortedPackageJson, + ); }); test('supports configuring the worker count', () => { @@ -52,17 +55,28 @@ test('supports configuring the worker count', () => { }); test('does not load Prettier config or ignore files', () => { - writeProjectFile('.prettierrc.json', '{ "singleQuote": true, "semi": false }\n'); + writeProjectFile( + '.prettierrc.json', + '{ "singleQuote": true, "semi": false }\n', + ); writeProjectFile('.prettierignore', 'index.ts\n'); - writeProjectFile('.editorconfig', 'root = true\n\n[*]\nindent_style = space\nindent_size = 8\n'); - writeProjectFile('index.ts', "function getMessage(){\n return 'hello'\n}"); + writeProjectFile( + '.editorconfig', + 'root = true\n\n[*]\nindent_style = space\nindent_size = 8\n', + ); + writeProjectFile( + 'index.ts', + "function getMessage(){\n return 'hello'\n}", + ); const result = runFmt(['index.ts']); expect(result.status).toBe(0); expectWriteSummary(result.stdout, 1, 1); expect(result.stderr).toBe(''); - expect(readProjectFile('index.ts')).toBe('function getMessage() {\n return "hello";\n}\n'); + expect(readProjectFile('index.ts')).toBe( + 'function getMessage() {\n return "hello";\n}\n', + ); }); test('applies repeated ignore paths', () => { @@ -84,8 +98,12 @@ test('applies repeated ignore paths', () => { expect(result.status).toBe(0); expectWriteSummary(result.stdout, 1, 1); expect(result.stderr).toBe(''); - expect(readProjectFile('src/ignored-by-root.ts')).toBe('const root="ignored"'); - expect(readProjectFile('src/ignored-by-extra.ts')).toBe('const extra="ignored"'); + expect(readProjectFile('src/ignored-by-root.ts')).toBe( + 'const root="ignored"', + ); + expect(readProjectFile('src/ignored-by-extra.ts')).toBe( + 'const extra="ignored"', + ); expect(readProjectFile('src/index.ts')).toBe('const index = "formatted";\n'); }); @@ -96,7 +114,9 @@ test('returns exit code 2 for an unreadable ignore path', () => { expect(result.status).toBe(2); expect(result.stdout).toBe(''); - expect(result.stderr).toContain('Failed to read ignore file "missing.ignore".'); + expect(result.stderr).toContain( + 'Failed to read ignore file "missing.ignore".', + ); expect(readProjectFile('index.ts')).toBe('const value=true'); }); @@ -156,7 +176,10 @@ define.fmt({ }); test('returns exit code 2 for config errors', () => { - writeProjectFile('rstack.config.ts', 'throw new Error("invalid fmt config");\n'); + writeProjectFile( + 'rstack.config.ts', + 'throw new Error("invalid fmt config");\n', + ); const result = runFmt(['index.ts']); diff --git a/packages/rstack/tests/cli/fmt/files.test.ts b/packages/rstack/tests/cli/fmt/files.test.ts index d650eac1..f0368df4 100644 --- a/packages/rstack/tests/cli/fmt/files.test.ts +++ b/packages/rstack/tests/cli/fmt/files.test.ts @@ -1,6 +1,10 @@ import { expect, test } from 'rstack/test'; import { normalizeHelpOutput } from '#test-helpers'; -import { expectWriteSummary, normalizeDuration, setupFmtTest } from './helpers.ts'; +import { + expectWriteSummary, + normalizeDuration, + setupFmtTest, +} from './helpers.ts'; const { readProjectFile, runCLI, runFmt, writeProjectFile } = setupFmtTest(); @@ -77,7 +81,9 @@ test('formats files in node_modules with --with-node-modules', () => { expect(result.status).toBe(0); expectWriteSummary(result.stdout, 1, 1); expect(result.stderr).toBe(''); - expect(readProjectFile('node_modules/example/index.ts')).toBe('const message = "hello";\n'); + expect(readProjectFile('node_modules/example/index.ts')).toBe( + 'const message = "hello";\n', + ); }); test('summarizes write mode when no files change', () => { @@ -116,18 +122,21 @@ test('checks formatting without writing files', () => { expect(formattedResult.stderr).toBe(''); }); -test.each(['-l', '--list-different'])('lists only paths that differ with %s', (option) => { - const source = 'const message="hello"'; - writeProjectFile('src/index.ts', source); - writeProjectFile('src/formatted.ts', 'const formatted = true;\n'); - - const result = runFmt([option, 'src/*.ts']); - - expect(result.status).toBe(1); - expect(result.stdout).toBe('src/index.ts\n'); - expect(result.stderr).toBe(''); - expect(readProjectFile('src/index.ts')).toBe(source); -}); +test.each(['-l', '--list-different'])( + 'lists only paths that differ with %s', + (option) => { + const source = 'const message="hello"'; + writeProjectFile('src/index.ts', source); + writeProjectFile('src/formatted.ts', 'const formatted = true;\n'); + + const result = runFmt([option, 'src/*.ts']); + + expect(result.status).toBe(1); + expect(result.stdout).toBe('src/index.ts\n'); + expect(result.stderr).toBe(''); + expect(readProjectFile('src/index.ts')).toBe(source); + }, +); test('returns exit code 2 for formatting errors', () => { writeProjectFile('index.ts', 'const value = ;'); diff --git a/packages/rstack/tests/cli/fmt/helpers.ts b/packages/rstack/tests/cli/fmt/helpers.ts index c33f7ecf..d3b62229 100644 --- a/packages/rstack/tests/cli/fmt/helpers.ts +++ b/packages/rstack/tests/cli/fmt/helpers.ts @@ -1,5 +1,12 @@ import { type SpawnSyncReturns, spawnSync } from 'node:child_process'; -import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from 'node:fs'; import path from 'node:path'; import { afterEach, beforeEach, expect } from 'rstack/test'; import { RSTACK_BIN_PATH } from '#test-helpers'; @@ -9,7 +16,11 @@ export const packageJsonSource = export const sortedPackageJson = '{\n "name": "fixture",\n "version": "1.0.0",\n "type": "module",\n "dependencies": {\n "a": "1.0.0",\n "z": "1.0.0"\n }\n}\n'; -type RunCLI = (args: string[], input?: string, cwd?: string) => SpawnSyncReturns; +type RunCLI = ( + args: string[], + input?: string, + cwd?: string, +) => SpawnSyncReturns; type FmtTestHarness = { projectFileExists: (filePath: string) => boolean; @@ -42,15 +53,19 @@ export const expectWriteSummary = ( const message = writtenCount ? `Formatted ${writtenCount} of ${matchedFileCount} ${files} in .` : `Checked ${matchedFileCount} ${files} in . No changes needed.`; - expect(normalizeDuration(output)).toBe(`start Formatting...\nsuccess ${message}\n`); + expect(normalizeDuration(output)).toBe( + `start Formatting...\nsuccess ${message}\n`, + ); }; export const setupFmtTest = (): FmtTestHarness => { let projectPath: string; - const resolveProjectPath = (filePath: string): string => path.join(projectPath, filePath); + const resolveProjectPath = (filePath: string): string => + path.join(projectPath, filePath); - const projectFileExists = (filePath: string): boolean => existsSync(resolveProjectPath(filePath)); + const projectFileExists = (filePath: string): boolean => + existsSync(resolveProjectPath(filePath)); const writeProjectFile = (filePath: string, content: string): void => { const absolutePath = resolveProjectPath(filePath); @@ -64,7 +79,10 @@ export const setupFmtTest = (): FmtTestHarness => { const writeFixturePlugin = (): void => { writeProjectFile( 'node_modules/prettier-plugin-fixture/package.json', - JSON.stringify({ name: 'prettier-plugin-fixture', exports: './index.mjs' }), + JSON.stringify({ + name: 'prettier-plugin-fixture', + exports: './index.mjs', + }), ); writeProjectFile( 'node_modules/prettier-plugin-fixture/index.mjs', @@ -86,7 +104,8 @@ export const setupFmtTest = (): FmtTestHarness => { const runFmt = (args: string[] = [], cwd = projectPath) => runCLI(['fmt', ...args], undefined, cwd); - const runFmtStdin = (args: string[], input: string) => runCLI(['fmt', ...args], input); + const runFmtStdin = (args: string[], input: string) => + runCLI(['fmt', ...args], input); beforeEach(() => { projectPath = mkdtempSync(path.join(import.meta.dirname, 'test-temp-fmt-')); diff --git a/packages/rstack/tests/cli/fmt/lsp.test.ts b/packages/rstack/tests/cli/fmt/lsp.test.ts index 4309feca..4e0ba51d 100644 --- a/packages/rstack/tests/cli/fmt/lsp.test.ts +++ b/packages/rstack/tests/cli/fmt/lsp.test.ts @@ -1,6 +1,11 @@ import { expect, test } from 'rstack/test'; import { setupFmtTest } from './helpers.ts'; -import { applyTextEdits, type LspClient, startLspServer, toFileUri } from './lspClient.ts'; +import { + applyTextEdits, + type LspClient, + startLspServer, + toFileUri, +} from './lspClient.ts'; const { resolveProjectPath, runFmt, writeProjectFile } = setupFmtTest(); @@ -27,7 +32,11 @@ const withLspServer = async ( } }; -const openDocument = (client: LspClient, filePath: string, text: string): string => { +const openDocument = ( + client: LspClient, + filePath: string, + text: string, +): string => { const uri = toFileUri(resolveProjectPath(filePath)); client.openDocument(uri, 'typescript', text); @@ -87,7 +96,9 @@ test( const edits = await client.formatDocument(uri); - expect(applyTextEdits(source, edits)).toBe('const inBuffer = "buffer";\n'); + expect(applyTextEdits(source, edits)).toBe( + 'const inBuffer = "buffer";\n', + ); }); }, TEST_TIMEOUT, @@ -252,8 +263,16 @@ test( await withLspServer( async (client) => { await client.initialize(resolveProjectPath('.')); - const ignoredUri = openDocument(client, 'src/ignored.ts', 'const ignored="ignored"\n'); - const formattedUri = openDocument(client, 'src/index.ts', 'const x=1\n'); + const ignoredUri = openDocument( + client, + 'src/ignored.ts', + 'const ignored="ignored"\n', + ); + const formattedUri = openDocument( + client, + 'src/index.ts', + 'const x=1\n', + ); expect(await client.formatDocument(ignoredUri)).toEqual([]); // The ignore file was read rather than reported as missing. @@ -330,7 +349,11 @@ define.fmt({ ignorePatterns: ['src/ignored.ts'] }); await withLspServer(async (client) => { await client.initialize(); - const uri = openDocument(client, 'src/ignored.ts', 'const ignored="ignored"\n'); + const uri = openDocument( + client, + 'src/ignored.ts', + 'const ignored="ignored"\n', + ); expect(await client.formatDocument(uri)).toEqual([]); }); @@ -379,12 +402,16 @@ test('returns exit code 2 for file arguments with --lsp', () => { const result = runFmt(['--lsp', 'src/index.ts']); expect(result.status).toBe(2); - expect(result.stderr).toContain('The --lsp option cannot be used with file arguments.'); + expect(result.stderr).toContain( + 'The --lsp option cannot be used with file arguments.', + ); }); test('returns exit code 2 for --stdin-filepath with --lsp', () => { const result = runFmt(['--lsp', '--stdin-filepath', 'src/index.ts']); expect(result.status).toBe(2); - expect(result.stderr).toContain('The --lsp option cannot be used with --stdin-filepath.'); + expect(result.stderr).toContain( + 'The --lsp option cannot be used with --stdin-filepath.', + ); }); diff --git a/packages/rstack/tests/cli/fmt/lspClient.ts b/packages/rstack/tests/cli/fmt/lspClient.ts index 31c2bb58..c2ac899a 100644 --- a/packages/rstack/tests/cli/fmt/lspClient.ts +++ b/packages/rstack/tests/cli/fmt/lspClient.ts @@ -13,14 +13,19 @@ type JsonRpcMessage = { }; export type Position = { line: number; character: number }; -export type TextEdit = { range: { start: Position; end: Position }; newText: string }; +export type TextEdit = { + range: { start: Position; end: Position }; + newText: string; +}; export type ShownMessage = { type: number; message: string }; export type LspClient = { notify: (method: string, params: unknown) => void; /** Initializes the server with `root` as the workspace root; defaults to the spawn cwd. */ - initialize: (root?: string) => Promise<{ capabilities: Record }>; + initialize: ( + root?: string, + ) => Promise<{ capabilities: Record }>; openDocument: (uri: string, languageId: string, text: string) => void; formatDocument: (uri: string) => Promise; /** `window/showMessage` notifications received so far, in order. */ @@ -34,13 +39,19 @@ const CONTENT_LENGTH_REGEXP = /content-length:\s*(\d+)/i; /** A header block is `key: value` lines separated by `\r\n` and nothing else. */ const HEADER_BLOCK_REGEXP = /^[^\r\n:]+:[^\r\n]*(?:\r\n[^\r\n:]+:[^\r\n]*)*$/; -export const toFileUri = (filePath: string): string => pathToFileURL(filePath).href; +export const toFileUri = (filePath: string): string => + pathToFileURL(filePath).href; /** Applies LSP text edits to a document, mirroring an editor. */ export const applyTextEdits = (text: string, edits: TextEdit[]): string => - TextDocument.applyEdits(TextDocument.create('file:///document', 'plaintext', 1, text), edits); + TextDocument.applyEdits( + TextDocument.create('file:///document', 'plaintext', 1, text), + edits, + ); -const readMessage = (buffer: Buffer): { message: JsonRpcMessage; rest: Buffer } | undefined => { +const readMessage = ( + buffer: Buffer, +): { message: JsonRpcMessage; rest: Buffer } | undefined => { const headerEnd = buffer.indexOf('\r\n\r\n'); if (headerEnd === -1) { return undefined; @@ -50,7 +61,9 @@ const readMessage = (buffer: Buffer): { message: JsonRpcMessage; rest: Buffer } // Real clients fall out of sync here, so anything that is not a header is a // failure rather than something to skip over. if (!HEADER_BLOCK_REGEXP.test(headers)) { - throw new Error(`Unexpected bytes on stdout before a message: ${JSON.stringify(headers)}.`); + throw new Error( + `Unexpected bytes on stdout before a message: ${JSON.stringify(headers)}.`, + ); } const contentLength = CONTENT_LENGTH_REGEXP.exec(headers); @@ -65,7 +78,9 @@ const readMessage = (buffer: Buffer): { message: JsonRpcMessage; rest: Buffer } } return { - message: JSON.parse(buffer.subarray(bodyStart, bodyEnd).toString('utf8')) as JsonRpcMessage, + message: JSON.parse( + buffer.subarray(bodyStart, bodyEnd).toString('utf8'), + ) as JsonRpcMessage, rest: buffer.subarray(bodyEnd), }; }; @@ -128,18 +143,26 @@ export const startLspServer = (cwd: string, args: string[] = []): LspClient => { const closed = new Promise((resolve) => { childProcess.once('close', (code) => { exitCode = code; - fail(new Error(`The language server exited with code ${code}.\n${stderr}`)); + fail( + new Error(`The language server exited with code ${code}.\n${stderr}`), + ); resolve(code); }); }); const send = (message: Record): void => { - const body = Buffer.from(JSON.stringify({ jsonrpc: '2.0', ...message }), 'utf8'); + const body = Buffer.from( + JSON.stringify({ jsonrpc: '2.0', ...message }), + 'utf8', + ); childProcess.stdin.write(`Content-Length: ${body.byteLength}\r\n\r\n`); childProcess.stdin.write(body); }; - const request = (method: string, params: unknown): Promise => { + const request = ( + method: string, + params: unknown, + ): Promise => { if (failure) { return Promise.reject(failure); } @@ -147,7 +170,10 @@ export const startLspServer = (cwd: string, args: string[] = []): LspClient => { const id = nextId++; return new Promise((resolve, reject) => { - pending.set(id, { resolve: resolve as (result: unknown) => void, reject }); + pending.set(id, { + resolve: resolve as (result: unknown) => void, + reject, + }); send({ id, method, params }); }); }; diff --git a/packages/rstack/tests/cli/fmt/patterns.test.ts b/packages/rstack/tests/cli/fmt/patterns.test.ts index 52ec4480..b87eee16 100644 --- a/packages/rstack/tests/cli/fmt/patterns.test.ts +++ b/packages/rstack/tests/cli/fmt/patterns.test.ts @@ -19,7 +19,11 @@ test('returns exit code 2 when no files match', () => { test('allows no files to match with --no-error-on-unmatched-pattern', () => { for (const modeArgs of [[], ['--check'], ['--list-different']]) { - const result = runFmt([...modeArgs, '--no-error-on-unmatched-pattern', 'missing/**/*.ts']); + const result = runFmt([ + ...modeArgs, + '--no-error-on-unmatched-pattern', + 'missing/**/*.ts', + ]); expect(result.status).toBe(0); expect(result.stdout).toBe(''); @@ -78,7 +82,9 @@ test('supports -u as an alias for --ignore-unknown', () => { const result = runFmt(['-u', 'notes.unknown']); expect(result.status).toBe(0); - expect(result.stdout).toBe('start Formatting...\nsuccess No supported files to format.\n'); + expect(result.stdout).toBe( + 'start Formatting...\nsuccess No supported files to format.\n', + ); expect(result.stderr).toBe(''); }); @@ -87,7 +93,9 @@ test('does not treat unmatched patterns as unknown files', () => { expect(result.status).toBe(2); expect(result.stdout).toBe(''); - expect(result.stderr).toContain('No supported files matched "missing/**/*.unknown"'); + expect(result.stderr).toContain( + 'No supported files matched "missing/**/*.unknown"', + ); }); test('does not treat unsupported files as unmatched patterns', () => { diff --git a/packages/rstack/tests/cli/fmt/stdin.test.ts b/packages/rstack/tests/cli/fmt/stdin.test.ts index 21e5a4c5..ebf0e676 100644 --- a/packages/rstack/tests/cli/fmt/stdin.test.ts +++ b/packages/rstack/tests/cli/fmt/stdin.test.ts @@ -1,10 +1,17 @@ import { expect, test } from 'rstack/test'; -import { packageJsonSource, setupFmtTest, sortedPackageJson } from './helpers.ts'; +import { + packageJsonSource, + setupFmtTest, + sortedPackageJson, +} from './helpers.ts'; const { projectFileExists, runFmtStdin, writeProjectFile } = setupFmtTest(); test('formats stdin for the given filepath', () => { - const result = runFmtStdin(['--stdin-filepath', 'src/index.ts'], 'const message="hello"'); + const result = runFmtStdin( + ['--stdin-filepath', 'src/index.ts'], + 'const message="hello"', + ); expect(result.status).toBe(0); expect(result.stdout).toBe('const message = "hello";\n'); @@ -31,7 +38,10 @@ define.fmt({ `, ); - const result = runFmtStdin(['--stdin-filepath', 'src/index.test.ts'], 'const test="test"'); + const result = runFmtStdin( + ['--stdin-filepath', 'src/index.test.ts'], + 'const test="test"', + ); expect(result.status).toBe(0); expect(result.stdout).toBe("const test = 'test'\n"); @@ -47,7 +57,10 @@ define.fmt({ sortPackageJson: true }); `, ); - const result = runFmtStdin(['--stdin-filepath', 'package.json'], packageJsonSource); + const result = runFmtStdin( + ['--stdin-filepath', 'package.json'], + packageJsonSource, + ); expect(result.status).toBe(0); expect(result.stdout).toBe(sortedPackageJson); @@ -99,11 +112,16 @@ test('returns exit code 2 when no parser can be inferred for stdin', () => { expect(result.status).toBe(2); expect(result.stdout).toBe(''); - expect(result.stderr).toContain('No parser could be inferred for "data.unknown".'); + expect(result.stderr).toContain( + 'No parser could be inferred for "data.unknown".', + ); }); test('ignores stdin when no parser can be inferred with --ignore-unknown', () => { - const result = runFmtStdin(['--stdin-filepath', 'data.unknown', '--ignore-unknown'], 'value'); + const result = runFmtStdin( + ['--stdin-filepath', 'data.unknown', '--ignore-unknown'], + 'value', + ); expect(result.status).toBe(0); expect(result.stdout).toBe(''); @@ -111,7 +129,10 @@ test('ignores stdin when no parser can be inferred with --ignore-unknown', () => }); test('returns exit code 2 for stdin parse errors', () => { - const result = runFmtStdin(['--stdin-filepath', 'index.ts'], 'const value = ;'); + const result = runFmtStdin( + ['--stdin-filepath', 'index.ts'], + 'const value = ;', + ); expect(result.status).toBe(2); expect(result.stdout).toBe(''); @@ -121,7 +142,10 @@ test('returns exit code 2 for stdin parse errors', () => { test.each(['--write', '--check', '--list-different'])( 'returns exit code 2 for %s with --stdin-filepath', (option) => { - const result = runFmtStdin(['--stdin-filepath', 'index.ts', option], 'const value=1'); + const result = runFmtStdin( + ['--stdin-filepath', 'index.ts', option], + 'const value=1', + ); expect(result.status).toBe(2); expect(result.stdout).toBe(''); @@ -132,7 +156,10 @@ test.each(['--write', '--check', '--list-different'])( ); test('returns exit code 2 for file arguments with --stdin-filepath', () => { - const result = runFmtStdin(['--stdin-filepath', 'index.ts', 'src/other.ts'], 'const value=1'); + const result = runFmtStdin( + ['--stdin-filepath', 'index.ts', 'src/other.ts'], + 'const value=1', + ); expect(result.status).toBe(2); expect(result.stdout).toBe(''); diff --git a/packages/rstack/tests/cli/fmt/vue.test.ts b/packages/rstack/tests/cli/fmt/vue.test.ts index ffa19c6a..bdfe2f79 100644 --- a/packages/rstack/tests/cli/fmt/vue.test.ts +++ b/packages/rstack/tests/cli/fmt/vue.test.ts @@ -6,13 +6,17 @@ const { readProjectFile, runFmt, writeProjectFile } = setupFmtTest(); test.each([ { name: 'TypeScript', - source: '\n', - expected: '\n', + source: + '\n', + expected: + '\n', }, { name: 'TSX', - source: '\n', - expected: '\n', + source: + '\n', + expected: + '\n', }, ])('formats $name embedded in Vue files', ({ source, expected }) => { writeProjectFile('App.vue', source); diff --git a/packages/rstack/tests/cli/setup/index.test.ts b/packages/rstack/tests/cli/setup/index.test.ts index 3f29104f..0780fd4a 100644 --- a/packages/rstack/tests/cli/setup/index.test.ts +++ b/packages/rstack/tests/cli/setup/index.test.ts @@ -1,5 +1,11 @@ import { spawnSync } from 'node:child_process'; -import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { + existsSync, + mkdirSync, + mkdtempSync, + rmSync, + writeFileSync, +} from 'node:fs'; import path from 'node:path'; import { afterEach, beforeEach } from 'rstack/test'; import { normalizeHelpOutput, RSTACK_BIN_PATH, test } from '#test-helpers'; @@ -61,7 +67,9 @@ test('reports missing and repeated hooks directory options', ({ expect }) => { const repeated = runSetup(['--hooks-dir', 'first', '--hooks-dir', 'second']); expect(repeated.status).toBe(1); - expect(repeated.stderr).toContain('The --hooks-dir option cannot be specified more than once.'); + expect(repeated.stderr).toContain( + 'The --hooks-dir option cannot be specified more than once.', + ); }); test('rejects invalid hooks directory options', ({ expect }) => { @@ -80,27 +88,42 @@ test('rejects invalid hooks directory options', ({ expect }) => { expect(parent.stderr).toContain('Git hooks directory must not contain "..".'); }); -test('installs hooks silently without loading Rstack config', ({ execCli, expect }) => { +test('installs hooks silently without loading Rstack config', ({ + execCli, + expect, +}) => { initRepository(); - writeFileSync(path.join(cwd, 'rstack.config.ts'), 'throw new Error("must not load");\n'); + writeFileSync( + path.join(cwd, 'rstack.config.ts'), + 'throw new Error("must not load");\n', + ); expect(execCli('setup', { cwd, env })).toBe(''); expect(git(['config', '--local', '--get', 'core.hooksPath'])).toBe(hooksPath); expect(existsSync(path.join(cwd, hooksPath, 'runner'))).toBe(true); - expect(existsSync(path.join(cwd, '.rstack', 'hooks', 'pre-commit'))).toBe(false); + expect(existsSync(path.join(cwd, '.rstack', 'hooks', 'pre-commit'))).toBe( + false, + ); expect(execCli('setup', { cwd, env })).toBe(''); }); -test('installs root-relative hooks and reports owner conflicts', ({ execCli, expect }) => { +test('installs root-relative hooks and reports owner conflicts', ({ + execCli, + expect, +}) => { initRepository(); const frontend = path.join(cwd, 'frontend'); const docs = path.join(cwd, 'docs'); mkdirSync(frontend); mkdirSync(docs); - expect(execCli('setup --hooks-dir "custom hooks"', { cwd: frontend, env })).toBe(''); - expect(git(['config', '--local', '--get', 'core.hooksPath'])).toBe('custom hooks/_'); + expect( + execCli('setup --hooks-dir "custom hooks"', { cwd: frontend, env }), + ).toBe(''); + expect(git(['config', '--local', '--get', 'core.hooksPath'])).toBe( + 'custom hooks/_', + ); expect(existsSync(path.join(cwd, 'custom hooks', '_', 'runner'))).toBe(true); const conflict = runSetup(['--hooks-dir', 'custom hooks'], docs); @@ -110,7 +133,10 @@ test('installs root-relative hooks and reports owner conflicts', ({ execCli, exp ); }); -test('skips non-Git directories without creating files', ({ execCli, expect }) => { +test('skips non-Git directories without creating files', ({ + execCli, + expect, +}) => { expect(execCli('setup', { cwd, env })).toContain( 'info Git hooks setup skipped: not a Git repository.', ); @@ -120,7 +146,9 @@ test('skips non-Git directories without creating files', ({ execCli, expect }) = test('skips setup when hooks are disabled', ({ execCli, expect }) => { const output = execCli('setup', { cwd, env: { ...env, RSTACK_HOOKS: '0' } }); - expect(output).toContain('info Git hooks setup skipped: disabled by RSTACK_HOOKS.'); + expect(output).toContain( + 'info Git hooks setup skipped: disabled by RSTACK_HOOKS.', + ); expect(existsSync(path.join(cwd, '.rstack'))).toBe(false); }); diff --git a/packages/rstack/tests/cli/specify-config/index.test.ts b/packages/rstack/tests/cli/specify-config/index.test.ts index 99a4d5e2..504f991c 100644 --- a/packages/rstack/tests/cli/specify-config/index.test.ts +++ b/packages/rstack/tests/cli/specify-config/index.test.ts @@ -1,7 +1,11 @@ import { getDistFiles, getFileContent } from '@rstackjs/test-utils'; import { test } from '#test-helpers'; -test('should build with rstack --config', async ({ prepareDist, execCli, expect }) => { +test('should build with rstack --config', async ({ + prepareDist, + execCli, + expect, +}) => { const distPath = await prepareDist(); execCli('build --config ./custom.config.ts'); diff --git a/packages/rstack/tests/cli/staged/fmt.test.ts b/packages/rstack/tests/cli/staged/fmt.test.ts index fa156f59..875a5e27 100644 --- a/packages/rstack/tests/cli/staged/fmt.test.ts +++ b/packages/rstack/tests/cli/staged/fmt.test.ts @@ -21,7 +21,9 @@ const git = (args: string[]): string => { }); if (result.status !== 0) { - throw new Error(result.stderr || `Git exited with status ${result.status}.`); + throw new Error( + result.stderr || `Git exited with status ${result.status}.`, + ); } return result.stdout; @@ -35,7 +37,9 @@ const runStaged = () => }); beforeEach(() => { - projectPath = mkdtempSync(path.join(import.meta.dirname, 'test-temp-staged-fmt-')); + projectPath = mkdtempSync( + path.join(import.meta.dirname, 'test-temp-staged-fmt-'), + ); env = { ...process.env, GIT_CONFIG_GLOBAL: path.join(projectPath, 'global.gitconfig'), @@ -74,11 +78,21 @@ test('formats staged files with rs fmt and applies ignore rules', () => { const result = runStaged(); expect(result.status).toBe(0); - expect(readProjectFile('file with spaces.ts')).toBe('const spaced = "spaced";\n'); - expect(readProjectFile('ignored-by-git.ts')).toBe('const gitIgnored = "git ignored";\n'); - expect(readProjectFile('ignored-by-fmt.ts')).toBe('const fmtIgnored="fmt ignored"'); - expect(git(['show', ':file with spaces.ts'])).toBe('const spaced = "spaced";\n'); - expect(git(['show', ':ignored-by-git.ts'])).toBe('const gitIgnored = "git ignored";\n'); + expect(readProjectFile('file with spaces.ts')).toBe( + 'const spaced = "spaced";\n', + ); + expect(readProjectFile('ignored-by-git.ts')).toBe( + 'const gitIgnored = "git ignored";\n', + ); + expect(readProjectFile('ignored-by-fmt.ts')).toBe( + 'const fmtIgnored="fmt ignored"', + ); + expect(git(['show', ':file with spaces.ts'])).toBe( + 'const spaced = "spaced";\n', + ); + expect(git(['show', ':ignored-by-git.ts'])).toBe( + 'const gitIgnored = "git ignored";\n', + ); }); test('allows rs fmt when all staged files are ignored', () => { @@ -91,7 +105,9 @@ test('allows rs fmt when all staged files are ignored', () => { expect(result.status).toBe(0); expect(readProjectFile('ignored-by-fmt.ts')).toBe(source); expect(git(['show', ':ignored-by-fmt.ts'])).toBe(source); - expect(`${result.stdout}\n${result.stderr}`).not.toContain('No supported files matched'); + expect(`${result.stdout}\n${result.stderr}`).not.toContain( + 'No supported files matched', + ); }); test('still rejects staged files unsupported by rs fmt', () => { @@ -101,7 +117,9 @@ test('still rejects staged files unsupported by rs fmt', () => { const result = runStaged(); expect(result.status).toBe(1); - expect(`${result.stdout}\n${result.stderr}`).toContain('No supported files matched'); + expect(`${result.stdout}\n${result.stderr}`).toContain( + 'No supported files matched', + ); }); test('allows staged files unsupported by rs fmt with --ignore-unknown', () => { @@ -120,7 +138,9 @@ define.staged({ const result = runStaged(); expect(result.status).toBe(0); - expect(`${result.stdout}\n${result.stderr}`).not.toContain('No supported files matched'); + expect(`${result.stdout}\n${result.stderr}`).not.toContain( + 'No supported files matched', + ); }); test('propagates rs fmt failures', () => { diff --git a/packages/rstack/tests/cli/staged/index.test.ts b/packages/rstack/tests/cli/staged/index.test.ts index 5fb293f1..b43a928b 100644 --- a/packages/rstack/tests/cli/staged/index.test.ts +++ b/packages/rstack/tests/cli/staged/index.test.ts @@ -58,9 +58,9 @@ test('should pass default options to lint-staged', async ({ expect }) => { }); test('should set the staged environment', async ({ expect }) => { - mocks.lintStaged.mockImplementation(async () => { + mocks.lintStaged.mockImplementation(() => { expect(process.env.RSTACK_STAGED).toBe('1'); - return true; + return Promise.resolve(true); }); await runStagedCLI([]); diff --git a/packages/rstack/tests/config/define-app-lib/index.test.ts b/packages/rstack/tests/config/define-app-lib/index.test.ts index 99f6a8a5..3ed77ad4 100644 --- a/packages/rstack/tests/config/define-app-lib/index.test.ts +++ b/packages/rstack/tests/config/define-app-lib/index.test.ts @@ -1,5 +1,7 @@ import { test } from '#test-helpers'; -test('should prefer define.app when app and lib are both defined', ({ execCli }) => { +test('should prefer define.app when app and lib are both defined', ({ + execCli, +}) => { execCli('test'); }); diff --git a/packages/rstack/tests/config/define-app/index.test.ts b/packages/rstack/tests/config/define-app/index.test.ts index 73bbe51f..745ea7bd 100644 --- a/packages/rstack/tests/config/define-app/index.test.ts +++ b/packages/rstack/tests/config/define-app/index.test.ts @@ -4,7 +4,11 @@ import { test } from '#test-helpers'; const expectedText = 'define.app works'; -test('should build app with define.app config', async ({ prepareDist, execCli, expect }) => { +test('should build app with define.app config', async ({ + prepareDist, + execCli, + expect, +}) => { const distPath = await prepareDist(); try { diff --git a/packages/rstack/tests/config/define-doc/index.test.ts b/packages/rstack/tests/config/define-doc/index.test.ts index 58ce1fc4..18142fc4 100644 --- a/packages/rstack/tests/config/define-doc/index.test.ts +++ b/packages/rstack/tests/config/define-doc/index.test.ts @@ -3,7 +3,11 @@ import { test } from '#test-helpers'; const expectedText = 'define.doc works'; -test('should build docs with define.doc config', async ({ prepareDist, execCli, expect }) => { +test('should build docs with define.doc config', async ({ + prepareDist, + execCli, + expect, +}) => { const distPath = await prepareDist('doc_build'); execCli('doc build'); @@ -12,4 +16,4 @@ test('should build docs with define.doc config', async ({ prepareDist, execCli, const output = getFileContent(files, 'index.html'); expect(output).toContain(expectedText); -}, 30_000); +}); diff --git a/packages/rstack/tests/config/define-lib/index.test.ts b/packages/rstack/tests/config/define-lib/index.test.ts index 53435b7e..db68d6e0 100644 --- a/packages/rstack/tests/config/define-lib/index.test.ts +++ b/packages/rstack/tests/config/define-lib/index.test.ts @@ -3,7 +3,11 @@ import { test } from '#test-helpers'; const expectedText = 'define.lib works'; -test('should build lib with define.lib config', async ({ prepareDist, execCli, expect }) => { +test('should build lib with define.lib config', async ({ + prepareDist, + execCli, + expect, +}) => { const distPath = await prepareDist(); execCli('lib'); diff --git a/packages/rstack/tests/config/define-lint/index.test.ts b/packages/rstack/tests/config/define-lint/index.test.ts index 402266fc..aff80258 100644 --- a/packages/rstack/tests/config/define-lint/index.test.ts +++ b/packages/rstack/tests/config/define-lint/index.test.ts @@ -7,7 +7,11 @@ test('should run lint with define.lint config', ({ execCli }) => { execCli('lint src/index.js'); }); -test('should fail when lint reports errors', async ({ cwd, execCli, logHelper }) => { +test('should fail when lint reports errors', async ({ + cwd, + execCli, + logHelper, +}) => { const filePath = path.join(cwd, 'src/test-temp-error.js'); await writeFile(filePath, 'debugger;'); expect(() => execCli('lint src/test-temp-error.js')).toThrow(); diff --git a/packages/rstack/tests/config/define-test-projects-app/index.test.ts b/packages/rstack/tests/config/define-test-projects-app/index.test.ts index 74f00e4d..d1e41c96 100644 --- a/packages/rstack/tests/config/define-test-projects-app/index.test.ts +++ b/packages/rstack/tests/config/define-test-projects-app/index.test.ts @@ -1,5 +1,7 @@ import { test } from '#test-helpers'; -test('should apply define.app config to every inline test project', ({ execCli }) => { +test('should apply define.app config to every inline test project', ({ + execCli, +}) => { execCli('test'); }); diff --git a/packages/rstack/tests/config/define-test-projects-lib/index.test.ts b/packages/rstack/tests/config/define-test-projects-lib/index.test.ts index 62372a46..724fd167 100644 --- a/packages/rstack/tests/config/define-test-projects-lib/index.test.ts +++ b/packages/rstack/tests/config/define-test-projects-lib/index.test.ts @@ -1,5 +1,7 @@ import { test } from '#test-helpers'; -test('should apply define.lib config to every inline test project', ({ execCli }) => { +test('should apply define.lib config to every inline test project', ({ + execCli, +}) => { execCli('test'); }); diff --git a/packages/rstack/tests/config/load-config/index.test.ts b/packages/rstack/tests/config/load-config/index.test.ts index a738f80e..fa2e9c43 100644 --- a/packages/rstack/tests/config/load-config/index.test.ts +++ b/packages/rstack/tests/config/load-config/index.test.ts @@ -15,7 +15,8 @@ declare global { } const state = getConfigState(); -const configPath = (fileName: string): string => path.join(import.meta.dirname, fileName); +const configPath = (fileName: string): string => + path.join(import.meta.dirname, fileName); const loadConfigFile = (fileName: string) => loadRstackConfig({ configFilePath: configPath(fileName) }); @@ -62,7 +63,9 @@ test('should resolve a relative explicit config path from cwd', async () => { }); test('should search for the config file in cwd', async () => { - await expect(loadRstackConfig({ cwd: import.meta.dirname })).rejects.toThrow('test config error'); + await expect(loadRstackConfig({ cwd: import.meta.dirname })).rejects.toThrow( + 'test config error', + ); }); test('should isolate parallel config sessions across top-level await', async () => { diff --git a/packages/rstack/tests/config/reload-app-config/index.test.ts b/packages/rstack/tests/config/reload-app-config/index.test.ts index cdf62067..7fb1ea1c 100644 --- a/packages/rstack/tests/config/reload-app-config/index.test.ts +++ b/packages/rstack/tests/config/reload-app-config/index.test.ts @@ -9,7 +9,10 @@ test('should restart dev server and reload config when Rstack config changes', a }) => { const dist1 = await prepareDist(); const dist2 = await prepareDist('dist-2'); - const configFile = path.join(import.meta.dirname, 'test-temp-rstack.config.ts'); + const configFile = path.join( + import.meta.dirname, + 'test-temp-rstack.config.ts', + ); await writeFile( configFile, @@ -45,10 +48,16 @@ define.app({ ); await waitForFile(dist2); -}, 30_000); +}); -test('should reload config when an imported file changes', async ({ execCliAsync, logHelper }) => { - const configFile = path.join(import.meta.dirname, 'test-temp-import.config.ts'); +test('should reload config when an imported file changes', async ({ + execCliAsync, + logHelper, +}) => { + const configFile = path.join( + import.meta.dirname, + 'test-temp-import.config.ts', + ); const importedFile = path.join(import.meta.dirname, 'test-temp-imported.ts'); await writeFile(importedFile, ''); @@ -69,5 +78,7 @@ define.app({ await writeFile(importedFile, '// changed\n'); - await logHelper.expectLog('restarting server as test-temp-imported.ts changed'); -}, 30_000); + await logHelper.expectLog( + 'restarting server as test-temp-imported.ts changed', + ); +}); diff --git a/packages/rstack/tests/config/reload-doc-config/docs/index.md b/packages/rstack/tests/config/reload-doc-config/docs/index.md new file mode 100644 index 00000000..f5a6303d --- /dev/null +++ b/packages/rstack/tests/config/reload-doc-config/docs/index.md @@ -0,0 +1 @@ +# Reload doc config diff --git a/packages/rstack/tests/config/reload-doc-config/index.test.ts b/packages/rstack/tests/config/reload-doc-config/index.test.ts new file mode 100644 index 00000000..6aece32f --- /dev/null +++ b/packages/rstack/tests/config/reload-doc-config/index.test.ts @@ -0,0 +1,105 @@ +import { writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { getRandomPort } from '@rstackjs/test-utils'; +import { test } from '#test-helpers'; + +test('should restart doc dev server when Rstack config changes', async ({ + execCliAsync, + logHelper, +}) => { + const configFile = path.join( + import.meta.dirname, + 'test-temp-rstack.config.ts', + ); + const userWatchFile = path.join( + import.meta.dirname, + 'test-temp-user-watch.txt', + ); + + const writeConfig = (title: string) => + writeFile( + configFile, + `import { define } from 'rstack'; + +define.doc({ + root: 'docs', + title: '${title}', + builderConfig: { + dev: { + watchFiles: { + paths: ${JSON.stringify(userWatchFile)}, + type: 'restart', + }, + }, + }, +}); +`, + ); + + await writeFile(userWatchFile, 'initial\n'); + await writeConfig('before config change'); + + execCliAsync( + `doc --config test-temp-rstack.config.ts --port ${await getRandomPort()}`, + ); + await logHelper.expectBuildEnd(); + logHelper.clearLogs(); + + await writeConfig('after config change'); + + await logHelper.expectLog( + 'restarting server as test-temp-rstack.config.ts changed', + ); + await logHelper.expectBuildEnd(); + logHelper.clearLogs(); + + await writeFile(userWatchFile, 'changed\n'); + + await logHelper.expectLog( + 'restarting server as test-temp-user-watch.txt changed', + ); + await logHelper.expectBuildEnd(); +}); + +test('should restart doc dev server when an imported config file changes', async ({ + execCliAsync, + logHelper, +}) => { + const configFile = path.join( + import.meta.dirname, + 'test-temp-import.config.ts', + ); + const importedFile = path.join(import.meta.dirname, 'test-temp-imported.ts'); + + await writeFile( + importedFile, + "export const title = 'before import change';\n", + ); + await writeFile( + configFile, + `import { define } from 'rstack'; +import { title } from './test-temp-imported.ts'; + +define.doc({ + root: 'docs', + title, +}); +`, + ); + + execCliAsync( + `doc --config test-temp-import.config.ts --port ${await getRandomPort()}`, + ); + await logHelper.expectBuildEnd(); + logHelper.clearLogs(); + + await writeFile( + importedFile, + "export const title = 'after import change';\n", + ); + + await logHelper.expectLog( + 'restarting server as test-temp-imported.ts changed', + ); + await logHelper.expectBuildEnd(); +}); diff --git a/packages/rstack/tests/config/reload-lib-config/index.test.ts b/packages/rstack/tests/config/reload-lib-config/index.test.ts new file mode 100644 index 00000000..d2f27179 --- /dev/null +++ b/packages/rstack/tests/config/reload-lib-config/index.test.ts @@ -0,0 +1,105 @@ +import { writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { waitForFile } from '@rstackjs/test-utils'; +import { test } from '#test-helpers'; + +test('should restart lib watch build when Rstack config changes', async ({ + prepareDist, + execCliAsync, + logHelper, +}) => { + const dist1 = await prepareDist(); + const dist2 = await prepareDist('dist-2'); + const configFile = path.join( + import.meta.dirname, + 'test-temp-rstack.config.ts', + ); + const userWatchFile = path.join( + import.meta.dirname, + 'test-temp-user-watch.txt', + ); + + const writeConfig = (distPath: string) => + writeFile( + configFile, + `import { define } from 'rstack'; + +define.lib({ + dev: { + watchFiles: { + paths: ${JSON.stringify(userWatchFile)}, + type: 'restart', + }, + }, + output: { + distPath: '${distPath}', + }, +}); +`, + ); + + await writeFile(userWatchFile, 'initial\n'); + await writeConfig('dist'); + + execCliAsync('lib --watch --config test-temp-rstack.config.ts'); + await logHelper.expectLog('build completed, watching for changes...'); + await waitForFile(path.join(dist1, 'index.js')); + logHelper.clearLogs(); + + await writeConfig('dist-2'); + + await logHelper.expectLog( + 'restarting build as test-temp-rstack.config.ts changed', + ); + await logHelper.expectLog('build completed, watching for changes...'); + await waitForFile(path.join(dist2, 'index.js')); + logHelper.clearLogs(); + + await writeFile(userWatchFile, 'changed\n'); + + await logHelper.expectLog( + 'restarting build as test-temp-user-watch.txt changed', + ); + await logHelper.expectLog('build completed, watching for changes...'); +}); + +test('should restart lib watch build when an imported config file changes', async ({ + prepareDist, + execCliAsync, + logHelper, +}) => { + const dist1 = await prepareDist('dist-import-1'); + const dist2 = await prepareDist('dist-import-2'); + const configFile = path.join( + import.meta.dirname, + 'test-temp-import.config.ts', + ); + const importedFile = path.join(import.meta.dirname, 'test-temp-imported.ts'); + + await writeFile(importedFile, "export const distPath = 'dist-import-1';\n"); + await writeFile( + configFile, + `import { define } from 'rstack'; +import { distPath } from './test-temp-imported.ts'; + +define.lib({ + output: { + distPath, + }, +}); +`, + ); + + execCliAsync('lib --watch --config test-temp-import.config.ts'); + await logHelper.expectLog('build completed, watching for changes...'); + await waitForFile(path.join(dist1, 'index.js')); + logHelper.clearLogs(); + + await writeFile(importedFile, "export const distPath = 'dist-import-2';\n"); + + await logHelper.expectLog( + 'restarting build as test-temp-imported.ts changed', + ); + await logHelper.expectLog('build completed, watching for changes...'); + await waitForFile(path.join(dist2, 'index.js')); +}); diff --git a/packages/rstack/tests/config/reload-lib-config/package.json b/packages/rstack/tests/config/reload-lib-config/package.json new file mode 100644 index 00000000..e986b24b --- /dev/null +++ b/packages/rstack/tests/config/reload-lib-config/package.json @@ -0,0 +1,4 @@ +{ + "private": true, + "type": "module" +} diff --git a/packages/rstack/tests/config/reload-lib-config/src/index.js b/packages/rstack/tests/config/reload-lib-config/src/index.js new file mode 100644 index 00000000..c62c9ec3 --- /dev/null +++ b/packages/rstack/tests/config/reload-lib-config/src/index.js @@ -0,0 +1 @@ +export const value = 'reload lib config'; diff --git a/packages/rstack/tests/exports/test-subpath/index.test.ts b/packages/rstack/tests/exports/test-subpath/index.test.ts index 74d693a7..d1da8169 100644 --- a/packages/rstack/tests/exports/test-subpath/index.test.ts +++ b/packages/rstack/tests/exports/test-subpath/index.test.ts @@ -1,6 +1,13 @@ import { expect, test } from 'rstack/test'; -const commonTestMethods = ['test', 'it', 'describe', 'expect', 'beforeAll', 'afterAll'] as const; +const commonTestMethods = [ + 'test', + 'it', + 'describe', + 'expect', + 'beforeAll', + 'afterAll', +] as const; test('should expose test APIs from `rstack/test`', async () => { const test = await import('rstack/test'); diff --git a/packages/rstack/tests/fmt/cacheIdentity.test.ts b/packages/rstack/tests/fmt/cacheIdentity.test.ts index e0d48789..56958b64 100644 --- a/packages/rstack/tests/fmt/cacheIdentity.test.ts +++ b/packages/rstack/tests/fmt/cacheIdentity.test.ts @@ -4,10 +4,11 @@ import prettierPkgJson from 'prettier/package.json' with { type: 'json' }; import { expect, test } from 'rstack/test'; import pkgJson from '../../package.json' with { type: 'json' }; import { + cacheHashLength, cacheNamespace, + createCacheHash, createCacheKeyResolver, createOptionsHasher, - sha256, } from '../../src/fmt/cacheIdentity.ts'; import { fmtCacheVersion } from '../../src/fmt/cacheStore.ts'; import type { ResolvedFmtOptions } from '../../src/fmt/types.ts'; @@ -17,7 +18,7 @@ const rootPath = path.join(import.meta.dirname, 'project'); const asOptions = (value: Record): ResolvedFmtOptions => value as ResolvedFmtOptions; -test('creates stable SHA-256 option hashes', () => { +test('creates stable SHA-256-derived option hashes', () => { const hashOptions = createOptionsHasher(); const left: ResolvedFmtOptions = { singleQuote: true, @@ -29,8 +30,8 @@ test('creates stable SHA-256 option hashes', () => { }; expect(hashOptions(left)).toBe(hashOptions(right)); - expect(hashOptions(left)).toHaveLength(64); - expect(sha256('abc')).toBe('ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'); + expect(hashOptions(left)).toHaveLength(cacheHashLength); + expect(createCacheHash('abc')).toBe('ungWv48Bz-pBQUDe'); }); test('invalidates hashes when final formatter options change', () => { @@ -52,8 +53,10 @@ test('includes plugin fingerprints in option hashes', () => { const first = createOptionsHasher(new Map([[plugin, 'plugin@1']])); const second = createOptionsHasher(new Map([[plugin, 'plugin@2']])); - expect(first({ plugins: [plugin] })).toHaveLength(64); - expect(first({ plugins: [new URL(plugin)] })).toBe(first({ plugins: [plugin] })); + expect(first({ plugins: [plugin] })).toHaveLength(cacheHashLength); + expect(first({ plugins: [new URL(plugin)] })).toBe( + first({ plugins: [plugin] }), + ); expect(first({ plugins: [plugin] })).not.toBe(second({ plugins: [plugin] })); }); @@ -70,8 +73,12 @@ test('bypasses user plugins and unserializable options', () => { ); cyclic.self = cyclic; - expect(hashOptions({ plugins: [path.resolve('plugin.mjs')] })).toBeUndefined(); - expect(hashOptions({ plugins: [pathToFileURL(path.resolve('plugin.mjs'))] })).toBeUndefined(); + expect( + hashOptions({ plugins: [path.resolve('plugin.mjs')] }), + ).toBeUndefined(); + expect( + hashOptions({ plugins: [pathToFileURL(path.resolve('plugin.mjs'))] }), + ).toBeUndefined(); expect(hashOptions(asOptions({ custom: cyclic }))).toBeUndefined(); expect(hashOptions(asOptions(unreadable))).toBeUndefined(); @@ -93,5 +100,7 @@ test('creates config-root-relative POSIX cache keys', () => { expect(resolveKey(firstPath)).toBe('src/nested/index.ts'); expect(resolveKey(secondPath)).toBe('src/other.ts'); expect(resolveKey(firstPath)).not.toBe(resolveKey(secondPath)); - expect(resolveKey(path.join(rootPath, '../shared/index.ts'))).toBe('../shared/index.ts'); + expect(resolveKey(path.join(rootPath, '../shared/index.ts'))).toBe( + '../shared/index.ts', + ); }); diff --git a/packages/rstack/tests/fmt/cacheStore.test.ts b/packages/rstack/tests/fmt/cacheStore.test.ts index 713d804d..5c466cd7 100644 --- a/packages/rstack/tests/fmt/cacheStore.test.ts +++ b/packages/rstack/tests/fmt/cacheStore.test.ts @@ -1,21 +1,38 @@ -import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; +import { + existsSync, + mkdirSync, + readFileSync, + readdirSync, + writeFileSync, +} from 'node:fs'; import path from 'node:path'; import { expect, test } from 'rstack/test'; -import { fmtCacheVersion, loadFmtCacheStore, type FmtCacheFile } from '../../src/fmt/cacheStore.ts'; +import { + fmtCacheFileName, + fmtCacheVersion, + loadFmtCacheStore, + type FmtCacheFile, +} from '../../src/fmt/cacheStore.ts'; import { withTempProject } from './helpers.ts'; const namespace = 'test-namespace'; -const firstEntry = ['content-a', 'options-a', 'clean'] as const; -const secondEntry = ['content-b', 'options-b', 'dirty'] as const; -const unsupportedEntry = [null, 'options-c', 'unsupported'] as const; -const hashedUnsupportedEntry = ['content-c', 'options-c', 'unsupported'] as const; +const contentA = 'content-a'; +const contentB = 'content-b'; +const contentC = 'content-c'; +const optionsA = 'options-a'; +const optionsB = 'options-b'; +const optionsC = 'options-c'; +const firstEntry = [contentA, optionsA, 'clean'] as const; +const secondEntry = [contentB, optionsB, 'dirty'] as const; +const unsupportedEntry = ['', optionsC, 'unsupported'] as const; +const hashedUnsupportedEntry = [contentC, optionsC, 'unsupported'] as const; const readCache = (filePath: string): FmtCacheFile => JSON.parse(readFileSync(filePath, 'utf8')) as FmtCacheFile; -test('writes entries that can be loaded by another store', async () => { +test('writes flat entries that can be loaded by another store', async () => { await withTempProject(async (rootPath) => { - const cachePath = path.join(rootPath, 'cache', 'fmt-v1.json'); + const cachePath = path.join(rootPath, 'cache', fmtCacheFileName); const store = await loadFmtCacheStore(cachePath, namespace); expect(await store.save()).toBe(false); @@ -26,6 +43,25 @@ test('writes entries that can be loaded by another store', async () => { store.set('script', hashedUnsupportedEntry); expect(await store.save()).toBe(true); expect(await store.save()).toBe(false); + expect(readCache(cachePath)).toEqual({ + version: fmtCacheVersion, + namespace, + options: [optionsA, optionsC], + files: [ + 'src/a.ts', + contentA, + 0, + 0, + 'src/unknown.fixture', + '', + 1, + 2, + 'script', + contentC, + 1, + 2, + ], + }); const loaded = await loadFmtCacheStore(cachePath, namespace); expect(loaded.get('src/a.ts')).toEqual(firstEntry); @@ -36,16 +72,14 @@ test('writes entries that can be loaded by another store', async () => { test('preserves unvisited entries and skips unchanged updates', async () => { await withTempProject(async (rootPath) => { - const cachePath = path.join(rootPath, 'fmt-v1.json'); + const cachePath = path.join(rootPath, fmtCacheFileName); writeFileSync( cachePath, `${JSON.stringify({ version: fmtCacheVersion, namespace, - files: { - 'src/a.ts': firstEntry, - 'src/b.ts': secondEntry, - }, + options: [optionsA, optionsB], + files: ['src/a.ts', contentA, 0, 0, 'src/b.ts', contentB, 1, 1], })}\n`, ); @@ -56,34 +90,30 @@ test('preserves unvisited entries and skips unchanged updates', async () => { store.set('src/a.ts', secondEntry); expect(await store.save()).toBe(true); - expect(readCache(cachePath).files).toEqual({ - 'src/a.ts': secondEntry, - 'src/b.ts': secondEntry, + expect(readCache(cachePath)).toEqual({ + version: fmtCacheVersion, + namespace, + options: [optionsB], + files: ['src/a.ts', contentB, 0, 1, 'src/b.ts', contentB, 0, 1], }); }); }); -test('discards invalid data and entries from another namespace', async () => { +test('discards invalid schemas and other namespaces', async () => { await withTempProject(async (rootPath) => { - const cachePath = path.join(rootPath, 'fmt-v1.json'); + const cachePath = path.join(rootPath, fmtCacheFileName); + const validCache = { + version: fmtCacheVersion, + namespace, + options: [optionsA], + files: ['src/a.ts', contentA, 0, 0], + }; const invalidContents = [ '{invalid', - JSON.stringify({ version: 2, namespace, files: {} }), - JSON.stringify({ - version: fmtCacheVersion, - namespace, - files: { 'src/a.ts': ['content', 'options', 'unknown'] }, - }), - JSON.stringify({ - version: fmtCacheVersion, - namespace, - files: { 'src/a.ts': [42, 'options', 'unsupported'] }, - }), - JSON.stringify({ - version: fmtCacheVersion, - namespace, - files: { 'src/a.ts': [null, 'options', 'clean'] }, - }), + JSON.stringify({ ...validCache, version: fmtCacheVersion - 1 }), + JSON.stringify({ version: fmtCacheVersion, namespace, files: [] }), + JSON.stringify({ ...validCache, files: { 'src/a.ts': firstEntry } }), + JSON.stringify({ ...validCache, files: ['src/a.ts', contentA, 0] }), ]; for (const content of invalidContents) { @@ -95,9 +125,8 @@ test('discards invalid data and entries from another namespace', async () => { writeFileSync( cachePath, JSON.stringify({ - version: fmtCacheVersion, + ...validCache, namespace: 'old-namespace', - files: { 'src/a.ts': firstEntry }, }), ); const store = await loadFmtCacheStore(cachePath, namespace); @@ -106,20 +135,23 @@ test('discards invalid data and entries from another namespace', async () => { expect(readCache(cachePath)).toEqual({ version: fmtCacheVersion, namespace, - files: {}, + options: [], + files: [], }); }); }); test('does not throw or leave temporary files when persistence fails', async () => { await withTempProject(async (rootPath) => { - const cachePath = path.join(rootPath, 'fmt-v1.json'); + const cachePath = path.join(rootPath, fmtCacheFileName); mkdirSync(cachePath); const store = await loadFmtCacheStore(cachePath, namespace); store.set('src/a.ts', firstEntry); await expect(store.save()).resolves.toBe(false); - expect(readdirSync(rootPath).filter((name) => name.endsWith('.tmp'))).toEqual([]); + expect( + readdirSync(rootPath).filter((name) => name.endsWith('.tmp')), + ).toEqual([]); }); }); diff --git a/packages/rstack/tests/fmt/config.test.ts b/packages/rstack/tests/fmt/config.test.ts index c2f32844..eb5a9b28 100644 --- a/packages/rstack/tests/fmt/config.test.ts +++ b/packages/rstack/tests/fmt/config.test.ts @@ -1,6 +1,9 @@ import path from 'node:path'; import { expect, test } from 'rstack/test'; -import { createOptionsResolver, normalizeFmtConfig } from '../../src/fmt/config.ts'; +import { + createOptionsResolver, + normalizeFmtConfig, +} from '../../src/fmt/config.ts'; const rootPath = path.join(import.meta.dirname, 'project'); @@ -14,7 +17,9 @@ test('reuses base options when no override matches', () => { ); const resolveOptions = createOptionsResolver(config); - expect(resolveOptions(path.join(rootPath, 'index.js'))).toBe(config.baseOptions); + expect(resolveOptions(path.join(rootPath, 'index.js'))).toBe( + config.baseOptions, + ); }); test('applies basename and path overrides in declaration order', () => { @@ -50,6 +55,29 @@ test('applies basename and path overrides in declaration order', () => { expect(config.baseOptions).toEqual({ singleQuote: false }); }); +test('reuses options for the same override combination', () => { + const config = normalizeFmtConfig( + { + singleQuote: false, + overrides: [ + { files: '*.ts', options: { semi: false } }, + { files: 'src/**/*.ts', options: { singleQuote: true } }, + ], + }, + rootPath, + ); + const resolveOptions = createOptionsResolver(config); + + const first = resolveOptions(path.join(rootPath, 'src/first.ts')); + const second = resolveOptions(path.join(rootPath, 'src/second.ts')); + const outside = resolveOptions(path.join(rootPath, 'outside.ts')); + + expect(first).toBe(second); + expect(first).not.toBe(outside); + expect(first).toEqual({ semi: false, singleQuote: true }); + expect(outside).toEqual({ semi: false, singleQuote: false }); +}); + test('applies overrides outside the config root', () => { const config = normalizeFmtConfig( { @@ -59,5 +87,7 @@ test('applies overrides outside the config root', () => { ); const resolveOptions = createOptionsResolver(config); - expect(resolveOptions(path.join(rootPath, '../shared/index.ts'))).toEqual({ semi: false }); + expect(resolveOptions(path.join(rootPath, '../shared/index.ts'))).toEqual({ + semi: false, + }); }); diff --git a/packages/rstack/tests/fmt/discoverPaths.test.ts b/packages/rstack/tests/fmt/discoverPaths.test.ts index 0fa2e6ca..6cad49f9 100644 --- a/packages/rstack/tests/fmt/discoverPaths.test.ts +++ b/packages/rstack/tests/fmt/discoverPaths.test.ts @@ -20,7 +20,10 @@ test('discovers non-binary files in stable order and skips hard-ignored paths', writeProjectFile(rootPath, '.jj/internal.js'); const files = await discoverFmtPaths({ cwd: rootPath }); - const filesWithNodeModules = await discoverFmtPaths({ cwd: rootPath, withNodeModules: true }); + const filesWithNodeModules = await discoverFmtPaths({ + cwd: rootPath, + withNodeModules: true, + }); expect(relativePaths(rootPath, files)).toEqual([ 'a.js', @@ -36,7 +39,10 @@ test('discovers non-binary files in stable order and skips hard-ignored paths', 'unknown.extension', ]); await expect( - discoverFmtPaths({ cwd: rootPath, patterns: ['node_modules/package/index.js'] }), + discoverFmtPaths({ + cwd: rootPath, + patterns: ['node_modules/package/index.js'], + }), ).resolves.toEqual([]); await expect( discoverFmtPaths({ @@ -54,7 +60,10 @@ test('keeps node_modules excluded by gitignore when built-in exclusion is disabl writeProjectFile(rootPath, 'node_modules/package/index.js'); writeProjectFile(rootPath, 'index.js'); - const files = await discoverFmtPaths({ cwd: rootPath, withNodeModules: true }); + const files = await discoverFmtPaths({ + cwd: rootPath, + withNodeModules: true, + }); expect(relativePaths(rootPath, files)).toEqual(['.gitignore', 'index.js']); }); @@ -93,7 +102,9 @@ test('combines files, directories, and globs without duplicates', async () => { path.join('src', 'a.ts'), path.join('test', 'c.ts'), ]); - expect(relativePaths(rootPath, dotFiles)).toEqual([path.join('dot', '.hidden.ts')]); + expect(relativePaths(rootPath, dotFiles)).toEqual([ + path.join('dot', '.hidden.ts'), + ]); await expect( discoverFmtPaths({ cwd: rootPath, patterns: ['missing/**/*.ts'] }), ).resolves.toEqual([]); @@ -112,13 +123,19 @@ test('applies nested gitignore rules with child negation', async () => { writeProjectFile(rootPath, 'dist/nested/keep.js'); writeProjectFile(rootPath, 'visible.ts'); - const files = await discoverFmtPaths({ cwd: rootPath, patterns: ['**/*.{js,ts}'] }); + const files = await discoverFmtPaths({ + cwd: rootPath, + patterns: ['**/*.{js,ts}'], + }); const ignoredNestedDirectory = await discoverFmtPaths({ cwd: rootPath, patterns: ['dist/nested'], }); - expect(relativePaths(rootPath, files)).toEqual([path.join('src', 'keep.js'), 'visible.ts']); + expect(relativePaths(rootPath, files)).toEqual([ + path.join('src', 'keep.js'), + 'visible.ts', + ]); expect(ignoredNestedDirectory).toEqual([]); }); }); @@ -129,7 +146,10 @@ test('does not extend a nested directory negation to its files', async () => { writeProjectFile(rootPath, 'scripts/.gitignore', '!debug\n'); writeProjectFile(rootPath, 'scripts/debug/launch.mjs'); - const files = await discoverFmtPaths({ cwd: rootPath, patterns: ['**/*.mjs'] }); + const files = await discoverFmtPaths({ + cwd: rootPath, + patterns: ['**/*.mjs'], + }); expect(files).toEqual([]); }); @@ -195,9 +215,15 @@ test('keeps valid nested gitignore rules around normalized and malformed lines', writeProjectFile(rootPath, 'src/drop.js'); writeProjectFile(rootPath, 'visible.ts'); - const files = await discoverFmtPaths({ cwd: rootPath, patterns: ['**/*.{js,ts}'] }); + const files = await discoverFmtPaths({ + cwd: rootPath, + patterns: ['**/*.{js,ts}'], + }); - expect(relativePaths(rootPath, files)).toEqual([path.join('src', 'keep.js'), 'visible.ts']); + expect(relativePaths(rootPath, files)).toEqual([ + path.join('src', 'keep.js'), + 'visible.ts', + ]); }); }); @@ -213,7 +239,9 @@ test('propagates native binding errors while loading a nested gitignore', async }); try { - await expect(discoverFmtPaths({ cwd: rootPath })).rejects.toBe(nativeError); + await expect(discoverFmtPaths({ cwd: rootPath })).rejects.toBe( + nativeError, + ); } finally { loadNativeBinding.mockRestore(); } @@ -230,10 +258,17 @@ test('lets explicit files bypass gitignore', async () => { cwd: rootPath, patterns: ['**/*.ts'], }); - const explicitFiles = await discoverFmtPaths({ cwd: rootPath, patterns: [keepPath] }); + const explicitFiles = await discoverFmtPaths({ + cwd: rootPath, + patterns: [keepPath], + }); - expect(relativePaths(rootPath, discoveredFiles)).toEqual([path.join('src', 'index.ts')]); - expect(relativePaths(rootPath, explicitFiles)).toEqual([path.join('generated', 'keep.ts')]); + expect(relativePaths(rootPath, discoveredFiles)).toEqual([ + path.join('src', 'index.ts'), + ]); + expect(relativePaths(rootPath, explicitFiles)).toEqual([ + path.join('generated', 'keep.ts'), + ]); }); }); @@ -249,7 +284,9 @@ test('applies an external ignore matcher to traversed and explicit paths', async path: path.relative(rootPath, filePath), isDirectory, }); - return isDirectory ? filePath === generatedPath : filePath === ignoredFilePath; + return isDirectory + ? filePath === generatedPath + : filePath === ignoredFilePath; }; const files = await discoverFmtPaths({ cwd: rootPath, isIgnored }); @@ -264,10 +301,15 @@ test('applies an external ignore matcher to traversed and explicit paths', async isIgnored, }); - expect(relativePaths(rootPath, files)).toEqual([path.join('src', 'index.ts')]); + expect(relativePaths(rootPath, files)).toEqual([ + path.join('src', 'index.ts'), + ]); expect(ignoredRoot).toEqual([]); expect(explicitIgnoredFile).toEqual([]); - expect(checkedPaths).toContainEqual({ path: 'generated', isDirectory: true }); + expect(checkedPaths).toContainEqual({ + path: 'generated', + isDirectory: true, + }); expect(checkedPaths).toContainEqual({ path: path.join('src', 'ignored.ts'), isDirectory: false, @@ -279,19 +321,27 @@ test('applies an external ignore matcher to traversed and explicit paths', async }); }); -test.runIf(process.platform !== 'win32')('does not follow file or directory symlinks', async () => { - await withTempProject(async (rootPath) => { - const targetPath = writeProjectFile(rootPath, 'target/index.ts'); - symlinkSync(path.join(rootPath, 'target'), path.join(rootPath, 'linked-directory')); - symlinkSync(targetPath, path.join(rootPath, 'linked-file.ts')); +test.runIf(process.platform !== 'win32')( + 'does not follow file or directory symlinks', + async () => { + await withTempProject(async (rootPath) => { + const targetPath = writeProjectFile(rootPath, 'target/index.ts'); + symlinkSync( + path.join(rootPath, 'target'), + path.join(rootPath, 'linked-directory'), + ); + symlinkSync(targetPath, path.join(rootPath, 'linked-file.ts')); + + const discoveredFiles = await discoverFmtPaths({ cwd: rootPath }); + const explicitFiles = await discoverFmtPaths({ + cwd: rootPath, + patterns: ['linked-directory', 'linked-file.ts'], + }); - const discoveredFiles = await discoverFmtPaths({ cwd: rootPath }); - const explicitFiles = await discoverFmtPaths({ - cwd: rootPath, - patterns: ['linked-directory', 'linked-file.ts'], + expect(relativePaths(rootPath, discoveredFiles)).toEqual([ + path.join('target', 'index.ts'), + ]); + expect(explicitFiles).toEqual([]); }); - - expect(relativePaths(rootPath, discoveredFiles)).toEqual([path.join('target', 'index.ts')]); - expect(explicitFiles).toEqual([]); - }); -}); + }, +); diff --git a/packages/rstack/tests/fmt/discovery.test.ts b/packages/rstack/tests/fmt/discovery.test.ts index 171fbaae..0e062435 100644 --- a/packages/rstack/tests/fmt/discovery.test.ts +++ b/packages/rstack/tests/fmt/discovery.test.ts @@ -1,21 +1,27 @@ import { mkdirSync } from 'node:fs'; import path from 'node:path'; -import { pathToFileURL } from 'node:url'; import { expect, test } from 'rstack/test'; import { normalizeFmtConfig } from '../../src/fmt/config.ts'; import { discoverFmtFiles } from '../../src/fmt/discovery.ts'; import type { FmtConfig } from '../../src/fmt/types.ts'; import { withTempProject, writeProjectFile } from './helpers.ts'; -const discover = async (cwd: string, patterns?: string[], config?: FmtConfig, configRoot = cwd) => +const discover = async ( + cwd: string, + patterns?: string[], + config?: FmtConfig, + configRoot = cwd, +) => discoverFmtFiles({ cwd, patterns, config: normalizeFmtConfig(config, configRoot), }); -const relativePaths = (rootPath: string, files: Awaited>): string[] => - files.map((file) => path.relative(rootPath, file.path)); +const relativePaths = ( + rootPath: string, + files: Awaited>, +): string[] => files.map((file) => path.relative(rootPath, file.path)); test('applies config ignore patterns to discovered and explicit files', async () => { await withTempProject(async (rootPath) => { @@ -25,13 +31,19 @@ test('applies config ignore patterns to discovered and explicit files', async () const config = { ignorePatterns: ['generated/blocked.ts'] }; const discoveredFiles = await discover(rootPath, undefined, config); - const explicitFiles = await discover(rootPath, [keepPath, blockedPath], config); + const explicitFiles = await discover( + rootPath, + [keepPath, blockedPath], + config, + ); expect(relativePaths(rootPath, discoveredFiles)).toEqual([ path.join('generated', 'keep.ts'), path.join('src', 'index.ts'), ]); - expect(relativePaths(rootPath, explicitFiles)).toEqual([path.join('generated', 'keep.ts')]); + expect(relativePaths(rootPath, explicitFiles)).toEqual([ + path.join('generated', 'keep.ts'), + ]); }); }); @@ -42,14 +54,23 @@ test('applies config ignore patterns outside the config root', async () => { mkdirSync(configRoot); await expect( - discover(configRoot, [filePath], { ignorePatterns: ['../shared/*.ts'] }, configRoot), + discover( + configRoot, + [filePath], + { ignorePatterns: ['../shared/*.ts'] }, + configRoot, + ), ).resolves.toEqual([]); }); }); test('excludes .rstack from discovery', async () => { await withTempProject(async (rootPath) => { - const cacheFile = writeProjectFile(rootPath, '.rstack/cache/fmt-v1.json', '{}'); + const cacheFile = writeProjectFile( + rootPath, + '.rstack/cache/fmt-v1.json', + '{}', + ); writeProjectFile(rootPath, 'index.ts'); const discoveredFiles = await discover(rootPath); @@ -86,7 +107,11 @@ test('excludes a custom cache directory', async () => { test('keeps files re-included by a CLI ignore file during directory traversal', async () => { await withTempProject(async (rootPath) => { - writeProjectFile(rootPath, '.prettierignore', 'generated/*\n!generated/keep.ts\n'); + writeProjectFile( + rootPath, + '.prettierignore', + 'generated/*\n!generated/keep.ts\n', + ); writeProjectFile(rootPath, 'generated/drop.ts'); writeProjectFile(rootPath, 'generated/keep.ts'); writeProjectFile(rootPath, 'src/index.ts'); @@ -113,7 +138,9 @@ test('defers parser inference to workers and preserves an explicit parser', asyn writeProjectFile(rootPath, 'unknown.extension'); const inferredFiles = await discover(rootPath); - const configuredFiles = await discover(rootPath, ['source.custom'], { parser: 'babel' }); + const configuredFiles = await discover(rootPath, ['source.custom'], { + parser: 'babel', + }); expect(relativePaths(rootPath, inferredFiles)).toEqual([ 'index.js', @@ -121,63 +148,12 @@ test('defers parser inference to workers and preserves an explicit parser', asyn 'source.custom', 'unknown.extension', ]); - expect(inferredFiles.every((file) => file.options.parser === undefined)).toBe(true); + expect( + inferredFiles.every((file) => file.options.parser === undefined), + ).toBe(true); expect(configuredFiles[0]).toEqual({ path: path.join(rootPath, 'source.custom'), options: { parser: 'babel' }, }); }); }); - -test('resolves plugins after applying matching overrides', async () => { - await withTempProject(async (rootPath) => { - const pluginEntry = writeProjectFile( - rootPath, - 'node_modules/prettier-plugin-fixture/index.mjs', - `export default { - languages: [ - { name: 'Fixture JSON', parsers: ['json'], extensions: ['.fixture'] }, - { name: 'Fixture TypeScript', parsers: ['babel'], extensions: ['.ts'] }, - ], -}; -`, - ); - writeProjectFile( - rootPath, - 'node_modules/prettier-plugin-fixture/package.json', - JSON.stringify({ name: 'prettier-plugin-fixture', exports: './index.mjs' }), - ); - writeProjectFile(rootPath, 'example.fixture'); - writeProjectFile(rootPath, 'example.ts'); - const config = { - overrides: [ - { - files: '*.fixture', - options: { plugins: ['prettier-plugin-fixture'] }, - }, - { - files: '*.ts', - options: { plugins: ['prettier-plugin-fixture'] }, - }, - { - files: '*.md', - options: { plugins: ['missing-plugin'] }, - }, - ], - }; - - const files = await discover(rootPath, ['example.fixture', 'example.ts'], config); - - expect(files).toHaveLength(2); - expect(files[0]).toMatchObject({ - options: { - plugins: [pathToFileURL(pluginEntry).href], - }, - }); - expect(files[1]).toMatchObject({ - options: { - plugins: [pathToFileURL(pluginEntry).href], - }, - }); - }); -}); diff --git a/packages/rstack/tests/fmt/fileResolver.test.ts b/packages/rstack/tests/fmt/fileResolver.test.ts new file mode 100644 index 00000000..e0089095 --- /dev/null +++ b/packages/rstack/tests/fmt/fileResolver.test.ts @@ -0,0 +1,62 @@ +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { expect, test } from 'rstack/test'; +import { normalizeFmtConfig } from '../../src/fmt/config.ts'; +import { createFmtFileResolver } from '../../src/fmt/fileResolver.ts'; +import { withTempProject, writeProjectFile } from './helpers.ts'; + +test('applies matching overrides before resolving plugins', async () => { + await withTempProject(async (rootPath) => { + const pluginEntry = writeProjectFile( + rootPath, + 'node_modules/prettier-plugin-fixture/index.mjs', + `export default { + languages: [ + { name: 'Fixture JSON', parsers: ['json'], extensions: ['.fixture'] }, + { name: 'Fixture TypeScript', parsers: ['babel'], extensions: ['.ts'] }, + ], +}; +`, + ); + writeProjectFile( + rootPath, + 'node_modules/prettier-plugin-fixture/package.json', + JSON.stringify({ + name: 'prettier-plugin-fixture', + exports: './index.mjs', + }), + ); + const config = normalizeFmtConfig( + { + overrides: [ + { + files: '*.{fixture,ts}', + options: { plugins: ['prettier-plugin-fixture'] }, + }, + { + files: '*.md', + options: { plugins: ['missing-plugin'] }, + }, + ], + }, + rootPath, + ); + const resolveFile = createFmtFileResolver(config); + + const files = await Promise.all([ + resolveFile(path.join(rootPath, 'example.fixture')), + resolveFile(path.join(rootPath, 'example.ts')), + ]); + + expect(files).toEqual([ + { + path: path.join(rootPath, 'example.fixture'), + options: { plugins: [pathToFileURL(pluginEntry).href] }, + }, + { + path: path.join(rootPath, 'example.ts'), + options: { plugins: [pathToFileURL(pluginEntry).href] }, + }, + ]); + }); +}); diff --git a/packages/rstack/tests/fmt/helpers.ts b/packages/rstack/tests/fmt/helpers.ts index 2a6c8ac3..1468b32d 100644 --- a/packages/rstack/tests/fmt/helpers.ts +++ b/packages/rstack/tests/fmt/helpers.ts @@ -1,7 +1,11 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import path from 'node:path'; import { fmtCacheFileName } from '../../src/fmt/cacheStore.ts'; -import type { FmtCacheContext, FmtFileRequest, ResolvedFmtOptions } from '../../src/fmt/types.ts'; +import type { + FmtCacheContext, + FmtFileRequest, + ResolvedFmtOptions, +} from '../../src/fmt/types.ts'; export const createFmtRequest = ( filePath: string, @@ -17,9 +21,11 @@ export const createFmtCacheContext = (rootPath: string): FmtCacheContext => ({ }); export const withTempProject = async ( - callback: (rootPath: string) => Promise, + callback: (rootPath: string) => void | Promise, ): Promise => { - const rootPath = mkdtempSync(path.join(import.meta.dirname, 'test-temp-fmt-')); + const rootPath = mkdtempSync( + path.join(import.meta.dirname, 'test-temp-fmt-'), + ); // Prevent repository-level ignore rules from affecting the fixture. mkdirSync(path.join(rootPath, '.git')); @@ -30,7 +36,11 @@ export const withTempProject = async ( } }; -export const writeProjectFile = (rootPath: string, filePath: string, content = ''): string => { +export const writeProjectFile = ( + rootPath: string, + filePath: string, + content = '', +): string => { const absolutePath = path.join(rootPath, filePath); mkdirSync(path.dirname(absolutePath), { recursive: true }); writeFileSync(absolutePath, content); diff --git a/packages/rstack/tests/fmt/ignore.test.ts b/packages/rstack/tests/fmt/ignore.test.ts index 68220775..b8aa2126 100644 --- a/packages/rstack/tests/fmt/ignore.test.ts +++ b/packages/rstack/tests/fmt/ignore.test.ts @@ -55,7 +55,11 @@ test('does not apply negated directory patterns to files', async () => { test('applies negated patterns in declaration order', async () => { const isIgnored = await createMatcher(['*.js', '!src/keep.js']); - const isIgnoredAgain = await createMatcher(['*.js', '!src/keep.js', 'src/keep.js']); + const isIgnoredAgain = await createMatcher([ + '*.js', + '!src/keep.js', + 'src/keep.js', + ]); const isIgnoredAfterReinclude = await createMatcher(['dist', '!dist']); const filePath = path.join(rootPath, 'src/keep.js'); @@ -70,11 +74,17 @@ test('ignores common lock files by default and allows explicit negation', async const isIgnoredAfterReinclude = await createMatcher(['!pnpm-lock.yaml']); expect(isIgnored(path.join(rootPath, 'package-lock.json'))).toBe(true); - expect(isIgnored(path.join(rootPath, 'packages/app/pnpm-lock.yaml'))).toBe(true); - expect(isIgnored(path.join(rootPath, 'packages/app/PNPM-LOCK.YAML'))).toBe(false); + expect(isIgnored(path.join(rootPath, 'packages/app/pnpm-lock.yaml'))).toBe( + true, + ); + expect(isIgnored(path.join(rootPath, 'packages/app/PNPM-LOCK.YAML'))).toBe( + false, + ); expect(isIgnored(path.join(rootPath, '../shared/pnpm-lock.yaml'))).toBe(true); expect(isIgnored(path.join(rootPath, 'pnpm-lock.yaml.backup'))).toBe(false); - expect(isIgnoredAfterReinclude(path.join(rootPath, 'pnpm-lock.yaml'))).toBe(false); + expect(isIgnoredAfterReinclude(path.join(rootPath, 'pnpm-lock.yaml'))).toBe( + false, + ); }); test('does not let explicit files bypass ignore patterns', async () => { @@ -99,11 +109,18 @@ test('does not ignore other files when no patterns are configured', async () => test('loads repeated ignore paths relative to cwd and each ignore file', async () => { await withTempProject(async (projectPath) => { - writeProjectFile(projectPath, '.prettierignore', 'src/*.js\n!src/keep.js\n'); + writeProjectFile( + projectPath, + '.prettierignore', + 'src/*.js\n!src/keep.js\n', + ); writeProjectFile(projectPath, 'config/extra.ignore', '../generated/*.js\n'); const isIgnored = await createIgnoreMatcher({ - config: normalizeFmtConfig({ ignorePatterns: ['configured.js'] }, projectPath), + config: normalizeFmtConfig( + { ignorePatterns: ['configured.js'] }, + projectPath, + ), cwd: projectPath, ignorePaths: ['.prettierignore', 'config/extra.ignore'], }); diff --git a/packages/rstack/tests/fmt/lsp/minimalEdit.test.ts b/packages/rstack/tests/fmt/lsp/minimalEdit.test.ts index cf94e86f..7fc1882c 100644 --- a/packages/rstack/tests/fmt/lsp/minimalEdit.test.ts +++ b/packages/rstack/tests/fmt/lsp/minimalEdit.test.ts @@ -1,6 +1,9 @@ import { expect, test } from 'rstack/test'; import { TextDocument } from 'vscode-languageserver-textdocument'; -import { computeMinimalEdit, computeMinimalTextEdit } from '../../../src/fmt/lsp/minimalEdit.ts'; +import { + computeMinimalEdit, + computeMinimalTextEdit, +} from '../../../src/fmt/lsp/minimalEdit.ts'; /** Applies an edit the way an editor does, to prove it rewrites the document. */ const applyMinimalEdit = (source: string, formatted: string): string => { @@ -17,7 +20,10 @@ const applyMinimalEdit = (source: string, formatted: string): string => { * does: offsets become positions on the server and positions become offsets * again on the client, which moves any offset that lands inside a `\r\n`. */ -const applyMinimalEditThroughPositions = (source: string, formatted: string): string => { +const applyMinimalEditThroughPositions = ( + source: string, + formatted: string, +): string => { const edit = computeMinimalEdit(source, formatted); if (!edit) { return source; @@ -35,7 +41,9 @@ const applyMinimalEditThroughPositions = (source: string, formatted: string): st test('returns no edit for identical sources', () => { expect(computeMinimalEdit('', '')).toBeUndefined(); - expect(computeMinimalEdit('const x = 1;\n', 'const x = 1;\n')).toBeUndefined(); + expect( + computeMinimalEdit('const x = 1;\n', 'const x = 1;\n'), + ).toBeUndefined(); }); test('replaces the whole document when nothing is shared', () => { @@ -44,7 +52,11 @@ test('replaces the whole document when nothing is shared', () => { end: 0, newText: 'const x = 1;\n', }); - expect(computeMinimalEdit('a\n', '')).toEqual({ start: 0, end: 2, newText: '' }); + expect(computeMinimalEdit('a\n', '')).toEqual({ + start: 0, + end: 2, + newText: '', + }); }); test('trims a shared prefix', () => { @@ -134,8 +146,12 @@ test('survives a round trip through a real text document', () => { const formatted = 'const a = 1;\r\nconst b = 2;\r\n'; expect(applyMinimalEditThroughPositions(source, formatted)).toBe(formatted); - expect(applyMinimalEditThroughPositions('a\nb\n', 'a\r\nb\r\n')).toBe('a\r\nb\r\n'); - expect(applyMinimalEditThroughPositions('a\r\nb\r\n', 'a\nb\n')).toBe('a\nb\n'); + expect(applyMinimalEditThroughPositions('a\nb\n', 'a\r\nb\r\n')).toBe( + 'a\r\nb\r\n', + ); + expect(applyMinimalEditThroughPositions('a\r\nb\r\n', 'a\nb\n')).toBe( + 'a\nb\n', + ); }); // Line terminators are where offsets stop being interchangeable with positions, @@ -146,13 +162,20 @@ test('addresses every combination of line terminators', () => { const texts: string[] = ['']; let current = ['']; for (let length = 0; length < 5; length++) { - current = current.flatMap((text) => alphabet.map((character) => text + character)); + current = current.flatMap((text) => + alphabet.map((character) => text + character), + ); texts.push(...current); } const failures: string[] = []; for (const source of texts) { - const document = TextDocument.create('file:///a.ts', 'typescript', 1, source); + const document = TextDocument.create( + 'file:///a.ts', + 'typescript', + 1, + source, + ); for (const formatted of texts) { const edit = computeMinimalEdit(source, formatted); if (!edit) { @@ -163,7 +186,9 @@ test('addresses every combination of line terminators', () => { const end = document.offsetAt(document.positionAt(edit.end)); const applied = source.slice(0, start) + edit.newText + source.slice(end); if (applied !== formatted) { - failures.push(`${JSON.stringify(source)} -> ${JSON.stringify(formatted)}`); + failures.push( + `${JSON.stringify(source)} -> ${JSON.stringify(formatted)}`, + ); } // The hand-rolled position mapping must agree with the reference @@ -174,7 +199,9 @@ test('addresses every combination of line terminators', () => { end: document.positionAt(edit.end), }; if (JSON.stringify(range) !== JSON.stringify(expected)) { - failures.push(`positions ${JSON.stringify(source)} -> ${JSON.stringify(formatted)}`); + failures.push( + `positions ${JSON.stringify(source)} -> ${JSON.stringify(formatted)}`, + ); } } } diff --git a/packages/rstack/tests/fmt/lsp/server.test.ts b/packages/rstack/tests/fmt/lsp/server.test.ts index c84475a8..ca764b21 100644 --- a/packages/rstack/tests/fmt/lsp/server.test.ts +++ b/packages/rstack/tests/fmt/lsp/server.test.ts @@ -4,12 +4,15 @@ import { createDocumentEdits } from '../../../src/fmt/lsp/server.ts'; test('maps the edit onto the formatted document', async () => { const edits = await createDocumentEdits( () => 'const a = 1;\nconst b=2;\n', - async () => 'const a = 1;\nconst b = 2;\n', + () => Promise.resolve('const a = 1;\nconst b = 2;\n'), ); expect(edits).toEqual([ { - range: { start: { line: 1, character: 7 }, end: { line: 1, character: 8 } }, + range: { + start: { line: 1, character: 7 }, + end: { line: 1, character: 8 }, + }, newText: ' = ', }, ]); @@ -18,15 +21,19 @@ test('maps the edit onto the formatted document', async () => { test('returns no edits for an already formatted document', async () => { const getText = () => 'const a = 1;\n'; - expect(await createDocumentEdits(getText, async () => 'const a = 1;\n')).toEqual([]); - expect(await createDocumentEdits(getText, async () => undefined)).toEqual([]); + expect( + await createDocumentEdits(getText, () => Promise.resolve('const a = 1;\n')), + ).toEqual([]); + expect( + await createDocumentEdits(getText, () => Promise.resolve(undefined)), + ).toEqual([]); }); test('returns no edits for a document that is not open', async () => { expect( await createDocumentEdits( () => undefined, - async () => '', + () => Promise.resolve(''), ), ).toEqual([]); }); @@ -38,10 +45,10 @@ test('returns no edits when the document changes while it is formatted', async ( const edits = await createDocumentEdits( () => text, - async (source) => { + (source) => { text = 'const b=2;\n'; - return source.replace('const b=2;', 'const b = 2;'); + return Promise.resolve(source.replace('const b=2;', 'const b = 2;')); }, ); diff --git a/packages/rstack/tests/fmt/plugins.test.ts b/packages/rstack/tests/fmt/plugins.test.ts index 80f998e8..5f25262e 100644 --- a/packages/rstack/tests/fmt/plugins.test.ts +++ b/packages/rstack/tests/fmt/plugins.test.ts @@ -1,10 +1,13 @@ import { pathToFileURL } from 'node:url'; import { expect, test } from 'rstack/test'; -import { createFingerprintResolver, createPluginResolver } from '../../src/fmt/plugins.ts'; +import { + createFingerprintResolver, + createPluginResolver, +} from '../../src/fmt/plugins.ts'; import { withTempProject, writeProjectFile } from './helpers.ts'; test('resolves plugin specifiers from the config root', async () => { - await withTempProject(async (rootPath) => { + await withTempProject((rootPath) => { const packageEntry = writeProjectFile( rootPath, 'node_modules/prettier-plugin-packagejson/import.mjs', @@ -40,7 +43,8 @@ test('resolves plugin specifiers from the config root', async () => { ], }; - const resolved = createPluginResolver(rootPath)(options); + const resolvePlugins = createPluginResolver(rootPath); + const resolved = resolvePlugins(options); expect(resolved.plugins).toEqual([ pathToFileURL(packageEntry).href, @@ -50,6 +54,7 @@ test('resolves plugin specifiers from the config root', async () => { 'data:text/javascript,export default {}', ]); expect(options.plugins[0]).toBe('prettier-plugin-packagejson'); + expect(resolvePlugins(options)).toBe(resolved); }); }); @@ -63,7 +68,10 @@ test('rejects imported plugin objects', () => { test('fingerprints installed package plugins once', async () => { await withTempProject(async (rootPath) => { - const entry = writeProjectFile(rootPath, 'node_modules/prettier-plugin-fixture/dist/index.mjs'); + const entry = writeProjectFile( + rootPath, + 'node_modules/prettier-plugin-fixture/dist/index.mjs', + ); const packageJsonPath = 'node_modules/prettier-plugin-fixture/package.json'; writeProjectFile( rootPath, diff --git a/packages/rstack/tests/fmt/runner.test.ts b/packages/rstack/tests/fmt/runner.test.ts index dbcd59e6..101151af 100644 --- a/packages/rstack/tests/fmt/runner.test.ts +++ b/packages/rstack/tests/fmt/runner.test.ts @@ -1,4 +1,10 @@ -import { chmodSync, readFileSync, statSync, utimesSync, writeFileSync } from 'node:fs'; +import { + chmodSync, + readFileSync, + statSync, + utimesSync, + writeFileSync, +} from 'node:fs'; import path from 'node:path'; import { expect, test } from 'rstack/test'; import { runFmtFiles } from '../../src/fmt/runner.ts'; @@ -46,17 +52,20 @@ test('writes changed files', async () => { }); }); -test.runIf(process.platform !== 'win32')('preserves file mode when writing', async () => { - await withTempProject(async (rootPath) => { - const filePath = path.join(rootPath, 'executable.ts'); - writeFileSync(filePath, 'const value=1'); - chmodSync(filePath, 0o744); +test.runIf(process.platform !== 'win32')( + 'preserves file mode when writing', + async () => { + await withTempProject(async (rootPath) => { + const filePath = path.join(rootPath, 'executable.ts'); + writeFileSync(filePath, 'const value=1'); + chmodSync(filePath, 0o744); - await run([createFmtRequest(filePath)]); + await run([createFmtRequest(filePath)]); - expect(statSync(filePath).mode & 0o777).toBe(0o744); - }); -}); + expect(statSync(filePath).mode & 0o777).toBe(0o744); + }); + }, +); for (const mode of ['check', 'list-different'] as const) { test(`${mode} reports differences without writing`, async () => { @@ -84,7 +93,10 @@ test('continues after a file fails and gives errors exit-code precedence', async writeFileSync(invalidPath, 'const value = ;'); writeFileSync(validPath, 'const value=1'); - const result = await run([createFmtRequest(invalidPath), createFmtRequest(validPath)], 'check'); + const result = await run( + [createFmtRequest(invalidPath), createFmtRequest(validPath)], + 'check', + ); expect(result).toMatchObject({ exitCode: 2, @@ -110,7 +122,11 @@ test('omits unsupported files from the result', async () => { }, ]); - expect(result).toMatchObject({ exitCode: 2, files: [], processedFileCount: 0 }); + expect(result).toMatchObject({ + exitCode: 2, + files: [], + processedFileCount: 0, + }); expect(readFileSync(filePath, 'utf8')).toBe('plain text'); }); }); diff --git a/packages/rstack/tests/fmt/runnerCache.test.ts b/packages/rstack/tests/fmt/runnerCache.test.ts index f9432a94..8426d5e1 100644 --- a/packages/rstack/tests/fmt/runnerCache.test.ts +++ b/packages/rstack/tests/fmt/runnerCache.test.ts @@ -2,10 +2,19 @@ import { readFileSync, statSync, utimesSync, writeFileSync } from 'node:fs'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; import { expect, test } from 'rstack/test'; -import { cacheNamespace, createOptionsHasher, sha256 } from '../../src/fmt/cacheIdentity.ts'; +import { + cacheHashLength, + cacheNamespace, + createCacheHash, + createOptionsHasher, +} from '../../src/fmt/cacheIdentity.ts'; import { loadFmtCacheStore } from '../../src/fmt/cacheStore.ts'; import { runFmtFiles } from '../../src/fmt/runner.ts'; -import type { FmtCacheContext, FmtFileRequest, FmtMode } from '../../src/fmt/types.ts'; +import type { + FmtCacheContext, + FmtFileRequest, + FmtMode, +} from '../../src/fmt/types.ts'; import { createFmtCacheContext, createFmtRequest, @@ -36,12 +45,12 @@ for (const mode of ['check', 'list-different'] as const) { const store = await loadFmtCacheStore(cache.filePath, cacheNamespace); expect(store.get('clean.ts')).toEqual([ - sha256(readFileSync(cleanPath)), + createCacheHash(readFileSync(cleanPath)), expect.any(String), 'clean', ]); expect(store.get('dirty.ts')).toEqual([ - sha256(readFileSync(dirtyPath)), + createCacheHash(readFileSync(dirtyPath)), expect.any(String), 'dirty', ]); @@ -72,14 +81,20 @@ test('uses content hashes instead of file metadata', async () => { size: Buffer.byteLength(clean), }); - await expect(run([createFmtRequest(filePath)], 'check', cache)).resolves.toMatchObject({ + await expect( + run([createFmtRequest(filePath)], 'check', cache), + ).resolves.toMatchObject({ exitCode: 1, files: [{ path: filePath, status: 'different' }], }); const secondStore = await loadFmtCacheStore(cache.filePath, cacheNamespace); const secondEntry = secondStore.get('index.ts'); - expect(secondEntry).toEqual([sha256(readFileSync(filePath)), expect.any(String), 'dirty']); + expect(secondEntry).toEqual([ + createCacheHash(readFileSync(filePath)), + expect.any(String), + 'dirty', + ]); expect(secondEntry?.[0]).not.toBe(firstEntry?.[0]); }); }); @@ -90,10 +105,16 @@ test('invalidates entries when final options change', async () => { const cache = createFmtCacheContext(rootPath); writeFileSync(filePath, 'const value = "text";\n'); - const initial = createFmtRequest(filePath, { parser: 'typescript', singleQuote: false }); + const initial = createFmtRequest(filePath, { + parser: 'typescript', + singleQuote: false, + }); await run([initial], 'check', cache); - const changed = createFmtRequest(filePath, { parser: 'typescript', singleQuote: true }); + const changed = createFmtRequest(filePath, { + parser: 'typescript', + singleQuote: true, + }); await expect(run([changed], 'check', cache)).resolves.toMatchObject({ exitCode: 1, files: [{ path: filePath, status: 'different' }], @@ -101,7 +122,7 @@ test('invalidates entries when final options change', async () => { const store = await loadFmtCacheStore(cache.filePath, cacheNamespace); expect(store.get('index.ts')).toEqual([ - sha256(readFileSync(filePath)), + createCacheHash(readFileSync(filePath)), createOptionsHasher()(changed.options), 'dirty', ]); @@ -110,7 +131,11 @@ test('invalidates entries when final options change', async () => { test('caches unsupported parser results until final options change', async () => { await withTempProject(async (rootPath) => { - const filePath = writeProjectFile(rootPath, 'data.unknown', '{"value":true}'); + const filePath = writeProjectFile( + rootPath, + 'data.unknown', + '{"value":true}', + ); const cache = createFmtCacheContext(rootPath); const unsupported = createFmtRequest(filePath, {}); @@ -120,11 +145,11 @@ test('caches unsupported parser results until final options change', async () => files: [], processedFileCount: 0, }); - expect((await loadFmtCacheStore(cache.filePath, cacheNamespace)).get('data.unknown')).toEqual([ - null, - createOptionsHasher()(unsupported.options), - 'unsupported', - ]); + expect( + (await loadFmtCacheStore(cache.filePath, cacheNamespace)).get( + 'data.unknown', + ), + ).toEqual(['', createOptionsHasher()(unsupported.options), 'unsupported']); await expect(run([unsupported], 'check', cache)).resolves.toEqual(first); @@ -134,8 +159,12 @@ test('caches unsupported parser results until final options change', async () => files: [{ path: filePath, status: 'different' }], processedFileCount: 1, }); - expect((await loadFmtCacheStore(cache.filePath, cacheNamespace)).get('data.unknown')).toEqual([ - sha256(readFileSync(filePath)), + expect( + (await loadFmtCacheStore(cache.filePath, cacheNamespace)).get( + 'data.unknown', + ), + ).toEqual([ + createCacheHash(readFileSync(filePath)), createOptionsHasher()(supported.options), 'dirty', ]); @@ -154,8 +183,10 @@ test('invalidates cached unsupported parser results when content changes without files: [], processedFileCount: 0, }); - expect((await loadFmtCacheStore(cache.filePath, cacheNamespace)).get('script')).toEqual([ - sha256(readFileSync(filePath)), + expect( + (await loadFmtCacheStore(cache.filePath, cacheNamespace)).get('script'), + ).toEqual([ + createCacheHash(readFileSync(filePath)), createOptionsHasher()(file.options), 'unsupported', ]); @@ -168,8 +199,10 @@ test('invalidates cached unsupported parser results when content changes without files: [{ path: filePath, status: 'different' }], processedFileCount: 1, }); - expect((await loadFmtCacheStore(cache.filePath, cacheNamespace)).get('script')).toEqual([ - sha256(readFileSync(filePath)), + expect( + (await loadFmtCacheStore(cache.filePath, cacheNamespace)).get('script'), + ).toEqual([ + createCacheHash(readFileSync(filePath)), createOptionsHasher()(file.options), 'dirty', ]); @@ -178,7 +211,11 @@ test('invalidates cached unsupported parser results when content changes without test('caches only plugins with stable fingerprints', async () => { await withTempProject(async (rootPath) => { - const filePath = writeProjectFile(rootPath, 'data.fixture', '{"value":true}'); + const filePath = writeProjectFile( + rootPath, + 'data.fixture', + '{"value":true}', + ); const pluginEntry = writeProjectFile( rootPath, 'node_modules/prettier-plugin-fixture/index.mjs', @@ -199,27 +236,31 @@ test('caches only plugins with stable fingerprints', async () => { }), ); const cache = createFmtCacheContext(rootPath); - const file = createFmtRequest(filePath, { plugins: [pathToFileURL(pluginEntry).href] }); + const file = createFmtRequest(filePath, { + plugins: [pathToFileURL(pluginEntry).href], + }); writePackageJson(); await run([file], 'check', cache); - expect((await loadFmtCacheStore(cache.filePath, cacheNamespace)).get('data.fixture')).toBe( - undefined, - ); + expect( + (await loadFmtCacheStore(cache.filePath, cacheNamespace)).get( + 'data.fixture', + ), + ).toBe(undefined); writePackageJson('1.0.0'); await run([file], 'check', cache); - const firstHash = (await loadFmtCacheStore(cache.filePath, cacheNamespace)).get( - 'data.fixture', - )?.[1]; - expect(firstHash).toHaveLength(64); + const firstHash = ( + await loadFmtCacheStore(cache.filePath, cacheNamespace) + ).get('data.fixture')?.[1]; + expect(firstHash).toHaveLength(cacheHashLength); writePackageJson('2.0.0'); await run([file], 'check', cache); - const secondHash = (await loadFmtCacheStore(cache.filePath, cacheNamespace)).get( - 'data.fixture', - )?.[1]; - expect(secondHash).toHaveLength(64); + const secondHash = ( + await loadFmtCacheStore(cache.filePath, cacheNamespace) + ).get('data.fixture')?.[1]; + expect(secondHash).toHaveLength(cacheHashLength); expect(secondHash).not.toBe(firstHash); }); }); @@ -232,7 +273,11 @@ test('preserves entries outside the formatted subset', async () => { writeFileSync(firstPath, 'const first = 1;\n'); writeFileSync(secondPath, 'const second = 2;\n'); - await run([createFmtRequest(firstPath), createFmtRequest(secondPath)], 'check', cache); + await run( + [createFmtRequest(firstPath), createFmtRequest(secondPath)], + 'check', + cache, + ); const firstStore = await loadFmtCacheStore(cache.filePath, cacheNamespace); const secondEntry = firstStore.get('second.ts'); @@ -253,7 +298,9 @@ test('does not cache formatting errors', async () => { writeFileSync(invalidPath, 'const invalid = ;'); await run([createFmtRequest(validPath)], 'check', cache); - await expect(run([createFmtRequest(invalidPath)], 'check', cache)).resolves.toMatchObject({ + await expect( + run([createFmtRequest(invalidPath)], 'check', cache), + ).resolves.toMatchObject({ exitCode: 2, files: [{ path: invalidPath, status: 'error' }], }); @@ -281,12 +328,12 @@ test('write persists clean results for misses and hits', async () => { const store = await loadFmtCacheStore(cache.filePath, cacheNamespace); expect(store.get('clean.ts')).toEqual([ - sha256(readFileSync(cleanPath)), + createCacheHash(readFileSync(cleanPath)), expect.any(String), 'clean', ]); expect(store.get('dirty.ts')).toEqual([ - sha256(readFileSync(dirtyPath)), + createCacheHash(readFileSync(dirtyPath)), expect.any(String), 'clean', ]); @@ -297,7 +344,9 @@ test('write persists clean results for misses and hits', async () => { files: [], processedFileCount: 2, }); - expect(files.map((file) => statSync(file.path).mtimeMs)).toEqual(timestamps); + expect(files.map((file) => statSync(file.path).mtimeMs)).toEqual( + timestamps, + ); }); }); @@ -318,7 +367,7 @@ test('write converts a dirty entry to clean', async () => { const store = await loadFmtCacheStore(cache.filePath, cacheNamespace); expect(store.get('index.ts')).toEqual([ - sha256(readFileSync(filePath)), + createCacheHash(readFileSync(filePath)), expect.any(String), 'clean', ]); diff --git a/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts b/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts index 71d48662..cb3dec45 100644 --- a/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts +++ b/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts @@ -1,5 +1,8 @@ import { beforeEach, expect, rs, test } from 'rstack/test'; -import { cacheNamespace, createOptionsHasher } from '../../src/fmt/cacheIdentity.ts'; +import { + cacheNamespace, + createOptionsHasher, +} from '../../src/fmt/cacheIdentity.ts'; import { loadFmtCacheStore } from '../../src/fmt/cacheStore.ts'; import { runFmtFiles } from '../../src/fmt/runner.ts'; import { @@ -24,7 +27,10 @@ beforeEach(() => { mocks.workerPoolCalls.length = 0; }); -const createCachedUnsupportedFile = async (rootPath: string, fileName: string) => { +const createCachedUnsupportedFile = async ( + rootPath: string, + fileName: string, +) => { const filePath = writeProjectFile(rootPath, fileName, 'plain text'); const cache = createFmtCacheContext(rootPath); const file = createFmtRequest(filePath, {}); @@ -34,7 +40,7 @@ const createCachedUnsupportedFile = async (rootPath: string, fileName: string) = } const store = await loadFmtCacheStore(cache.filePath, cacheNamespace); - store.set(fileName, [null, optionsHash, 'unsupported']); + store.set(fileName, ['', optionsHash, 'unsupported']); await expect(store.save()).resolves.toBe(true); return { cache, file }; @@ -42,7 +48,10 @@ const createCachedUnsupportedFile = async (rootPath: string, fileName: string) = test('does not start the worker pool when every parser result is cached as unsupported', async () => { await withTempProject(async (rootPath) => { - const { cache, file } = await createCachedUnsupportedFile(rootPath, 'example.unknown'); + const { cache, file } = await createCachedUnsupportedFile( + rootPath, + 'example.unknown', + ); await expect( runFmtFiles({ @@ -61,7 +70,10 @@ test('does not start the worker pool when every parser result is cached as unsup test('starts the worker pool for a path-only unsupported entry without an extension', async () => { await withTempProject(async (rootPath) => { - const { cache, file } = await createCachedUnsupportedFile(rootPath, 'script'); + const { cache, file } = await createCachedUnsupportedFile( + rootPath, + 'script', + ); await expect( runFmtFiles({ diff --git a/packages/rstack/tests/fmt/worker.test.ts b/packages/rstack/tests/fmt/worker.test.ts index 6f3631d5..ddc35ba9 100644 --- a/packages/rstack/tests/fmt/worker.test.ts +++ b/packages/rstack/tests/fmt/worker.test.ts @@ -1,7 +1,7 @@ import path from 'node:path'; import { readFileSync } from 'node:fs'; import { expect, test } from 'rstack/test'; -import { sha256 } from '../../src/fmt/cacheIdentity.ts'; +import { createCacheHash } from '../../src/fmt/cacheIdentity.ts'; import { formatFile } from '../../src/fmt/worker.ts'; import { withTempProject, writeProjectFile } from './helpers.ts'; @@ -11,17 +11,27 @@ test('returns cached states before resolving the parser', async () => { const filePath = writeProjectFile(rootPath, 'example.ts', source); const noExtensionPath = writeProjectFile(rootPath, 'script', source); const missingPath = path.join(rootPath, 'missing.unknown'); - const contentHash = sha256(source); + const contentHash = createCacheHash(source); const optionsHash = 'options'; for (const [entry, targetPath, shouldWrite, status] of [ [[contentHash, optionsHash, 'clean'], filePath, false, 'unchanged'], [[contentHash, optionsHash, 'dirty'], filePath, false, 'changed'], [[contentHash, optionsHash, 'clean'], filePath, true, 'unchanged'], - [[contentHash, optionsHash, 'unsupported'], noExtensionPath, false, 'unsupported'], - [[contentHash, optionsHash, 'unsupported'], noExtensionPath, true, 'unsupported'], - [[null, optionsHash, 'unsupported'], missingPath, false, 'unsupported'], - [[null, optionsHash, 'unsupported'], missingPath, true, 'unsupported'], + [ + [contentHash, optionsHash, 'unsupported'], + noExtensionPath, + false, + 'unsupported', + ], + [ + [contentHash, optionsHash, 'unsupported'], + noExtensionPath, + true, + 'unsupported', + ], + [['', optionsHash, 'unsupported'], missingPath, false, 'unsupported'], + [['', optionsHash, 'unsupported'], missingPath, true, 'unsupported'], ] as const) { await expect( formatFile({ @@ -44,7 +54,11 @@ test('returns cached states before resolving the parser', async () => { test('does not trust path-only unsupported entries for files without extensions', async () => { await withTempProject(async (rootPath) => { - const filePath = writeProjectFile(rootPath, 'script', '#!/usr/bin/env node\nconst value=1'); + const filePath = writeProjectFile( + rootPath, + 'script', + '#!/usr/bin/env node\nconst value=1', + ); await expect( formatFile({ @@ -54,13 +68,13 @@ test('does not trust path-only unsupported entries for files without extensions' }, shouldWrite: false, cache: { - entry: [null, 'options', 'unsupported'], + entry: ['', 'options', 'unsupported'], optionsHash: 'options', }, }), ).resolves.toEqual({ status: 'changed', - cacheEntry: [sha256(readFileSync(filePath)), 'options', 'dirty'], + cacheEntry: [createCacheHash(readFileSync(filePath)), 'options', 'dirty'], }); }); }); @@ -81,7 +95,7 @@ test('resolves parser support before reading on a cache miss', async () => { }), ).resolves.toEqual({ status: 'unsupported', - cacheEntry: [null, 'options', 'unsupported'], + cacheEntry: ['', 'options', 'unsupported'], }); }); }); diff --git a/packages/rstack/tests/fmt/yukuPlugin.test.ts b/packages/rstack/tests/fmt/yukuPlugin.test.ts index b4841ce3..b8e79457 100644 --- a/packages/rstack/tests/fmt/yukuPlugin.test.ts +++ b/packages/rstack/tests/fmt/yukuPlugin.test.ts @@ -1,4 +1,9 @@ -import { format, getFileInfo, type Options, type ParserOptions } from 'prettier'; +import { + format, + getFileInfo, + type Options, + type ParserOptions, +} from 'prettier'; import { expect, test } from 'rstack/test'; import { yukuPlugin } from '../../src/fmt/yukuPlugin.ts'; @@ -9,11 +14,14 @@ const formatWithYuku = ( format(source, { plugins: [yukuPlugin], ...options, - filepath: options.filepath ?? `example.${options.parser === 'yuku' ? 'js' : 'ts'}`, + filepath: + options.filepath ?? `example.${options.parser === 'yuku' ? 'js' : 'ts'}`, }); test('exposes the same JavaScript and TypeScript language mappings as the official plugin', async () => { - expect(yukuPlugin.languages?.map(({ name, parsers }) => ({ name, parsers }))).toEqual([ + expect( + yukuPlugin.languages?.map(({ name, parsers }) => ({ name, parsers })), + ).toEqual([ { name: 'JavaScript', parsers: ['yuku', 'yuku-ts'] }, { name: 'JSX', parsers: ['yuku', 'yuku-ts'] }, { name: 'TypeScript', parsers: ['yuku-ts'] }, @@ -63,7 +71,9 @@ test.each(['example.d.ts', 'example.d.mts', 'example.d.cts'])( filepath, parser: 'yuku-ts', }), - ).rejects.toThrow('An implementation cannot be declared in ambient contexts'); + ).rejects.toThrow( + 'An implementation cannot be declared in ambient contexts', + ); }, ); @@ -115,7 +125,8 @@ test.each([ parser: 'yuku-ts' as const, filepath: 'example.tsx', source: 'const view=({(item)})', - expected: 'const view = {item};\n', + expected: + 'const view = {item};\n', }, ])('normalizes $name for the ESTree printer', async (fixture) => { await expect( @@ -179,15 +190,18 @@ test.each([ hasPragma: false, hasIgnorePragma: false, }, -])('matches Prettier pragma detection for $source', ({ source, hasPragma, hasIgnorePragma }) => { - const parser = yukuPlugin.parsers?.yuku; - if (!parser?.hasPragma || !parser.hasIgnorePragma) { - throw new Error('The Yuku parser does not expose pragma handlers.'); - } +])( + 'matches Prettier pragma detection for $source', + ({ source, hasPragma, hasIgnorePragma }) => { + const parser = yukuPlugin.parsers?.yuku; + if (!parser?.hasPragma || !parser.hasIgnorePragma) { + throw new Error('The Yuku parser does not expose pragma handlers.'); + } - expect(parser.hasPragma(source)).toBe(hasPragma); - expect(parser.hasIgnorePragma(source)).toBe(hasIgnorePragma); -}); + expect(parser.hasPragma(source)).toBe(hasPragma); + expect(parser.hasIgnorePragma(source)).toBe(hasIgnorePragma); + }, +); test('matches Prettier JavaScript location overrides', () => { const parser = yukuPlugin.parsers?.yuku; @@ -278,10 +292,10 @@ test('matches the official hashbang AST shape', async () => { } const options = { filepath: 'example.js' } as ParserOptions; - const astWithoutHashbang = (await parser.parse('const value = 1', options)) as Record< - string, - unknown - >; + const astWithoutHashbang = (await parser.parse( + 'const value = 1', + options, + )) as Record; const astWithHashbang = (await parser.parse( '#!/usr/bin/env node\nconst value = 1', options, diff --git a/packages/rstack/tests/helpers/cli.ts b/packages/rstack/tests/helpers/cli.ts index d1d287d0..790e34f4 100644 --- a/packages/rstack/tests/helpers/cli.ts +++ b/packages/rstack/tests/helpers/cli.ts @@ -2,7 +2,10 @@ import { type ExecSyncOptions, execSync } from 'node:child_process'; import path from 'node:path'; import type { LogHelper } from '@rstackjs/test-utils'; -export const RSTACK_BIN_PATH: string = path.join(import.meta.dirname, '../../bin/rs.js'); +export const RSTACK_BIN_PATH: string = path.join( + import.meta.dirname, + '../../bin/rs.js', +); export type ExecCliOptions = ExecSyncOptions & { logHelper?: LogHelper; @@ -18,7 +21,10 @@ type ExecCliError = Error & { stderr?: Buffer | string; }; -const addLog = (logHelper: LogHelper | undefined, output: Buffer | string | undefined) => { +const addLog = ( + logHelper: LogHelper | undefined, + output: Buffer | string | undefined, +) => { if (output) { logHelper?.addLog(output.toString()); } @@ -28,14 +34,17 @@ export const execCli: ExecCli = (command, options = {}) => { const { logHelper, ...execOptions } = options; try { - const output = execSync(`"${process.execPath}" "${RSTACK_BIN_PATH}" ${command}`, { - stdio: 'pipe', - ...execOptions, - env: { - ...process.env, - ...execOptions.env, + const output = execSync( + `"${process.execPath}" "${RSTACK_BIN_PATH}" ${command}`, + { + stdio: 'pipe', + ...execOptions, + env: { + ...process.env, + ...execOptions.env, + }, }, - }); + ); addLog(logHelper, output); return output.toString(); diff --git a/packages/rstack/tests/helpers/cliTest.ts b/packages/rstack/tests/helpers/cliTest.ts index f3ff322d..895d54c8 100644 --- a/packages/rstack/tests/helpers/cliTest.ts +++ b/packages/rstack/tests/helpers/cliTest.ts @@ -1,8 +1,16 @@ -import { type ChildProcess, type SpawnOptions, spawn as nodeSpawn } from 'node:child_process'; +import { + type ChildProcess, + type SpawnOptions, + spawn as nodeSpawn, +} from 'node:child_process'; import path from 'node:path'; import { prepareDist as basePrepareDist } from '@rstackjs/test-utils'; import { test as baseTest } from 'rstack/test'; -import { execCli as baseExecCli, type ExecCli, RSTACK_BIN_PATH } from './cli.ts'; +import { + execCli as baseExecCli, + type ExecCli, + RSTACK_BIN_PATH, +} from './cli.ts'; import { type ExtendedLogHelper, proxyConsole } from './logs.ts'; type Exec = ( @@ -33,7 +41,10 @@ function makeBox(title: string) { }; } -const setupExecOptions = (options: T, cwd: string): T => { +const setupExecOptions = ( + options: T, + cwd: string, +): T => { // inherit process.env from current process const { NODE_ENV: _, ...restEnv } = process.env; options.env ||= {}; @@ -47,7 +58,9 @@ export const test: CliTest = baseTest.extend({ const { testPath } = expect.getState(); if (!testPath) { - throw new Error('Unable to resolve current test file path from expect state.'); + throw new Error( + 'Unable to resolve current test file path from expect state.', + ); } await use(path.dirname(testPath)); @@ -86,7 +99,10 @@ export const test: CliTest = baseTest.extend({ const closes: Array<() => void> = []; const exec: Exec = (command, options = {}) => { - const childProcess = nodeSpawn(command, setupExecOptions({ shell: true, ...options }, cwd)); + const childProcess = nodeSpawn( + command, + setupExecOptions({ shell: true, ...options }, cwd), + ); const onData = (data: Buffer) => { logHelper.addLog(data.toString()); diff --git a/packages/rstack/tests/helpers/logs.ts b/packages/rstack/tests/helpers/logs.ts index 2a0d19fa..12646a26 100644 --- a/packages/rstack/tests/helpers/logs.ts +++ b/packages/rstack/tests/helpers/logs.ts @@ -14,7 +14,9 @@ export type LogHelper = BaseLogHelper & ExpectBuildEnd; export type ExtendedLogHelper = BaseExtendedLogHelper & ExpectBuildEnd; -export const proxyConsole = (options?: ProxyConsoleOptions): ExtendedLogHelper => { +export const proxyConsole = ( + options?: ProxyConsoleOptions, +): ExtendedLogHelper => { const logHelper = baseProxyConsole(options); return { diff --git a/packages/rstack/tests/setup/directories.test.ts b/packages/rstack/tests/setup/directories.test.ts index dfaf62da..d27c662a 100644 --- a/packages/rstack/tests/setup/directories.test.ts +++ b/packages/rstack/tests/setup/directories.test.ts @@ -2,7 +2,13 @@ import { existsSync, mkdirSync, readFileSync } from 'node:fs'; import path from 'node:path'; import { expect, test } from 'rstack/test'; import { installHooks } from '../../src/setup/install.ts'; -import { hooksPath, runGit, runHook, withRepository, writeHook } from './helpers.ts'; +import { + hooksPath, + runGit, + runHook, + withRepository, + writeHook, +} from './helpers.ts'; test('installs a custom hooks directory from the Git root and runs its hook', () => { withRepository((cwd) => { @@ -14,11 +20,15 @@ test('installs a custom hooks directory from the Git root and runs its hook', () status: 'installed', hooksPath: customHooksPath, }); - expect(runGit(cwd, ['config', '--local', '--get', 'core.hooksPath'])).toBe(customHooksPath); + expect(runGit(cwd, ['config', '--local', '--get', 'core.hooksPath'])).toBe( + customHooksPath, + ); expect(existsSync(path.join(cwd, customHooksPath, 'runner'))).toBe(true); expect(runHook(cwd).status).toBe(0); - expect(readFileSync(path.join(cwd, 'custom-hook-ran'), 'utf8')).toBe('ran\n'); + expect(readFileSync(path.join(cwd, 'custom-hook-ran'), 'utf8')).toBe( + 'ran\n', + ); }); }); @@ -36,12 +46,18 @@ test('installs repository-level hooks from a nested project', () => { status: 'unchanged', hooksPath, }); - expect(runGit(cwd, ['config', '--local', '--get', 'core.hooksPath'])).toBe(hooksPath); + expect(runGit(cwd, ['config', '--local', '--get', 'core.hooksPath'])).toBe( + hooksPath, + ); expect(existsSync(path.join(cwd, hooksPath, 'runner'))).toBe(true); - expect(readFileSync(path.join(cwd, hooksPath, '.owner'), 'utf8')).toBe('frontend\n'); + expect(readFileSync(path.join(cwd, hooksPath, '.owner'), 'utf8')).toBe( + 'frontend\n', + ); expect(runHook(cwd).status).toBe(0); - expect(readFileSync(path.join(projectDirectory, 'nested-hook-ran'), 'utf8')).toBe('ran\n'); + expect( + readFileSync(path.join(projectDirectory, 'nested-hook-ran'), 'utf8'), + ).toBe('ran\n'); }); }); @@ -50,15 +66,23 @@ test('installs a root-relative custom hooks directory from a nested project', () const projectDirectory = path.join(cwd, 'frontend app'); mkdirSync(projectDirectory); - expect(installHooks({ cwd: projectDirectory, hooksDir: 'config\\hooks' })).toEqual({ + expect( + installHooks({ cwd: projectDirectory, hooksDir: 'config\\hooks' }), + ).toEqual({ status: 'installed', hooksPath: 'config/hooks/_', }); - expect(installHooks({ cwd: projectDirectory, hooksDir: 'config\\hooks' })).toEqual({ + expect( + installHooks({ cwd: projectDirectory, hooksDir: 'config\\hooks' }), + ).toEqual({ status: 'unchanged', hooksPath: 'config/hooks/_', }); - expect(runGit(cwd, ['config', '--local', '--get', 'core.hooksPath'])).toBe('config/hooks/_'); - expect(existsSync(path.join(cwd, 'config', 'hooks', '_', 'runner'))).toBe(true); + expect(runGit(cwd, ['config', '--local', '--get', 'core.hooksPath'])).toBe( + 'config/hooks/_', + ); + expect(existsSync(path.join(cwd, 'config', 'hooks', '_', 'runner'))).toBe( + true, + ); }); }); diff --git a/packages/rstack/tests/setup/helpers.ts b/packages/rstack/tests/setup/helpers.ts index 206e54c6..794691f2 100644 --- a/packages/rstack/tests/setup/helpers.ts +++ b/packages/rstack/tests/setup/helpers.ts @@ -10,7 +10,8 @@ export const git = ( cwd: string, args: string[], env: NodeJS.ProcessEnv = process.env, -): SpawnSyncReturns => spawnSync('git', args, { cwd, encoding: 'utf8', env }); +): SpawnSyncReturns => + spawnSync('git', args, { cwd, encoding: 'utf8', env }); export const runGit = (cwd: string, args: string[]): string => { const result = git(cwd, args); @@ -21,7 +22,9 @@ export const runGit = (cwd: string, args: string[]): string => { }; export const withDirectory = (callback: (cwd: string) => void): void => { - const cwd = mkdtempSync(path.join(import.meta.dirname, 'test-temp-rstack hooks ')); + const cwd = mkdtempSync( + path.join(import.meta.dirname, 'test-temp-rstack hooks '), + ); const gitCeilingDirectories = process.env.GIT_CEILING_DIRECTORIES; // Keep Git from treating the temporary directory as part of this repository. process.env.GIT_CEILING_DIRECTORIES = import.meta.dirname; @@ -55,7 +58,11 @@ const hookEnv = (cwd: string, value?: string): NodeJS.ProcessEnv => { return env; }; -export const writeHook = (cwd: string, content: string, directory: string = hooksDir): void => { +export const writeHook = ( + cwd: string, + content: string, + directory: string = hooksDir, +): void => { const filePath = path.join(cwd, directory, 'pre-commit'); mkdirSync(path.dirname(filePath), { recursive: true }); writeFileSync(filePath, content); @@ -67,10 +74,17 @@ export const writeInit = (cwd: string, content: string): void => { writeFileSync(filePath, content); }; -export const runHook = (cwd: string, value?: string): SpawnSyncReturns => +export const runHook = ( + cwd: string, + value?: string, +): SpawnSyncReturns => git(cwd, ['hook', 'run', 'pre-commit'], hookEnv(cwd, value)); -export const runGitHook = (cwd: string, name: string, args: string[]): SpawnSyncReturns => +export const runGitHook = ( + cwd: string, + name: string, + args: string[], +): SpawnSyncReturns => git(cwd, ['hook', 'run', name, '--', ...args], hookEnv(cwd)); export const withRepository = (callback: (cwd: string) => void): void => diff --git a/packages/rstack/tests/setup/hooks.test.ts b/packages/rstack/tests/setup/hooks.test.ts index 3baa5348..a2bd5762 100644 --- a/packages/rstack/tests/setup/hooks.test.ts +++ b/packages/rstack/tests/setup/hooks.test.ts @@ -6,7 +6,9 @@ import { createHookFiles } from '../../src/setup/hooks.ts'; import { withDirectory } from './helpers.ts'; test('generates the runner and all client-side Git hook shims', () => { - expect(Object.keys(createHookFiles()).filter((name) => name !== 'runner')).toEqual([ + expect( + Object.keys(createHookFiles()).filter((name) => name !== 'runner'), + ).toEqual([ 'pre-commit', 'pre-merge-commit', 'prepare-commit-msg', @@ -25,17 +27,24 @@ test('generates the runner and all client-side Git hook shims', () => { }); test.runIf(process.platform === 'win32')('converts Windows Node paths', () => { - const { runner } = createHookFiles(String.raw`C:\Program Files\nodejs\node.exe`); + const { runner } = createHookFiles( + String.raw`C:\Program Files\nodejs\node.exe`, + ); - expect(runner).toContain("rs_node_fallback='/c/Program Files/nodejs/node.exe'"); + expect(runner).toContain( + "rs_node_fallback='/c/Program Files/nodejs/node.exe'", + ); }); -test.runIf(process.platform !== 'win32')('preserves backslashes in POSIX Node paths', () => { - const nodeExecutable = String.raw`/opt/node\24/bin/node`; - const { runner } = createHookFiles(nodeExecutable); +test.runIf(process.platform !== 'win32')( + 'preserves backslashes in POSIX Node paths', + () => { + const nodeExecutable = String.raw`/opt/node\24/bin/node`; + const { runner } = createHookFiles(nodeExecutable); - expect(runner).toContain(`rs_node_fallback='${nodeExecutable}'`); -}); + expect(runner).toContain(`rs_node_fallback='${nodeExecutable}'`); + }, +); test.runIf(process.platform !== 'win32')('runs generated hooks', () => { withDirectory((directory) => { @@ -58,7 +67,9 @@ test.runIf(process.platform !== 'win32')('runs generated hooks', () => { writeFileSync(path.join(generatedDirectory, 'runner'), files.runner); writeFileSync(generatedHook, files['pre-commit']); - expect(spawnSync('sh', [generatedHook], { cwd: directory, env }).status).toBe(0); + expect( + spawnSync('sh', [generatedHook], { cwd: directory, env }).status, + ).toBe(0); writeFileSync( userHook, @@ -89,7 +100,9 @@ printf 'unreachable\\n' }); expect(errexitResult.status).toBe(1); - expect(errexitResult.stdout).toBe('Rstack - pre-commit hook failed (code 1)\n'); + expect(errexitResult.stdout).toBe( + 'Rstack - pre-commit hook failed (code 1)\n', + ); mkdirSync(runtimeDirectory, { recursive: true }); writeFileSync(init, `export PATH="${runtimeDirectory}"\n`); diff --git a/packages/rstack/tests/setup/install.test.ts b/packages/rstack/tests/setup/install.test.ts index 355d77c7..f91b2b45 100644 --- a/packages/rstack/tests/setup/install.test.ts +++ b/packages/rstack/tests/setup/install.test.ts @@ -1,19 +1,38 @@ -import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'; +import { + chmodSync, + existsSync, + mkdirSync, + readFileSync, + statSync, + writeFileSync, +} from 'node:fs'; import path from 'node:path'; import { expect, test } from 'rstack/test'; import { createHookFiles } from '../../src/setup/hooks.ts'; import { installHooks } from '../../src/setup/install.ts'; -import { git, hooksPath, restoreEnv, runGit, withRepository } from './helpers.ts'; +import { + git, + hooksPath, + restoreEnv, + runGit, + withRepository, +} from './helpers.ts'; test('installs generated hooks and configures the repository', () => { withRepository((cwd) => { expect(installHooks({ cwd })).toEqual({ status: 'installed', hooksPath }); - expect(runGit(cwd, ['config', '--local', '--get', 'core.hooksPath'])).toBe(hooksPath); + expect(runGit(cwd, ['config', '--local', '--get', 'core.hooksPath'])).toBe( + hooksPath, + ); const directory = path.join(cwd, hooksPath); - expect(readFileSync(path.join(directory, '.gitignore'), 'utf8')).toBe('*\n'); + expect(readFileSync(path.join(directory, '.gitignore'), 'utf8')).toBe( + '*\n', + ); expect(readFileSync(path.join(directory, '.owner'), 'utf8')).toBe('.\n'); - expect(runGit(cwd, ['status', '--short', '--untracked-files=all'])).toBe(''); + expect(runGit(cwd, ['status', '--short', '--untracked-files=all'])).toBe( + '', + ); for (const [name, content] of Object.entries(createHookFiles())) { const filePath = path.join(directory, name); @@ -40,16 +59,19 @@ test('is idempotent and preserves user hooks', () => { }); }); -test.runIf(process.platform !== 'win32')('restores executable mode on existing shims', () => { - withRepository((cwd) => { - expect(installHooks({ cwd }).status).toBe('installed'); - const shim = path.join(cwd, hooksPath, 'pre-commit'); - chmodSync(shim, 0o644); +test.runIf(process.platform !== 'win32')( + 'restores executable mode on existing shims', + () => { + withRepository((cwd) => { + expect(installHooks({ cwd }).status).toBe('installed'); + const shim = path.join(cwd, hooksPath, 'pre-commit'); + chmodSync(shim, 0o644); - expect(installHooks({ cwd }).status).toBe('installed'); - expect(statSync(shim).mode & 0o777).toBe(0o755); - }); -}); + expect(installHooks({ cwd }).status).toBe('installed'); + expect(statSync(shim).mode & 0o777).toBe(0o755); + }); + }, +); test('repairs generated files without rewriting an unchanged hooksPath', () => { withRepository((cwd) => { @@ -79,7 +101,7 @@ test('resolves repository context with a single Git process when unchanged', () const starts = readFileSync(tracePath, 'utf8') .trim() .split('\n') - .map((line) => JSON.parse(line)) + .map((line) => JSON.parse(line) as { argv: string[]; event: string }) .filter((event) => event.event === 'start'); expect(starts).toHaveLength(1); expect(starts[0].argv).toContain('rev-parse'); @@ -94,7 +116,9 @@ test('does not configure Git when writing generated files fails', () => { status: 'failed', reason: 'write-failed', }); - expect(git(cwd, ['config', '--local', '--get', 'core.hooksPath']).status).toBe(1); + expect( + git(cwd, ['config', '--local', '--get', 'core.hooksPath']).status, + ).toBe(1); }); }); @@ -106,7 +130,9 @@ test('reports Git configuration failures without changing hooksPath', () => { status: 'failed', reason: 'git-config-failed', }); - expect(git(cwd, ['config', '--local', '--get', 'core.hooksPath']).status).toBe(1); + expect( + git(cwd, ['config', '--local', '--get', 'core.hooksPath']).status, + ).toBe(1); expect(existsSync(path.join(cwd, hooksPath, 'runner'))).toBe(true); }); }); @@ -119,7 +145,9 @@ test('does not replace another Git hooks path', () => { status: 'skipped', reason: 'hooks-path-conflict', }); - expect(runGit(cwd, ['config', '--local', '--get', 'core.hooksPath'])).toBe('.husky/_'); + expect(runGit(cwd, ['config', '--local', '--get', 'core.hooksPath'])).toBe( + '.husky/_', + ); expect(existsSync(path.join(cwd, hooksPath))).toBe(false); }); }); @@ -134,7 +162,9 @@ test('does not bypass existing Git hooks', () => { reason: 'existing-git-hooks', message: 'existing Git hooks were found: pre-commit', }); - expect(git(cwd, ['config', '--local', '--get', 'core.hooksPath']).status).toBe(1); + expect( + git(cwd, ['config', '--local', '--get', 'core.hooksPath']).status, + ).toBe(1); expect(readFileSync(existingHook, 'utf8')).toBe('#!/usr/bin/env sh\n'); }); }); diff --git a/packages/rstack/tests/setup/runtime-errors.test.ts b/packages/rstack/tests/setup/runtime-errors.test.ts index ac85a13c..adcf553a 100644 --- a/packages/rstack/tests/setup/runtime-errors.test.ts +++ b/packages/rstack/tests/setup/runtime-errors.test.ts @@ -28,6 +28,8 @@ missing-command expect(missing.status).toBe(127); expect(output).toContain('Rstack - pre-commit hook failed (code 127)'); - expect(output).toContain(`Rstack - command not found in PATH=${actualPath}`); + expect(output).toContain( + `Rstack - command not found in PATH=${actualPath}`, + ); }); }); diff --git a/packages/rstack/tests/setup/runtime.test.ts b/packages/rstack/tests/setup/runtime.test.ts index 455f1a88..6fcff7c4 100644 --- a/packages/rstack/tests/setup/runtime.test.ts +++ b/packages/rstack/tests/setup/runtime.test.ts @@ -1,8 +1,20 @@ -import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { + chmodSync, + existsSync, + mkdirSync, + readFileSync, + writeFileSync, +} from 'node:fs'; import path from 'node:path'; import { expect, test } from 'rstack/test'; import { installHooks } from '../../src/setup/install.ts'; -import { runGitHook, runHook, withRepository, writeHook, writeInit } from './helpers.ts'; +import { + runGitHook, + runHook, + withRepository, + writeHook, + writeInit, +} from './helpers.ts'; test('loads user init and project binaries', () => { withRepository((cwd) => { @@ -30,8 +42,12 @@ rstack-hook-command expect(installHooks({ cwd: projectDirectory }).status).toBe('installed'); expect(runHook(cwd).status).toBe(0); - expect(readFileSync(path.join(projectDirectory, 'init-ran'), 'utf8')).toBe('loaded\n'); - expect(readFileSync(path.join(projectDirectory, 'project-bin-ran'), 'utf8')).toBe('ran\n'); + expect(readFileSync(path.join(projectDirectory, 'init-ran'), 'utf8')).toBe( + 'loaded\n', + ); + expect( + readFileSync(path.join(projectDirectory, 'project-bin-ran'), 'utf8'), + ).toBe('ran\n'); }); }); diff --git a/packages/rstack/tests/types/resolution-bundler/index.ts b/packages/rstack/tests/types/resolution-bundler/index.ts index 44477318..0dc37311 100644 --- a/packages/rstack/tests/types/resolution-bundler/index.ts +++ b/packages/rstack/tests/types/resolution-bundler/index.ts @@ -11,24 +11,31 @@ import { type LoadRstackConfigOptions, } from 'rstack/config'; import { defineConfig as defineLibConfig } from 'rstack/lib'; -import { js, ts } from 'rstack/lint'; +import { defineConfig as defineLintConfig } from 'rstack/lint'; import { expect as importedExpect, test as importedTest } from 'rstack/test'; const appConfig = defineAppConfig({}); const libConfig = defineLibConfig({}); -const loadOptions: LoadRstackConfigOptions = { configFilePath: 'rstack.config.ts' }; +const lintConfig = defineLintConfig([]); +const loadOptions: LoadRstackConfigOptions = { + configFilePath: 'rstack.config.ts', +}; const loadedConfig: Promise = loadRstackConfig(loadOptions); const configs: Configs = {}; void loadedConfig; void configs; -createRsbuild({ config: appConfig }); +void createRsbuild({ config: appConfig }); define.app(appConfig); define.lib(libConfig); +define.lint(lintConfig); +define.lint(({ js, ts }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, +]); define.doc({}); define.test({}); -define.lint([js.configs.recommended, ts.configs.recommended]); define.staged({}); importedTest('exposes the Rstest APIs', () => { diff --git a/packages/rstack/tests/types/resolution-nodenext/index.ts b/packages/rstack/tests/types/resolution-nodenext/index.ts index 95bf176a..0dc37311 100644 --- a/packages/rstack/tests/types/resolution-nodenext/index.ts +++ b/packages/rstack/tests/types/resolution-nodenext/index.ts @@ -1,4 +1,4 @@ -// This folder checks Rstack's exports and APIs with NodeNext resolution. +// This folder checks Rstack's exports and APIs with bundler resolution. import 'rstack/test/globals'; import 'rstack/test/importMeta'; import 'rstack/types'; @@ -11,24 +11,31 @@ import { type LoadRstackConfigOptions, } from 'rstack/config'; import { defineConfig as defineLibConfig } from 'rstack/lib'; -import { js, ts } from 'rstack/lint'; +import { defineConfig as defineLintConfig } from 'rstack/lint'; import { expect as importedExpect, test as importedTest } from 'rstack/test'; const appConfig = defineAppConfig({}); const libConfig = defineLibConfig({}); -const loadOptions: LoadRstackConfigOptions = { configFilePath: 'rstack.config.ts' }; +const lintConfig = defineLintConfig([]); +const loadOptions: LoadRstackConfigOptions = { + configFilePath: 'rstack.config.ts', +}; const loadedConfig: Promise = loadRstackConfig(loadOptions); const configs: Configs = {}; void loadedConfig; void configs; -createRsbuild({ config: appConfig }); +void createRsbuild({ config: appConfig }); define.app(appConfig); define.lib(libConfig); +define.lint(lintConfig); +define.lint(({ js, ts }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, +]); define.doc({}); define.test({}); -define.lint([js.configs.recommended, ts.configs.recommended]); define.staged({}); importedTest('exposes the Rstest APIs', () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c3ad4f86..13440900 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,11 +8,11 @@ settings: catalogs: default: '@napi-rs/cli': - specifier: ^3.8.3 - version: 3.8.3 + specifier: ^3.8.6 + version: 3.8.6 '@rsbuild/core': - specifier: ~2.1.10 - version: 2.1.10 + specifier: ~2.1.13 + version: 2.1.13 '@rsbuild/plugin-react': specifier: ^2.1.0 version: 2.1.0 @@ -20,8 +20,8 @@ catalogs: specifier: ^2.0.1 version: 2.0.1 '@rslib/core': - specifier: ~1.0.0-beta.2 - version: 1.0.0-beta.2 + specifier: ~1.0.0-beta.3 + version: 1.0.0-beta.3 '@rslint/core': specifier: ~0.8.0 version: 0.8.0 @@ -47,23 +47,23 @@ catalogs: specifier: ^0.2.0 version: 0.2.0 '@rstest/adapter-rsbuild': - specifier: ~0.11.6 - version: 0.11.6 + specifier: ~0.11.8 + version: 0.11.8 '@rstest/adapter-rslib': - specifier: ~0.11.6 - version: 0.11.6 + specifier: ~0.11.8 + version: 0.11.8 '@rstest/core': - specifier: ~0.11.6 - version: 0.11.6 + specifier: ~0.11.8 + version: 0.11.8 '@shikijs/transformers': - specifier: ^4.4.2 - version: 4.4.2 + specifier: ^4.4.3 + version: 4.4.3 '@testing-library/dom': specifier: ^10.4.1 version: 10.4.1 '@testing-library/jest-dom': - specifier: ^7.0.0 - version: 7.0.0 + specifier: ^7.0.1 + version: 7.0.1 '@testing-library/react': specifier: ^16.3.2 version: 16.3.2 @@ -86,8 +86,8 @@ catalogs: specifier: 2.1.0 version: 2.1.0 globals: - specifier: ^17.7.0 - version: 17.9.0 + specifier: ^17.11.0 + version: 17.11.0 happy-dom: specifier: ^20.11.2 version: 20.11.2 @@ -131,8 +131,8 @@ catalogs: specifier: 4.0.0 version: 4.0.0 svelte: - specifier: ^5.56.8 - version: 5.56.8 + specifier: ^5.56.9 + version: 5.56.9 tiny-readdir: specifier: 3.1.1 version: 3.1.1 @@ -149,8 +149,8 @@ catalogs: specifier: 1.0.12 version: 1.0.12 yuku-parser: - specifier: 0.8.4 - version: 0.8.4 + specifier: 0.8.7 + version: 0.8.7 importers: @@ -164,7 +164,7 @@ importers: version: 0.0.4 globals: specifier: 'catalog:' - version: 17.9.0 + version: 17.11.0 heading-case: specifier: 'catalog:' version: 1.1.5 @@ -189,13 +189,13 @@ importers: devDependencies: '@rsbuild/plugin-react': specifier: 'catalog:' - version: 2.1.0(@rsbuild/core@2.1.10)(@rspack/core@2.1.8) + version: 2.1.0(@rsbuild/core@2.1.10)(@rspack/core@2.1.10) '@testing-library/dom': specifier: 'catalog:' version: 10.4.1 '@testing-library/jest-dom': specifier: 'catalog:' - version: 7.0.0(@testing-library/dom@10.4.1) + version: 7.0.1(@testing-library/dom@10.4.1) '@testing-library/react': specifier: 'catalog:' version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.4)(@types/react@19.2.18)(react-dom@19.2.8)(react@19.2.8) @@ -222,7 +222,7 @@ importers: version: 10.4.1 '@testing-library/jest-dom': specifier: 'catalog:' - version: 7.0.0(@testing-library/dom@10.4.1) + version: 7.0.1(@testing-library/dom@10.4.1) '@types/node': specifier: 'catalog:' version: 24.13.3 @@ -277,13 +277,13 @@ importers: devDependencies: '@rsbuild/plugin-react': specifier: 'catalog:' - version: 2.1.0(@rsbuild/core@2.1.10)(@rspack/core@2.1.8) + version: 2.1.0(@rsbuild/core@2.1.10)(@rspack/core@2.1.10) '@testing-library/dom': specifier: 'catalog:' version: 10.4.1 '@testing-library/jest-dom': specifier: 'catalog:' - version: 7.0.0(@testing-library/dom@10.4.1) + version: 7.0.1(@testing-library/dom@10.4.1) '@testing-library/react': specifier: 'catalog:' version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.4)(@types/react@19.2.18)(react-dom@19.2.8)(react@19.2.8) @@ -309,7 +309,7 @@ importers: specifier: 'catalog:' version: 7.0.2 - examples/rstest-inline-projects: + examples/test-inline-projects: dependencies: react: specifier: 'catalog:' @@ -320,7 +320,7 @@ importers: devDependencies: '@rsbuild/plugin-react': specifier: 'catalog:' - version: 2.1.0(@rsbuild/core@2.1.10)(@rspack/core@2.1.8) + version: 2.1.0(@rsbuild/core@2.1.13)(@rspack/core@2.1.10) '@testing-library/dom': specifier: 'catalog:' version: 10.4.1 @@ -366,16 +366,16 @@ importers: dependencies: '@rsbuild/core': specifier: 'catalog:' - version: 2.1.10 + version: 2.1.13 '@rslib/core': specifier: 'catalog:' - version: 1.0.0-beta.2(typescript@7.0.2) + version: 1.0.0-beta.3(typescript@7.0.2) '@rslint/core': specifier: 'catalog:' version: 0.8.0 '@rstest/core': specifier: 'catalog:' - version: 0.11.6(happy-dom@20.11.2) + version: 0.11.8(happy-dom@20.11.2) prettier: specifier: 'catalog:' version: 3.9.6 @@ -384,11 +384,11 @@ importers: version: 2.1.0 yuku-parser: specifier: 'catalog:' - version: 0.8.4 + version: 0.8.7 devDependencies: '@napi-rs/cli': specifier: 'catalog:' - version: 3.8.3(@types/node@24.13.3)(node-addon-api@7.1.1)(supports-color@8.1.1) + version: 3.8.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(supports-color@8.1.1) '@rspress/core': specifier: 'catalog:' version: 2.0.19(micromark-util-types@2.0.2)(micromark@4.0.2)(supports-color@8.1.1) @@ -400,10 +400,10 @@ importers: version: 0.2.0 '@rstest/adapter-rsbuild': specifier: 'catalog:' - version: 0.11.6(@rsbuild/core@2.1.10)(@rstest/core@0.11.6) + version: 0.11.8(@rsbuild/core@2.1.13)(@rstest/core@0.11.8) '@rstest/adapter-rslib': specifier: 'catalog:' - version: 0.11.6(@rslib/core@1.0.0-beta.2)(@rstest/core@0.11.6)(typescript@7.0.2) + version: 0.11.8(@rslib/core@1.0.0-beta.3)(@rstest/core@0.11.8)(typescript@7.0.2) '@types/micromatch': specifier: 'catalog:' version: 4.0.10 @@ -427,7 +427,7 @@ importers: version: 4.0.8 prettier-plugin-svelte: specifier: 'catalog:' - version: 4.1.1(prettier@3.9.6)(svelte@5.56.8) + version: 4.1.1(prettier@3.9.6)(svelte@5.56.9) rslog: specifier: 'catalog:' version: 2.3.0 @@ -436,7 +436,7 @@ importers: version: 4.0.0 svelte: specifier: 'catalog:' - version: 5.56.8 + version: 5.56.9 tiny-readdir: specifier: 'catalog:' version: 3.1.1 @@ -469,7 +469,7 @@ importers: version: 1.14.7(@rspress/core@2.0.19) '@shikijs/transformers': specifier: 'catalog:' - version: 4.4.2 + version: 4.4.3 '@types/node': specifier: 'catalog:' version: 24.13.3 @@ -766,15 +766,21 @@ packages: '@types/react': '>=16' react: '>=16' - '@napi-rs/cli@3.8.3': - resolution: {integrity: sha512-f5vr9ih+ROvX5x9yZ4ywGj+kqcMXTzc4TsXUT4KUmfYlcdKTJ0uROuzeDP6rfDKhCqWo7EL6nBvfMWkhv5TMeQ==} + '@napi-rs/cli@3.8.6': + resolution: {integrity: sha512-FnJ9fghsV9Q4zh2aJGPSvQiUlJRC27B6KhzAXcIW2rlSD8keak3mhXw4tJYa3KJkP9whETfsPwqp/DJRnQg5ng==} engines: {node: ^20.17.0 || ^22.13.0 || >= 23.5.0} hasBin: true peerDependencies: - '@emnapi/runtime': 2.0.0-alpha.3 + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 + emnapi: ^1.7.1 || ^2.0.0-alpha.4 peerDependenciesMeta: + '@emnapi/core': + optional: true '@emnapi/runtime': optional: true + emnapi: + optional: true '@napi-rs/cross-toolchain@1.0.3': resolution: {integrity: sha512-ENPfLe4937bsKVTDA6zdABx4pq9w0tHqRrJHyaGxgaPq03a2Bd1unD5XSKjXJjebsABJ+MjAv1A2OvCgK9yehg==} @@ -1275,6 +1281,26 @@ packages: core-js: optional: true + '@rsbuild/core@2.1.12': + resolution: {integrity: sha512-xRqNHj/svDqeUzXPahmN4BdxEFCU1rxnVdjxyVV7WgsFfH+L3yAoQMJtIXVGhr00IQseDKkc3eonMF3NGrFj2Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + core-js: '>= 3.0.0' + peerDependenciesMeta: + core-js: + optional: true + + '@rsbuild/core@2.1.13': + resolution: {integrity: sha512-Z+6MzmjOio4+bFZQ24k+7ge/oNCOdXIunAssrswTNE8AIf6mcyXpJZevRXRiOEMZasRPA8VNyh+9JngQLg729Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + core-js: '>= 3.0.0' + peerDependenciesMeta: + core-js: + optional: true + '@rsbuild/plugin-react@2.1.0': resolution: {integrity: sha512-RQTIAWB/CwPjoWt9iAl+8HixeQVgZ7kEIBrWPCixfITyHdiD84h0YpUTpEUuz6kGHw1KXT9mHZ3Rwy6WG7aRDA==} peerDependencies: @@ -1291,8 +1317,8 @@ packages: '@rsbuild/core': optional: true - '@rslib/core@1.0.0-beta.2': - resolution: {integrity: sha512-A0j3MBP8Kga8Qrh7znO2UKf00Sga37PJCiTGUqVVBHb+u58fNoGXZtFAgeH6qKjf5eU1wYt4WptXX/1blOAfRw==} + '@rslib/core@1.0.0-beta.3': + resolution: {integrity: sha512-OtfmaBoGlHo1KYvQ3+B0ZLy3zUsAdoRZfWieaxoJMJ9uKAws2//afFgvUqeSDkkSJDlp8TDdgt4hib+QaFOaGQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -1357,74 +1383,166 @@ packages: cpu: [x64] os: [win32] + '@rspack/binding-darwin-arm64@2.1.10': + resolution: {integrity: sha512-DZlcTpbIb2mjeS1aSG4k01UH33Zj7T+k8ZylPK6HmsKs4JvK4wgpWFC78WVv3p/Aj3MZS6DwtDLwpZ2Ihj/fpg==} + cpu: [arm64] + os: [darwin] + '@rspack/binding-darwin-arm64@2.1.8': resolution: {integrity: sha512-kia+eWtyWPvR4ntg1bWYoVU8nLPbUg2fG3zgBEocsTcsh5ZENSiEPxEKymDgMyIMONUqj611E0775cdUBoNmqw==} cpu: [arm64] os: [darwin] + '@rspack/binding-darwin-x64@2.1.10': + resolution: {integrity: sha512-my/0h2LwxCRT6cg3oDDC2e0ZOxQLVajAdIcv0fqnQk5JRNvVuL89PuTutitnSqie1A0/JSL8OQz5XHwmoS3kow==} + cpu: [x64] + os: [darwin] + '@rspack/binding-darwin-x64@2.1.8': resolution: {integrity: sha512-08pBkFhlD3Y3Qzh94w/Fc3skaIE3e96kl2P14m8+tnYTcglpOfpA2OwS3iHt9fOqy0HjoAVe6/MW3cBgs5iabA==} cpu: [x64] os: [darwin] + '@rspack/binding-linux-arm64-gnu@2.1.10': + resolution: {integrity: sha512-laevn9g+E5PAUEGqiKe6Ju5KApsuQYp+bPI17XS3Lkl8eqL5pS/BmHYU7QMlst4GzV8+wlruVTMh//+st6Vqzg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@rspack/binding-linux-arm64-gnu@2.1.8': resolution: {integrity: sha512-KLniMc9GzhKpVqhPzaJo3KJwzdAllXVVqZIk/uL1QipXOxs57fgM4u7IexKPFVla0o/u1PQG/Ah2YLDmda24Ow==} cpu: [arm64] os: [linux] libc: [glibc] + '@rspack/binding-linux-arm64-musl@2.1.10': + resolution: {integrity: sha512-V71+Qz5G72+ROZXrJn5zxOszdG1AEbO8pcC/itXXtf4yRR6a3bVHKNKGhipBNxb8eI6cnD/01FH1h3ZG655jLw==} + cpu: [arm64] + os: [linux] + libc: [musl] + '@rspack/binding-linux-arm64-musl@2.1.8': resolution: {integrity: sha512-yUKAxHNGnICtw5RnxFWu4dHtsz/tdt7rbeFcsINNVre9HcrRxf5XP+FbOGL/SMxd9oM9XCo10paU2WckTKwbEA==} cpu: [arm64] os: [linux] libc: [musl] + '@rspack/binding-linux-ppc64-gnu@2.1.10': + resolution: {integrity: sha512-U7HlNzHcDtZ+LYOtOJmtx67kHEybZzUUAaP7aEXjGYO5WTCgh/176sW2UYP0rmZLrgUNFUuzn+B98RLaClNaVg==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rspack/binding-linux-riscv64-gnu@2.1.10': + resolution: {integrity: sha512-GMGTJpy9/ecE+5F5IfxZH4bXv0Wx/b2TiehTlCbTksbL+pKpLHYy0rwGdjWDKbmBkhxMMqPiC7PDnn9LbdnnLA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + '@rspack/binding-linux-riscv64-gnu@2.1.8': resolution: {integrity: sha512-gg4S1jaitwYPHR9HZ3zNGH1EK2GXINm66p4kEpOP1gbc+akyOouVF/dMcu9NGPlRg58FbEhVRZYKu7Z/zcpKHg==} cpu: [riscv64] os: [linux] libc: [glibc] + '@rspack/binding-linux-riscv64-musl@2.1.10': + resolution: {integrity: sha512-rkurnAWc04vIbzG1QCrPBWSJadZvaOt1mazFH3EdiJO8VUiu0I1T9zdiwuDOPrd50lOKIZlcTXbd5aaAkWEnvQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + '@rspack/binding-linux-riscv64-musl@2.1.8': resolution: {integrity: sha512-b/aU5j1h368SLNyz5u+flqpZVhzSZ1UIslaj9sZJuAvqkGWv3xsjc/28/PTo/RYXCxd0FNVAxTxWHKvRiAAS8w==} cpu: [riscv64] os: [linux] libc: [musl] + '@rspack/binding-linux-s390x-gnu@2.1.10': + resolution: {integrity: sha512-X+DyxkriZEAF/wihI7ERDv+CAS0mbMv36aEuQ+vXzTlvS6cSmpou/r29AHbvIF3NlG1UeAbDVlOs9QrMBZjpUQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rspack/binding-linux-x64-gnu@2.1.10': + resolution: {integrity: sha512-Fat09V6jUuyo9qG7Wyj9cQ31VDfLmokXyBtGqKxY5OvSWHereB7QUub5btbPXHwbp6Iq4aAQyUbbLTzvR1YaBw==} + cpu: [x64] + os: [linux] + libc: [glibc] + '@rspack/binding-linux-x64-gnu@2.1.8': resolution: {integrity: sha512-EyegohSx0BJRqieCg9f/caCqFARRWkqI5hwJt6k530MoOTLeq8I3vsbeg24/2MktwIC1dmJi8bl0+WhPKQs4eQ==} cpu: [x64] os: [linux] libc: [glibc] + '@rspack/binding-linux-x64-musl@2.1.10': + resolution: {integrity: sha512-lhHOnIJ4ClpIlA1f1L8aoxEZivYLjnjq5A6jKKz7BKsm+cHK8kqqEm6lO5KqA5xQT0Lonq1o28bmKHEj6JHInw==} + cpu: [x64] + os: [linux] + libc: [musl] + '@rspack/binding-linux-x64-musl@2.1.8': resolution: {integrity: sha512-I6E+goN+UQ297q4r1qdbiAyNCI3t0+a5Y0xDIAPOZfRDRxDTnH/LF8/y65gjsJoKRKyn7zxRC0T/NURTkRNQ9A==} cpu: [x64] os: [linux] libc: [musl] + '@rspack/binding-wasm32-wasi@2.1.10': + resolution: {integrity: sha512-KY5YbWbuvYcoaLXnV+vzZOvGRCeb6jt4EpVpKdph1h1IJjwX/ju15EQ+GOe3iecZEdf0OttQcNVcwBkLkFT9ag==} + cpu: [wasm32] + '@rspack/binding-wasm32-wasi@2.1.8': resolution: {integrity: sha512-om7GAKWAU3lcSvbCon2m7mzw8v9OTrO2LW2MZ1lGe/uVJJmwGGkl9HVoXFyWFLrN6YVFyx8iP+AkN4owDWB9Cw==} cpu: [wasm32] + '@rspack/binding-win32-arm64-msvc@2.1.10': + resolution: {integrity: sha512-z4GWzMLofaDGpAt9Z+MlN88LlUBDm+zM6R2GdOOPM6/4g/h3/+47OP7casmSL3AwTGYBEJqogwt08sRSosB6Cg==} + cpu: [arm64] + os: [win32] + '@rspack/binding-win32-arm64-msvc@2.1.8': resolution: {integrity: sha512-WDnsP/SUb9zbxyGX9XjPw5AXrX86u5oidn0MDdfJduOOqdCSpHwmRjlQ8NUJhbBq9WqVJMFlcab7NwZVWX/yyg==} cpu: [arm64] os: [win32] + '@rspack/binding-win32-ia32-msvc@2.1.10': + resolution: {integrity: sha512-7qcWdsZ+GuGtzKjqgy7wTN7Dso/ezIY8yhx1r2yIbcczdmXj4FhaEampMDp/25HwtKwIGBBoh6HHSt3JWxpTUg==} + cpu: [ia32] + os: [win32] + '@rspack/binding-win32-ia32-msvc@2.1.8': resolution: {integrity: sha512-QiMQMPNDiY3dhhaIdaFPzcPDC06cEYkNY89ea+EmDvNVgZq6V+2mFS/WnzZVMeEbGAYJCjsv/ABhhLT1hlYMvg==} cpu: [ia32] os: [win32] + '@rspack/binding-win32-x64-msvc@2.1.10': + resolution: {integrity: sha512-pgp23pLrzfhGnKycxzr7ifP17lAbWZEfnx1bX8gXtYrnpJ66DRNyTKSzxB6sa/HBWjS1L8PX5TjMZ44WfPydqQ==} + cpu: [x64] + os: [win32] + '@rspack/binding-win32-x64-msvc@2.1.8': resolution: {integrity: sha512-b7sA5eB64vo2mbsuc//MOYzVLeCKHPn0dfP/GmNEoHdWbhRgZ/orZLWurYMQj04ELTLW6YCJEy59g5KRzNYHfw==} cpu: [x64] os: [win32] + '@rspack/binding@2.1.10': + resolution: {integrity: sha512-vnu/UP5HnrND15lO9+VeG6eUrbTyycHNQNQ3XEiRiFojuoiGZkIZC3Hbzr8qQH44C6vScPODEPvvIVvLcO2LpQ==} + '@rspack/binding@2.1.8': resolution: {integrity: sha512-tmAyHzDbPiy8V7HvQqtuPsbs6dPgwV0YjzW5XrPRV9gzf+Hdm7pvsZJKE1QKO9WV5RuvGYav98xIX6O+abZxzQ==} + '@rspack/core@2.1.10': + resolution: {integrity: sha512-YSS2/Xxz8uiG/KXDkqOoA3dTetNo/vysk7bAexQOrU8iuq7JuzDTTAwLKvWZnwmvME8M8m5wcM4YvfIwYmidHA==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@module-federation/runtime-tools': ^0.24.1 || ^2.0.0 + '@swc/helpers': ^0.5.23 + peerDependenciesMeta: + '@module-federation/runtime-tools': + optional: true + '@swc/helpers': + optional: true + '@rspack/core@2.1.8': resolution: {integrity: sha512-na1kyA6Mj8/LWw9O3A8NsrG9rNKN3Iq2WiXrEuIwsU5r/Nl/evm3hO7bWKHxgsRyydI6W7okwx3MXgf8rzel6g==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1489,14 +1607,14 @@ packages: '@rstackjs/test-utils@0.2.0': resolution: {integrity: sha512-P+LOo1WE3xYeGkHmEthyq2cIpN69k4LhiB/4UBSceD+nW9hDlhWv8MC0LTLWokZXccWl4ntcfOBjQFllkcBlPA==} - '@rstest/adapter-rsbuild@0.11.6': - resolution: {integrity: sha512-l2bKftH1IEuY3Sj7ZEb+k6OoZf2FO0vTeKfk1Xxo2ons9fL1LHsbNDWEXNw9lNHg0fv92sai6ygQkGkvCpxkjg==} + '@rstest/adapter-rsbuild@0.11.8': + resolution: {integrity: sha512-FIpljMHWjsZzWTBkGqIuvtPFj3ru1SL5FGhMGtvyGjFi126SwCcVHHTIF5hsrs8Ou8vFEF8S5eFxz7hXrzvxKg==} peerDependencies: '@rsbuild/core': ^1.0.0 || ^2.0.0 '@rstest/core': ^0.11.0 - '@rstest/adapter-rslib@0.11.6': - resolution: {integrity: sha512-0NOU3W63TWtbWExgT/gvpbQ5ZtWqW5HepvJ/mNne7FgZfjm2bNwDF2Saglpg/M83KuBpA9xmnrOUQXNosJkCBQ==} + '@rstest/adapter-rslib@0.11.8': + resolution: {integrity: sha512-PnRrCgTbRH+sFuS/6ZbhDAUEl/n0PkhmzQJxZCYQQEl3w+jGOrLQSAMgh9g/Z2XGM1yfbysn+5HZmGH2kL+E6w==} peerDependencies: '@rslib/core': '>=0.18.6 || ^1.0.0-0' '@rstest/core': ^0.11.0 @@ -1505,8 +1623,8 @@ packages: typescript: optional: true - '@rstest/core@0.11.6': - resolution: {integrity: sha512-P3wgYGDF3JmhapwN3p4DnbNV9N6+E+dlbp0coT1lAuXN5Po6bHeP/rduGLsTUIX85qWxmJD7ekF7nlOKm9RoOA==} + '@rstest/core@0.11.8': + resolution: {integrity: sha512-XworMa277b5Cf4/Box18frFjWGP4dO/NIals+Ck/Q8nhe1z1t8j0P67zh6rv5xTdE3CCEfItICGvdjzz6VRhLg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -1522,8 +1640,8 @@ packages: resolution: {integrity: sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==} engines: {node: '>=20'} - '@shikijs/core@4.4.2': - resolution: {integrity: sha512-StyzbAyxg2/tBGf78gwbBkGyeQ73lf8UiJArFaQhTQIDqQOCKPCQFanvrs4/Yv3Yfyc+ONInJM6K+FMIf+P+kA==} + '@shikijs/core@4.4.3': + resolution: {integrity: sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==} engines: {node: '>=20'} '@shikijs/engine-javascript@4.3.1': @@ -1542,8 +1660,8 @@ packages: resolution: {integrity: sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==} engines: {node: '>=20'} - '@shikijs/primitive@4.4.2': - resolution: {integrity: sha512-l6fQQKsOMlz72n38fztmSgZ76MO6KSWuw8o+GJ+FhmqrpC9pIOJNQNXGgbb5yX2AwpzlEHwsaLPnk/8o4Fm+rA==} + '@shikijs/primitive@4.4.3': + resolution: {integrity: sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==} engines: {node: '>=20'} '@shikijs/rehype@4.3.1': @@ -1554,16 +1672,16 @@ packages: resolution: {integrity: sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==} engines: {node: '>=20'} - '@shikijs/transformers@4.4.2': - resolution: {integrity: sha512-d81PJ9KkR1tVP95FH/9296HTtDo0mh76wv10u9T1YmsZq/UcXgt0OLdBszfUQ1i+umkRMCjDnFbFZU7/tCODTQ==} + '@shikijs/transformers@4.4.3': + resolution: {integrity: sha512-oJSARV6NaWd+rnNJbtnpAdj3Zg0ZVyzsnMgb3vi3HA+35y8lBWUCpOnWsmyiXZIikY+x1BDqrQUgmxfzWh7Jvw==} engines: {node: '>=20'} '@shikijs/types@4.3.1': resolution: {integrity: sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==} engines: {node: '>=20'} - '@shikijs/types@4.4.2': - resolution: {integrity: sha512-PFYitV4vpDr/iPCIhnHp+Q4ftic5N5VeNJ3KQ1O8gn3h2ar8qgwMAXF7tq4m1CWaMS60fV4VqF6vfnWH4F7vqQ==} + '@shikijs/types@4.4.3': + resolution: {integrity: sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==} engines: {node: '>=20'} '@shikijs/vscode-textmate@10.0.2': @@ -1581,11 +1699,15 @@ packages: resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} - '@testing-library/jest-dom@7.0.0': - resolution: {integrity: sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==} + '@testing-library/jest-dom@7.0.1': + resolution: {integrity: sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==} engines: {node: '>=22', npm: '>=6', yarn: '>=1'} peerDependencies: '@testing-library/dom': '>=10 <11' + vitest: '>= 0.32' + peerDependenciesMeta: + vitest: + optional: true '@testing-library/react@16.3.2': resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} @@ -1795,74 +1917,74 @@ packages: peerDependencies: react: '>=18.3.1' - '@yuku-parser/binding-android-arm64@0.8.4': - resolution: {integrity: sha512-+HIMmv08Zrh9ugIAEMnKBMMePOl7CDxrjc8Vui1+GG2TJHM1yI1+3wo1pnXB6Nj2IiHugDkQw8ycUI6SA2EUkQ==} + '@yuku-parser/binding-android-arm64@0.8.7': + resolution: {integrity: sha512-eGKYiUDX7Y0V7tDTmg+JTVnXnjMqfXXsorZ+EDf5kxwchQ3Or1HS14MzI2fw+jFhHR85fCWt+mtX33Yao73hIQ==} cpu: [arm64] os: [android] - '@yuku-parser/binding-darwin-arm64@0.8.4': - resolution: {integrity: sha512-Elf/B/2m3OsyvxoQnBk8Dtu+9csHkzBNs5Yv9GbHjT3x0kVKNWjFusyZgm41VwxcPDqdpRi8tWxNX7OqXkmf/A==} + '@yuku-parser/binding-darwin-arm64@0.8.7': + resolution: {integrity: sha512-Re0RHelKLnjEURulY2/KxW+Ngb8zuNA4BRZuMwgGQNzVumT6u4U2N2hc01oeYVNVof0i7GrXE4UCNBgbpRRnjQ==} cpu: [arm64] os: [darwin] - '@yuku-parser/binding-darwin-x64@0.8.4': - resolution: {integrity: sha512-CjZuMoXnL5XUkVpDqh4WDPwpAw8CwmtHHnTerGkS45So/sNuwkXdyIAEqqIZfaLopi5W/V9NApAT2md9XizjsQ==} + '@yuku-parser/binding-darwin-x64@0.8.7': + resolution: {integrity: sha512-Hn8DROtQkjlA1ACbPgj4a7eP9IuVOI504oiTwpkWPbpaDWD9KdmnVYCqW+1LfenNK/g7O9NhWGpXEdaCNX7lIA==} cpu: [x64] os: [darwin] - '@yuku-parser/binding-freebsd-x64@0.8.4': - resolution: {integrity: sha512-ibLKORdz71iI4Vs+fyFgvwQ51P5XcxJIyQLa8cSEWqwptRdo+BTcZHIQEcZnFDPUuvmJ19RRH9CoiJdfhr7pZw==} + '@yuku-parser/binding-freebsd-x64@0.8.7': + resolution: {integrity: sha512-bAP2OV8wRuzplX/jYxv9+vvqQT8JxyNphI8fLfXGL054Xs+4/J5u33cIm3y4rxY8rdoLmmdiJs2Tq7r7lrDRfA==} cpu: [x64] os: [freebsd] - '@yuku-parser/binding-linux-arm-gnu@0.8.4': - resolution: {integrity: sha512-Fo3r5fYhGDcFnl+KN+L9PgtiQPS4AIE1n1mG1o5jZ11p7g5yZ/1EjLFmSHAUsoXreun8KjTsFjEL7P1Sb93PZQ==} + '@yuku-parser/binding-linux-arm-gnu@0.8.7': + resolution: {integrity: sha512-kTYwJQQgmZeAWdDIWabiReIZMpmfLueIj1tCmjStUtFGhR1Z0qwxonKVfUC4N7h/VhGGzLZ//7O1kgt1QKqgCg==} cpu: [arm] os: [linux] libc: [glibc] - '@yuku-parser/binding-linux-arm-musl@0.8.4': - resolution: {integrity: sha512-BEB31vUEgXPWf7WkoMPSzzJhpC/wWCBXyysRCCPsw47BJ/OtbQsvJbxX9fFDuSRLy3kbyIV/WbdUTgbQ9COxiw==} + '@yuku-parser/binding-linux-arm-musl@0.8.7': + resolution: {integrity: sha512-uL4jE8HPT2BLlxAXyD10LqgPuXa9eDa0BKpCdSANmzIJghq/2eZo3/gQNtaxPZMupWoxjYzSad9IXrwu7aYPXQ==} cpu: [arm] os: [linux] libc: [musl] - '@yuku-parser/binding-linux-arm64-gnu@0.8.4': - resolution: {integrity: sha512-xGLCRcHn9xVz7JVNyyKtiNJSf503qtUmih9XVSsghgzOmiKUMnHObs69OMMwXN3788tg1jsl11fNjjQlB8idMA==} + '@yuku-parser/binding-linux-arm64-gnu@0.8.7': + resolution: {integrity: sha512-3gVN4pWSKZmXiNX7cU164dR9MPvesCHnlH6nPfpK+yQsCuYphjKKplcb4SnZBrhiqmXbgb2HR0c2TS0IZUPhgA==} cpu: [arm64] os: [linux] libc: [glibc] - '@yuku-parser/binding-linux-arm64-musl@0.8.4': - resolution: {integrity: sha512-3kNRi8NJT2q6FQRVCUFHIQ99+kXdi8cVJEEUi6+xtFqpMgNrZNnbgAj28ILsDC7zmclaU+v47eUCeJoKLSCbww==} + '@yuku-parser/binding-linux-arm64-musl@0.8.7': + resolution: {integrity: sha512-S0mwfEjoLpxzXeZw802Wa4RaELsQiPtWqG6INcy8j4GtvNFtl4LCX3eGO1XLn9pyLAISLzTRyU3zUCBUPin8lg==} cpu: [arm64] os: [linux] libc: [musl] - '@yuku-parser/binding-linux-x64-gnu@0.8.4': - resolution: {integrity: sha512-isi62oMy94Z3OXwGs2l2rkqRiRyqLmfHeTRHpA/uWZbsNhnm7IdVvkF7e7wHNKAtd5jwGzOqYSroxKv2fOm7Cw==} + '@yuku-parser/binding-linux-x64-gnu@0.8.7': + resolution: {integrity: sha512-lnbWdPmerE5D1uH1G4IEZKnPzCrWCStRGrtgpSIe1RibAo5bZIjDbbbPYXmMHCEh4F+x/JaJpElh26a3r+BPbg==} cpu: [x64] os: [linux] libc: [glibc] - '@yuku-parser/binding-linux-x64-musl@0.8.4': - resolution: {integrity: sha512-9RsEw2xYHqU/pjSRBTOupWN3sF8uz9stjJdARfz6o0llvF+yfrj5QHmTiocGGBeAI80fvKmDtr2RIQClUzAPcA==} + '@yuku-parser/binding-linux-x64-musl@0.8.7': + resolution: {integrity: sha512-769uwndMvMzUvATWbAcEvyLHKA+DzhHSCl/obBUrRdYfRo26yxui6S8y3z7uJ+Naup7UKrDxrpK7OnQkxkl9KQ==} cpu: [x64] os: [linux] libc: [musl] - '@yuku-parser/binding-win32-arm64@0.8.4': - resolution: {integrity: sha512-VEZHo9rEGOBKR20sA3vCO00aQvwWND5aLu7YxeX+YupMZJh9hd1f17AbClJN37Q2iL1PCHht+wDTOKX6tZYqXg==} + '@yuku-parser/binding-win32-arm64@0.8.7': + resolution: {integrity: sha512-mEB/9PlaAkisJ6KWGz0zvywXoU6+80dTlR2LwS7s/jcXXoU6fm2+sitBZXtqu3+Q4DcDgPxM45uWMCzPs0TSRw==} cpu: [arm64] os: [win32] - '@yuku-parser/binding-win32-x64@0.8.4': - resolution: {integrity: sha512-PeH3VzN1feGjPtDpVEAqf000fPT+nxtw/696LKp/5Z9RJi/MaXpB636QC+5QtrAPSoEnIkyEe+c+mq2QLZPWBA==} + '@yuku-parser/binding-win32-x64@0.8.7': + resolution: {integrity: sha512-8vNB2DP0ou61nGb8tc/qfi41gfyDXz1MHr2zqL3nR+cJ6CEbiuWV/l/a/vv151gCgiZLLAyGkQGENpozdg716w==} cpu: [x64] os: [win32] - '@yuku-toolchain/types@0.8.4': - resolution: {integrity: sha512-p7JE8flrj7ijZ/qLjHi4UwKqMarMD6zumbKXhrjp2I2iLJOuTYiQyci2U36VlXcUlNyzsY7E/mLnKCHotbzJVw==} + '@yuku-toolchain/types@0.8.7': + resolution: {integrity: sha512-2Z53dNxAJL6UvFoIrDZvYf3zlO8s4VJK4O2hhaB4mXVwwpX/7ajtss3cmfqKvamlNLWyt9FSWs4eoYdlbxpnHA==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -2044,14 +2166,6 @@ packages: dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} - emnapi@2.0.0-alpha.3: - resolution: {integrity: sha512-K9bc9Xx4OwSfhJpdSOpcfIKzn7/6emuubaIorf6I5e7WBAM79665rf6iHr9y50NL4qYMUP/AheTpD1Z4yU1EBw==} - peerDependencies: - node-addon-api: '>= 6.1.0' - peerDependenciesMeta: - node-addon-api: - optional: true - emojis-list@3.0.0: resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} engines: {node: '>= 4'} @@ -2147,8 +2261,8 @@ packages: git-hooks-list@4.2.1: resolution: {integrity: sha512-WNvqJjOxxs/8ZP9+DWdwWJ7cDsd60NHf39XnD82pDVrKO5q7xfPqpkK6hwEAmBa/ZSEE4IOoR75EzbbIuwGlMw==} - globals@17.9.0: - resolution: {integrity: sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==} + globals@17.11.0: + resolution: {integrity: sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==} engines: {node: '>=18'} happy-dom@20.11.2: @@ -2715,8 +2829,8 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} - rsbuild-plugin-dts@1.0.0-beta.2: - resolution: {integrity: sha512-xOYa/kw/y29kKFFNjd+CIemlq+CB8E7LhqNkIzg7HT9dYNBVBpZvnnL6OEsOgjeuqzKpGSCRgZN/dDoVOk4VhQ==} + rsbuild-plugin-dts@1.0.0-beta.3: + resolution: {integrity: sha512-Q8x/yyOsy8sNR8sHn0xuZsul7ErT5kXkTmDwCIxQ8esiMCW7QKjfyXDa4kvTxWn7oSQ5NP/O6qZM2vEA07thKw==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: '@microsoft/api-extractor': ^7 @@ -2935,8 +3049,8 @@ packages: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} engines: {node: '>=10'} - svelte@5.56.8: - resolution: {integrity: sha512-PY8LOw7xP6c8IOiVqdo0sbbZVYhXRSfklOQLAUyGBKqjTX0wx/z4l/9J+PmBpmlLnxzEb1NqltxQ5/wZme/Cmg==} + svelte@5.56.9: + resolution: {integrity: sha512-VT8kSnlEg8069w7AiCcAk3Yf5xvMnrGTagVOmU/OpOLHaHnNqXhWZCH/4EVga/bT/HtWhvE6/fHrXLErx7OnJA==} engines: {node: '>=18'} sync-child-process@1.0.2: @@ -3077,11 +3191,11 @@ packages: engines: {node: '>= 14.6'} hasBin: true - yuku-ast@0.8.4: - resolution: {integrity: sha512-s7EWfWIQkaGmsGnyr/BU0jli9YTN5TvrKIsSmALyRD9elumDQInuhv0BrVObENKVCxr9W3Ikmnx5u02KvfuUmw==} + yuku-ast@0.8.7: + resolution: {integrity: sha512-h6+4bDfyootiMB9vckk5uKo5r5j0GHrkr17FQTDNfEsFT3DWlN9uu1HJwQwc64pgmLCI945fWM3lbTIqxjT3GQ==} - yuku-parser@0.8.4: - resolution: {integrity: sha512-sw41wouvT5rUmLIp87hmvm5vtF+MRSI3x6yjq6xqpYmtkQj+Ht6N7xRQ8lMhLv8N7JAzughGj0Rfi0jQRSu9HQ==} + yuku-parser@0.8.7: + resolution: {integrity: sha512-vRD9nwt4L3aYpxNqeSC4WqLv58xrXef0Ong1Mc45CTXTIpvLafx7JO05sczmQZwdLEZvywrLOGdNC5+Rp5N1BQ==} zimmerframe@1.1.4: resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} @@ -3366,7 +3480,7 @@ snapshots: '@types/react': 19.2.18 react: 19.2.8 - '@napi-rs/cli@3.8.3(@types/node@24.13.3)(node-addon-api@7.1.1)(supports-color@8.1.1)': + '@napi-rs/cli@3.8.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@24.13.3)(supports-color@8.1.1)': dependencies: '@inquirer/prompts': 8.5.2(@types/node@24.13.3) '@napi-rs/cross-toolchain': 1.0.3(supports-color@8.1.1) @@ -3374,13 +3488,15 @@ snapshots: '@octokit/rest': 22.0.1 clipanion: 4.0.0-rc.4 colorette: 2.0.20 - emnapi: 2.0.0-alpha.3(node-addon-api@7.1.1) es-toolkit: 1.50.0 js-yaml: 4.3.1 obug: 2.1.4 semver: 7.8.5 typanion: 3.14.0 typescript: 6.0.3 + optionalDependencies: + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 transitivePeerDependencies: - '@napi-rs/cross-toolchain-arm64-target-aarch64' - '@napi-rs/cross-toolchain-arm64-target-armv7' @@ -3393,7 +3509,6 @@ snapshots: - '@napi-rs/cross-toolchain-x64-target-s390x' - '@napi-rs/cross-toolchain-x64-target-x86_64' - '@types/node' - - node-addon-api - supports-color '@napi-rs/cross-toolchain@1.0.3(supports-color@8.1.1)': @@ -3767,15 +3882,47 @@ snapshots: transitivePeerDependencies: - '@module-federation/runtime-tools' - '@rsbuild/plugin-react@2.1.0(@rsbuild/core@2.1.10)(@rspack/core@2.1.8)': + '@rsbuild/core@2.1.12': + dependencies: + '@rspack/core': 2.1.10(@swc/helpers@0.5.23) + '@swc/helpers': 0.5.23 + transitivePeerDependencies: + - '@module-federation/runtime-tools' + + '@rsbuild/core@2.1.13': + dependencies: + '@rspack/core': 2.1.10(@swc/helpers@0.5.23) + '@swc/helpers': 0.5.23 + transitivePeerDependencies: + - '@module-federation/runtime-tools' + + '@rsbuild/plugin-react@2.1.0(@rsbuild/core@2.1.10)': dependencies: - '@rspack/plugin-react-refresh': 2.0.2(@rspack/core@2.1.8)(react-refresh@0.18.0) + '@rspack/plugin-react-refresh': 2.0.2(@rspack/core@2.1.10)(react-refresh@0.18.0) react-refresh: 0.18.0 optionalDependencies: '@rsbuild/core': 2.1.10 transitivePeerDependencies: - '@rspack/core' + '@rsbuild/plugin-react@2.1.0(@rsbuild/core@2.1.10)(@rspack/core@2.1.10)': + dependencies: + '@rspack/plugin-react-refresh': 2.0.2(@rspack/core@2.1.10)(react-refresh@0.18.0) + react-refresh: 0.18.0 + optionalDependencies: + '@rsbuild/core': 2.1.10 + transitivePeerDependencies: + - '@rspack/core' + + '@rsbuild/plugin-react@2.1.0(@rsbuild/core@2.1.13)(@rspack/core@2.1.10)': + dependencies: + '@rspack/plugin-react-refresh': 2.0.2(@rspack/core@2.1.10)(react-refresh@0.18.0) + react-refresh: 0.18.0 + optionalDependencies: + '@rsbuild/core': 2.1.13 + transitivePeerDependencies: + - '@rspack/core' + '@rsbuild/plugin-sass@2.0.1(@rsbuild/core@2.1.10)': dependencies: deepmerge: 4.3.1 @@ -3786,10 +3933,10 @@ snapshots: optionalDependencies: '@rsbuild/core': 2.1.10 - '@rslib/core@1.0.0-beta.2(typescript@7.0.2)': + '@rslib/core@1.0.0-beta.3(typescript@7.0.2)': dependencies: - '@rsbuild/core': 2.1.10 - rsbuild-plugin-dts: 1.0.0-beta.2(@rsbuild/core@2.1.10)(typescript@7.0.2) + '@rsbuild/core': 2.1.13 + rsbuild-plugin-dts: 1.0.0-beta.3(@rsbuild/core@2.1.13)(typescript@7.0.2) optionalDependencies: typescript: 7.0.2 transitivePeerDependencies: @@ -3833,30 +3980,67 @@ snapshots: '@rslint/native-win32-x64-msvc@0.8.0': optional: true + '@rspack/binding-darwin-arm64@2.1.10': + optional: true + '@rspack/binding-darwin-arm64@2.1.8': optional: true + '@rspack/binding-darwin-x64@2.1.10': + optional: true + '@rspack/binding-darwin-x64@2.1.8': optional: true + '@rspack/binding-linux-arm64-gnu@2.1.10': + optional: true + '@rspack/binding-linux-arm64-gnu@2.1.8': optional: true + '@rspack/binding-linux-arm64-musl@2.1.10': + optional: true + '@rspack/binding-linux-arm64-musl@2.1.8': optional: true + '@rspack/binding-linux-ppc64-gnu@2.1.10': + optional: true + + '@rspack/binding-linux-riscv64-gnu@2.1.10': + optional: true + '@rspack/binding-linux-riscv64-gnu@2.1.8': optional: true + '@rspack/binding-linux-riscv64-musl@2.1.10': + optional: true + '@rspack/binding-linux-riscv64-musl@2.1.8': optional: true + '@rspack/binding-linux-s390x-gnu@2.1.10': + optional: true + + '@rspack/binding-linux-x64-gnu@2.1.10': + optional: true + '@rspack/binding-linux-x64-gnu@2.1.8': optional: true + '@rspack/binding-linux-x64-musl@2.1.10': + optional: true + '@rspack/binding-linux-x64-musl@2.1.8': optional: true + '@rspack/binding-wasm32-wasi@2.1.10': + dependencies: + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) + optional: true + '@rspack/binding-wasm32-wasi@2.1.8': dependencies: '@emnapi/core': 1.11.3 @@ -3864,15 +4048,41 @@ snapshots: '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) optional: true + '@rspack/binding-win32-arm64-msvc@2.1.10': + optional: true + '@rspack/binding-win32-arm64-msvc@2.1.8': optional: true + '@rspack/binding-win32-ia32-msvc@2.1.10': + optional: true + '@rspack/binding-win32-ia32-msvc@2.1.8': optional: true + '@rspack/binding-win32-x64-msvc@2.1.10': + optional: true + '@rspack/binding-win32-x64-msvc@2.1.8': optional: true + '@rspack/binding@2.1.10': + optionalDependencies: + '@rspack/binding-darwin-arm64': 2.1.10 + '@rspack/binding-darwin-x64': 2.1.10 + '@rspack/binding-linux-arm64-gnu': 2.1.10 + '@rspack/binding-linux-arm64-musl': 2.1.10 + '@rspack/binding-linux-ppc64-gnu': 2.1.10 + '@rspack/binding-linux-riscv64-gnu': 2.1.10 + '@rspack/binding-linux-riscv64-musl': 2.1.10 + '@rspack/binding-linux-s390x-gnu': 2.1.10 + '@rspack/binding-linux-x64-gnu': 2.1.10 + '@rspack/binding-linux-x64-musl': 2.1.10 + '@rspack/binding-wasm32-wasi': 2.1.10 + '@rspack/binding-win32-arm64-msvc': 2.1.10 + '@rspack/binding-win32-ia32-msvc': 2.1.10 + '@rspack/binding-win32-x64-msvc': 2.1.10 + '@rspack/binding@2.1.8': optionalDependencies: '@rspack/binding-darwin-arm64': 2.1.8 @@ -3888,24 +4098,30 @@ snapshots: '@rspack/binding-win32-ia32-msvc': 2.1.8 '@rspack/binding-win32-x64-msvc': 2.1.8 + '@rspack/core@2.1.10(@swc/helpers@0.5.23)': + dependencies: + '@rspack/binding': 2.1.10 + optionalDependencies: + '@swc/helpers': 0.5.23 + '@rspack/core@2.1.8(@swc/helpers@0.5.23)': dependencies: '@rspack/binding': 2.1.8 optionalDependencies: '@swc/helpers': 0.5.23 - '@rspack/plugin-react-refresh@2.0.2(@rspack/core@2.1.8)(react-refresh@0.18.0)': + '@rspack/plugin-react-refresh@2.0.2(@rspack/core@2.1.10)(react-refresh@0.18.0)': dependencies: react-refresh: 0.18.0 optionalDependencies: - '@rspack/core': 2.1.8(@swc/helpers@0.5.23) + '@rspack/core': 2.1.10(@swc/helpers@0.5.23) '@rspress/core@2.0.19(micromark-util-types@2.0.2)(micromark@4.0.2)(supports-color@8.1.1)': dependencies: '@mdx-js/mdx': 3.1.1(supports-color@8.1.1) '@mdx-js/react': 3.1.1(@types/react@19.2.18)(react@19.2.8) '@rsbuild/core': 2.1.10 - '@rsbuild/plugin-react': 2.1.0(@rsbuild/core@2.1.10)(@rspack/core@2.1.8) + '@rsbuild/plugin-react': 2.1.0(@rsbuild/core@2.1.10) '@rspress/shared': 2.0.19(supports-color@8.1.1) '@shikijs/rehype': 4.3.1 '@types/mdast': 4.0.4 @@ -3958,7 +4174,7 @@ snapshots: '@rspress/shared@2.0.19(supports-color@8.1.1)': dependencies: - '@rsbuild/core': 2.1.10 + '@rsbuild/core': 2.1.12 '@shikijs/rehype': 4.3.1 '@types/react': 19.2.18 mdast-util-mdx-jsx: 3.2.0(supports-color@8.1.1) @@ -3978,21 +4194,21 @@ snapshots: '@rstackjs/test-utils@0.2.0': {} - '@rstest/adapter-rsbuild@0.11.6(@rsbuild/core@2.1.10)(@rstest/core@0.11.6)': + '@rstest/adapter-rsbuild@0.11.8(@rsbuild/core@2.1.13)(@rstest/core@0.11.8)': dependencies: - '@rsbuild/core': 2.1.10 - '@rstest/core': 0.11.6(happy-dom@20.11.2) + '@rsbuild/core': 2.1.13 + '@rstest/core': 0.11.8(happy-dom@20.11.2) - '@rstest/adapter-rslib@0.11.6(@rslib/core@1.0.0-beta.2)(@rstest/core@0.11.6)(typescript@7.0.2)': + '@rstest/adapter-rslib@0.11.8(@rslib/core@1.0.0-beta.3)(@rstest/core@0.11.8)(typescript@7.0.2)': dependencies: - '@rslib/core': 1.0.0-beta.2(typescript@7.0.2) - '@rstest/core': 0.11.6(happy-dom@20.11.2) + '@rslib/core': 1.0.0-beta.3(typescript@7.0.2) + '@rstest/core': 0.11.8(happy-dom@20.11.2) optionalDependencies: typescript: 7.0.2 - '@rstest/core@0.11.6(happy-dom@20.11.2)': + '@rstest/core@0.11.8(happy-dom@20.11.2)': dependencies: - '@rsbuild/core': 2.1.10 + '@rsbuild/core': 2.1.12 '@types/chai': 5.2.3 optionalDependencies: happy-dom: 20.11.2 @@ -4008,10 +4224,10 @@ snapshots: '@types/hast': 3.0.5 hast-util-to-html: 9.0.5 - '@shikijs/core@4.4.2': + '@shikijs/core@4.4.3': dependencies: - '@shikijs/primitive': 4.4.2 - '@shikijs/types': 4.4.2 + '@shikijs/primitive': 4.4.3 + '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 hast-util-to-html: 9.0.5 @@ -4037,9 +4253,9 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 - '@shikijs/primitive@4.4.2': + '@shikijs/primitive@4.4.3': dependencies: - '@shikijs/types': 4.4.2 + '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 @@ -4056,17 +4272,17 @@ snapshots: dependencies: '@shikijs/types': 4.3.1 - '@shikijs/transformers@4.4.2': + '@shikijs/transformers@4.4.3': dependencies: - '@shikijs/core': 4.4.2 - '@shikijs/types': 4.4.2 + '@shikijs/core': 4.4.3 + '@shikijs/types': 4.4.3 '@shikijs/types@4.3.1': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 - '@shikijs/types@4.4.2': + '@shikijs/types@4.4.3': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 @@ -4092,7 +4308,7 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 - '@testing-library/jest-dom@7.0.0(@testing-library/dom@10.4.1)': + '@testing-library/jest-dom@7.0.1(@testing-library/dom@10.4.1)': dependencies: '@adobe/css-tools': 4.5.0 '@testing-library/dom': 10.4.1 @@ -4245,43 +4461,43 @@ snapshots: react: 19.2.8 unhead: 2.1.16 - '@yuku-parser/binding-android-arm64@0.8.4': + '@yuku-parser/binding-android-arm64@0.8.7': optional: true - '@yuku-parser/binding-darwin-arm64@0.8.4': + '@yuku-parser/binding-darwin-arm64@0.8.7': optional: true - '@yuku-parser/binding-darwin-x64@0.8.4': + '@yuku-parser/binding-darwin-x64@0.8.7': optional: true - '@yuku-parser/binding-freebsd-x64@0.8.4': + '@yuku-parser/binding-freebsd-x64@0.8.7': optional: true - '@yuku-parser/binding-linux-arm-gnu@0.8.4': + '@yuku-parser/binding-linux-arm-gnu@0.8.7': optional: true - '@yuku-parser/binding-linux-arm-musl@0.8.4': + '@yuku-parser/binding-linux-arm-musl@0.8.7': optional: true - '@yuku-parser/binding-linux-arm64-gnu@0.8.4': + '@yuku-parser/binding-linux-arm64-gnu@0.8.7': optional: true - '@yuku-parser/binding-linux-arm64-musl@0.8.4': + '@yuku-parser/binding-linux-arm64-musl@0.8.7': optional: true - '@yuku-parser/binding-linux-x64-gnu@0.8.4': + '@yuku-parser/binding-linux-x64-gnu@0.8.7': optional: true - '@yuku-parser/binding-linux-x64-musl@0.8.4': + '@yuku-parser/binding-linux-x64-musl@0.8.7': optional: true - '@yuku-parser/binding-win32-arm64@0.8.4': + '@yuku-parser/binding-win32-arm64@0.8.7': optional: true - '@yuku-parser/binding-win32-x64@0.8.4': + '@yuku-parser/binding-win32-x64@0.8.7': optional: true - '@yuku-toolchain/types@0.8.4': {} + '@yuku-toolchain/types@0.8.7': {} acorn-jsx@5.3.2(acorn@8.17.0): dependencies: @@ -4407,10 +4623,6 @@ snapshots: dom-accessibility-api@0.6.3: {} - emnapi@2.0.0-alpha.3(node-addon-api@7.1.1): - optionalDependencies: - node-addon-api: 7.1.1 - emojis-list@3.0.0: {} entities@6.0.1: {} @@ -4502,7 +4714,7 @@ snapshots: git-hooks-list@4.2.1: {} - globals@17.9.0: {} + globals@17.11.0: {} happy-dom@20.11.2: dependencies: @@ -5265,10 +5477,10 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - prettier-plugin-svelte@4.1.1(prettier@3.9.6)(svelte@5.56.8): + prettier-plugin-svelte@4.1.1(prettier@3.9.6)(svelte@5.56.9): dependencies: prettier: 3.9.6 - svelte: 5.56.8 + svelte: 5.56.9 prettier@3.9.6: {} @@ -5459,10 +5671,10 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 - rsbuild-plugin-dts@1.0.0-beta.2(@rsbuild/core@2.1.10)(typescript@7.0.2): + rsbuild-plugin-dts@1.0.0-beta.3(@rsbuild/core@2.1.13)(typescript@7.0.2): dependencies: '@ast-grep/napi': 0.37.0 - '@rsbuild/core': 2.1.10 + '@rsbuild/core': 2.1.13 optionalDependencies: typescript: 7.0.2 @@ -5642,7 +5854,7 @@ snapshots: dependencies: has-flag: 4.0.0 - svelte@5.56.8: + svelte@5.56.9: dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 @@ -5811,27 +6023,27 @@ snapshots: yaml@2.9.0: optional: true - yuku-ast@0.8.4: + yuku-ast@0.8.7: dependencies: - '@yuku-toolchain/types': 0.8.4 + '@yuku-toolchain/types': 0.8.7 - yuku-parser@0.8.4: + yuku-parser@0.8.7: dependencies: - '@yuku-toolchain/types': 0.8.4 - yuku-ast: 0.8.4 + '@yuku-toolchain/types': 0.8.7 + yuku-ast: 0.8.7 optionalDependencies: - '@yuku-parser/binding-android-arm64': 0.8.4 - '@yuku-parser/binding-darwin-arm64': 0.8.4 - '@yuku-parser/binding-darwin-x64': 0.8.4 - '@yuku-parser/binding-freebsd-x64': 0.8.4 - '@yuku-parser/binding-linux-arm-gnu': 0.8.4 - '@yuku-parser/binding-linux-arm-musl': 0.8.4 - '@yuku-parser/binding-linux-arm64-gnu': 0.8.4 - '@yuku-parser/binding-linux-arm64-musl': 0.8.4 - '@yuku-parser/binding-linux-x64-gnu': 0.8.4 - '@yuku-parser/binding-linux-x64-musl': 0.8.4 - '@yuku-parser/binding-win32-arm64': 0.8.4 - '@yuku-parser/binding-win32-x64': 0.8.4 + '@yuku-parser/binding-android-arm64': 0.8.7 + '@yuku-parser/binding-darwin-arm64': 0.8.7 + '@yuku-parser/binding-darwin-x64': 0.8.7 + '@yuku-parser/binding-freebsd-x64': 0.8.7 + '@yuku-parser/binding-linux-arm-gnu': 0.8.7 + '@yuku-parser/binding-linux-arm-musl': 0.8.7 + '@yuku-parser/binding-linux-arm64-gnu': 0.8.7 + '@yuku-parser/binding-linux-arm64-musl': 0.8.7 + '@yuku-parser/binding-linux-x64-gnu': 0.8.7 + '@yuku-parser/binding-linux-x64-musl': 0.8.7 + '@yuku-parser/binding-win32-arm64': 0.8.7 + '@yuku-parser/binding-win32-x64': 0.8.7 zimmerframe@1.1.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 460655d2..0ea73bd0 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -12,11 +12,11 @@ catalogMode: prefer cleanupUnusedCatalogs: true catalog: - '@napi-rs/cli': '^3.8.3' - '@rsbuild/core': '~2.1.10' + '@napi-rs/cli': '^3.8.6' + '@rsbuild/core': '~2.1.13' '@rsbuild/plugin-react': '^2.1.0' '@rsbuild/plugin-sass': '^2.0.1' - '@rslib/core': '~1.0.0-beta.2' + '@rslib/core': '~1.0.0-beta.3' '@rslint/core': '~0.8.0' '@rspress/core': '^2.0.19' '@rspress/plugin-client-redirects': '^2.0.19' @@ -25,20 +25,20 @@ catalog: '@rstackjs/create-toolkit': '2.2.3' '@rstackjs/load-config': ^0.1.2 '@rstackjs/test-utils': ^0.2.0 - '@rstest/adapter-rsbuild': '~0.11.6' - '@rstest/adapter-rslib': '~0.11.6' - '@rstest/core': '~0.11.6' + '@rstest/adapter-rsbuild': '~0.11.8' + '@rstest/adapter-rslib': '~0.11.8' + '@rstest/core': '~0.11.8' '@testing-library/dom': '^10.4.1' - '@testing-library/jest-dom': '^7.0.0' + '@testing-library/jest-dom': '^7.0.1' '@testing-library/react': '^16.3.2' '@types/micromatch': '^4.0.10' '@types/node': '^24.13.3' '@types/react': '^19.2.18' '@types/react-dom': '^19.2.4' - '@shikijs/transformers': '^4.4.2' + '@shikijs/transformers': '^4.4.3' 'cspell-ban-words': '^0.0.4' 'fast-json-stable-stringify': '2.1.0' - globals: '^17.7.0' + globals: '^17.11.0' 'happy-dom': '^20.11.2' 'heading-case': '^1.1.5' 'import-meta-resolve': '4.2.0' @@ -53,13 +53,13 @@ catalog: rslog: ^2.3.0 'rspress-plugin-font-open-sans': '^1.0.4' 'sort-package-json': '4.0.0' - svelte: '^5.56.8' + svelte: '^5.56.9' tinypool: '2.1.0' tiny-readdir: 3.1.1 'typescript': '^7.0.2' 'vscode-languageserver': '10.1.0' 'vscode-languageserver-textdocument': '1.0.12' - yuku-parser: '0.8.4' + yuku-parser: '0.8.7' dedupePeers: true diff --git a/rstack.config.ts b/rstack.config.ts index 0ad535cf..066eace0 100644 --- a/rstack.config.ts +++ b/rstack.config.ts @@ -1,12 +1,11 @@ -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; -define.lint(async () => { +define.lint(async ({ js, ts }) => { const { default: globals } = await import('globals'); - const { js, ts } = await import('rstack/lint'); return [ js.configs.recommended, - ts.configs.recommended, + ts.configs.recommendedTypeChecked, { files: ['**/*.{js,jsx,cjs,mjs}'], languageOptions: { @@ -53,16 +52,10 @@ define.lint(async () => { }); define.fmt({ - ignorePatterns: ['packages/rstack/binding.cjs', 'packages/rstack/binding.d.cts'], - overrides: [ - { - files: 'packages/create-rstack/template-*/**/*', - options: { - printWidth: 80, - }, - }, + ignorePatterns: [ + 'packages/rstack/binding.cjs', + 'packages/rstack/binding.d.cts', ], - printWidth: 100, singleQuote: true, sortPackageJson: true, }); diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 6f8397f5..7002caa4 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] # Required by the release-only -Zlocation-detail=none flag. -channel = "nightly-2026-04-16" +channel = "nightly-2026-08-13" components = ["clippy", "rustfmt"] profile = "minimal" diff --git a/scripts/benchmark-fmt-discovery.js b/scripts/benchmark-fmt-discovery.js index ecf4f457..2891d082 100644 --- a/scripts/benchmark-fmt-discovery.js +++ b/scripts/benchmark-fmt-discovery.js @@ -30,7 +30,9 @@ const readValue = (args, index, flag) => { const parseInteger = (value, flag, minimum) => { const result = Number(value); if (!Number.isSafeInteger(result) || result < minimum) { - throw new Error(`${flag} must be an integer greater than or equal to ${minimum}.`); + throw new Error( + `${flag} must be an integer greater than or equal to ${minimum}.`, + ); } return result; }; @@ -58,7 +60,11 @@ const parseArgs = (args) => { index++; break; case '--explicit-count': - options.explicitCount = parseInteger(readValue(args, index, arg), arg, 1); + options.explicitCount = parseInteger( + readValue(args, index, arg), + arg, + 1, + ); index++; break; case '--runs': diff --git a/scripts/prepare-release.js b/scripts/prepare-release.js index cab8f15b..a18ff738 100644 --- a/scripts/prepare-release.js +++ b/scripts/prepare-release.js @@ -1,6 +1,13 @@ #!/usr/bin/env node import { spawn } from 'node:child_process'; -import { copyFile, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'; +import { + copyFile, + mkdir, + readFile, + readdir, + rm, + writeFile, +} from 'node:fs/promises'; import path from 'node:path'; const rootDir = path.resolve(import.meta.dirname, '..'); diff --git a/website/docs/en/guide/ai.mdx b/website/docs/en/guide/ai.mdx index 80ef7217..83a9cf79 100644 --- a/website/docs/en/guide/ai.mdx +++ b/website/docs/en/guide/ai.mdx @@ -50,7 +50,10 @@ The [migrate-to-rstack-cli](https://github.com/rstackjs/rstack-cli/tree/main/.ag To migrate an existing project, install the Skill: - + For supported tools and migration instructions, see [Migrate to Rstack CLI](./migration). diff --git a/website/docs/en/guide/api-reference.mdx b/website/docs/en/guide/api-reference.mdx index 8c61e7fc..dd2eba08 100644 --- a/website/docs/en/guide/api-reference.mdx +++ b/website/docs/en/guide/api-reference.mdx @@ -1,12 +1,12 @@ # API reference -Rstack provides a unified configuration API and re-exports the public APIs of Rsbuild, Rslib, Rstest, and Rslint through dedicated subpaths. Prefer these subpaths to direct imports from each tool's core package so that dependency entry points and tool versions remain aligned with Rstack. +Rstack CLI provides a unified configuration API and re-exports the public APIs of Rsbuild, Rslib, Rstest, and Rslint through dedicated subpaths. Prefer these subpaths to direct imports from each tool's core package so that dependency entry points stay unified and APIs match the tool versions integrated by Rstack CLI. ## Import paths | Import path | Contents | Use case | | ------------------------ | ------------------------------------------------- | --------------------------------------- | -| `rstack` | Rstack configuration API | Register tool configurations | +| `rstack` | Rstack CLI configuration API | Register tool configurations | | `rstack/app` | Public APIs from `@rsbuild/core` | Build applications and extend Rsbuild | | `rstack/lib` | Public APIs from `@rslib/core` | Build libraries and extend Rslib | | `rstack/test` | Public APIs from `@rstest/core` | Write tests and configure test projects | @@ -23,7 +23,7 @@ Import `define` from `rstack` to register tool configurations in `rstack.config. ## Re-exports -The tool-specific subpaths below re-export the public APIs from their corresponding core packages. Using these Rstack entry points keeps dependency entry points and tool versions aligned with the toolchain integrated by Rstack. +The tool-specific subpaths below re-export the public APIs from their corresponding core packages. Using these entry points keeps imports unified and APIs aligned with the tool versions integrated by Rstack CLI. ### `rstack/app` diff --git a/website/docs/en/guide/cli/_meta.json b/website/docs/en/guide/cli/_meta.json index acdaffbe..5357606a 100644 --- a/website/docs/en/guide/cli/_meta.json +++ b/website/docs/en/guide/cli/_meta.json @@ -1 +1,13 @@ -["dev", "build", "preview", "lib", "doc", "test", "check", "lint", "fmt", "setup", "staged"] +[ + "dev", + "build", + "preview", + "lib", + "doc", + "test", + "check", + "lint", + "fmt", + "setup", + "staged" +] diff --git a/website/docs/en/guide/cli/doc.mdx b/website/docs/en/guide/cli/doc.mdx index 08877b45..fb6494af 100644 --- a/website/docs/en/guide/cli/doc.mdx +++ b/website/docs/en/guide/cli/doc.mdx @@ -1,3 +1,7 @@ +--- +description: 'Develop, build, and preview Rspress documentation sites with the rs doc command.' +--- + # doc import { PackageManagerTabs } from '@rspress/core/theme'; diff --git a/website/docs/en/guide/cli/lint.mdx b/website/docs/en/guide/cli/lint.mdx index 07b15922..aea5fa8a 100644 --- a/website/docs/en/guide/cli/lint.mdx +++ b/website/docs/en/guide/cli/lint.mdx @@ -29,14 +29,13 @@ rs lint --type-check ## Configuration -Configure linting through [`define.lint()`](../configuration#define-lint) in the [Rstack configuration file](/guide/configuration#configuration-file). It accepts the standard [Rslint configuration](https://rslint.rs/config/). Presets and plugins can be imported from `rstack/lint` on demand: +Configure linting through [`define.lint()`](../configuration#define-lint) in the [Rstack configuration file](/guide/configuration#configuration-file). It accepts the standard [Rslint configuration](https://rslint.rs/config/). A configuration function receives all exports from `rstack/lint`, so presets and plugins do not need to be imported manually: ```ts title="rstack.config.ts" import { define } from 'rstack'; -define.lint(async () => { - const { js, ts } = await import('rstack/lint'); - - return [js.configs.recommended, ts.configs.recommended]; -}); +define.lint(({ js, ts }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, +]); ``` diff --git a/website/docs/en/guide/cli/setup.mdx b/website/docs/en/guide/cli/setup.mdx index e61403d4..2ea8bbe9 100644 --- a/website/docs/en/guide/cli/setup.mdx +++ b/website/docs/en/guide/cli/setup.mdx @@ -99,7 +99,7 @@ Files next to `_` are repository hook scripts. The `_` directory contains genera ## Supported hooks -Rstack supports these client-side Git hooks: +Rstack CLI supports these client-side Git hooks: - `pre-commit` - `pre-merge-commit` @@ -120,7 +120,7 @@ Create a file with the matching name next to the `_` directory. ## Hook runtime -Rstack runs hook scripts with POSIX `sh -e`, forwards Git's arguments and standard input, and returns the hook's exit code. Before running a hook, it changes to the project that installed the hooks and prepends that project's `node_modules/.bin` to `PATH`. +Rstack CLI runs hook scripts with POSIX `sh -e`, forwards Git's arguments and standard input, and returns the hook's exit code. Before running a hook, it changes to the project that installed the hooks and prepends that project's `node_modules/.bin` to `PATH`. ### Disable and debug @@ -130,7 +130,7 @@ Set `RSTACK_HOOKS=0` to skip installation or hook execution: RSTACK_HOOKS=0 git commit -m "Skip hooks" ``` -Set `RSTACK_HOOKS=2` to trace Rstack's hook runtime, including how it invokes the hook script and handles its exit code; to trace commands inside the hook script, add `set -x` to the script: +Set `RSTACK_HOOKS=2` to trace the Rstack CLI hook runtime, including how it invokes the hook script and handles its exit code; to trace commands inside the hook script, add `set -x` to the script: ```bash RSTACK_HOOKS=2 git commit -m "Trace hooks" @@ -138,7 +138,7 @@ RSTACK_HOOKS=2 git commit -m "Trace hooks" ### Configure the hook environment -Before running a hook script, Rstack loads this optional POSIX shell file: +Before running a hook script, Rstack CLI loads this optional POSIX shell file: ```text ${XDG_CONFIG_HOME:-$HOME/.config}/rstack/hooks-init.sh @@ -148,7 +148,7 @@ Use it to initialize a Node.js version manager, update `PATH`, or set `RSTACK_HO ## Monorepo -In a monorepo, the project that provides Rstack may be located in a subdirectory such as `frontend/`. Running `rs setup` from that directory still installs hooks at the Git repository root: +In a monorepo, the project that provides Rstack CLI may be located in a subdirectory such as `frontend/`. Running `rs setup` from that directory still installs hooks at the Git repository root: ```text repo/.rstack/hooks/ @@ -156,7 +156,7 @@ repo/.rstack/hooks/_/ core.hooksPath=.rstack/hooks/_ ``` -Rstack records `frontend` as the project that owns the hooks. Hook scripts remain at the repository root, but run from `frontend`, so they can use its configuration and dependencies without an explicit `cd`: +Rstack CLI records `frontend` as the project that owns the hooks. Hook scripts remain at the repository root, but run from `frontend`, so they can use its configuration and dependencies without an explicit `cd`: ```sh title=".rstack/hooks/pre-commit" rs staged @@ -168,7 +168,7 @@ To change the owner, remove `rs setup` from the previous project's `prepare` scr ## Remove hooks -To remove Rstack-managed hooks: +To remove hooks managed by Rstack CLI: 1. Remove `rs setup` from the `prepare` script. 2. Unset the repository's hooks path: @@ -188,13 +188,13 @@ To remove Rstack-managed hooks: - Rerun `rs setup` to restore generated files and executable permissions. - Check that `RSTACK_HOOKS` is not set to `0` in the environment or initialization file. - If another hooks setup is reported, migrate or remove the conflicting setup before rerunning the command. -- If another Rstack owner is reported, follow the ownership transfer steps in [Monorepo](#monorepo). +- If another project is reported as the hooks owner, follow the ownership transfer steps in [Monorepo](#monorepo). -Hook scripts do not need to be executable because Rstack runs them with `sh`. +Hook scripts do not need to be executable because Rstack CLI runs them with `sh`. ### Command not found -For exit code 127, Rstack prints the effective `PATH`. If a GUI Git client cannot find Node.js or the package manager, initialize them in `hooks-init.sh`. +For exit code 127, Rstack CLI prints the effective `PATH`. If a GUI Git client cannot find Node.js or the package manager, initialize them in `hooks-init.sh`. ### Windows and Yarn diff --git a/website/docs/en/guide/configuration.mdx b/website/docs/en/guide/configuration.mdx index e9db58fe..d1aaa0aa 100644 --- a/website/docs/en/guide/configuration.mdx +++ b/website/docs/en/guide/configuration.mdx @@ -2,14 +2,14 @@ import { PackageManagerTabs } from '@rspress/core/theme'; -Rstack centralizes the configuration for your project's tools in a single file. Define only the configurations your project needs with the `define.*()` APIs. +Rstack CLI centralizes the configuration for your project's tools in a single file. Define only the configurations your project needs with the `define.*()` APIs. ## Configuration file Create `rstack.config.ts` in the project root and call the relevant `define.*()` APIs: ```ts title="rstack.config.ts" -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.app({ @@ -31,7 +31,7 @@ define.fmt({ The configuration file does not require a default export. Each `define.*()` API can be called at most once; defining the same configuration type more than once throws an error. -By default, Rstack looks for a file with one of the following names: +By default, Rstack CLI looks for a file with one of the following names: - `rstack.config.ts` - `rstack.config.js` @@ -55,7 +55,6 @@ import { define } from 'rstack'; define.app(async () => { const { pluginReact } = await import('@rsbuild/plugin-react'); - return { plugins: [pluginReact()], }; @@ -64,7 +63,7 @@ define.app(async () => { ## Configuration APIs -Configuration options follow the formats of the underlying tools. When using APIs and helpers that Rstack re-exports, prefer the `rstack/app`, `rstack/lib`, `rstack/test`, and `rstack/lint` entry points. +Configuration options follow the formats of the underlying tools. When using APIs and helpers that Rstack CLI re-exports, prefer the `rstack/app`, `rstack/lib`, `rstack/test`, and `rstack/lint` entry points. | API | Tool | Commands | | ----------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------- | @@ -121,7 +120,7 @@ define.doc({ }); ``` -`@rspress/core` is an optional dependency of Rstack. Install it in every project that uses the `rs doc` command: +`@rspress/core` is an optional dependency of Rstack CLI. Install it in every project that uses the `rs doc` command: @@ -142,24 +141,23 @@ define.test({ }); ``` -When `extends` is omitted, Rstack automatically connects the test configuration to `define.app()` through the Rsbuild adapter. If no application configuration is defined, it falls back to `define.lib()` through the Rslib adapter. The application configuration takes precedence when both are defined. Set `extends` explicitly to opt out of this automatic inheritance. +When `extends` is omitted, Rstack CLI automatically connects the test configuration to `define.app()` through the Rsbuild adapter. If no application configuration is defined, it falls back to `define.lib()` through the Rslib adapter. The application configuration takes precedence when both are defined. Set `extends` explicitly to opt out of this automatic inheritance. -If the root test configuration does not define `extends` and contains `projects`, Rstack applies automatic inheritance to each inline project that omits its own `extends`. A function-based application or library configuration is resolved once and shared by those projects. String project entries are passed to Rstest unchanged; they load their external configurations independently and do not inherit the current application or library configuration. +If the root test configuration does not define `extends` and contains `projects`, Rstack CLI applies automatic inheritance to each inline project that omits its own `extends`. A function-based application or library configuration is resolved once and shared by those projects. String project entries are passed to Rstest unchanged; they load their external configurations independently and do not inherit the current application or library configuration. > For more guidance on testing, see [Testing](./testing). ### `define.lint()` \{#define-lint} -Defines the [Rslint configuration](https://rslint.rs/config/). Pass the configuration directly, or use an async function to load presets and plugins from `rstack/lint` on demand. +Defines the [Rslint configuration](https://rslint.rs/config/). Pass the configuration directly, or use a synchronous or asynchronous function. The function receives all exports from `rstack/lint`, so presets and plugins do not need to be imported manually. ```ts title="rstack.config.ts" import { define } from 'rstack'; -define.lint(async () => { - const { js, ts } = await import('rstack/lint'); - - return [js.configs.recommended, ts.configs.recommended]; -}); +define.lint(({ js, ts }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, +]); ``` ### `define.fmt()` \{#define-fmt} diff --git a/website/docs/en/guide/formatting.mdx b/website/docs/en/guide/formatting.mdx index a6c60054..c1c3a1ce 100644 --- a/website/docs/en/guide/formatting.mdx +++ b/website/docs/en/guide/formatting.mdx @@ -1,3 +1,7 @@ +--- +description: 'Format files with Rstack CLI using Prettier-compatible options, plugins, parallel formatting, and a persistent cache.' +--- + # Formatting import { PackageManagerTabs } from '@rspress/core/theme'; @@ -39,7 +43,7 @@ define.fmt({ }); ``` -In addition to Prettier options and `overrides`, Rstack provides two options: +In addition to Prettier options and `overrides`, Rstack CLI provides two options: - [`ignorePatterns`](#ignore-files): exclude files with Gitignore-compatible patterns. - [`sortPackageJson`](#sort-package-json): sort fields in `package.json` files. The default value is `false`. @@ -192,7 +196,7 @@ You can safely delete `.rstack/cache` to clear cached results. Do not treat the ## Prettier plugins -To add formatting capabilities that are not built into Rstack, install the corresponding [Prettier plugin](https://prettier.io/docs/plugins) and add it to `plugins`. Plugins can be referenced by package name, file path, or URL. Package names and relative paths are resolved from the directory containing the Rstack configuration file. +To add formatting capabilities that are not built into Rstack CLI, install the corresponding [Prettier plugin](https://prettier.io/docs/plugins) and add it to `plugins`. Plugins can be referenced by package name, file path, or URL. Package names and relative paths are resolved from the directory containing the Rstack configuration file. Because `rs fmt` loads plugins in workers, plugin objects cannot be passed directly. Reference each plugin by package name, path, or URL instead. For example, install and enable [`prettier-plugin-tailwindcss`](https://github.com/tailwindlabs/prettier-plugin-tailwindcss): diff --git a/website/docs/en/guide/monorepo.mdx b/website/docs/en/guide/monorepo.mdx index d8b4d501..41282191 100644 --- a/website/docs/en/guide/monorepo.mdx +++ b/website/docs/en/guide/monorepo.mdx @@ -1,18 +1,18 @@ --- -description: 'Configure shared Rstack checks, formatting, staged tasks, and project workflows in a monorepo.' +description: 'Use Rstack CLI to configure shared checks, formatting, staged tasks, and project workflows in a monorepo.' --- # Monorepo This guide explains how to use Rstack CLI in a monorepo, including how it works with task orchestrators such as [Turborepo](https://turborepo.com/docs) and [Nx](https://nx.dev/docs/getting-started/intro). -It covers managing Rstack dependencies, configuring lint, formatting, and staged-file tasks at the root, and defining separate configurations for web applications and libraries. +It covers managing the Rstack CLI dependency, configuring lint, formatting, and staged-file tasks at the root, and defining separate configurations for web applications and libraries. ## Project structure The recommended setup has two levels: -- The root manages the shared Rstack version, lint and formatting rules, and staged-file tasks. +- The root manages the shared Rstack CLI version, lint and formatting rules, and staged-file tasks. - Each application or library has its own [Rstack configuration](./configuration) for build, test, or documentation configuration. ```text @@ -29,13 +29,13 @@ The recommended setup has two levels: └── rstack.config.ts ``` -This structure keeps the Rstack version in one place while keeping build and test configuration close to the project that uses it. +This structure keeps the Rstack CLI version in one place while keeping build and test configuration close to the project that uses it. -## Rstack dependency management +## Rstack CLI dependency management \{#rstack-dependency-management} -Declare Rstack in the root `package.json` so projects use one version by default. See [Quick start](./quick-start#install-rstack) for installation instructions. +Declare the `rstack` package in the root `package.json` so projects use one Rstack CLI version by default. See [Quick start](./quick-start#install-rstack) for installation instructions. -If a project needs a different Rstack version from the root, declare that version as a dependency of the project. +If a project needs a different Rstack CLI version from the root, declare that version as a dependency of the project. Project-specific dependencies, such as Rsbuild plugins and testing libraries, should be declared in the projects that use them. @@ -46,11 +46,10 @@ Use [`define.lint()`](./configuration#define-lint), [`define.fmt()`](./configura ```ts title="rstack.config.ts" import { define } from 'rstack'; -define.lint(async () => { - const { js, ts } = await import('rstack/lint'); - - return [js.configs.recommended, ts.configs.recommended]; -}); +define.lint(({ js, ts }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, +]); define.fmt({ singleQuote: true, @@ -86,27 +85,23 @@ If some projects need different lint rules, use [`files`](https://rslint.rs/conf ```ts title="rstack.config.ts" import { define } from 'rstack'; -define.lint(async () => { - const { js, ts } = await import('rstack/lint'); - - return [ - js.configs.recommended, - ts.configs.recommended, - { - files: ['apps/web/**/*.{ts,tsx}'], - rules: { - '@typescript-eslint/no-explicit-any': 'off', - }, +define.lint(({ js, ts }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, + { + files: ['apps/web/**/*.{ts,tsx}'], + rules: { + '@typescript-eslint/no-explicit-any': 'off', }, - ]; -}); + }, +]); ``` ## Project configuration -For each project that uses [Rstack commands](./quick-start#cli-commands), create a [`rstack.config.ts`](./configuration#configuration-file) and register only the configuration that project needs. +For each project that uses [Rstack CLI commands](./quick-start#cli-commands), create a [`rstack.config.ts`](./configuration#configuration-file) and register only the configuration that project needs. -Rstack loads the configuration from the current working directory. It does not merge a project's configuration with the root configuration. +Rstack CLI loads the configuration from the current working directory. It does not merge a project's configuration with the root configuration. ### Web application @@ -117,7 +112,6 @@ import { define } from 'rstack'; define.app(async () => { const { pluginReact } = await import('@rsbuild/plugin-react'); - return { plugins: [pluginReact()], }; diff --git a/website/docs/en/guide/quick-start.mdx b/website/docs/en/guide/quick-start.mdx index cadaf016..ec64840d 100644 --- a/website/docs/en/guide/quick-start.mdx +++ b/website/docs/en/guide/quick-start.mdx @@ -6,11 +6,11 @@ description: 'Create a Rstack project or add Rstack CLI to an existing project a import { PackageManagerTabs } from '@rspress/core/theme'; -Rstack CLI brings the Rstack toolchain together with one CLI and one configuration file. This guide shows how to create a new Rstack project or add Rstack to an existing project, and introduces the available workflows. +Rstack CLI brings the Rstack toolchain together with one CLI and one configuration file. This guide shows how to create a new Rstack project or add Rstack CLI to an existing project, and introduces the available workflows. ## Environment preparation -Rstack supports using [Node.js](https://nodejs.org/), [Deno](https://deno.com/), or [Bun](https://bun.sh/) as the JavaScript runtime. +Rstack CLI supports using [Node.js](https://nodejs.org/), [Deno](https://deno.com/), or [Bun](https://bun.sh/) as the JavaScript runtime. Use one of the following installation guides to set up a runtime: @@ -20,7 +20,7 @@ Use one of the following installation guides to set up a runtime: :::tip Version requirements -Rstack requires Node.js 22.12.0 or higher when using Node.js as the runtime. +Rstack CLI requires Node.js 22.12.0 or higher when using Node.js as the runtime. ::: @@ -104,7 +104,7 @@ Options: Available templates: app-vanilla, app-vanilla-ts, app-react, app-react-ts, app-preact, app-preact-ts, app-vue, app-vue-ts, app-lit, app-lit-ts, app-svelte, app-svelte-ts, app-solid, app-solid-ts, lib-node, lib-node-ts, lib-react, lib-react-ts, lib-vue, lib-vue-ts, lib-svelte, lib-svelte-ts, lib-solid, lib-solid-ts, doc, doc-i18n ``` -## Install Rstack +## Install Rstack CLI \{#install-rstack} Install [`rstack`](https://www.npmjs.com/package/rstack) as a development dependency in a project that has a `package.json`: @@ -135,7 +135,7 @@ Add the commands your project needs to the `scripts` field in `package.json`. Fo } ``` -Package scripts use the project-local `rs` binary, so Rstack does not need to be installed globally. +Package scripts use the project-local `rs` binary, so Rstack CLI does not need to be installed globally. The following commands are available: @@ -151,12 +151,12 @@ The following commands are available: - [`rs setup`](./cli/setup): Install repository-level Git hooks. - [`rs staged`](./cli/staged): Run tasks against files staged in Git with lint-staged. -## Configure Rstack +## Configure Rstack CLI \{#configure-rstack} Create `rstack.config.ts` in the project root and register the configurations your project needs. The following is a minimal example for an application with testing and linting: ```ts title="rstack.config.ts" -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.app({ diff --git a/website/docs/en/guide/testing.mdx b/website/docs/en/guide/testing.mdx index 39e494bf..5fcb5494 100644 --- a/website/docs/en/guide/testing.mdx +++ b/website/docs/en/guide/testing.mdx @@ -1,6 +1,6 @@ # Testing -Rstack uses [Rstest](https://rstest.rs/) to run tests. +Rstack CLI uses [Rstest](https://rstest.rs/) to run tests. ```bash rs test @@ -44,7 +44,7 @@ define.test({ }); ``` -When `extends` is omitted, Rstack uses the Rsbuild adapter to extend the test configuration from `define.app()`. If no application configuration is defined, it uses the Rslib adapter with `define.lib()` instead. `define.app()` takes precedence when both are defined. +When `extends` is omitted, Rstack CLI uses the Rsbuild adapter to extend the test configuration from `define.app()`. If no application configuration is defined, it uses the Rslib adapter with `define.lib()` instead. `define.app()` takes precedence when both are defined. ## Multiple projects @@ -78,7 +78,7 @@ define.test({ }); ``` -Rstack applies the corresponding adapter to each inline project that omits `extends`. A function-based `define.app()` or `define.lib()` configuration is resolved once, then shared by those inline projects. +Rstack CLI applies the corresponding adapter to each inline project that omits `extends`. A function-based `define.app()` or `define.lib()` configuration is resolved once, then shared by those inline projects. Run one project by name: @@ -86,7 +86,7 @@ Run one project by name: rs test --project dom ``` -See [`examples/rstest-inline-projects`](https://github.com/rstackjs/rstack-cli/tree/main/examples/rstest-inline-projects) for a complete React SSR example using Node.js and happy-dom. +See [`examples/test-inline-projects`](https://github.com/rstackjs/rstack-cli/tree/main/examples/test-inline-projects) for a complete React SSR example using Node.js and happy-dom. ### External projects @@ -100,7 +100,7 @@ define.test({ }); ``` -Rstack passes string entries to Rstest unchanged. External projects load their own configuration and do not inherit the current `define.app()` or `define.lib()` configuration. Use external projects when each project manages its configuration independently. +Rstack CLI passes string entries to Rstest unchanged. External projects load their own configuration and do not inherit the current `define.app()` or `define.lib()` configuration. Use external projects when each project manages its configuration independently. ## Customize inheritance diff --git a/website/docs/public/horizontal-logo.svg b/website/docs/public/horizontal-logo.svg new file mode 100644 index 00000000..ca1eac2d --- /dev/null +++ b/website/docs/public/horizontal-logo.svg @@ -0,0 +1 @@ +Rstack CLI horizontal logo lockup. The claw logo is embedded from the original source without changes. \ No newline at end of file diff --git a/website/docs/zh/guide/ai.mdx b/website/docs/zh/guide/ai.mdx index a9ab69f1..581e94d5 100644 --- a/website/docs/zh/guide/ai.mdx +++ b/website/docs/zh/guide/ai.mdx @@ -50,7 +50,10 @@ Rstack CLI 提供面向特定领域的 Agent Skills,帮助 Coding Agent 更准 迁移现有项目时,安装该 Skill: - + 支持的工具和迁移说明请参阅[迁移到 Rstack CLI](./migration)。 diff --git a/website/docs/zh/guide/api-reference.mdx b/website/docs/zh/guide/api-reference.mdx index 0f7a2dc3..d61ebb1b 100644 --- a/website/docs/zh/guide/api-reference.mdx +++ b/website/docs/zh/guide/api-reference.mdx @@ -1,12 +1,12 @@ # API 参考 \{#api-reference} -Rstack 提供统一的配置 API,并通过专用子路径重导出 Rsbuild、Rslib、Rstest 和 Rslint 的公开 API。建议优先从这些子路径导入,以统一依赖入口,并确保 API 与 Rstack 集成的工具版本保持一致。 +Rstack CLI 提供统一的配置 API,并通过专用子路径重导出 Rsbuild、Rslib、Rstest 和 Rslint 的公开 API。建议优先从这些子路径导入,而不是直接从各工具的 core 包导入,以统一依赖入口,并确保 API 与 Rstack CLI 集成的工具版本匹配。 ## 导入路径 \{#import-paths} | 导入路径 | 内容 | 使用场景 | | ------------------------ | ----------------------------------------- | ------------------------ | -| `rstack` | Rstack 配置 API | 注册各项工具配置 | +| `rstack` | Rstack CLI 配置 API | 注册各项工具配置 | | `rstack/app` | `@rsbuild/core` 的公开 API | 构建应用及扩展 Rsbuild | | `rstack/lib` | `@rslib/core` 的公开 API | 构建库及扩展 Rslib | | `rstack/test` | `@rstest/core` 的公开 API | 编写测试及配置测试项目 | @@ -23,7 +23,7 @@ Rstack 提供统一的配置 API,并通过专用子路径重导出 Rsbuild、R ## 重导出 \{#re-exports} -以下工具子路径均会重导出对应 core 包的公开 API。通过这些 Rstack 入口导入,可以让依赖入口和工具版本与 Rstack 集成的工具链保持一致。 +以下工具子路径均会重导出对应 core 包的公开 API。通过这些入口导入,可以统一依赖入口,并确保 API 与 Rstack CLI 集成的工具版本匹配。 ### `rstack/app` diff --git a/website/docs/zh/guide/cli/_meta.json b/website/docs/zh/guide/cli/_meta.json index acdaffbe..5357606a 100644 --- a/website/docs/zh/guide/cli/_meta.json +++ b/website/docs/zh/guide/cli/_meta.json @@ -1 +1,13 @@ -["dev", "build", "preview", "lib", "doc", "test", "check", "lint", "fmt", "setup", "staged"] +[ + "dev", + "build", + "preview", + "lib", + "doc", + "test", + "check", + "lint", + "fmt", + "setup", + "staged" +] diff --git a/website/docs/zh/guide/cli/doc.mdx b/website/docs/zh/guide/cli/doc.mdx index 647184de..cfe4c138 100644 --- a/website/docs/zh/guide/cli/doc.mdx +++ b/website/docs/zh/guide/cli/doc.mdx @@ -1,3 +1,7 @@ +--- +description: '使用 rs doc 命令开发、构建和预览 Rspress 文档站点。' +--- + # doc import { PackageManagerTabs } from '@rspress/core/theme'; diff --git a/website/docs/zh/guide/cli/lint.mdx b/website/docs/zh/guide/cli/lint.mdx index 658ddb8a..0634bd38 100644 --- a/website/docs/zh/guide/cli/lint.mdx +++ b/website/docs/zh/guide/cli/lint.mdx @@ -29,14 +29,13 @@ rs lint --type-check ## 配置 \{#configuration} -在 [Rstack 配置文件](/guide/configuration#configuration-file)中通过 [`define.lint()`](../configuration#define-lint) 配置代码检查。该 API 支持标准的 [Rslint 配置](https://rslint.rs/config/)。预设和插件可以从 `rstack/lint` 按需导入: +在 [Rstack 配置文件](/guide/configuration#configuration-file)中通过 [`define.lint()`](../configuration#define-lint) 配置代码检查。该 API 支持标准的 [Rslint 配置](https://rslint.rs/config/)。配置函数会接收 `rstack/lint` 的全部导出,因此无需手动导入预设和插件: ```ts title="rstack.config.ts" import { define } from 'rstack'; -define.lint(async () => { - const { js, ts } = await import('rstack/lint'); - - return [js.configs.recommended, ts.configs.recommended]; -}); +define.lint(({ js, ts }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, +]); ``` diff --git a/website/docs/zh/guide/cli/setup.mdx b/website/docs/zh/guide/cli/setup.mdx index 4144e9b6..ea39d804 100644 --- a/website/docs/zh/guide/cli/setup.mdx +++ b/website/docs/zh/guide/cli/setup.mdx @@ -99,7 +99,7 @@ rs setup --help ## 支持的 hooks \{#supported-hooks} -Rstack 支持以下客户端 Git hooks: +Rstack CLI 支持以下客户端 Git hooks: - `pre-commit` - `pre-merge-commit` @@ -120,7 +120,7 @@ Rstack 支持以下客户端 Git hooks: ## Hook 运行时 \{#hook-runtime} -Rstack 使用 POSIX `sh -e` 运行 hook 脚本,并转发 Git 提供的参数和标准输入,同时返回 hook 的退出码。运行 hook 前,Rstack 会切换到安装 hooks 的项目,并将该项目的 `node_modules/.bin` 添加到 `PATH` 开头。 +Rstack CLI 使用 POSIX `sh -e` 运行 hook 脚本,并转发 Git 提供的参数和标准输入,同时返回 hook 的退出码。运行 hook 前,Rstack CLI 会切换到安装 hooks 的项目,并将该项目的 `node_modules/.bin` 添加到 `PATH` 开头。 ### 禁用与调试 \{#disable-and-debug} @@ -130,7 +130,7 @@ Rstack 使用 POSIX `sh -e` 运行 hook 脚本,并转发 Git 提供的参数 RSTACK_HOOKS=0 git commit -m "Skip hooks" ``` -将 `RSTACK_HOOKS` 设为 `2`,可以跟踪 Rstack hook 运行时,包括调用 hook 脚本和处理退出码等步骤;如需跟踪 hook 脚本内部的命令,请在脚本中添加 `set -x`: +将 `RSTACK_HOOKS` 设为 `2`,可以跟踪 Rstack CLI 的 hook 运行时,包括调用 hook 脚本和处理退出码等步骤;如需跟踪 hook 脚本内部的命令,请在脚本中添加 `set -x`: ```bash RSTACK_HOOKS=2 git commit -m "Trace hooks" @@ -138,7 +138,7 @@ RSTACK_HOOKS=2 git commit -m "Trace hooks" ### 配置 hook 运行环境 \{#configure-the-hook-environment} -运行 hook 脚本前,Rstack 会加载以下可选的 POSIX shell 文件: +运行 hook 脚本前,Rstack CLI 会加载以下可选的 POSIX shell 文件: ```text ${XDG_CONFIG_HOME:-$HOME/.config}/rstack/hooks-init.sh @@ -148,7 +148,7 @@ ${XDG_CONFIG_HOME:-$HOME/.config}/rstack/hooks-init.sh ## Monorepo \{#monorepo} -在 monorepo 中,提供 Rstack 的项目可能位于 `frontend/` 等子目录。从该目录运行 `rs setup` 时,hooks 仍会安装到 Git 仓库根目录: +在 monorepo 中,提供 Rstack CLI 的项目可能位于 `frontend/` 等子目录。从该目录运行 `rs setup` 时,hooks 仍会安装到 Git 仓库根目录: ```text repo/.rstack/hooks/ @@ -156,7 +156,7 @@ repo/.rstack/hooks/_/ core.hooksPath=.rstack/hooks/_ ``` -Rstack 会将 `frontend` 记录为负责管理 hooks 的项目。hook 脚本仍位于仓库根目录,但会从 `frontend` 目录运行,因此可以直接使用其中的配置和依赖,无需显式执行 `cd`: +Rstack CLI 会将 `frontend` 记录为负责管理 hooks 的项目。hook 脚本仍位于仓库根目录,但会从 `frontend` 目录运行,因此可以直接使用其中的配置和依赖,无需显式执行 `cd`: ```sh title=".rstack/hooks/pre-commit" rs staged @@ -168,7 +168,7 @@ rs staged ## 移除 hooks \{#remove-hooks} -如需移除由 Rstack 管理的 hooks: +如需移除由 Rstack CLI 管理的 hooks: 1. 从 `prepare` 脚本中移除 `rs setup`。 2. 删除仓库的 hooks 路径配置: @@ -188,13 +188,13 @@ rs staged - 重新运行 `rs setup`,恢复生成文件及其可执行权限。 - 检查环境变量或初始化文件中是否设置了 `RSTACK_HOOKS=0`。 - 如果命令提示存在其他 hooks 配置,请先迁移或移除冲突配置,再重新运行该命令。 -- 如果命令提示存在其他 Rstack owner,请按照 [Monorepo](#monorepo) 中的步骤转移 owner。 +- 如果命令提示其他项目是 hooks owner,请按照 [Monorepo](#monorepo) 中的步骤转移 owner。 -hook 脚本不需要可执行权限,因为 Rstack 会使用 `sh` 运行它。 +hook 脚本不需要可执行权限,因为 Rstack CLI 会使用 `sh` 运行它。 ### 找不到命令 \{#command-not-found} -退出码为 127 时,Rstack 会打印实际生效的 `PATH`。如果 GUI Git 客户端找不到 Node.js 或包管理器,请在 `hooks-init.sh` 中初始化相关环境。 +退出码为 127 时,Rstack CLI 会打印实际生效的 `PATH`。如果 GUI Git 客户端找不到 Node.js 或包管理器,请在 `hooks-init.sh` 中初始化相关环境。 ### Windows 与 Yarn \{#windows-and-yarn} diff --git a/website/docs/zh/guide/configuration.mdx b/website/docs/zh/guide/configuration.mdx index f4cbf631..06939f84 100644 --- a/website/docs/zh/guide/configuration.mdx +++ b/website/docs/zh/guide/configuration.mdx @@ -2,14 +2,14 @@ import { PackageManagerTabs } from '@rspress/core/theme'; -Rstack 将项目所用工具的配置集中到一份文件中。通过 `define.*()` API 定义项目实际需要的配置即可。 +Rstack CLI 将项目所用工具的配置集中到一份文件中。通过 `define.*()` API 定义项目实际需要的配置即可。 ## 配置文件 \{#configuration-file} 在项目根目录创建 `rstack.config.ts`,并调用对应的 `define.*()` API: ```ts title="rstack.config.ts" -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.app({ @@ -31,7 +31,7 @@ define.fmt({ 配置文件无需默认导出。每个 `define.*()` API 最多调用一次;重复定义同一类型的配置会抛出错误。 -Rstack 默认会查找使用以下任一文件名的配置文件: +Rstack CLI 默认会查找使用以下任一文件名的配置文件: - `rstack.config.ts` - `rstack.config.js` @@ -46,7 +46,7 @@ rs build --config ./configs/rstack.config.ts ## 按需加载依赖 \{#loading-dependencies-on-demand} -每次执行 `rs` 命令时,Rstack 都会加载并执行配置文件,然后只解析当前命令需要的配置函数。 +每次执行 `rs` 命令时,Rstack CLI 都会加载并执行配置文件,然后只解析当前命令需要的配置函数。 如果配置需要导入插件或其他工具专属依赖,请使用异步配置函数,并在函数内通过动态 `import()` 加载这些依赖。这样只有解析该配置时才会加载相关依赖。 @@ -55,7 +55,6 @@ import { define } from 'rstack'; define.app(async () => { const { pluginReact } = await import('@rsbuild/plugin-react'); - return { plugins: [pluginReact()], }; @@ -64,7 +63,7 @@ define.app(async () => { ## 配置 API \{#configuration-apis} -各 API 沿用底层工具的配置格式。使用 Rstack 已重导出的 API 和辅助函数时,推荐从 `rstack/app`、`rstack/lib`、`rstack/test` 和 `rstack/lint` 入口导入。 +各 API 沿用底层工具的配置格式。使用 Rstack CLI 已重导出的 API 和辅助函数时,推荐从 `rstack/app`、`rstack/lib`、`rstack/test` 和 `rstack/lint` 入口导入。 | API | 底层工具 | 对应命令 | | ----------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------- | @@ -121,7 +120,7 @@ define.doc({ }); ``` -`@rspress/core` 是 Rstack 的可选依赖。每个使用 `rs doc` 命令的项目都需要安装该依赖: +`@rspress/core` 是 Rstack CLI 的可选依赖。每个使用 `rs doc` 命令的项目都需要安装该依赖: @@ -142,24 +141,23 @@ define.test({ }); ``` -未设置 `extends` 时,Rstack 会通过 Rsbuild 适配器让测试配置自动继承 `define.app()`;如果未定义应用配置,则通过 Rslib 适配器回退到 `define.lib()`。二者同时存在时,应用配置的优先级更高。显式设置 `extends` 可关闭自动继承。 +未设置 `extends` 时,Rstack CLI 会通过 Rsbuild 适配器让测试配置自动继承 `define.app()`;如果未定义应用配置,则通过 Rslib 适配器回退到 `define.lib()`。二者同时存在时,应用配置的优先级更高。显式设置 `extends` 可关闭自动继承。 -如果测试根配置未定义 `extends` 且包含 `projects`,Rstack 会为每个未自行设置 `extends` 的内联项目应用自动继承。函数形式的应用或库配置只会解析一次,并由这些项目共享。字符串形式的项目会原样传给 Rstest;它们会独立加载外部配置,不继承当前应用或库的配置。 +如果测试根配置未定义 `extends` 且包含 `projects`,Rstack CLI 会为每个未自行设置 `extends` 的内联项目应用自动继承。函数形式的应用或库配置只会解析一次,并由这些项目共享。字符串形式的项目会原样传给 Rstest;它们会独立加载外部配置,不继承当前应用或库的配置。 > 如需了解更多测试相关用法,请参阅[测试](./testing)。 ### `define.lint()` \{#define-lint} -定义 [Rslint 配置](https://rslint.rs/config/)。可以直接传入配置,也可以使用异步函数,按需从 `rstack/lint` 加载预设和插件。 +定义 [Rslint 配置](https://rslint.rs/config/)。可以直接传入配置,也可以传入同步或异步函数。函数会接收 `rstack/lint` 的全部导出,因此无需手动导入预设和插件。 ```ts title="rstack.config.ts" import { define } from 'rstack'; -define.lint(async () => { - const { js, ts } = await import('rstack/lint'); - - return [js.configs.recommended, ts.configs.recommended]; -}); +define.lint(({ js, ts }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, +]); ``` ### `define.fmt()` \{#define-fmt} diff --git a/website/docs/zh/guide/formatting.mdx b/website/docs/zh/guide/formatting.mdx index 8659e673..157bd54c 100644 --- a/website/docs/zh/guide/formatting.mdx +++ b/website/docs/zh/guide/formatting.mdx @@ -1,3 +1,7 @@ +--- +description: '使用 Rstack CLI 格式化文件,支持与 Prettier 兼容的选项、插件、并行格式化和持久化缓存。' +--- + # 格式化 \{#formatting} import { PackageManagerTabs } from '@rspress/core/theme'; @@ -39,7 +43,7 @@ define.fmt({ }); ``` -除了 Prettier 选项和 `overrides`,Rstack 还提供两个选项: +除了 Prettier 选项和 `overrides`,Rstack CLI 还提供两个选项: - [`ignorePatterns`](#ignore-files):使用兼容 Gitignore 的模式排除文件。 - [`sortPackageJson`](#sort-package-json):对 `package.json` 中的字段排序,默认值为 `false`。 @@ -163,7 +167,7 @@ define.fmt({ ### 合并顺序 \{#merge-order} -如果同一文件匹配多条 override 规则,Rstack 会按声明顺序合并配置,后面的值优先。下面的 `README.md` 会同时匹配两条规则,因此最终的 `printWidth` 为 `80`: +如果同一文件匹配多条 override 规则,Rstack CLI 会按声明顺序合并配置,后面的值优先。下面的 `README.md` 会同时匹配两条规则,因此最终的 `printWidth` 为 `80`: ```ts define.fmt({ @@ -192,7 +196,7 @@ rs fmt --no-cache ## Prettier 插件 \{#prettier-plugins} -如果需要使用 Rstack 未内置的格式化能力,可以安装相应的 [Prettier 插件](https://prettier.io/docs/plugins),并添加到 `plugins` 中。插件支持通过包名、文件路径或 URL 引用,其中包名和相对路径基于 Rstack 配置文件所在的目录解析。 +如果需要使用 Rstack CLI 未内置的格式化能力,可以安装相应的 [Prettier 插件](https://prettier.io/docs/plugins),并添加到 `plugins` 中。插件支持通过包名、文件路径或 URL 引用,其中包名和相对路径基于 Rstack 配置文件所在的目录解析。 由于 `rs fmt` 会在 worker 中加载插件,因此不支持直接传入插件对象。请通过包名、路径或 URL 引用插件。例如,安装并启用 [`prettier-plugin-tailwindcss`](https://github.com/tailwindlabs/prettier-plugin-tailwindcss): diff --git a/website/docs/zh/guide/monorepo.mdx b/website/docs/zh/guide/monorepo.mdx index ea1c5ea6..aa898b82 100644 --- a/website/docs/zh/guide/monorepo.mdx +++ b/website/docs/zh/guide/monorepo.mdx @@ -1,18 +1,18 @@ --- -description: '在 Monorepo 中配置共享的 Rstack 检查、格式化、暂存文件任务和项目工作流。' +description: '在 Monorepo 中使用 Rstack CLI 配置共享检查、格式化、暂存文件任务和项目工作流。' --- # Monorepo 本指南介绍如何在 Monorepo 中使用 Rstack CLI,以及如何让它与 [Turborepo](https://turborepo.com/docs)、[Nx](https://nx.dev/docs/getting-started/intro) 等任务编排工具协同工作。 -主要内容包括管理 Rstack 依赖、在根目录统一配置代码检查、格式化和暂存文件任务,以及为 Web 应用和库项目定义独立配置。 +主要内容包括管理 Rstack CLI 依赖、在根目录统一配置代码检查、格式化和暂存文件任务,以及为 Web 应用和库项目定义独立配置。 ## 目录结构 \{#project-structure} 推荐使用两层配置: -- 根目录统一管理 Rstack 版本、lint 和格式化规则,以及暂存文件任务。 +- 根目录统一管理 Rstack CLI 版本、lint 和格式化规则,以及暂存文件任务。 - 每个应用或库使用自己的 [Rstack 配置](./configuration),定义构建、测试或文档配置。 ```text @@ -29,13 +29,13 @@ description: '在 Monorepo 中配置共享的 Rstack 检查、格式化、暂存 └── rstack.config.ts ``` -这种结构既能统一 Rstack 版本,也能让构建和测试配置靠近实际使用它们的项目。 +这种结构既能统一 Rstack CLI 版本,也能让构建和测试配置靠近实际使用它们的项目。 -## Rstack 依赖管理 \{#rstack-dependency-management} +## Rstack CLI 依赖管理 \{#rstack-dependency-management} -在根目录的 `package.json` 中声明 Rstack,让各个项目默认使用同一个版本。安装方法请参考[快速上手](./quick-start#install-rstack)。 +在根目录的 `package.json` 中声明 `rstack` 包,让各个项目默认使用同一个 Rstack CLI 版本。安装方法请参考[快速上手](./quick-start#install-rstack)。 -如果子项目需要使用与根目录不同版本的 `rstack`,可以在该项目中单独声明对应版本的 `rstack` 依赖。 +如果子项目需要使用与根目录不同版本的 Rstack CLI,可以在该项目中单独声明对应版本的 `rstack` 依赖。 Rsbuild 插件、测试库等项目专属依赖,建议定义在实际使用它们的子项目中。 @@ -46,11 +46,10 @@ Rsbuild 插件、测试库等项目专属依赖,建议定义在实际使用它 ```ts title="rstack.config.ts" import { define } from 'rstack'; -define.lint(async () => { - const { js, ts } = await import('rstack/lint'); - - return [js.configs.recommended, ts.configs.recommended]; -}); +define.lint(({ js, ts }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, +]); define.fmt({ singleQuote: true, @@ -86,27 +85,23 @@ define.staged({ ```ts title="rstack.config.ts" import { define } from 'rstack'; -define.lint(async () => { - const { js, ts } = await import('rstack/lint'); - - return [ - js.configs.recommended, - ts.configs.recommended, - { - files: ['apps/web/**/*.{ts,tsx}'], - rules: { - '@typescript-eslint/no-explicit-any': 'off', - }, +define.lint(({ js, ts }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, + { + files: ['apps/web/**/*.{ts,tsx}'], + rules: { + '@typescript-eslint/no-explicit-any': 'off', }, - ]; -}); + }, +]); ``` ## 子项目配置 \{#project-configuration} -为每个使用 [Rstack 命令](./quick-start#cli-commands)的子项目创建 [`rstack.config.ts`](./configuration#configuration-file),并且只配置该项目需要的功能。 +为每个使用 [Rstack CLI 命令](./quick-start#cli-commands)的子项目创建 [`rstack.config.ts`](./configuration#configuration-file),并且只配置该项目需要的功能。 -Rstack 会加载当前工作目录中的配置,不会将子项目配置与根配置自动合并。 +Rstack CLI 会加载当前工作目录中的配置,不会将子项目配置与根配置自动合并。 ### Web 应用 \{#web-application} @@ -117,7 +112,6 @@ import { define } from 'rstack'; define.app(async () => { const { pluginReact } = await import('@rsbuild/plugin-react'); - return { plugins: [pluginReact()], }; diff --git a/website/docs/zh/guide/quick-start.mdx b/website/docs/zh/guide/quick-start.mdx index 1c6aaa6c..4cc43cca 100644 --- a/website/docs/zh/guide/quick-start.mdx +++ b/website/docs/zh/guide/quick-start.mdx @@ -6,11 +6,11 @@ description: '创建 Rstack 项目,或在现有项目中安装 Rstack CLI 并 import { PackageManagerTabs } from '@rspress/core/theme'; -Rstack CLI 通过统一的命令行和配置文件整合 Rstack 工具链。本指南将介绍如何创建新的 Rstack 项目或在现有项目中添加 Rstack,以及可以使用的工作流。 +Rstack CLI 通过统一的命令行和配置文件整合 Rstack 工具链。本指南将介绍如何创建新的 Rstack 项目或在现有项目中添加 Rstack CLI,以及可以使用的工作流。 ## 环境准备 \{#environment-preparation} -Rstack 支持使用 [Node.js](https://nodejs.org/)、[Deno](https://deno.com/) 或 [Bun](https://bun.sh/) 作为 JavaScript 运行时。 +Rstack CLI 支持使用 [Node.js](https://nodejs.org/)、[Deno](https://deno.com/) 或 [Bun](https://bun.sh/) 作为 JavaScript 运行时。 参考以下安装指南,选择一种运行时: @@ -20,7 +20,7 @@ Rstack 支持使用 [Node.js](https://nodejs.org/)、[Deno](https://deno.com/) :::tip 版本要求 -使用 Node.js 作为运行时时,Rstack 要求 Node.js 版本为 22.12.0 或更高版本。 +使用 Node.js 作为运行时时,Rstack CLI 要求 Node.js 版本为 22.12.0 或更高版本。 ::: @@ -104,7 +104,7 @@ Options: Available templates: app-vanilla, app-vanilla-ts, app-react, app-react-ts, app-preact, app-preact-ts, app-vue, app-vue-ts, app-lit, app-lit-ts, app-svelte, app-svelte-ts, app-solid, app-solid-ts, lib-node, lib-node-ts, lib-react, lib-react-ts, lib-vue, lib-vue-ts, lib-svelte, lib-svelte-ts, lib-solid, lib-solid-ts, doc, doc-i18n ``` -## 安装 Rstack \{#install-rstack} +## 安装 Rstack CLI \{#install-rstack} 在已有 `package.json` 的项目中,将 [`rstack`](https://www.npmjs.com/package/rstack) 安装为开发依赖: @@ -135,9 +135,9 @@ Available templates: app-vanilla, app-vanilla-ts, app-react, app-react-ts, app-p } ``` -package scripts 会使用项目本地安装的 `rs` 命令,因此无需全局安装 Rstack。 +package scripts 会使用项目本地安装的 `rs` 命令,因此无需全局安装 Rstack CLI。 -Rstack 提供以下命令: +Rstack CLI 提供以下命令: - [`rs dev`](./cli/dev):启动应用开发服务器。 - [`rs build`](./cli/build):构建应用的生产版本。 @@ -151,12 +151,12 @@ Rstack 提供以下命令: - [`rs setup`](./cli/setup):安装仓库级 Git hooks。 - [`rs staged`](./cli/staged):使用 lint-staged 对 Git 暂存区中的文件运行任务。 -## 配置 Rstack \{#configure-rstack} +## 配置 Rstack CLI \{#configure-rstack} 在项目根目录创建 `rstack.config.ts`,并注册项目所需的配置。以下是一个包含应用、测试和代码检查的最小示例: ```ts title="rstack.config.ts" -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.app({ diff --git a/website/docs/zh/guide/testing.mdx b/website/docs/zh/guide/testing.mdx index ed89dc71..fd969ad2 100644 --- a/website/docs/zh/guide/testing.mdx +++ b/website/docs/zh/guide/testing.mdx @@ -1,6 +1,6 @@ # 测试 \{#testing} -Rstack 使用 [Rstest](https://rstest.rs/zh/) 运行测试。 +Rstack CLI 使用 [Rstest](https://rstest.rs/zh/) 运行测试。 ```bash rs test @@ -44,7 +44,7 @@ define.test({ }); ``` -未设置 `extends` 时,Rstack 会通过 Rsbuild 适配器让测试配置继承 `define.app()`。如果没有应用配置,则通过 Rslib 适配器回退到 `define.lib()`。同时定义两者时,`define.app()` 的优先级更高。 +未设置 `extends` 时,Rstack CLI 会通过 Rsbuild 适配器让测试配置继承 `define.app()`。如果没有应用配置,则通过 Rslib 适配器回退到 `define.lib()`。同时定义两者时,`define.app()` 的优先级更高。 ## 多项目 \{#multiple-projects} @@ -78,7 +78,7 @@ define.test({ }); ``` -Rstack 会将对应的适配器应用到每个未设置 `extends` 的内联项目。函数形式的 `define.app()` 或 `define.lib()` 配置只会解析一次,再由这些内联项目共享。 +Rstack CLI 会将对应的适配器应用到每个未设置 `extends` 的内联项目。函数形式的 `define.app()` 或 `define.lib()` 配置只会解析一次,再由这些内联项目共享。 按项目名称运行单个项目: @@ -86,7 +86,7 @@ Rstack 会将对应的适配器应用到每个未设置 `extends` 的内联项 rs test --project dom ``` -完整的 React SSR 示例请参阅 [`examples/rstest-inline-projects`](https://github.com/rstackjs/rstack-cli/tree/main/examples/rstest-inline-projects),该示例使用 Node.js 和 happy-dom 两种测试环境。 +完整的 React SSR 示例请参阅 [`examples/test-inline-projects`](https://github.com/rstackjs/rstack-cli/tree/main/examples/test-inline-projects),该示例使用 Node.js 和 happy-dom 两种测试环境。 ### 外部项目 \{#external-projects} @@ -100,7 +100,7 @@ define.test({ }); ``` -Rstack 会将字符串形式的项目原样传给 Rstest。外部项目会加载自己的配置,不会继承当前的 `define.app()` 或 `define.lib()` 配置。每个项目需要独立管理配置时,请使用外部项目。 +Rstack CLI 会将字符串形式的项目原样传给 Rstest。外部项目会加载自己的配置,不会继承当前的 `define.app()` 或 `define.lib()` 配置。每个项目需要独立管理配置时,请使用外部项目。 ## 自定义继承 \{#customize-inheritance} diff --git a/website/i18n.json b/website/i18n.json index ba475596..a00be44e 100644 --- a/website/i18n.json +++ b/website/i18n.json @@ -3,13 +3,29 @@ "en": "Quick start", "zh": "快速上手" }, + "viewSource": { + "en": "View the code", + "zh": "查看源码" + }, + "copyCommand": { + "en": "Copy command", + "zh": "复制命令" + }, + "copiedCommand": { + "en": "Command copied", + "zh": "命令已复制" + }, + "title": { + "en": "Unified Toolchain for", + "zh": "统一工具链" + }, "subtitle": { - "en": "The Unified JavaScript Toolchain", - "zh": "统一的 JavaScript 工具链" + "en": "Shipping JavaScript Faster", + "zh": "加速 JavaScript 开发" }, "slogan": { - "en": "One CLI, one configuration, one consistent workflow", - "zh": "一个命令行、一份配置、一致的工作流" + "en": "One CLI unifies development, builds, testing, linting, and formatting across all your JavaScript projects. Powered by the Rspack ecosystem.", + "zh": "只需一个 CLI,即可统一所有 JavaScript 项目的开发、构建、测试、代码检查与格式化。由 Rspack 生态驱动。" }, "unifiedCli": { "en": "One CLI", diff --git a/website/rstack.config.ts b/website/rstack.config.ts index 79821ca1..29116243 100644 --- a/website/rstack.config.ts +++ b/website/rstack.config.ts @@ -1,18 +1,23 @@ -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import path from 'node:path'; import { define } from 'rstack'; const title = 'Rstack CLI'; const description = 'Rstack CLI brings the Rstack toolchain together with one CLI, one configuration, and one consistent workflow.'; -const descriptionZh = 'Rstack CLI 通过统一的命令行、配置和工作流整合 Rstack 工具链。'; +const descriptionZh = + 'Rstack CLI 通过统一的命令行、配置和工作流整合 Rstack 工具链。'; const injectLlmsHint = process.env.RSPRESS_INJECT_LLMS_HINT !== 'false'; define.doc(async () => { const { pluginSass } = await import('@rsbuild/plugin-sass'); - const { transformerNotationDiff, transformerNotationFocus, transformerNotationHighlight } = - await import('@shikijs/transformers'); - const { pluginClientRedirects } = await import('@rspress/plugin-client-redirects'); + const { + transformerNotationDiff, + transformerNotationFocus, + transformerNotationHighlight, + } = await import('@shikijs/transformers'); + const { pluginClientRedirects } = + await import('@rspress/plugin-client-redirects'); const { pluginSitemap } = await import('@rspress/plugin-sitemap'); const { pluginOpenGraph } = await import('rsbuild-plugin-open-graph'); const { pluginFontOpenSans } = await import('rspress-plugin-font-open-sans'); @@ -21,8 +26,8 @@ define.doc(async () => { return { root: path.join(import.meta.dirname, 'docs'), title, - icon: 'https://assets.rspack.rs/rspack/favicon-128x128.png', - logoText: title, + icon: 'https://assets.rspack.rs/rspack/rspack-claw-logo.svg', + logo: '/horizontal-logo.svg', description, lang: 'en', llms: true, @@ -57,6 +62,20 @@ define.doc(async () => { pluginFontOpenSans(), pluginSitemap({ siteUrl }), ], + locales: [ + { + lang: 'en', + label: 'English', + title, + description, + }, + { + lang: 'zh', + label: '简体中文', + title, + description: descriptionZh, + }, + ], themeConfig: { llmsUI: { injectLlmsHint, @@ -74,22 +93,9 @@ define.doc(async () => { }, ], editLink: { - docRepoBaseUrl: 'https://github.com/rstackjs/rstack-cli/tree/main/website/docs', + docRepoBaseUrl: + 'https://github.com/rstackjs/rstack-cli/tree/main/website/docs', }, - locales: [ - { - lang: 'en', - label: 'English', - title, - description, - }, - { - lang: 'zh', - label: '简体中文', - title, - description: descriptionZh, - }, - ], }, builderConfig: { plugins: [ diff --git a/website/theme/components/Copyright.tsx b/website/theme/components/Copyright.tsx index 5ef14a01..75e3049a 100644 --- a/website/theme/components/Copyright.tsx +++ b/website/theme/components/Copyright.tsx @@ -5,7 +5,10 @@ export const CopyRight = () => {