diff --git a/.agents/skills/migrate-to-rstack-cli/SKILL.md b/.agents/skills/migrate-to-rstack-cli/SKILL.md index 917de99c..01612d1b 100644 --- a/.agents/skills/migrate-to-rstack-cli/SKILL.md +++ b/.agents/skills/migrate-to-rstack-cli/SKILL.md @@ -34,14 +34,18 @@ Read every matching reference before editing. Load only the tools present in the Rsbuild, Rslib, Rstest, Rslint, and Prettier remain transitive `rstack` dependencies. Remove obsolete direct dependencies and imports from the migrated scope; do not expect their names to disappear from the lockfile. -## Configuration Rules +## Configuration + +### Config Files + +Treat each workspace or config root independently. A monorepo may need multiple Rstack config files when commands run from different package directories; validate config discovery from each directory. 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 +57,23 @@ define.test({ }); ``` -Prefer async config functions and dynamic imports for runtime plugins and presets: +### Modules and Imports + +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/prettier.md b/.agents/skills/migrate-to-rstack-cli/references/prettier.md index 280cadbf..b4c4932d 100644 --- a/.agents/skills/migrate-to-rstack-cli/references/prettier.md +++ b/.agents/skills/migrate-to-rstack-cli/references/prettier.md @@ -6,14 +6,15 @@ Read this reference when the project uses the `prettier` CLI or API, `package.js ## Steps -1. Inventory formatting commands and inputs, Prettier options and overrides, ignore rules, `.editorconfig`, plugins, package.json sorting, and programmatic API calls. +1. Inventory formatting commands and inputs, Prettier options and overrides, ignore rules, `.editorconfig`, plugins, package.json sorting, programmatic API calls, and tracked VS Code settings. 2. Move Prettier options and overrides into `define.fmt` in `rstack.config.*`. 3. Move `.prettierignore` or custom `--ignore-path` rules into `ignorePatterns`. Rebase patterns from each ignore file's directory to the Rstack configuration directory when they differ, preserving rule order and negations. Translate relevant `.editorconfig` values into explicit formatting options. 4. Replace Prettier CLI commands with the matching `rs fmt` commands and preserve their file or glob arguments. 5. Reference plugins by package name, file path, or URL. Do not pass imported plugin objects, and keep each plugin package as a direct dependency. 6. When replacing `prettier-plugin-packagejson`, enable `sortPackageJson` and preserve the original manifest paths. -7. Delete old config and ignore files only after their behavior is represented in `define.fmt`. -8. Remove direct dependencies only when no script, config, API call, plugin peer requirement, or other tool still needs them. +7. If tracked VS Code configuration recommends `esbenp.prettier-vscode` or selects it with `editor.defaultFormatter`, replace it with `rstack.rstack` for scopes migrated to `rs fmt`, and move supported `prettier.*` formatting options into `define.fmt`. Preserve `editor.formatOnSave`, remove `source.fixAll.prettier` when no remaining scope uses it, and keep the Prettier extension for any scope that still does. +8. Delete old config and ignore files only after their behavior is represented in `define.fmt`. +9. Remove direct dependencies only when no script, config, API call, plugin peer requirement, or other tool still needs them. `rs fmt` ignores `package-lock.json` and `pnpm-lock.yaml` by default. Drop redundant ignore entries during migration, but keep intentional negations. diff --git a/.agents/skills/migrate-to-rstack-cli/references/rslint.md b/.agents/skills/migrate-to-rstack-cli/references/rslint.md index 96b31d59..e2c68154 100644 --- a/.agents/skills/migrate-to-rstack-cli/references/rslint.md +++ b/.agents/skills/migrate-to-rstack-cli/references/rslint.md @@ -5,20 +5,36 @@ 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. +2. Move the old config into `define.lint`, replacing Rslint's `defineConfig()` wrapper and import. Receive `@rslint/core` exports from the factory parameter. +3. If the old config imports the `globals` package for environment maps such as `globals.browser`, receive `globals` from the factory parameter instead. Remove the direct `globals` dependency after confirming that no other file uses it. 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.*`. +6. If tracked VS Code configuration recommends `rstack.rslint`, replace it with the unified `rstack.rstack` extension. Move relevant `rslint.*` settings to their current `rstack.rslint.*` equivalents according to the [Rstack extension documentation](https://github.com/rstackjs/rstack-editor/blob/main/packages/vscode/README.md). Keep `source.fixAll.rslint` unchanged. ## 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. + +The factory also provides Rslint's built-in globals catalog, so an external `globals` import is unnecessary: + +```ts +define.lint(({ globals }) => [ + { + files: ['**/*.{js,cjs,mjs}'], + languageOptions: { + globals: globals.browser, + }, + }, +]); ``` ## Script Pattern diff --git a/.agents/skills/migrate-to-rstack-cli/references/rstest.md b/.agents/skills/migrate-to-rstack-cli/references/rstest.md index 60878dea..3a28a2d3 100644 --- a/.agents/skills/migrate-to-rstack-cli/references/rstest.md +++ b/.agents/skills/migrate-to-rstack-cli/references/rstest.md @@ -14,6 +14,7 @@ Read this reference when the project uses `@rstest/core`, `@rstest/adapter-rsbui - `@rstest/core/importMeta` to `rstack/test/importMeta` 6. Search for remaining direct core or adapter imports. Remove `@rstest/core` and adapter dependencies only when no direct use remains. 7. Delete `rstest.config.*` after all behavior is represented or intentionally supplied by automatic app/library extension. +8. If tracked VS Code configuration recommends `rstack.rstest`, replace it with the unified `rstack.rstack` extension. Move supported `rstest.*` settings to `rstack.rstest.*` according to the [Rstack extension documentation](https://github.com/rstackjs/rstack-editor/blob/main/packages/vscode/README.md); `rstest.nodeExecutable` instead becomes the shared `rstack.nodeExecutable` setting. ## Config Pattern diff --git a/.github/renovate.json b/.github/renovate.json index 7fb2dd94..23b48ba4 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,4 +1,23 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["github>rstackjs/renovate"] + "extends": ["github>rstackjs/renovate"], + "packageRules": [ + { + "description": "Disable Rust toolchain updates", + "matchManagers": ["rust-toolchain"], + "enabled": false + }, + { + "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..4cc3d97c 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 @@ -55,7 +55,7 @@ jobs: - name: Install cargo-zigbuild if: contains(inputs.target, 'musl') - uses: taiki-e/install-action@6c6fd71fe4fb72c3697d269963d0e15df8adedad # v2.85.10 + uses: taiki-e/install-action@7f4eb899022d8fe70b20c4f3de697aa85c309026 # v2.85.11 with: tool: cargo-zigbuild@0.23.0 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/.vscode/extensions.json b/.vscode/extensions.json index f172f18c..72d54cae 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,3 @@ { - "recommendations": ["rstack.rslint", "esbenp.prettier-vscode"] + "recommendations": ["rstack.rstack"] } diff --git a/.vscode/settings.json b/.vscode/settings.json index a8bf25da..6aac1c1a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,18 +13,17 @@ "**/.DS_Store": true }, "mdx.validate.validateFileLinks": "ignore", - "editor.defaultFormatter": "esbenp.prettier-vscode", - // Temporary workaround until we switch to the Rstack VS Code extension. - "prettier.printWidth": 100, - "prettier.singleQuote": true, + "editor.defaultFormatter": "rstack.rstack", "js/ts.tsdk.path": "node_modules/typescript/lib", - "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[mdx]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - } + "[javascript]": { "editor.defaultFormatter": "rstack.rstack" }, + "[javascriptreact]": { "editor.defaultFormatter": "rstack.rstack" }, + "[json]": { "editor.defaultFormatter": "rstack.rstack" }, + "[json5]": { "editor.defaultFormatter": "rstack.rstack" }, + "[jsonc]": { "editor.defaultFormatter": "rstack.rstack" }, + "[markdown]": { "editor.defaultFormatter": "rstack.rstack" }, + "[mdx]": { "editor.defaultFormatter": "rstack.rstack" }, + "[toml]": { "editor.defaultFormatter": "rstack.rstack" }, + "[typescript]": { "editor.defaultFormatter": "rstack.rstack" }, + "[typescriptreact]": { "editor.defaultFormatter": "rstack.rstack" }, + "[yaml]": { "editor.defaultFormatter": "rstack.rstack" } } 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..48c4f025 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,13 +6,12 @@ resolver = "2" edition = "2021" license = "MIT" repository = "https://github.com/rstackjs/rstack-cli" -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/crates/rstack-binding/Cargo.toml b/crates/rstack-binding/Cargo.toml index 74dfbcbe..a1b861a4 100644 --- a/crates/rstack-binding/Cargo.toml +++ b/crates/rstack-binding/Cargo.toml @@ -4,7 +4,6 @@ version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true -rust-version.workspace = true publish = false [lib] diff --git a/crates/rstack-ignore/Cargo.toml b/crates/rstack-ignore/Cargo.toml index fe80d3c6..2f273e1b 100644 --- a/crates/rstack-ignore/Cargo.toml +++ b/crates/rstack-ignore/Cargo.toml @@ -4,7 +4,6 @@ version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true -rust-version.workspace = true publish = false [dependencies] 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/app-vanilla/tests/dom.test.ts b/examples/app-vanilla/tests/dom.test.ts index 45c267a8..181f4c43 100644 --- a/examples/app-vanilla/tests/dom.test.ts +++ b/examples/app-vanilla/tests/dom.test.ts @@ -1,7 +1,7 @@ import { expect, test } from 'rstack/test'; import { screen } from '@testing-library/dom'; -test('test dom', () => { +test('renders content', () => { document.body.innerHTML = `
Visible Example
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/package.json b/package.json index 682f4b0a..a241757d 100644 --- a/package.json +++ b/package.json @@ -20,11 +20,10 @@ "devDependencies": { "@types/node": "catalog:", "cspell-ban-words": "catalog:", - "globals": "catalog:", "heading-case": "catalog:", "prettier": "catalog:", "rstack": "workspace:*", "typescript": "catalog:" }, - "packageManager": "pnpm@11.21.0" + "packageManager": "pnpm@11.22.0" } 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..11a52995 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.2", "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..31b5aac3 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.2", "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..777b1ed9 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,14 @@ 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, rstestPlugin }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, + { + files: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + ...rstestPlugin.configs.recommended, + }, +]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-app-lit-ts/tests/tsconfig.json b/packages/create-rstack/template-app-lit-ts/tests/tsconfig.json new file mode 100644 index 00000000..b7601d95 --- /dev/null +++ b/packages/create-rstack/template-app-lit-ts/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["rstack/types", "node"] + }, + "include": ["./"] +} diff --git a/packages/create-rstack/template-app-lit/package.json b/packages/create-rstack/template-app-lit/package.json index 6d260528..60d5d499 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.2" } } diff --git a/packages/create-rstack/template-app-lit/rstack.config.js b/packages/create-rstack/template-app-lit/rstack.config.js index d09e1329..9bd6b868 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,13 @@ define.app({ }, }); -define.test({ - testEnvironment: 'happy-dom', -}); - -define.lint(async () => { - const { js } = await import('rstack/lint'); - - return [js.configs.recommended]; -}); +define.lint(({ js, rstestPlugin }) => [ + js.configs.recommended, + { + files: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + ...rstestPlugin.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..16a81439 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.2", "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..fed67464 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,16 @@ 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, rstestPlugin }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, + reactPlugin.configs.recommended, + reactHooksPlugin.configs.recommended, + { + files: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + ...rstestPlugin.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..bdb4bd7c 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.2" } } diff --git a/packages/create-rstack/template-app-preact/rstack.config.js b/packages/create-rstack/template-app-preact/rstack.config.js index e2b64e47..9a562a82 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,15 @@ 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, rstestPlugin }) => [ + js.configs.recommended, + reactPlugin.configs.recommended, + reactHooksPlugin.configs.recommended, + { + files: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + ...rstestPlugin.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..7515f087 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.2", "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..0f9b5f64 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,16 @@ 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, rstestPlugin }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, + reactPlugin.configs.recommended, + reactHooksPlugin.configs.recommended, + { + files: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + ...rstestPlugin.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..e39bc21a 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.2" } } diff --git a/packages/create-rstack/template-app-react/rstack.config.js b/packages/create-rstack/template-app-react/rstack.config.js index ddd9f056..3c8c41e4 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,15 @@ 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, rstestPlugin }) => [ + js.configs.recommended, + reactPlugin.configs.recommended, + reactHooksPlugin.configs.recommended, + { + files: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + ...rstestPlugin.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..f9a52ef9 100644 --- a/packages/create-rstack/template-app-solid-ts/package.json +++ b/packages/create-rstack/template-app-solid-ts/package.json @@ -14,16 +14,16 @@ "test:watch": "rs test --watch" }, "dependencies": { - "solid-js": "^1.9.14" + "solid-js": "^1.9.15" }, "devDependencies": { - "@rsbuild/plugin-babel": "^2.0.1", + "@rsbuild/plugin-babel": "^2.1.0", "@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.2", "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..dd0c08d3 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,14 @@ 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, rstestPlugin }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, + { + files: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + ...rstestPlugin.configs.recommended, + }, +]); 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..f49d6aaa 100644 --- a/packages/create-rstack/template-app-solid/package.json +++ b/packages/create-rstack/template-app-solid/package.json @@ -14,14 +14,14 @@ "test:watch": "rs test --watch" }, "dependencies": { - "solid-js": "^1.9.14" + "solid-js": "^1.9.15" }, "devDependencies": { - "@rsbuild/plugin-babel": "^2.0.1", + "@rsbuild/plugin-babel": "^2.1.0", "@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.2" } } diff --git a/packages/create-rstack/template-app-solid/rstack.config.js b/packages/create-rstack/template-app-solid/rstack.config.js index 80b7b6f7..59a18fbd 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,13 @@ define.test({ setupFiles: ['./tests/rstest.setup.js'], }); -define.lint(async () => { - const { js } = await import('rstack/lint'); - - return [js.configs.recommended]; -}); +define.lint(({ js, rstestPlugin }) => [ + js.configs.recommended, + { + files: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + ...rstestPlugin.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..b1b11c6c 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.2", + "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..38217bbd 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,14 @@ 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, rstestPlugin }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, + { + files: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + ...rstestPlugin.configs.recommended, + }, +]); define.fmt({ plugins: ['prettier-plugin-svelte'], diff --git a/packages/create-rstack/template-app-svelte-ts/tests/tsconfig.json b/packages/create-rstack/template-app-svelte-ts/tests/tsconfig.json index 3eb5dc12..ef4bbdfc 100644 --- a/packages/create-rstack/template-app-svelte-ts/tests/tsconfig.json +++ b/packages/create-rstack/template-app-svelte-ts/tests/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../tsconfig.json", "compilerOptions": { - "types": ["rstack/types", "node", "@testing-library/jest-dom"] + "types": ["rstack/types", "node", "svelte", "@testing-library/jest-dom"] }, "include": ["./"] } diff --git a/packages/create-rstack/template-app-svelte/package.json b/packages/create-rstack/template-app-svelte/package.json index d3ef1e7c..49f33444 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.2" } } diff --git a/packages/create-rstack/template-app-svelte/rstack.config.js b/packages/create-rstack/template-app-svelte/rstack.config.js index 742edde8..539bb604 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,13 @@ define.test({ setupFiles: ['./tests/rstest.setup.js'], }); -define.lint(async () => { - const { js } = await import('rstack/lint'); - - return [js.configs.recommended]; -}); +define.lint(({ js, rstestPlugin }) => [ + js.configs.recommended, + { + files: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + ...rstestPlugin.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..589bf191 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.2", "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..836f85a1 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,14 @@ 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, rstestPlugin }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, + { + files: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + ...rstestPlugin.configs.recommended, + }, +]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-app-vanilla-ts/tests/dom.test.ts b/packages/create-rstack/template-app-vanilla-ts/tests/dom.test.ts index 45c267a8..181f4c43 100644 --- a/packages/create-rstack/template-app-vanilla-ts/tests/dom.test.ts +++ b/packages/create-rstack/template-app-vanilla-ts/tests/dom.test.ts @@ -1,7 +1,7 @@ import { expect, test } from 'rstack/test'; import { screen } from '@testing-library/dom'; -test('test dom', () => { +test('renders content', () => { document.body.innerHTML = `
Visible Example
diff --git a/packages/create-rstack/template-app-vanilla/package.json b/packages/create-rstack/template-app-vanilla/package.json index 32376dc9..299c97d1 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.2" } } diff --git a/packages/create-rstack/template-app-vanilla/rstack.config.js b/packages/create-rstack/template-app-vanilla/rstack.config.js index 23e75fdf..23b45439 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,13 @@ define.test({ setupFiles: ['./tests/rstest.setup.js'], }); -define.lint(async () => { - const { js } = await import('rstack/lint'); - - return [js.configs.recommended]; -}); +define.lint(({ js, rstestPlugin }) => [ + js.configs.recommended, + { + files: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + ...rstestPlugin.configs.recommended, + }, +]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-app-vanilla/tests/dom.test.js b/packages/create-rstack/template-app-vanilla/tests/dom.test.js index 45c267a8..181f4c43 100644 --- a/packages/create-rstack/template-app-vanilla/tests/dom.test.js +++ b/packages/create-rstack/template-app-vanilla/tests/dom.test.js @@ -1,7 +1,7 @@ import { expect, test } from 'rstack/test'; import { screen } from '@testing-library/dom'; -test('test dom', () => { +test('renders content', () => { document.body.innerHTML = `
Visible Example
diff --git a/packages/create-rstack/template-app-vue-ts/package.json b/packages/create-rstack/template-app-vue-ts/package.json index f7103c91..bf87341d 100644 --- a/packages/create-rstack/template-app-vue-ts/package.json +++ b/packages/create-rstack/template-app-vue-ts/package.json @@ -18,12 +18,12 @@ }, "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.2", "typescript": "^6.0.3", - "vue-tsc": "^3.3.9" + "vue-tsc": "^3.3.10" } } 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..1f44dbe6 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,14 @@ 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, rstestPlugin }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, + { + files: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + ...rstestPlugin.configs.recommended, + }, +]); 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..bcad8242 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.2" } } diff --git a/packages/create-rstack/template-app-vue/rstack.config.js b/packages/create-rstack/template-app-vue/rstack.config.js index b67b85e1..bfd7613f 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,13 @@ define.test({ setupFiles: ['./tests/rstest.setup.js'], }); -define.lint(async () => { - const { js } = await import('rstack/lint'); - - return [js.configs.recommended]; -}); +define.lint(({ js, rstestPlugin }) => [ + js.configs.recommended, + { + files: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + ...rstestPlugin.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..1465a69e 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.2", "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..926b4b36 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.2", "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..3fcccd5a 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.2", "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..ff3333eb 100644 --- a/packages/create-rstack/template-lib-node-ts/rstack.config.ts +++ b/packages/create-rstack/template-lib-node-ts/rstack.config.ts @@ -1,20 +1,18 @@ -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; define.lib({ - syntax: ['node 22'], 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, rstestPlugin }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, + { + files: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + ...rstestPlugin.configs.recommended, + }, +]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-lib-node-ts/tests/tsconfig.json b/packages/create-rstack/template-lib-node-ts/tests/tsconfig.json new file mode 100644 index 00000000..4845ee95 --- /dev/null +++ b/packages/create-rstack/template-lib-node-ts/tests/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "..", + "types": ["rstack/types", "node"] + }, + "include": ["./"] +} 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..5fcda629 100644 --- a/packages/create-rstack/template-lib-node/package.json +++ b/packages/create-rstack/template-lib-node/package.json @@ -3,11 +3,7 @@ "version": "0.0.0", "sideEffects": false, "type": "module", - "exports": { - ".": { - "default": "./dist/index.js" - } - }, + "exports": "./dist/index.js", "files": [ "dist", "README.md" @@ -22,7 +18,7 @@ "test:watch": "rs test --watch" }, "devDependencies": { - "rstack": "^0.5.2" + "rstack": "^0.6.2" }, "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..b97ea949 100644 --- a/packages/create-rstack/template-lib-node/rstack.config.js +++ b/packages/create-rstack/template-lib-node/rstack.config.js @@ -1,20 +1,16 @@ // @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.lib({}); -define.lint(async () => { - const { js } = await import('rstack/lint'); - - return [js.configs.recommended]; -}); +define.lint(({ js, rstestPlugin }) => [ + js.configs.recommended, + { + files: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + ...rstestPlugin.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..af6269d5 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.2", "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..e8a72cff 100644 --- a/packages/create-rstack/template-lib-react-ts/rstack.config.ts +++ b/packages/create-rstack/template-lib-react-ts/rstack.config.ts @@ -1,17 +1,11 @@ -// 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, - source: { - entry: { - index: ['./src/**'], - }, - }, output: { target: 'web', }, @@ -23,16 +17,16 @@ 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, rstestPlugin }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, + reactPlugin.configs.recommended, + reactHooksPlugin.configs.recommended, + { + files: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + ...rstestPlugin.configs.recommended, + }, +]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-lib-react-ts/tests/tsconfig.json b/packages/create-rstack/template-lib-react-ts/tests/tsconfig.json index b13d0ac4..fd11d809 100644 --- a/packages/create-rstack/template-lib-react-ts/tests/tsconfig.json +++ b/packages/create-rstack/template-lib-react-ts/tests/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../tsconfig.json", "compilerOptions": { "rootDir": "..", - "types": ["@testing-library/jest-dom"] + "types": ["rstack/types", "node", "@testing-library/jest-dom"] }, "include": ["./"] } diff --git a/packages/create-rstack/template-lib-react-ts/tsconfig.json b/packages/create-rstack/template-lib-react-ts/tsconfig.json index fd016f3d..d89a9952 100644 --- a/packages/create-rstack/template-lib-react-ts/tsconfig.json +++ b/packages/create-rstack/template-lib-react-ts/tsconfig.json @@ -5,7 +5,7 @@ "target": "ES2022", "noEmit": true, "skipLibCheck": true, - "types": ["rstack/types", "node"], + "types": ["rstack/types"], "useDefineForClassFields": true, "rootDir": "src", 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..45a32b13 100644 --- a/packages/create-rstack/template-lib-react/package.json +++ b/packages/create-rstack/template-lib-react/package.json @@ -2,11 +2,7 @@ "name": "rstack-lib-react", "version": "0.0.0", "type": "module", - "exports": { - ".": { - "default": "./dist/index.js" - } - }, + "exports": "./dist/index.js", "files": [ "dist", "README.md" @@ -23,13 +19,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.2" }, "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..dbcd1de1 100644 --- a/packages/create-rstack/template-lib-react/rstack.config.js +++ b/packages/create-rstack/template-lib-react/rstack.config.js @@ -1,17 +1,11 @@ // @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: { - entry: { - index: ['./src/**'], - }, - }, output: { target: 'web', }, @@ -23,15 +17,15 @@ 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, rstestPlugin }) => [ + js.configs.recommended, + reactPlugin.configs.recommended, + reactHooksPlugin.configs.recommended, + { + files: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + ...rstestPlugin.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..435fcae8 100644 --- a/packages/create-rstack/template-lib-solid-ts/package.json +++ b/packages/create-rstack/template-lib-solid-ts/package.json @@ -4,8 +4,8 @@ "type": "module", "exports": { ".": { - "solid": "./dist/index.jsx", "types": "./dist/index.d.ts", + "solid": "./dist/index.jsx", "default": "./dist/index.js" } }, @@ -24,14 +24,14 @@ "test:watch": "rs test --watch" }, "devDependencies": { - "@rsbuild/plugin-babel": "^2.0.1", + "@rsbuild/plugin-babel": "^2.1.0", "@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", - "solid-js": "^1.9.14", + "rstack": "^0.6.2", + "solid-js": "^1.9.15", "typescript": "^7.0.2" }, "peerDependencies": { 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..8e3b113c 100644 --- a/packages/create-rstack/template-lib-solid-ts/rstack.config.ts +++ b/packages/create-rstack/template-lib-solid-ts/rstack.config.ts @@ -1,15 +1,13 @@ -// 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: [ { id: 'compiled', - bundle: false, dts: true, plugins: [ pluginBabel({ @@ -20,7 +18,6 @@ define.lib(async () => { }, { id: 'source', - bundle: false, output: { filename: { js: '[name].jsx', @@ -49,11 +46,7 @@ define.lib(async () => { }, }, ], - source: { - entry: { - index: ['./src/**'], - }, - }, + bundle: false, output: { target: 'web', }, @@ -63,7 +56,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 +67,14 @@ define.test(async () => { }; }); -define.lint(async () => { - const { js, ts } = await import('rstack/lint'); - - return [js.configs.recommended, ts.configs.recommended]; -}); +define.lint(({ js, ts, rstestPlugin }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, + { + files: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + ...rstestPlugin.configs.recommended, + }, +]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-lib-solid-ts/tests/tsconfig.json b/packages/create-rstack/template-lib-solid-ts/tests/tsconfig.json index b13d0ac4..fd11d809 100644 --- a/packages/create-rstack/template-lib-solid-ts/tests/tsconfig.json +++ b/packages/create-rstack/template-lib-solid-ts/tests/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../tsconfig.json", "compilerOptions": { "rootDir": "..", - "types": ["@testing-library/jest-dom"] + "types": ["rstack/types", "node", "@testing-library/jest-dom"] }, "include": ["./"] } diff --git a/packages/create-rstack/template-lib-solid-ts/tsconfig.json b/packages/create-rstack/template-lib-solid-ts/tsconfig.json index c0b73036..b5e76e95 100644 --- a/packages/create-rstack/template-lib-solid-ts/tsconfig.json +++ b/packages/create-rstack/template-lib-solid-ts/tsconfig.json @@ -6,7 +6,7 @@ "target": "ES2022", "noEmit": true, "skipLibCheck": true, - "types": ["rstack/types", "node"], + "types": ["rstack/types"], "useDefineForClassFields": true, "rootDir": "src", 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..84a062de 100644 --- a/packages/create-rstack/template-lib-solid/package.json +++ b/packages/create-rstack/template-lib-solid/package.json @@ -22,13 +22,13 @@ "test:watch": "rs test --watch" }, "devDependencies": { - "@rsbuild/plugin-babel": "^2.0.1", + "@rsbuild/plugin-babel": "^2.1.0", "@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", - "solid-js": "^1.9.14" + "rstack": "^0.6.2", + "solid-js": "^1.9.15" }, "peerDependencies": { "solid-js": ">=1.8.0" diff --git a/packages/create-rstack/template-lib-solid/rstack.config.js b/packages/create-rstack/template-lib-solid/rstack.config.js index 75f1d03e..289b3390 100644 --- a/packages/create-rstack/template-lib-solid/rstack.config.js +++ b/packages/create-rstack/template-lib-solid/rstack.config.js @@ -1,16 +1,14 @@ // @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: [ { id: 'compiled', - bundle: false, plugins: [ pluginBabel({ include: /\.(?:jsx|tsx)$/, @@ -20,7 +18,6 @@ define.lib(async () => { }, { id: 'source', - bundle: false, output: { filename: { js: '[name].jsx', @@ -49,11 +46,7 @@ define.lib(async () => { }, }, ], - source: { - entry: { - index: ['./src/**'], - }, - }, + bundle: false, output: { target: 'web', }, @@ -63,7 +56,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 +67,13 @@ define.test(async () => { }; }); -define.lint(async () => { - const { js } = await import('rstack/lint'); - - return [js.configs.recommended]; -}); +define.lint(({ js, rstestPlugin }) => [ + js.configs.recommended, + { + files: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + ...rstestPlugin.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..e2a0ec2c 100644 --- a/packages/create-rstack/template-lib-svelte-ts/package.json +++ b/packages/create-rstack/template-lib-svelte-ts/package.json @@ -6,7 +6,8 @@ ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" - } + }, + "./style.css": "./dist/index.css" }, "types": "./dist/index.d.ts", "files": [ @@ -27,10 +28,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.2", + "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..2fb3b6f8 100644 --- a/packages/create-rstack/template-lib-svelte-ts/rstack.config.ts +++ b/packages/create-rstack/template-lib-svelte-ts/rstack.config.ts @@ -1,17 +1,10 @@ -// 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: { - entry: { - index: ['./src/**'], - }, - }, output: { target: 'web', }, @@ -19,15 +12,14 @@ 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, rstestPlugin }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, + { + files: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + ...rstestPlugin.configs.recommended, + }, +]); define.fmt({ plugins: ['prettier-plugin-svelte'], diff --git a/packages/create-rstack/template-lib-svelte-ts/tests/tsconfig.json b/packages/create-rstack/template-lib-svelte-ts/tests/tsconfig.json index 91484919..1c928a81 100644 --- a/packages/create-rstack/template-lib-svelte-ts/tests/tsconfig.json +++ b/packages/create-rstack/template-lib-svelte-ts/tests/tsconfig.json @@ -1,7 +1,9 @@ { "extends": "../tsconfig.json", "compilerOptions": { - "rootDir": ".." + "noEmit": true, + "rootDir": "..", + "types": ["rstack/types", "node", "svelte"] }, "include": ["./"] } diff --git a/packages/create-rstack/template-lib-svelte-ts/tsconfig.json b/packages/create-rstack/template-lib-svelte-ts/tsconfig.json index e69b0204..bc9a1615 100644 --- a/packages/create-rstack/template-lib-svelte-ts/tsconfig.json +++ b/packages/create-rstack/template-lib-svelte-ts/tsconfig.json @@ -4,13 +4,13 @@ "target": "ES2022", "declaration": true, "emitDeclarationOnly": true, - "isolatedModules": true, + "outDir": "dist", "skipLibCheck": true, - "types": ["rstack/types", "node", "svelte"], + "types": ["rstack/types", "svelte"], "useDefineForClassFields": true, + "rootDir": "src", /* modules */ - "module": "preserve", "moduleDetection": "force", "moduleResolution": "bundler", "verbatimModuleSyntax": true, @@ -21,5 +21,5 @@ "noUnusedLocals": true, "noUnusedParameters": true }, - "include": ["src", "scripts", "*.config.ts"] + "include": ["src"] } 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..2472ccb2 100644 --- a/packages/create-rstack/template-lib-svelte/package.json +++ b/packages/create-rstack/template-lib-svelte/package.json @@ -3,9 +3,8 @@ "version": "0.0.0", "type": "module", "exports": { - ".": { - "default": "./dist/index.js" - } + ".": "./dist/index.js", + "./style.css": "./dist/index.css" }, "files": [ "dist", @@ -24,8 +23,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.2", + "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..b0573e3b 100644 --- a/packages/create-rstack/template-lib-svelte/rstack.config.js +++ b/packages/create-rstack/template-lib-svelte/rstack.config.js @@ -1,17 +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 { pluginSvelte } = await import('@rsbuild/plugin-svelte'); - return { - bundle: false, - source: { - entry: { - index: ['./src/**'], - }, - }, output: { target: 'web', }, @@ -19,15 +12,13 @@ define.lib(async () => { }; }); -define.test({ - testEnvironment: 'happy-dom', -}); - -define.lint(async () => { - const { js } = await import('rstack/lint'); - - return [js.configs.recommended]; -}); +define.lint(({ js, rstestPlugin }) => [ + js.configs.recommended, + { + files: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + ...rstestPlugin.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..d6fe0aab 100644 --- a/packages/create-rstack/template-lib-vue-ts/package.json +++ b/packages/create-rstack/template-lib-vue-ts/package.json @@ -24,14 +24,14 @@ }, "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.2", "typescript": "^6.0.3", "vue": "^3.5.41", - "vue-tsc": "^3.3.9" + "vue-tsc": "^3.3.10" }, "peerDependencies": { "vue": ">=3.2.0" 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..5ad995e0 100644 --- a/packages/create-rstack/template-lib-vue-ts/rstack.config.ts +++ b/packages/create-rstack/template-lib-vue-ts/rstack.config.ts @@ -1,16 +1,10 @@ -// 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: { - entry: { - index: ['./src/**'], - }, - }, output: { target: 'web', }, @@ -22,11 +16,14 @@ 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, rstestPlugin }) => [ + js.configs.recommended, + ts.configs.recommendedTypeChecked, + { + files: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + ...rstestPlugin.configs.recommended, + }, +]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-lib-vue-ts/tests/index.test.ts b/packages/create-rstack/template-lib-vue-ts/tests/index.test.ts index 991849c2..078d9778 100644 --- a/packages/create-rstack/template-lib-vue-ts/tests/index.test.ts +++ b/packages/create-rstack/template-lib-vue-ts/tests/index.test.ts @@ -10,8 +10,9 @@ test('The button should have correct background color', () => { label: 'Demo Button', }, }); - expect(wrapper.get('button').element).toHaveStyle({ + const button = wrapper.get('button'); + expect(button.text()).toBe('Demo Button'); + expect(button.element).toHaveStyle({ backgroundColor: '#ccc', }); - wrapper.unmount(); }); diff --git a/packages/create-rstack/template-lib-vue-ts/tests/tsconfig.json b/packages/create-rstack/template-lib-vue-ts/tests/tsconfig.json index b13d0ac4..388de3e0 100644 --- a/packages/create-rstack/template-lib-vue-ts/tests/tsconfig.json +++ b/packages/create-rstack/template-lib-vue-ts/tests/tsconfig.json @@ -1,8 +1,9 @@ { "extends": "../tsconfig.json", "compilerOptions": { + "noEmit": true, "rootDir": "..", - "types": ["@testing-library/jest-dom"] + "types": ["rstack/types", "node", "@testing-library/jest-dom"] }, "include": ["./"] } diff --git a/packages/create-rstack/template-lib-vue-ts/tsconfig.json b/packages/create-rstack/template-lib-vue-ts/tsconfig.json index 9d608297..fa55998b 100644 --- a/packages/create-rstack/template-lib-vue-ts/tsconfig.json +++ b/packages/create-rstack/template-lib-vue-ts/tsconfig.json @@ -7,7 +7,7 @@ "emitDeclarationOnly": true, "outDir": "dist", "skipLibCheck": true, - "types": ["rstack/types", "node"], + "types": ["rstack/types"], "jsxImportSource": "vue", "useDefineForClassFields": true, "rootDir": "src", 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..2a7103f0 100644 --- a/packages/create-rstack/template-lib-vue/package.json +++ b/packages/create-rstack/template-lib-vue/package.json @@ -2,11 +2,7 @@ "name": "rstack-lib-vue", "version": "0.0.0", "type": "module", - "exports": { - ".": { - "default": "./dist/index.js" - } - }, + "exports": "./dist/index.js", "files": [ "dist", "README.md" @@ -22,10 +18,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.2", "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..76c26263 100644 --- a/packages/create-rstack/template-lib-vue/rstack.config.js +++ b/packages/create-rstack/template-lib-vue/rstack.config.js @@ -1,17 +1,11 @@ // @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: { - entry: { - index: ['./src/**'], - }, - }, output: { target: 'web', }, @@ -23,11 +17,13 @@ define.test({ setupFiles: ['./tests/rstest.setup.js'], }); -define.lint(async () => { - const { js } = await import('rstack/lint'); - - return [js.configs.recommended]; -}); +define.lint(({ js, rstestPlugin }) => [ + js.configs.recommended, + { + files: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'], + ...rstestPlugin.configs.recommended, + }, +]); define.fmt({ singleQuote: true, diff --git a/packages/create-rstack/template-lib-vue/tests/index.test.js b/packages/create-rstack/template-lib-vue/tests/index.test.js index 991849c2..078d9778 100644 --- a/packages/create-rstack/template-lib-vue/tests/index.test.js +++ b/packages/create-rstack/template-lib-vue/tests/index.test.js @@ -10,8 +10,9 @@ test('The button should have correct background color', () => { label: 'Demo Button', }, }); - expect(wrapper.get('button').element).toHaveStyle({ + const button = wrapper.get('button'); + expect(button.text()).toBe('Demo Button'); + expect(button.element).toHaveStyle({ backgroundColor: '#ccc', }); - wrapper.unmount(); }); 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..8b3d2189 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.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.6.2 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.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.6.2 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.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.6.2 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.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.6.2 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.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.6.2 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.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.6.2 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.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.6.2 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.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.6.2 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.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.6.2 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.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.6.2 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.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.6.2 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.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.6.2 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.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.6.2 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.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.6.2 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.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.6.2 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.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.6.2 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.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.6.2 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.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.6.2 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.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.6.2 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.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.6.2 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.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.6.2 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.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.6.2 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.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.6.2 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.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.6.2 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.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.6.2 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.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.6.2 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.2') { + throw new Error(`WASI binding package version mismatch, expected 0.6.2 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..b7a85a00 100644 --- a/packages/rstack/package.json +++ b/packages/rstack/package.json @@ -1,6 +1,6 @@ { "name": "rstack", - "version": "0.5.2", + "version": "0.6.2", "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..891f67d5 100644 --- a/packages/rstack/src/cli/commands.ts +++ b/packages/rstack/src/cli/commands.ts @@ -1,4 +1,5 @@ import { join, resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; import { getConfigState } from '../config.ts'; import { insertConfigArg, parseArgs, parseCliArgs } from './args.ts'; import { hasHelpFlag, printCommandHelp } from './help.ts'; @@ -18,7 +19,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 +51,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 +79,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 +96,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 +118,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 { @@ -120,6 +139,8 @@ async function runRspressCLI(args: string[]): Promise { } } +const RSLINT_CONFIG_PATH = join(import.meta.dirname, 'rslintConfig.js'); + async function runRslintCLI(args: string[]): Promise { if (hasHelpFlag(args)) { return printCommandHelp('lint'); @@ -128,7 +149,7 @@ async function runRslintCLI(args: string[]): Promise { const argv = [ process.execPath, 'rslint', - ...insertConfigArg(args, '--config', join(import.meta.dirname, 'rslintConfig.js')), + ...insertConfigArg(args, '--config', RSLINT_CONFIG_PATH), ]; const { runCLI } = await import('@rslint/core'); @@ -155,11 +176,16 @@ async function runCheckCLI(args: string[]): Promise { return; } + // Rslint loads its one-shot config through Node's module cache. Import the + // same URL to read the Rstack config exported for the following fmt phase. + const { loadedConfig } = (await import( + pathToFileURL(RSLINT_CONFIG_PATH).href + )) as typeof import('../rslintConfig.ts'); const { runFmtCLI } = await import( /* rspackChunkName: 'fmt' */ '../fmt/cli.ts' ); - await runFmtCLI(['--check']); + await runFmtCLI(['--check'], { loadedConfig }); } export async function setupCommands(): Promise { @@ -171,7 +197,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..0eb4e626 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -3,7 +3,7 @@ import { performance } from 'node:perf_hooks'; import { color, logger } from 'rslog'; import { parseArgs } from '../cli/args.ts'; import { printCommandHelp } from '../cli/help.ts'; -import { loadRstackConfig } from '../config.ts'; +import { loadRstackConfig, type LoadedRstackConfig } from '../config.ts'; import { ensureProjectCacheDir } from '../projectCache.ts'; import { fmtCacheFileName } from './cacheStore.ts'; import { resolveFmtConfig } from './config.ts'; @@ -29,21 +29,36 @@ interface ParsedFmtCLIArgs { lsp: boolean; } +type RunFmtCLIOptions = { + /** Rstack config already loaded by the lint phase of `rs check`. */ + loadedConfig?: LoadedRstackConfig; +}; + const parseMaxWorkers = (value: string | undefined): number | undefined => { if (value === undefined) { return 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 +97,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 +147,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 +176,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 +194,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; }; @@ -235,8 +260,12 @@ const logFmtResult = ( } }; -const loadFmtConfig = async (cwd: string): Promise => { - const { configs, filePath } = await loadRstackConfig({ cwd }); +const loadFmtConfig = async ( + cwd: string, + loadedConfig?: LoadedRstackConfig, +): Promise => { + const { configs, filePath } = + loadedConfig ?? (await loadRstackConfig({ cwd })); return resolveFmtConfig({ definition: configs.fmt, @@ -245,7 +274,10 @@ const loadFmtConfig = async (cwd: string): Promise => { }); }; -const runFmtCLI = async (args: string[]): Promise => { +const runFmtCLI = async ( + args: string[], + { loadedConfig }: RunFmtCLIOptions = {}, +): Promise => { const cwd = process.cwd(); const startTime = performance.now(); @@ -303,7 +335,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 @@ -315,7 +349,7 @@ const runFmtCLI = async (args: string[]): Promise => { } } - const config = await loadFmtConfig(cwd); + const config = await loadFmtConfig(cwd, loadedConfig); const files = await discoverFmtFiles({ cwd, patterns, @@ -327,7 +361,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..c7ebbd8a 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'; @@ -35,7 +39,7 @@ interface RunCache { hashOptions: ReturnType; } -interface FmtWorkerPoolResult { +interface FmtFilesResult { files: FmtFileResult[]; processedFileCount: number; } @@ -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) @@ -183,13 +193,38 @@ const runPriorityTasks = async ( return results; }; -/** Processes files in a worker pool while preserving input order. */ -const runWithWorkers = async ( +/** Collects per-file outcomes while preserving cache and processed-count semantics. */ +const collectFmtResults = ( + results: FmtFileRun[], + cache?: RunCache, +): FmtFilesResult => { + const processedFiles: FmtFileResult[] = []; + let processedFileCount = 0; + + for (const { outcome, key, entry } of results) { + if (key !== undefined && entry) { + cache?.store.set(key, entry); + } + if (outcome === 'unsupported') { + continue; + } + + processedFileCount++; + if (outcome !== 'unchanged') { + processedFiles.push(outcome); + } + } + + return { files: processedFiles, processedFileCount }; +}; + +/** Processes one pending file locally and multiple pending files in a worker pool. */ +const runFmtTasks = async ( files: FmtFileRequest[], shouldWrite: boolean, maxWorkers?: number, cache?: RunCache, -): Promise => { +): Promise => { const tasks = files.map((file) => createRunTask(file, cache)); const pendingFileCount = tasks.reduce( (count, task) => count + (isCachedUnsupported(task) ? 0 : 1), @@ -199,6 +234,19 @@ const runWithWorkers = async ( return { files: [], processedFileCount: 0 }; } + // One pending file cannot benefit from parallelism, so avoid worker startup and IPC overhead. + if (pendingFileCount === 1) { + const { formatFile } = await import('./worker.ts'); + const formatFileOnMainThread: FormatFile = (file, write, fileCache) => + formatFile({ file, shouldWrite: write, cache: fileCache }); + const results = await Promise.all( + tasks.map((task) => + runFmtFile(task, shouldWrite, formatFileOnMainThread), + ), + ); + return collectFmtResults(results, cache); + } + const { createWorkerPool } = await import('./workerPool.ts'); const workerPool = await createWorkerPool(pendingFileCount, maxWorkers); @@ -207,26 +255,11 @@ 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; - - for (const { outcome, key, entry } of results) { - if (key !== undefined && entry) { - cache?.store.set(key, entry); - } - if (outcome === 'unsupported') { - continue; - } - - processedFileCount++; - if (outcome !== 'unchanged') { - processedFiles.push(outcome); - } - } - - return { files: processedFiles, processedFileCount }; + return collectFmtResults(results, cache); } finally { await workerPool.terminate(); } @@ -272,12 +305,15 @@ const runFmtFiles = async ({ const result = files.length === 0 ? { files: [], processedFileCount: 0 } - : await runWithWorkers(files, shouldWrite, maxWorkers, runCache); + : await runFmtTasks(files, shouldWrite, maxWorkers, runCache); await runCache?.store.save().catch(() => false); 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..40d7a764 100644 --- a/packages/rstack/src/fmt/worker.ts +++ b/packages/rstack/src/fmt/worker.ts @@ -12,11 +12,12 @@ 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 - * scheduling overhead. This prioritizes throughput over crash-safe replacement. + * Synchronous file I/O avoids libuv scheduling overhead in workers and single-file + * main-thread runs. This favors throughput over crash-safe file replacement. */ const formatFile = async ({ file, @@ -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..f704684d 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; } @@ -248,6 +260,29 @@ const isTypeCastComment = (comment: PrettierComment): boolean => comment.value.startsWith('*') && /@(?:type|satisfies)\b/.test(comment.value); +/** + * Returns the greatest value less than or equal to `target` from an ascending + * array, or `undefined` when no such value exists. + */ +const findLastAtOrBefore = ( + sortedValues: number[], + target: number, +): number | undefined => { + let lower = 0; + let upper = sortedValues.length; + + while (lower < upper) { + const middle = lower + Math.floor((upper - lower) / 2); + if (sortedValues[middle] <= target) { + lower = middle + 1; + } else { + upper = middle; + } + } + + return sortedValues[lower - 1]; +}; + type VisitOptions = { onEnter?: (node: AstNode) => AstNode | undefined; onLeave?: (node: AstNode) => AstNode | undefined; @@ -287,7 +322,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 => { @@ -347,11 +385,15 @@ const postprocess = ( const expression = asAstNode(node.expression); const start = locStart(node); + // Yuku comments are in source order, so these end offsets are sorted. typeCastCommentEnds ??= comments .filter(isTypeCastComment) .map((comment) => locEnd(comment)); - const previousCommentEnd = typeCastCommentEnds.findLast((end) => end <= start); + const previousCommentEnd = findLastAtOrBefore( + typeCastCommentEnds, + start, + ); const shouldKeepParentheses = previousCommentEnd !== undefined && text.slice(previousCommentEnd, start).trim().length === 0; @@ -402,12 +444,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 +470,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 +510,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 +546,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 +606,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..2a0cb25f 100644 --- a/packages/rstack/src/rslintConfig.ts +++ b/packages/rstack/src/rslintConfig.ts @@ -1,16 +1,19 @@ -import { loadRstackConfig } from './config.ts'; +import { loadRstackConfig, type LoadedRstackConfig } from './config.ts'; import type { RslintConfig } from '@rslint/core'; -const { configs } = await loadRstackConfig(); -const lintExports = configs.lint ?? []; +// Expose the loaded config so `rs check` can pass it to fmt instead of loading +// and executing the Rstack config a second time. +export const loadedConfig: LoadedRstackConfig = await loadRstackConfig(); +const { configs } = loadedConfig; +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..4f1aeba5 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({ @@ -59,9 +68,13 @@ 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 () => { +test('rechecks a path-only unsupported entry on the main thread', async () => { await withTempProject(async (rootPath) => { - const { cache, file } = await createCachedUnsupportedFile(rootPath, 'script'); + const { cache, file } = await createCachedUnsupportedFile( + rootPath, + 'script', + ); + writeProjectFile(rootPath, 'script', '#!/usr/bin/env node\nconst value=1'); await expect( runFmtFiles({ @@ -69,7 +82,11 @@ test('starts the worker pool for a path-only unsupported entry without an extens mode: 'check', cache, }), - ).rejects.toThrow('worker startup failed'); - expect(mocks.workerPoolCalls).toEqual([[1, undefined]]); + ).resolves.toEqual({ + exitCode: 1, + files: [{ path: file.path, status: 'different' }], + processedFileCount: 1, + }); + expect(mocks.workerPoolCalls).toEqual([]); }); }); 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..136751c0 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,11 +20,11 @@ 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 + specifier: ~0.8.1 + version: 0.8.1 '@rspress/core': specifier: ^2.0.19 version: 2.0.19 @@ -35,8 +35,8 @@ catalogs: specifier: ^2.0.19 version: 2.0.19 '@rstack-dev/doc-ui': - specifier: 1.14.7 - version: 1.14.7 + specifier: 1.14.8 + version: 1.14.8 '@rstackjs/create-toolkit': specifier: 2.2.3 version: 2.2.3 @@ -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 @@ -85,9 +85,6 @@ catalogs: fast-json-stable-stringify: specifier: 2.1.0 version: 2.1.0 - globals: - specifier: ^17.7.0 - version: 17.9.0 happy-dom: specifier: ^20.11.2 version: 20.11.2 @@ -131,8 +128,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 +146,8 @@ catalogs: specifier: 1.0.12 version: 1.0.12 yuku-parser: - specifier: 0.8.4 - version: 0.8.4 + specifier: 0.9.0 + version: 0.9.0 importers: @@ -162,9 +159,6 @@ importers: cspell-ban-words: specifier: 'catalog:' version: 0.0.4 - globals: - specifier: 'catalog:' - version: 17.9.0 heading-case: specifier: 'catalog:' version: 1.1.5 @@ -189,13 +183,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 +216,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 +271,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 +303,7 @@ importers: specifier: 'catalog:' version: 7.0.2 - examples/rstest-inline-projects: + examples/test-inline-projects: dependencies: react: specifier: 'catalog:' @@ -320,7 +314,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 +360,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 + version: 0.8.1 '@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 +378,11 @@ importers: version: 2.1.0 yuku-parser: specifier: 'catalog:' - version: 0.8.4 + version: 0.9.0 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 +394,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 +421,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 +430,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 @@ -466,10 +460,10 @@ importers: version: 2.0.19(@rspress/core@2.0.19) '@rstack-dev/doc-ui': specifier: 'catalog:' - version: 1.14.7(@rspress/core@2.0.19) + version: 1.14.8(@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 +760,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 +1275,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 +1311,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: @@ -1304,8 +1324,8 @@ packages: typescript: optional: true - '@rslint/core@0.8.0': - resolution: {integrity: sha512-MfMC6lxiXoKPWsYRu9fuAxWA9mCD/I4E5Aa6Sl20a6q5w8r2C12fJM+bceC01AMGYuvWygKHqS3QBJdJHjniRw==} + '@rslint/core@0.8.1': + resolution: {integrity: sha512-cqpDcgJ8TgBC+I/OZkICze0sSNcsdjtxNCv01fQTYvgIvBkwbhNlg4celD3XEPfA8B2B2H/woYgeaX0IPEeTMA==} hasBin: true peerDependencies: jiti: ^2.7.0 @@ -1313,118 +1333,210 @@ packages: jiti: optional: true - '@rslint/native-darwin-arm64@0.8.0': - resolution: {integrity: sha512-Bo6kXL1/TkjVUl6maZ3Sw+JEnZUkgpUD35v06jyBTcjpxlXE6yqFj66NAzB/G2CVu220ADp/FEE7l2Kocrefhg==} + '@rslint/native-darwin-arm64@0.8.1': + resolution: {integrity: sha512-HlucYn3RLELMHRjlvKcr0vPlE/2RUs6BVn5ClRKFcLShON4j+q8NWaIPHwY5zrQjqq7GJxX3b/7G9skU+TrQ2w==} cpu: [arm64] os: [darwin] - '@rslint/native-darwin-x64@0.8.0': - resolution: {integrity: sha512-F5pabdH7dluxoj7PGuQjPYuoNU+yxnctcJzqrb/CZ122FLP3uJPrbiLufh+ZF9e5ui700rNyb7macJqnHlBV2w==} + '@rslint/native-darwin-x64@0.8.1': + resolution: {integrity: sha512-HV4FOwq3ofuoMkUxjEyGvaOGyr79OzFhAIpMcT9uOO6BxEA/PndepwBAPmkBt33aewm2oPKM2khsC582XTsxew==} cpu: [x64] os: [darwin] - '@rslint/native-linux-arm64-gnu@0.8.0': - resolution: {integrity: sha512-SSgSjyaeXI032Wk8bq6VmkLQTWOEqHZH5MWAk3831pd/G2rWr2XcoDi5Wf6w0o2rSCeYzkYbIejOePYwIKT4Lg==} + '@rslint/native-linux-arm64-gnu@0.8.1': + resolution: {integrity: sha512-O9kvvw2O7WAzeBAp7KSfXVbyl4Q4hAY1rUSRpeJp9hOY3yQUipE98qF29QicbXTRsoZZooMTmrjV/0EiYGL8OA==} cpu: [arm64] os: [linux] libc: [glibc] - '@rslint/native-linux-arm64-musl@0.8.0': - resolution: {integrity: sha512-50VDZQFAc9kp6rbOUOjyppVjC3d3AdbOJyA6JxlhK3mp8a/8sPIqWG0BlfcN/7ZpwdZrFsQt2Ds+0yMXITfu5A==} + '@rslint/native-linux-arm64-musl@0.8.1': + resolution: {integrity: sha512-I92YjEMPcdeXz29rGuWR7kKjlek3LG/kcLHaBop6ucYkAXbuwdCwALM/JKxjcnRjoEZA3qR4QCRu92Llbzh+Ig==} cpu: [arm64] os: [linux] libc: [musl] - '@rslint/native-linux-x64-gnu@0.8.0': - resolution: {integrity: sha512-wfc/UfnuTBAofwLPK5MLB2Hus1YYnsxP2MVchp380fUvMfQuGFPzz1WOAUY66zqrsrDKpUJsh3V1bx7c8DTlJA==} + '@rslint/native-linux-x64-gnu@0.8.1': + resolution: {integrity: sha512-9uh4XygsKs2lj7JWM2yi92qjrgrXM7slppAgTQnFfkiRoKASVt4M2anSiJfs2v5hU3UhsmVotXAOYtWoV5Hd3g==} cpu: [x64] os: [linux] libc: [glibc] - '@rslint/native-linux-x64-musl@0.8.0': - resolution: {integrity: sha512-MTYcAMz6IZWb6ZmLo12pakCBA8mS3EW9cOI0jd3At6vs0Yia9YbeOAUiWbIC4Z9yyp75b8ZQCQMRSnY6rDnbaA==} + '@rslint/native-linux-x64-musl@0.8.1': + resolution: {integrity: sha512-pcWlxs9ZLaETAoDaYcO6f8jeuE+nZliIVrhCJ1eWX+aTB+WPKHVHK6dFMPjxqsvUtwbK1zfpTeo8rOaeGRrSfQ==} cpu: [x64] os: [linux] libc: [musl] - '@rslint/native-win32-arm64-msvc@0.8.0': - resolution: {integrity: sha512-hids2jgNWBxZf2mSBLZJxubWRLWlJapEfeJHVokzjpRpBZZNv7O06enoyXSb4Lswff9WaHssIqu1sfZIc9lC2g==} + '@rslint/native-win32-arm64-msvc@0.8.1': + resolution: {integrity: sha512-GGRcKGBOQs7WsxSfXWRQnY15nRyikaDajWoefhKnYVj+AMg6BEWWsdH3Q2xOOuVMGNCC/SoXUAGb9cGpV7Smdg==} cpu: [arm64] os: [win32] - '@rslint/native-win32-x64-msvc@0.8.0': - resolution: {integrity: sha512-bun7uURKl6NdChwmw/i2mk3Yj9klZQyh1uRbIQG0RDEJ9oTIbU5M3c7K5sd/Rukat0TVCGgbBAzR+YB0DAXPZQ==} + '@rslint/native-win32-x64-msvc@0.8.1': + resolution: {integrity: sha512-rokvuC3MKTNMbGU5eh/p6a715xzl1yZvjh9gQByNPKGHrF3OUmqN1c6QGCtlNU8HGHekvMS9dUNzjTCoMenZjQ==} 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} @@ -1466,8 +1578,8 @@ packages: '@rspress/shared@2.0.19': resolution: {integrity: sha512-INrETllWuR49lksqCz+xeLSO6rKtHA+5Ix2YQWcP9nerCuTSyGYwH8eBC0HrIacJkGHxB8wkkbV99tyzGtY8Wg==} - '@rstack-dev/doc-ui@1.14.7': - resolution: {integrity: sha512-PnY2JKleZxQTbSrxRAyG84lBKNwXwB+v05sUOFxF5qD1UPmBn/bzRmL4zNOPq1qG4d0PjTG62weetryub40M2A==} + '@rstack-dev/doc-ui@1.14.8': + resolution: {integrity: sha512-CAibkpvCnJEcxsypu3ZE9JjtQw+1+iJGv9X09Yg4//9FevHpfaHW+b2onFG+Z/xOlUXGg83rJTb7CO6YbToP0w==} peerDependencies: '@rspress/core': '>=2.0.0' peerDependenciesMeta: @@ -1489,14 +1601,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 +1617,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 +1634,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 +1654,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 +1666,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 +1693,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 +1911,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.9.0': + resolution: {integrity: sha512-Wm/kEWpUkB1etH6N8+4Rcv3dlYO5asapNlaOEQBqsZc2LAl0TrCYMrx+pQTV+AmQE+JDbSYLzFjtHfLLvJ4W1Q==} 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.9.0': + resolution: {integrity: sha512-6OAkwsfjIwLE8f/Seu61MjoK3+WHl2KfdkUFxBq2kuz2+H1e1CFFRL5SXQb2WV1BA8kVbl6JFFsS48XJcUzxOQ==} cpu: [arm64] os: [darwin] - '@yuku-parser/binding-darwin-x64@0.8.4': - resolution: {integrity: sha512-CjZuMoXnL5XUkVpDqh4WDPwpAw8CwmtHHnTerGkS45So/sNuwkXdyIAEqqIZfaLopi5W/V9NApAT2md9XizjsQ==} + '@yuku-parser/binding-darwin-x64@0.9.0': + resolution: {integrity: sha512-TIR+Tkm56bDPl5U1QKR3NaAt5Rrr9TmvEX6/j4Nz1bGgWBcRkaWaPNm5ear+M+obMQqNsKhCGSUtpE0wTKzv1w==} cpu: [x64] os: [darwin] - '@yuku-parser/binding-freebsd-x64@0.8.4': - resolution: {integrity: sha512-ibLKORdz71iI4Vs+fyFgvwQ51P5XcxJIyQLa8cSEWqwptRdo+BTcZHIQEcZnFDPUuvmJ19RRH9CoiJdfhr7pZw==} + '@yuku-parser/binding-freebsd-x64@0.9.0': + resolution: {integrity: sha512-KWUk89CD+ldIDl5Wa0cVEq5o/PY3MtcfrzEzy2yYFbezzaE/J7k9ioOBof7YwFSzPGDWgt2WZwC/DufwjSIifA==} 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.9.0': + resolution: {integrity: sha512-T3xhIVqrjeOn+rEURRZnUg+T34wvB6hCKWg+rIhkAE14pjnCQ/8oUussDk68tGBdq4pZUPhtksw0D2As7xIq1Q==} 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.9.0': + resolution: {integrity: sha512-g3EEh5tYUqhhrcJr+VLjHAKu91PlIq1WwWnlcy9UTVd9TjGX0ZZ8QA4HeYeEb/WwjAscITXF7viHMLHEkqVbzw==} 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.9.0': + resolution: {integrity: sha512-/iubliwkPbZPMVV/REE+MjZ0O55RzJs1Qt8eTDWfqsGefJzCqbBEN2F63KP/Oryu6ZJaPLQ/VSYvPFjTeXfV9w==} 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.9.0': + resolution: {integrity: sha512-F1hgfSXcPEl3F6fDi6f5arPfm3wrTitVHWWCVf9VUP33ZGlxeFfbc+8qDRARHnNT48OwIFmgrGhm13WmKFRWOw==} 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.9.0': + resolution: {integrity: sha512-FNLAkP2WB/M3mnnG18f6Bm+CZtId6377g2tcQk79zDeGP2yssaQyXOFKji5lRqop/AVoZWNo8m45zPxxoEsJbw==} 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.9.0': + resolution: {integrity: sha512-q4fxNn9dZWbKRl79NuekdXNNGz58gu2uy3GjJmghDghDPVukEGhaNqNjYN5XZq7mo7L6EDj5IcUtyMPQlhTWqg==} 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.9.0': + resolution: {integrity: sha512-tVzfdCycadoBrtONw9O2skhjGozQDJKHt/PLfH7pf5jVsCpi5OpWI4ZKNxQKhPI91CqUCqR2kVtXebFML0c7Zw==} 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.9.0': + resolution: {integrity: sha512-mVXePNGj/N2MMQvtks3wE53QCpg0D2I6SpFwmVHT5gsAdOvlK2onLyFOkNCpIWDjpEu749z3QQZzBeIymkLsGA==} cpu: [x64] os: [win32] - '@yuku-toolchain/types@0.8.4': - resolution: {integrity: sha512-p7JE8flrj7ijZ/qLjHi4UwKqMarMD6zumbKXhrjp2I2iLJOuTYiQyci2U36VlXcUlNyzsY7E/mLnKCHotbzJVw==} + '@yuku-toolchain/types@0.9.0': + resolution: {integrity: sha512-ta11OgDlY5ESPaO4mer9on6BMYtbQc5itQB+JYxQ7DWVZBgxgJmQkMTUlSDddBg0ueYOpxBrp9vLjkZlhQfClA==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -2044,14 +2160,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,10 +2255,6 @@ packages: git-hooks-list@4.2.1: resolution: {integrity: sha512-WNvqJjOxxs/8ZP9+DWdwWJ7cDsd60NHf39XnD82pDVrKO5q7xfPqpkK6hwEAmBa/ZSEE4IOoR75EzbbIuwGlMw==} - globals@17.9.0: - resolution: {integrity: sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==} - engines: {node: '>=18'} - happy-dom@20.11.2: resolution: {integrity: sha512-7MB+bJLkxu3SowAfBJbjW+c55kNz5tkR45gu2qzrxznezhLeN5YIlJbwUgSzlGc+qWoZ8Ykg71H5ezz69xixrw==} engines: {node: '>=20.0.0'} @@ -2715,8 +2819,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 +3039,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 +3181,11 @@ packages: engines: {node: '>= 14.6'} hasBin: true - yuku-ast@0.8.4: - resolution: {integrity: sha512-s7EWfWIQkaGmsGnyr/BU0jli9YTN5TvrKIsSmALyRD9elumDQInuhv0BrVObENKVCxr9W3Ikmnx5u02KvfuUmw==} + yuku-ast@0.9.0: + resolution: {integrity: sha512-+cfaDdcTQuOH5EnfxPvcB3ju4fzWYxZVpbYjKUJ/LMkrfBnuajiY9OcZBwfugKtk+mOTsTLIX87+Vth1zm7r/w==} - yuku-parser@0.8.4: - resolution: {integrity: sha512-sw41wouvT5rUmLIp87hmvm5vtF+MRSI3x6yjq6xqpYmtkQj+Ht6N7xRQ8lMhLv8N7JAzughGj0Rfi0jQRSu9HQ==} + yuku-parser@0.9.0: + resolution: {integrity: sha512-B8azpS7EuyRyNBRYHPhMiBDsvlI01mxDapQqkEjd3QjTdx6d+io9xiAx52xl0IIHV53GPseo39LUlsbKK/CS6g==} zimmerframe@1.1.4: resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} @@ -3366,7 +3470,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 +3478,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 +3499,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 +3872,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.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.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.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,77 +3923,114 @@ 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: - '@module-federation/runtime-tools' - core-js - '@rslint/core@0.8.0': + '@rslint/core@0.8.1': dependencies: picomatch: 4.0.5 optionalDependencies: - '@rslint/native-darwin-arm64': 0.8.0 - '@rslint/native-darwin-x64': 0.8.0 - '@rslint/native-linux-arm64-gnu': 0.8.0 - '@rslint/native-linux-arm64-musl': 0.8.0 - '@rslint/native-linux-x64-gnu': 0.8.0 - '@rslint/native-linux-x64-musl': 0.8.0 - '@rslint/native-win32-arm64-msvc': 0.8.0 - '@rslint/native-win32-x64-msvc': 0.8.0 + '@rslint/native-darwin-arm64': 0.8.1 + '@rslint/native-darwin-x64': 0.8.1 + '@rslint/native-linux-arm64-gnu': 0.8.1 + '@rslint/native-linux-arm64-musl': 0.8.1 + '@rslint/native-linux-x64-gnu': 0.8.1 + '@rslint/native-linux-x64-musl': 0.8.1 + '@rslint/native-win32-arm64-msvc': 0.8.1 + '@rslint/native-win32-x64-msvc': 0.8.1 + + '@rslint/native-darwin-arm64@0.8.1': + optional: true - '@rslint/native-darwin-arm64@0.8.0': + '@rslint/native-darwin-x64@0.8.1': optional: true - '@rslint/native-darwin-x64@0.8.0': + '@rslint/native-linux-arm64-gnu@0.8.1': optional: true - '@rslint/native-linux-arm64-gnu@0.8.0': + '@rslint/native-linux-arm64-musl@0.8.1': optional: true - '@rslint/native-linux-arm64-musl@0.8.0': + '@rslint/native-linux-x64-gnu@0.8.1': optional: true - '@rslint/native-linux-x64-gnu@0.8.0': + '@rslint/native-linux-x64-musl@0.8.1': optional: true - '@rslint/native-linux-x64-musl@0.8.0': + '@rslint/native-win32-arm64-msvc@0.8.1': optional: true - '@rslint/native-win32-arm64-msvc@0.8.0': + '@rslint/native-win32-x64-msvc@0.8.1': optional: true - '@rslint/native-win32-x64-msvc@0.8.0': + '@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 +4038,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 +4088,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 +4164,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) @@ -3968,7 +4174,7 @@ snapshots: - core-js - supports-color - '@rstack-dev/doc-ui@1.14.7(@rspress/core@2.0.19)': + '@rstack-dev/doc-ui@1.14.8(@rspress/core@2.0.19)': optionalDependencies: '@rspress/core': 2.0.19(micromark-util-types@2.0.2)(micromark@4.0.2)(supports-color@8.1.1) @@ -3978,21 +4184,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 +4214,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 +4243,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 +4262,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 +4298,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 +4451,43 @@ snapshots: react: 19.2.8 unhead: 2.1.16 - '@yuku-parser/binding-android-arm64@0.8.4': + '@yuku-parser/binding-android-arm64@0.9.0': optional: true - '@yuku-parser/binding-darwin-arm64@0.8.4': + '@yuku-parser/binding-darwin-arm64@0.9.0': optional: true - '@yuku-parser/binding-darwin-x64@0.8.4': + '@yuku-parser/binding-darwin-x64@0.9.0': optional: true - '@yuku-parser/binding-freebsd-x64@0.8.4': + '@yuku-parser/binding-freebsd-x64@0.9.0': optional: true - '@yuku-parser/binding-linux-arm-gnu@0.8.4': + '@yuku-parser/binding-linux-arm-gnu@0.9.0': optional: true - '@yuku-parser/binding-linux-arm-musl@0.8.4': + '@yuku-parser/binding-linux-arm-musl@0.9.0': optional: true - '@yuku-parser/binding-linux-arm64-gnu@0.8.4': + '@yuku-parser/binding-linux-arm64-gnu@0.9.0': optional: true - '@yuku-parser/binding-linux-arm64-musl@0.8.4': + '@yuku-parser/binding-linux-arm64-musl@0.9.0': optional: true - '@yuku-parser/binding-linux-x64-gnu@0.8.4': + '@yuku-parser/binding-linux-x64-gnu@0.9.0': optional: true - '@yuku-parser/binding-linux-x64-musl@0.8.4': + '@yuku-parser/binding-linux-x64-musl@0.9.0': optional: true - '@yuku-parser/binding-win32-arm64@0.8.4': + '@yuku-parser/binding-win32-arm64@0.9.0': optional: true - '@yuku-parser/binding-win32-x64@0.8.4': + '@yuku-parser/binding-win32-x64@0.9.0': optional: true - '@yuku-toolchain/types@0.8.4': {} + '@yuku-toolchain/types@0.9.0': {} acorn-jsx@5.3.2(acorn@8.17.0): dependencies: @@ -4407,10 +4613,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,8 +4704,6 @@ snapshots: git-hooks-list@4.2.1: {} - globals@17.9.0: {} - happy-dom@20.11.2: dependencies: '@types/node': 24.13.3 @@ -5265,10 +5465,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 +5659,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 +5842,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 +6011,27 @@ snapshots: yaml@2.9.0: optional: true - yuku-ast@0.8.4: + yuku-ast@0.9.0: dependencies: - '@yuku-toolchain/types': 0.8.4 + '@yuku-toolchain/types': 0.9.0 - yuku-parser@0.8.4: + yuku-parser@0.9.0: dependencies: - '@yuku-toolchain/types': 0.8.4 - yuku-ast: 0.8.4 + '@yuku-toolchain/types': 0.9.0 + yuku-ast: 0.9.0 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.9.0 + '@yuku-parser/binding-darwin-arm64': 0.9.0 + '@yuku-parser/binding-darwin-x64': 0.9.0 + '@yuku-parser/binding-freebsd-x64': 0.9.0 + '@yuku-parser/binding-linux-arm-gnu': 0.9.0 + '@yuku-parser/binding-linux-arm-musl': 0.9.0 + '@yuku-parser/binding-linux-arm64-gnu': 0.9.0 + '@yuku-parser/binding-linux-arm64-musl': 0.9.0 + '@yuku-parser/binding-linux-x64-gnu': 0.9.0 + '@yuku-parser/binding-linux-x64-musl': 0.9.0 + '@yuku-parser/binding-win32-arm64': 0.9.0 + '@yuku-parser/binding-win32-x64': 0.9.0 zimmerframe@1.1.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 460655d2..4d7e2214 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -12,33 +12,32 @@ 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' - '@rslint/core': '~0.8.0' + '@rslib/core': '~1.0.0-beta.3' + '@rslint/core': '~0.8.1' '@rspress/core': '^2.0.19' '@rspress/plugin-client-redirects': '^2.0.19' '@rspress/plugin-sitemap': '^2.0.19' - '@rstack-dev/doc-ui': '1.14.7' + '@rstack-dev/doc-ui': '1.14.8' '@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' 'happy-dom': '^20.11.2' 'heading-case': '^1.1.5' 'import-meta-resolve': '4.2.0' @@ -53,13 +52,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.9.0' dedupePeers: true diff --git a/rstack.config.ts b/rstack.config.ts index 0ad535cf..c46a8517 100644 --- a/rstack.config.ts +++ b/rstack.config.ts @@ -1,12 +1,10 @@ -// Rstack configuration guide: https://rstack.rs/config +// Configuration guide: https://rstack.rs/config import { define } from 'rstack'; -define.lint(async () => { - const { default: globals } = await import('globals'); - const { js, ts } = await import('rstack/lint'); +define.lint(({ globals, js, ts, rstestPlugin }) => { return [ js.configs.recommended, - ts.configs.recommended, + ts.configs.recommendedTypeChecked, { files: ['**/*.{js,jsx,cjs,mjs}'], languageOptions: { @@ -19,6 +17,10 @@ define.lint(async () => { }, }, }, + { + files: ['**/*.test.{ts,tsx}'], + ...rstestPlugin.configs.recommended, + }, // Source imports use .ts for Node.js native TypeScript execution; builds rewrite them to .js. { files: ['packages/rstack/src/**/*.ts'], @@ -53,16 +55,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..990ebee5 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-18" 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/dictionary.txt b/scripts/dictionary.txt index 06b17167..36841a6a 100644 --- a/scripts/dictionary.txt +++ b/scripts/dictionary.txt @@ -6,12 +6,14 @@ dirents editmsg errexit esac +esbenp extglob fnames huskyrc indentable jsonline llms +MJML napi noformat noprettier @@ -30,6 +32,7 @@ rstest shiki shikijs solidjs +Trae turborepo typicode worktank 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/_meta.json b/website/docs/en/guide/_meta.json index 630bde07..e59fcba1 100644 --- a/website/docs/en/guide/_meta.json +++ b/website/docs/en/guide/_meta.json @@ -42,11 +42,21 @@ "name": "formatting", "label": "Formatting" }, + { + "type": "file", + "name": "git-hooks", + "label": "Git hooks" + }, { "type": "file", "name": "monorepo", "label": "Monorepo" }, + { + "type": "file", + "name": "ide-integration", + "label": "IDE integration" + }, { "type": "dir-section-header", "name": "cli", 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/cli/staged.mdx b/website/docs/en/guide/cli/staged.mdx index 0ff122f8..582bb554 100644 --- a/website/docs/en/guide/cli/staged.mdx +++ b/website/docs/en/guide/cli/staged.mdx @@ -96,7 +96,7 @@ Configure staged-file tasks through [`define.staged()`](../configuration#define- import { define } from 'rstack'; define.staged({ - '*.{js,jsx,ts,tsx}': ['rs lint', 'rs fmt'], - '*.{json,md,mdx,css,html}': 'rs fmt', + '*.{js,jsx,ts,tsx,mjs,cjs,mts,cts}': ['rs lint --fix', 'rs fmt'], + '*.{json,md,mdx,css,scss,less,html,yml,yaml}': 'rs fmt', }); ``` 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..d77d469d 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`. @@ -50,6 +54,19 @@ In addition to Prettier options and `overrides`, Rstack provides two options: ::: +## Supported languages + +`rs fmt` supports the same built-in languages as [Prettier](https://prettier.io/docs/) and normally infers the language from the file name: + +- [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript), [JSX](https://react.github.io/jsx/), [Flow](https://flow.org/), and [TypeScript](https://www.typescriptlang.org/) +- [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS), [Less](https://lesscss.org/), and [SCSS](https://sass-lang.com/) +- [HTML](https://en.wikipedia.org/wiki/HTML), [Angular](https://angular.dev/), [Vue](https://vuejs.org/), [Ember/Handlebars](https://emberjs.com/), [Lightning Web Components (LWC)](https://developer.salesforce.com/developer-centers/lightning-web-components), and [MJML](https://mjml.io/) +- [JSON](https://json.org/) and [YAML](https://yaml.org/) +- [GraphQL](https://graphql.org/) +- [Markdown](https://commonmark.org/), including [GFM](https://github.github.com/gfm/) and [MDX v1](https://mdxjs.com/) + +You can add support for other languages with [Prettier plugins](#prettier-plugins). + ## Formatting scope `rs fmt` determines the formatting scope from the paths passed on the command line. You can combine the following inputs: @@ -192,7 +209,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/git-hooks.mdx b/website/docs/en/guide/git-hooks.mdx new file mode 100644 index 00000000..b57fa088 --- /dev/null +++ b/website/docs/en/guide/git-hooks.mdx @@ -0,0 +1,81 @@ +--- +description: 'Set up repository Git hooks with Rstack CLI and automatically lint and format staged files before each commit.' +--- + +# Git hooks + +import { PackageManagerTabs } from '@rspress/core/theme'; + +Use [`rs setup`](./cli/setup) to manage repository-level Git hooks that run project commands. By default, hook scripts live in `.rstack/hooks`. You can use them to validate commit messages, check code before pushing, or format files before committing. + +This page uses `pre-commit` as an example: first install Git hooks with `rs setup`, then run [`rs staged`](./cli/staged) from the `pre-commit` hook to lint and format the files staged for the commit. + +## Set up hooks + +Add `rs setup` to the `prepare` script of the project that owns the repository hooks: + +```json title="package.json" +{ + "scripts": { + "prepare": "rs setup" + } +} +``` + +Run the script once to install the hooks: + + + +`rs setup` sets the repository's `core.hooksPath` to `.rstack/hooks/_`. Verify the installation with: + +```bash +git config --local --get core.hooksPath +# .rstack/hooks/_ +``` + +:::tip + +- The `_` directory is generated dynamically and ignored by Git by default. +- If `rs setup` detects another hooks path or existing Git hooks, it skips installation. Migrate any hooks you want to keep, remove the existing configuration, and then try again. See the [`rs setup` guide](./cli/setup#hook-files) for details. + +::: + +## Pre-commit checks + +A `pre-commit` hook can lint and format the files staged for the current commit. + +### Configure tasks + +Add staged-file tasks to the Rstack config file. Adjust the glob patterns for the languages used by your project: + +```ts title="rstack.config.ts" +import { define } from 'rstack'; + +define.staged({ + '*.{js,jsx,ts,tsx,mjs,cjs,mts,cts}': ['rs lint --fix', 'rs fmt'], + '*.{json,md,mdx,css,scss,less,html,yml,yaml}': 'rs fmt', +}); +``` + +### Add the hook + +Create `.rstack/hooks/pre-commit` and run `rs staged` from it: + +```sh title=".rstack/hooks/pre-commit" +rs staged +``` + +### How it works + +When you run `git commit`, Git invokes the hook installed by `rs setup`. The hook executes `.rstack/hooks/pre-commit`, and `rs staged` then runs the configured tasks on the staged files. + +`rs staged` passes matching staged files to each command. Commands in an array run in order: [`rs lint --fix`](./cli/lint) first applies available fixes, then [`rs fmt`](./cli/fmt) formats the result. Remove `--fix` if lint errors should block the commit without changing files. + +After every task passes, the commit continues and includes the fixed and formatted results. If any task fails, the commit stops; fix the issue and then try again. diff --git a/website/docs/en/guide/ide-integration.mdx b/website/docs/en/guide/ide-integration.mdx new file mode 100644 index 00000000..b01c3757 --- /dev/null +++ b/website/docs/en/guide/ide-integration.mdx @@ -0,0 +1,89 @@ +--- +description: 'Set up the official Rstack extension for linting, formatting, and testing in VS Code.' +--- + +# IDE integration + +Rstack currently provides official VS Code integration through the [Rstack extension](https://github.com/rstackjs/rstack-editor). The extension brings Rstack CLI's linting, formatting, and testing capabilities into the editor. + +## Installation + +Install `Rstack` from the registry for your editor: + +- [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=rstack.rstack) for VS Code. +- [Open VSX Registry](https://open-vsx.org/extension/rstack/rstack) for Cursor, VSCodium, Trae, and other VS Code-compatible editors. + +You can also search for the extension identifier `rstack.rstack` in your editor's Extensions view. + +> The extension does not bundle Rstack CLI. It uses the `rstack` package installed in the project's `node_modules`. Install the project dependencies first so the editor and CLI use the same Rstack CLI version. + +## Configuration + +To use Rstack as the default formatter and format files on save, add the following settings: + +```json title=".vscode/settings.json" +{ + "editor.defaultFormatter": "rstack.rstack", + "editor.formatOnSave": true +} +``` + +The following language-specific settings are optional. They prevent existing language-specific formatter preferences from overriding the workspace default. Add settings only for the languages your project needs: + +```json title=".vscode/settings.json" +{ + "[javascript]": { "editor.defaultFormatter": "rstack.rstack" }, + "[javascriptreact]": { "editor.defaultFormatter": "rstack.rstack" }, + "[json]": { "editor.defaultFormatter": "rstack.rstack" }, + "[json5]": { "editor.defaultFormatter": "rstack.rstack" }, + "[jsonc]": { "editor.defaultFormatter": "rstack.rstack" }, + "[markdown]": { "editor.defaultFormatter": "rstack.rstack" }, + "[mdx]": { "editor.defaultFormatter": "rstack.rstack" }, + "[toml]": { "editor.defaultFormatter": "rstack.rstack" }, + "[typescript]": { "editor.defaultFormatter": "rstack.rstack" }, + "[typescriptreact]": { "editor.defaultFormatter": "rstack.rstack" }, + "[yaml]": { "editor.defaultFormatter": "rstack.rstack" } +} +``` + +To apply lint fixes when you save a file manually, add: + +```json title=".vscode/settings.json" +{ + "editor.codeActionsOnSave": { + "source.fixAll.rslint": "explicit" + } +} +``` + +> `"explicit"` applies lint fixes only on manual saves. Use `"always"` to apply them during auto-save as well. + +To recommend the extension to team members who open the repository, add it to the workspace recommendations: + +```json title=".vscode/extensions.json" +{ + "recommendations": ["rstack.rstack"] +} +``` + +## Features + +### Linting + +Shows lint diagnostics as you edit, provides quick fixes, and fixes issues on save. + +### Formatting + +Formats documents through the project-local [`rs fmt`](./cli/fmt) language server. It loads `define.fmt()` from [`rstack.config.*`](./configuration#configuration-file) at the workspace root, keeping the editor and CLI on the same formatting rules. + +### Testing + +Adds project tests to VS Code's Test Explorer. You can run or debug an individual test, suite, or file, and failed tests also appear as editor diagnostics. + +## Troubleshooting + +The `Rstack` status bar item shows the active features and their status, including configuration discovery and version compatibility problems. If its status does not update after installing dependencies or editing configuration, open the Command Palette and run `Rstack: Relaunch Extension`. + +For more usage details, see the [extension documentation](https://github.com/rstackjs/rstack-editor/blob/main/packages/vscode/README.md). + +Report bugs and feature requests through [Rstack Editor Issues](https://github.com/rstackjs/rstack-editor/issues). diff --git a/website/docs/en/guide/migration.mdx b/website/docs/en/guide/migration.mdx index 5a020498..3cde78d9 100644 --- a/website/docs/en/guide/migration.mdx +++ b/website/docs/en/guide/migration.mdx @@ -4,22 +4,40 @@ description: 'Migrate an existing project to Rstack CLI with the recommended mig # Migrate to Rstack CLI -To migrate an existing project, we recommend using the `migrate-to-rstack-cli` Skill. It inspects the project and automatically migrates supported tools used in the repository—including Rstack tools, Prettier, and Husky—to Rstack CLI. +To migrate an existing project, we recommend using the `migrate-to-rstack-cli` Skill. It inspects the repository and migrates the Rstack toolchain, Prettier, Husky, and other supported tools to Rstack CLI. ## Use the migration skill -First, install the Skill: +### Migrate in one pass + +To migrate all supported tools in one pass, send this prompt to your coding agent: + +```text +Run `npx --yes skills@latest use rstackjs/rstack-cli@migrate-to-rstack-cli` and follow the generated Skill instructions to migrate this project to Rstack CLI. +``` + +### Migrate in stages + +If you want to keep each set of changes small and easy to review, migrate one tool at a time. Start by installing the Skill in your repository: ```bash npx skills add rstackjs/rstack-cli --skill migrate-to-rstack-cli ``` -Then ask your coding agent to perform the migration with this prompt: +Then ask your coding agent to migrate a single tool. For example, start with Rslint: ```text -Use the migrate-to-rstack-cli Skill to migrate this project to Rstack CLI. +Use the migrate-to-rstack-cli Skill to migrate Rslint in this project to Rstack CLI, leaving all other tools unchanged. ``` +Once you have reviewed and validated the changes, move on to Rstest: + +```text +Use the migrate-to-rstack-cli Skill to migrate Rstest in this project to Rstack CLI, leaving all other tools unchanged. +``` + +Repeat this process for each remaining tool. + ## Supported tools The Skill can directly migrate the following standalone tools: 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/_meta.json b/website/docs/zh/guide/_meta.json index 518da9ca..a1a41dc9 100644 --- a/website/docs/zh/guide/_meta.json +++ b/website/docs/zh/guide/_meta.json @@ -42,11 +42,21 @@ "name": "formatting", "label": "格式化" }, + { + "type": "file", + "name": "git-hooks", + "label": "Git hooks" + }, { "type": "file", "name": "monorepo", "label": "Monorepo" }, + { + "type": "file", + "name": "ide-integration", + "label": "IDE 集成" + }, { "type": "dir-section-header", "name": "cli", 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/cli/staged.mdx b/website/docs/zh/guide/cli/staged.mdx index 6191a678..8b716498 100644 --- a/website/docs/zh/guide/cli/staged.mdx +++ b/website/docs/zh/guide/cli/staged.mdx @@ -96,7 +96,7 @@ rs staged --help import { define } from 'rstack'; define.staged({ - '*.{js,jsx,ts,tsx}': ['rs lint', 'rs fmt'], - '*.{json,md,mdx,css,html}': 'rs fmt', + '*.{js,jsx,ts,tsx,mjs,cjs,mts,cts}': ['rs lint --fix', 'rs fmt'], + '*.{json,md,mdx,css,scss,less,html,yml,yaml}': 'rs fmt', }); ``` 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..31a6cd19 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`。 @@ -50,6 +54,19 @@ define.fmt({ ::: +## 支持的语言 \{#supported-languages} + +`rs fmt` 支持与 [Prettier](https://prettier.io/docs/) 相同的内置语言,通常会根据文件名自动推断语言: + +- [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript)、[JSX](https://react.github.io/jsx/)、[Flow](https://flow.org/) 和 [TypeScript](https://www.typescriptlang.org/) +- [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS)、[Less](https://lesscss.org/) 和 [SCSS](https://sass-lang.com/) +- [HTML](https://en.wikipedia.org/wiki/HTML)、[Angular](https://angular.dev/)、[Vue](https://vuejs.org/)、[Ember/Handlebars](https://emberjs.com/)、[Lightning Web Components(LWC)](https://developer.salesforce.com/developer-centers/lightning-web-components) 和 [MJML](https://mjml.io/) +- [JSON](https://json.org/) 和 [YAML](https://yaml.org/) +- [GraphQL](https://graphql.org/) +- [Markdown](https://commonmark.org/),包括 [GFM](https://github.github.com/gfm/) 和 [MDX v1](https://mdxjs.com/) + +你可以通过 [Prettier 插件](#prettier-plugins)支持其他语言。 + ## 格式化范围 \{#formatting-scope} `rs fmt` 根据命令行中传入的路径确定格式化范围。以下输入可以组合使用: @@ -163,7 +180,7 @@ define.fmt({ ### 合并顺序 \{#merge-order} -如果同一文件匹配多条 override 规则,Rstack 会按声明顺序合并配置,后面的值优先。下面的 `README.md` 会同时匹配两条规则,因此最终的 `printWidth` 为 `80`: +如果同一文件匹配多条 override 规则,Rstack CLI 会按声明顺序合并配置,后面的值优先。下面的 `README.md` 会同时匹配两条规则,因此最终的 `printWidth` 为 `80`: ```ts define.fmt({ @@ -192,7 +209,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/git-hooks.mdx b/website/docs/zh/guide/git-hooks.mdx new file mode 100644 index 00000000..ade6da5d --- /dev/null +++ b/website/docs/zh/guide/git-hooks.mdx @@ -0,0 +1,81 @@ +--- +description: '使用 Rstack CLI 配置仓库级 Git hooks,并在提交前自动检查和格式化暂存文件。' +--- + +# Git hooks \{#git-hooks} + +import { PackageManagerTabs } from '@rspress/core/theme'; + +使用 [`rs setup`](./cli/setup) 可以统一管理仓库级 Git hooks,并通过 hook 脚本运行项目命令。hook 脚本默认存放在 `.rstack/hooks` 中,可用于校验提交信息、推送前检查代码、提交前格式化文件等场景。 + +下面以 `pre-commit` 为例:先通过 `rs setup` 安装 Git hooks,再在 `pre-commit` hook 中运行 [`rs staged`](./cli/staged),对本次提交的暂存文件进行代码检查和格式化。 + +## 安装 hooks \{#set-up-hooks} + +在负责管理仓库 hooks 的项目中,将 `rs setup` 添加到 `package.json` 的 `prepare` 脚本: + +```json title="package.json" +{ + "scripts": { + "prepare": "rs setup" + } +} +``` + +执行一次该脚本,完成 hooks 安装: + + + +`rs setup` 会将仓库的 `core.hooksPath` 设为 `.rstack/hooks/_`,可以通过以下命令确认是否安装成功: + +```bash +git config --local --get core.hooksPath +# .rstack/hooks/_ +``` + +:::tip + +- `_` 目录由命令动态生成,且默认被 Git 忽略。 +- 如果检测到其他 hooks 路径或已有 Git hooks,`rs setup` 会跳过安装。请先迁移需要保留的 hooks,移除原有配置,然后重试。详细说明请参考 [`rs setup` 指南](./cli/setup#hook-files)。 + +::: + +## 提交前检查 \{#pre-commit-checks} + +通过 `pre-commit` hook 可以检查和格式化本次提交的暂存文件。 + +### 配置任务 \{#configure-tasks} + +在 Rstack 配置文件中添加暂存文件任务,根据项目实际使用的语言来调整 glob 模式: + +```ts title="rstack.config.ts" +import { define } from 'rstack'; + +define.staged({ + '*.{js,jsx,ts,tsx,mjs,cjs,mts,cts}': ['rs lint --fix', 'rs fmt'], + '*.{json,md,mdx,css,scss,less,html,yml,yaml}': 'rs fmt', +}); +``` + +### 添加 hook \{#add-the-hook} + +创建 `.rstack/hooks/pre-commit`,并在其中运行 `rs staged`: + +```sh title=".rstack/hooks/pre-commit" +rs staged +``` + +### 执行流程 \{#how-it-works} + +运行 `git commit` 时,Git 会调用 `rs setup` 安装的 hook。该 hook 会执行 `.rstack/hooks/pre-commit`,再由 `rs staged` 对暂存文件运行配置的任务。 + +`rs staged` 会将匹配的暂存文件传给对应命令,数组中的命令按顺序执行,[`rs lint --fix`](./cli/lint) 先修复可自动处理的问题,再由 [`rs fmt`](./cli/fmt) 统一格式。如果只希望代码检查阻止提交而不修改文件,可以移除 `--fix`。 + +全部任务通过后,提交会继续,并包含修复和格式化结果。任一任务失败都会中止提交,解决问题后重新提交即可。 diff --git a/website/docs/zh/guide/ide-integration.mdx b/website/docs/zh/guide/ide-integration.mdx new file mode 100644 index 00000000..450cfd74 --- /dev/null +++ b/website/docs/zh/guide/ide-integration.mdx @@ -0,0 +1,89 @@ +--- +description: '介绍如何在 VS Code 中使用 Rstack 官方扩展进行代码检查、格式化和测试。' +--- + +# IDE 集成 \{#ide-integration} + +Rstack 目前通过 [Rstack 扩展](https://github.com/rstackjs/rstack-editor) 提供官方的 VS Code 集成。该扩展将 Rstack CLI 的代码检查、格式化和测试能力集成到编辑器中。 + +## 安装 \{#installation} + +根据所用编辑器,从对应的扩展市场安装 `Rstack`: + +- VS Code 用户从 [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=rstack.rstack) 安装。 +- Cursor、VSCodium、Trae 及其他兼容 VS Code 的编辑器用户从 [Open VSX Registry](https://open-vsx.org/extension/rstack/rstack) 安装。 + +也可以在编辑器的扩展视图中搜索扩展标识 `rstack.rstack`。 + +> 扩展本身不内置 Rstack CLI,而是使用项目 `node_modules` 中安装的 `rstack` 包。请先安装项目依赖,以确保编辑器和 CLI 使用相同版本的 Rstack CLI。 + +## 配置 \{#configuration} + +要将 Rstack 设为默认格式化工具,并在保存文件时执行格式化,请添加以下配置: + +```json title=".vscode/settings.json" +{ + "editor.defaultFormatter": "rstack.rstack", + "editor.formatOnSave": true +} +``` + +以下语言级配置是可选的,可避免开发者已有的语言级格式化设置覆盖工作区默认值。你可以根据项目需要,只为所需的语言添加配置: + +```json title=".vscode/settings.json" +{ + "[javascript]": { "editor.defaultFormatter": "rstack.rstack" }, + "[javascriptreact]": { "editor.defaultFormatter": "rstack.rstack" }, + "[json]": { "editor.defaultFormatter": "rstack.rstack" }, + "[json5]": { "editor.defaultFormatter": "rstack.rstack" }, + "[jsonc]": { "editor.defaultFormatter": "rstack.rstack" }, + "[markdown]": { "editor.defaultFormatter": "rstack.rstack" }, + "[mdx]": { "editor.defaultFormatter": "rstack.rstack" }, + "[toml]": { "editor.defaultFormatter": "rstack.rstack" }, + "[typescript]": { "editor.defaultFormatter": "rstack.rstack" }, + "[typescriptreact]": { "editor.defaultFormatter": "rstack.rstack" }, + "[yaml]": { "editor.defaultFormatter": "rstack.rstack" } +} +``` + +如需在手动保存文件时同时修复代码检查问题,可以添加: + +```json title=".vscode/settings.json" +{ + "editor.codeActionsOnSave": { + "source.fixAll.rslint": "explicit" + } +} +``` + +> `"explicit"` 表示仅在手动保存时应用代码检查修复;如需在自动保存时也执行修复,可以改为 `"always"`。 + +若希望团队成员打开仓库时收到安装建议,可以将该扩展添加到工作区推荐列表: + +```json title=".vscode/extensions.json" +{ + "recommendations": ["rstack.rstack"] +} +``` + +## 支持能力 \{#features} + +### 代码检查 \{#linting} + +在编辑过程中显示代码检查诊断、提供快速修复,并支持保存时修复。 + +### 格式化 \{#formatting} + +扩展会通过项目本地的 [`rs fmt`](./cli/fmt) language server 格式化文档,并读取工作区根目录 [`rstack.config.*`](./configuration#configuration-file) 中的 `define.fmt()` 配置,使编辑器与 CLI 使用一致的格式化规则。 + +### 测试 \{#testing} + +将项目测试添加到 VS Code 的测试资源管理器。你可以运行或调试单个测试、测试套件或测试文件,失败的测试也会显示为编辑器诊断信息。 + +## 排查问题 \{#troubleshooting} + +状态栏中的 `Rstack` 项会显示当前启用的功能及其状态,并提示配置发现或版本兼容问题。如果安装依赖或修改配置后状态未更新,请打开命令面板并运行 `Rstack: Relaunch Extension`。 + +更多用法请参阅[扩展文档](https://github.com/rstackjs/rstack-editor/blob/main/packages/vscode/README.md)。 + +如需报告问题或提出功能建议,请前往 [Rstack Editor Issues](https://github.com/rstackjs/rstack-editor/issues)。 diff --git a/website/docs/zh/guide/migration.mdx b/website/docs/zh/guide/migration.mdx index 41f7d94c..07da6039 100644 --- a/website/docs/zh/guide/migration.mdx +++ b/website/docs/zh/guide/migration.mdx @@ -4,22 +4,40 @@ description: '使用推荐的迁移 Skill,将现有项目迁移到 Rstack CLI # 迁移到 Rstack CLI \{#migrate-to-rstack-cli} -迁移现有项目时,推荐使用 `migrate-to-rstack-cli` Skill。该 Skill 会分析项目,并自动将仓库中使用的 Rstack 工具及 Prettier、Husky 等受支持工具迁移到 Rstack CLI。 +迁移现有项目时,推荐使用 `migrate-to-rstack-cli` Skill。该 Skill 会分析仓库,并将其中受支持的工具(包括 Rstack 工具链、Prettier 和 Husky 等)迁移到 Rstack CLI。 ## 使用迁移 Skill \{#use-the-migration-skill} -首先安装该 Skill: +### 一次性迁移 \{#migrate-in-one-pass} + +如需一次性迁移所有受支持的工具,请向 Coding Agent 发送以下 Prompt: + +```text +运行 `npx --yes skills@latest use rstackjs/rstack-cli@migrate-to-rstack-cli`,然后按照生成的 Skill 指令将当前项目迁移到 Rstack CLI。 +``` + +### 分批迁移 \{#migrate-in-stages} + +如果希望每次改动更小、更便于审查,可以按工具分批迁移。首先,将 Skill 安装到当前仓库: ```bash npx skills add rstackjs/rstack-cli --skill migrate-to-rstack-cli ``` -安装完成后,向 Coding Agent 发送以下 Prompt: +然后让 Coding Agent 每次只迁移一种工具。例如,可以先迁移 Rslint: ```text -使用 migrate-to-rstack-cli Skill 将当前项目迁移到 Rstack CLI。 +使用 migrate-to-rstack-cli Skill 将当前项目中的 Rslint 迁移到 Rstack CLI,并保持其他工具不变。 ``` +审查并验证本次改动后,再迁移 Rstest: + +```text +使用 migrate-to-rstack-cli Skill 将当前项目中的 Rstest 迁移到 Rstack CLI,并保持其他工具不变。 +``` + +其余工具也可以按此方式依次迁移。 + ## 支持的工具 \{#supported-tools} 该 Skill 可以直接迁移以下独立工具: 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..f9e061fd 100644 --- a/website/i18n.json +++ b/website/i18n.json @@ -3,13 +3,29 @@ "en": "Quick start", "zh": "快速上手" }, + "getStarted": { + "en": "Get Started", + "zh": "开始使用" + }, + "github": { + "en": "GitHub", + "zh": "GitHub" + }, + "latestRelease": { + "en": "Latest release", + "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 = () => {