diff --git a/.agents/skills/migrate-to-rstack-cli/SKILL.md b/.agents/skills/migrate-to-rstack-cli/SKILL.md index 992cf06c..01612d1b 100644 --- a/.agents/skills/migrate-to-rstack-cli/SKILL.md +++ b/.agents/skills/migrate-to-rstack-cli/SKILL.md @@ -34,7 +34,11 @@ 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`. @@ -53,6 +57,8 @@ define.test({ }); ``` +### Modules and Imports + Use dynamic imports in async config functions only for external plugins, presets, and other dependencies: ```ts diff --git a/.agents/skills/migrate-to-rstack-cli/references/git-hooks.md b/.agents/skills/migrate-to-rstack-cli/references/git-hooks.md index 09a54143..8625a249 100644 --- a/.agents/skills/migrate-to-rstack-cli/references/git-hooks.md +++ b/.agents/skills/migrate-to-rstack-cli/references/git-hooks.md @@ -4,11 +4,12 @@ Migrate [Husky](https://typicode.github.io/husky/) or [simple-git-hooks](https:/ ## Shared Steps -1. Inspect the hook manager configuration, hook scripts, lifecycle scripts, custom paths, environment overrides, and `git config --local --get core.hooksPath`. +1. Inspect the hook manager configuration, hook scripts, lifecycle scripts, custom paths, environment overrides, and `git config --show-scope --get core.hooksPath`. 2. Inventory every active hook before editing. Confirm each hook is [supported by `rs setup`](https://rstack.rs/guide/cli/setup#supported-hooks); stop or design an explicit alternative for unsupported hooks. 3. Create each migrated hook in the selected hooks directory, `.rstack/hooks` by default, before running `rs setup`, because the command changes the repository's `core.hooksPath`. Preserve commands and any explicit directory changes. 4. Ensure the `prepare` script in the root `package.json` runs `rs setup`, adding it if necessary. Remove the old installer invocation from any lifecycle script while preserving other commands. Use `--hooks-dir` consistently when choosing a custom directory. -5. Run the updated lifecycle script, exercise the migrated hooks, and remove the old dependency and configuration only after behavior matches. +5. If the previous manager's hooks or `core.hooksPath` block installation, run `rs setup --force` once after migrating every required hook. The command preserves the previous files but makes them inactive. Do not add `--force` to the lifecycle script. +6. Exercise the migrated hooks, then remove the old dependency, configuration, and generated hook files only after behavior matches and their ownership and paths are confirmed. `rs setup` creates `.rstack/hooks/_/.gitignore`. Do not list `.rstack/hooks/_` in the root `.gitignore`. @@ -44,7 +45,7 @@ pnpm test 3. Replace the simple-git-hooks lifecycle command with `rs setup`, preserving other chained commands. 4. Replace `SKIP_INSTALL_SIMPLE_GIT_HOOKS=1` and `SKIP_SIMPLE_GIT_HOOKS=1` usage with `RSTACK_HOOKS=0`. Move required commands from the file referenced by `SIMPLE_GIT_HOOKS_RC` to the Rstack user initialization file, with user permission. 5. Do not run the simple-git-hooks uninstall script after `rs setup`; it follows the current `core.hooksPath` and can delete Rstack's generated hook shims. -6. After validation, remove the simple-git-hooks dependency, config, installer, and stale package-manager metadata such as pnpm `allowBuilds`. Remove old generated hook files only after confirming their ownership and paths. +6. After validation, remove the simple-git-hooks dependency, config, installer, old generated hook files, and stale package-manager metadata such as pnpm `allowBuilds`. Confirm the generated files' ownership and paths before removing them. For example, migrate: @@ -72,6 +73,7 @@ pnpm test ## Validate -- Confirm `git config --local --get core.hooksPath` points to the expected Rstack-generated directory. +- Confirm `git config --show-scope --get core.hooksPath` reports the expected Rstack-generated directory and whether it is configured in the local or worktree scope. - Test each migrated hook and confirm that its commands run as expected. +- Remove previous generated hooks after validation so they cannot become active again if `core.hooksPath` is later unset or changed. - Search for old manager commands, configuration, environment variables, and user instructions before removing dependencies. 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 5e9b6d43..e2c68154 100644 --- a/.agents/skills/migrate-to-rstack-cli/references/rslint.md +++ b/.agents/skills/migrate-to-rstack-cli/references/rslint.md @@ -6,8 +6,10 @@ Read this reference when the project uses `@rslint/core`, `rslint.config.*`, `rs 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. Receive `@rslint/core` exports from the factory parameter. -3. Replace custom `--config` paths with the migrated `rstack.config.*` path. -4. Remove `@rslint/core` only when no uncovered direct runtime API remains. Delete `rslint.config.*`. +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 @@ -22,6 +24,19 @@ define.lint(({ js, ts }) => [ 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 If a script also runs Prettier, migrate its formatting command as described in [prettier.md](prettier.md). 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/.vscode/settings.json b/.vscode/settings.json index 0007908b..6aac1c1a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,19 +15,15 @@ "mdx.validate.validateFileLinks": "ignore", "editor.defaultFormatter": "rstack.rstack", "js/ts.tsdk.path": "node_modules/typescript/lib", - "[typescript]": { - "editor.defaultFormatter": "rstack.rstack" - }, - "[javascript]": { - "editor.defaultFormatter": "rstack.rstack" - }, - "[mdx]": { - "editor.defaultFormatter": "rstack.rstack" - }, - "[json]": { - "editor.defaultFormatter": "rstack.rstack" - }, - "[jsonc]": { - "editor.defaultFormatter": "rstack.rstack" - } + "[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.toml b/Cargo.toml index f7e5884d..48c4f025 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,6 @@ 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 } 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-binding/src/lib.rs b/crates/rstack-binding/src/lib.rs index da3a361d..68596e1c 100644 --- a/crates/rstack-binding/src/lib.rs +++ b/crates/rstack-binding/src/lib.rs @@ -44,6 +44,75 @@ impl IgnoreMatcher { pub fn is_ignored(&mut self, file_path: String, is_directory: bool) -> bool { self.inner.is_ignored(Path::new(&file_path), is_directory) } + + /// Matches children with nonzero candidate flags and returns one byte per input name. + #[napi] + pub fn is_ignored_batch( + &mut self, + parent_path: String, + names: Vec, + directory_flags: Uint8Array, + candidate_flags: Uint8Array, + ) -> napi::Result { + if names.len() != directory_flags.len() { + return Err(Error::new( + Status::InvalidArg, + "Name and directory flag counts must match.", + )); + } + if names.len() != candidate_flags.len() { + return Err(Error::new( + Status::InvalidArg, + "Name and candidate flag counts must match.", + )); + } + + Ok(self + .inner + .is_ignored_batch( + Path::new(&parent_path), + &names, + directory_flags.as_ref(), + candidate_flags.as_ref(), + ) + .into()) + } + + /// Matches up to 32 candidate-selected children and returns an ignored-entry bit mask. + #[napi] + pub fn is_ignored_batch_mask( + &mut self, + parent_path: String, + names: Vec, + directory_mask: u32, + candidate_mask: u32, + ) -> napi::Result { + if names.len() > u32::BITS as usize { + return Err(Error::new( + Status::InvalidArg, + "A bit-mask batch cannot contain more than 32 names.", + )); + } + + Ok(self.inner.is_ignored_batch_mask( + Path::new(&parent_path), + &names, + directory_mask, + candidate_mask, + )) + } + + /// Matches a single child without constructing an intermediate names array. + #[napi] + pub fn is_ignored_child( + &mut self, + parent_path: String, + name: String, + is_directory: bool, + ) -> bool { + self.inner + .is_ignored_child(Path::new(&parent_path), &name, is_directory) + } } /// JavaScript-facing hierarchy for repository `.gitignore` files. 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/crates/rstack-ignore/src/lib.rs b/crates/rstack-ignore/src/lib.rs index c0944109..8a81a500 100644 --- a/crates/rstack-ignore/src/lib.rs +++ b/crates/rstack-ignore/src/lib.rs @@ -66,6 +66,73 @@ impl IgnoreMatcher { .iter_mut() .any(|source| source.is_ignored(file_path, is_directory)) } + + /// Matches children with nonzero candidate flags and returns one byte per input name. + pub fn is_ignored_batch( + &mut self, + parent_path: &Path, + names: &[String], + directory_flags: &[u8], + candidate_flags: &[u8], + ) -> Vec { + debug_assert_eq!(names.len(), directory_flags.len()); + debug_assert_eq!(names.len(), candidate_flags.len()); + + let mut ignored = vec![0; names.len()]; + let mut remaining = candidate_flags.iter().filter(|flag| **flag != 0).count(); + + for source in &mut self.sources { + if remaining == 0 { + break; + } + + remaining -= source.mark_ignored_batch( + parent_path, + names, + directory_flags, + candidate_flags, + &mut ignored, + ); + } + + ignored + } + + /// Matches up to 32 candidate-selected children and returns an ignored-entry bit mask. + pub fn is_ignored_batch_mask( + &mut self, + parent_path: &Path, + names: &[String], + directory_mask: u32, + candidate_mask: u32, + ) -> u32 { + debug_assert!(names.len() <= u32::BITS as usize); + + let valid_mask = if names.len() == u32::BITS as usize { + u32::MAX + } else { + (1 << names.len()) - 1 + }; + let candidate_mask = candidate_mask & valid_mask; + let mut ignored_mask = 0; + + for source in &mut self.sources { + let remaining_mask = candidate_mask & !ignored_mask; + if remaining_mask == 0 { + break; + } + + ignored_mask |= + source.ignored_batch_mask(parent_path, names, directory_mask, remaining_mask); + } + + ignored_mask + } + + /// Matches one child without constructing an intermediate names array. + pub fn is_ignored_child(&mut self, parent_path: &Path, name: &str, is_directory: bool) -> bool { + self.is_ignored(&parent_path.join(name), is_directory) + } } /// A hierarchy of repository `.gitignore` files keyed by their root-relative directories. @@ -328,11 +395,84 @@ impl SourceMatcher { fn is_ignored(&mut self, file_path: &Path, is_directory: bool) -> bool { let relative_path = self.relative_path(file_path); + self.is_relative_path_ignored(relative_path.as_ref(), is_directory) + } + + fn mark_ignored_batch( + &mut self, + parent_path: &Path, + names: &[String], + directory_flags: &[u8], + candidate_flags: &[u8], + ignored: &mut [u8], + ) -> usize { + let relative_parent = self.relative_path(parent_path); + let name_capacity = names.iter().map(String::len).max().unwrap_or(0); + let mut relative_path = PathBuf::with_capacity( + relative_parent.as_os_str().len() + + usize::from(!relative_parent.as_os_str().is_empty()) + + name_capacity, + ); + let mut matched = 0; + + for (index, name) in names.iter().enumerate() { + if candidate_flags[index] == 0 || ignored[index] != 0 { + continue; + } + + relative_path.clear(); + relative_path.push(relative_parent.as_ref()); + relative_path.push(name); + if self.is_relative_path_ignored(&relative_path, directory_flags[index] != 0) { + ignored[index] = 1; + matched += 1; + } + } + + matched + } + + fn ignored_batch_mask( + &mut self, + parent_path: &Path, + names: &[String], + directory_mask: u32, + candidate_mask: u32, + ) -> u32 { + debug_assert_ne!(candidate_mask, 0); + + let relative_parent = self.relative_path(parent_path); + let name_capacity = names[candidate_mask.trailing_zeros() as usize].len(); + let mut relative_path = PathBuf::with_capacity( + relative_parent.as_os_str().len() + + usize::from(!relative_parent.as_os_str().is_empty()) + + name_capacity, + ); + let mut ignored_mask = 0; + let mut remaining_mask = candidate_mask; + + while remaining_mask != 0 { + let index = remaining_mask.trailing_zeros() as usize; + let entry_mask = 1_u32 << index; + remaining_mask &= remaining_mask - 1; + + relative_path.clear(); + relative_path.push(relative_parent.as_ref()); + relative_path.push(&names[index]); + if self.is_relative_path_ignored(&relative_path, directory_mask & entry_mask != 0) { + ignored_mask |= entry_mask; + } + } + + ignored_mask + } + + fn is_relative_path_ignored(&mut self, relative_path: &Path, is_directory: bool) -> bool { if relative_path.as_os_str().is_empty() { return false; } - let relative_path = to_posix_path(&relative_path); + let relative_path = to_posix_path(relative_path); if is_directory { return self.is_directory_ignored(&relative_path); } @@ -423,6 +563,73 @@ mod tests { assert!(!matcher.is_ignored(Path::new("project/keep.ts"), false)); } + #[test] + fn matches_config_batches_with_independent_sources_and_candidates() { + let mut matcher = IgnoreMatcher::new([ + IgnoreSource::new("project", "*.js\n!keep.js\ndist/"), + IgnoreSource::new("project", "keep.js"), + ]) + .unwrap(); + let names = vec![ + "drop.js".into(), + "keep.js".into(), + "keep.ts".into(), + "dist".into(), + "skipped.js".into(), + ]; + + assert_eq!( + matcher.is_ignored_batch( + Path::new("project"), + &names, + &[0, 0, 0, 1, 0], + &[1, 1, 1, 1, 0], + ), + vec![1, 1, 0, 1, 0] + ); + assert_eq!( + matcher.is_ignored_batch_mask(Path::new("project"), &names, 0b01000, 0b01111), + 0b01011 + ); + assert!(matcher.is_ignored_child(Path::new("project"), "drop.js", false)); + assert!(!matcher.is_ignored_child(Path::new("project"), "keep.ts", false)); + } + + #[test] + fn matches_config_batches_outside_a_source_root() { + let mut matcher = + IgnoreMatcher::new([IgnoreSource::new("project/config", "../generated/*.js")]).unwrap(); + let names = vec!["output.js".into(), "output.ts".into()]; + + assert_eq!( + matcher.is_ignored_batch(Path::new("project/generated"), &names, &[0, 0], &[1, 1],), + vec![1, 0] + ); + assert_eq!( + matcher.is_ignored_batch_mask(Path::new("project/generated"), &names, 0, 0b11,), + 0b01 + ); + } + + #[test] + fn matches_sparse_config_batch_masks_and_supports_the_high_bit() { + let mut matcher = IgnoreMatcher::new([ + IgnoreSource::new("project", "*.js"), + IgnoreSource::new("project", "*.css"), + ]) + .unwrap(); + let mut names = vec!["skipped.js".into(); 32]; + names[3] = "keep.ts".into(); + names[17] = "drop.js".into(); + names[31] = "drop.css".into(); + let candidate_mask = (1_u32 << 3) | (1_u32 << 17) | (1_u32 << 31); + + assert_eq!( + matcher.is_ignored_batch_mask(Path::new("project"), &names, 0, candidate_mask), + (1_u32 << 17) | (1_u32 << 31) + ); + } + #[test] fn applies_nested_sources_and_child_negation() { let mut matcher = GitIgnoreMatcher::new(); diff --git a/packages/create-rstack/package.json b/packages/create-rstack/package.json index 11a52995..1dacc8b8 100644 --- a/packages/create-rstack/package.json +++ b/packages/create-rstack/package.json @@ -1,6 +1,6 @@ { "name": "create-rstack", - "version": "3.2.2", + "version": "3.2.3", "description": "Create a new Rstack project", "homepage": "https://rstack.rs", "bugs": { diff --git a/packages/create-rstack/template-app-lit-ts/package.json b/packages/create-rstack/template-app-lit-ts/package.json index 31b5aac3..f9b2b75e 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.6.2", + "rstack": "^0.6.3", "typescript": "^7.0.2" } } diff --git a/packages/create-rstack/template-app-lit/package.json b/packages/create-rstack/template-app-lit/package.json index 60d5d499..9d33cd4e 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.6.2" + "rstack": "^0.6.3" } } diff --git a/packages/create-rstack/template-app-preact-ts/package.json b/packages/create-rstack/template-app-preact-ts/package.json index 16a81439..b7c9850b 100644 --- a/packages/create-rstack/template-app-preact-ts/package.json +++ b/packages/create-rstack/template-app-preact-ts/package.json @@ -22,7 +22,7 @@ "@testing-library/preact": "^3.2.4", "@types/node": "^24.13.3", "happy-dom": "^20.11.2", - "rstack": "^0.6.2", + "rstack": "^0.6.3", "typescript": "^7.0.2" } } diff --git a/packages/create-rstack/template-app-preact/package.json b/packages/create-rstack/template-app-preact/package.json index bdb4bd7c..16cbc48e 100644 --- a/packages/create-rstack/template-app-preact/package.json +++ b/packages/create-rstack/template-app-preact/package.json @@ -21,6 +21,6 @@ "@testing-library/jest-dom": "^7.0.1", "@testing-library/preact": "^3.2.4", "happy-dom": "^20.11.2", - "rstack": "^0.6.2" + "rstack": "^0.6.3" } } diff --git a/packages/create-rstack/template-app-react-ts/package.json b/packages/create-rstack/template-app-react-ts/package.json index 7515f087..1972e3c2 100644 --- a/packages/create-rstack/template-app-react-ts/package.json +++ b/packages/create-rstack/template-app-react-ts/package.json @@ -26,7 +26,7 @@ "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", "happy-dom": "^20.11.2", - "rstack": "^0.6.2", + "rstack": "^0.6.3", "typescript": "^7.0.2" } } diff --git a/packages/create-rstack/template-app-react/package.json b/packages/create-rstack/template-app-react/package.json index e39bc21a..b442af05 100644 --- a/packages/create-rstack/template-app-react/package.json +++ b/packages/create-rstack/template-app-react/package.json @@ -23,6 +23,6 @@ "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.2", "happy-dom": "^20.11.2", - "rstack": "^0.6.2" + "rstack": "^0.6.3" } } diff --git a/packages/create-rstack/template-app-solid-ts/package.json b/packages/create-rstack/template-app-solid-ts/package.json index f9a52ef9..caeff60e 100644 --- a/packages/create-rstack/template-app-solid-ts/package.json +++ b/packages/create-rstack/template-app-solid-ts/package.json @@ -23,7 +23,7 @@ "@testing-library/jest-dom": "^7.0.1", "@types/node": "^24.13.3", "happy-dom": "^20.11.2", - "rstack": "^0.6.2", + "rstack": "^0.6.3", "typescript": "^7.0.2" } } diff --git a/packages/create-rstack/template-app-solid/package.json b/packages/create-rstack/template-app-solid/package.json index f49d6aaa..c9710c81 100644 --- a/packages/create-rstack/template-app-solid/package.json +++ b/packages/create-rstack/template-app-solid/package.json @@ -22,6 +22,6 @@ "@solidjs/testing-library": "^0.8.10", "@testing-library/jest-dom": "^7.0.1", "happy-dom": "^20.11.2", - "rstack": "^0.6.2" + "rstack": "^0.6.3" } } diff --git a/packages/create-rstack/template-app-svelte-ts/package.json b/packages/create-rstack/template-app-svelte-ts/package.json index b1b11c6c..29ee4cdb 100644 --- a/packages/create-rstack/template-app-svelte-ts/package.json +++ b/packages/create-rstack/template-app-svelte-ts/package.json @@ -23,7 +23,7 @@ "@types/node": "^24.13.3", "happy-dom": "^20.11.2", "prettier-plugin-svelte": "^4.1.1", - "rstack": "^0.6.2", + "rstack": "^0.6.3", "svelte-check": "^4.7.6", "typescript": "^6.0.3" } diff --git a/packages/create-rstack/template-app-svelte/package.json b/packages/create-rstack/template-app-svelte/package.json index 49f33444..1c04678f 100644 --- a/packages/create-rstack/template-app-svelte/package.json +++ b/packages/create-rstack/template-app-svelte/package.json @@ -22,6 +22,6 @@ "@testing-library/svelte": "^5.4.2", "happy-dom": "^20.11.2", "prettier-plugin-svelte": "^4.1.1", - "rstack": "^0.6.2" + "rstack": "^0.6.3" } } diff --git a/packages/create-rstack/template-app-vanilla-ts/package.json b/packages/create-rstack/template-app-vanilla-ts/package.json index 589bf191..8f00acb6 100644 --- a/packages/create-rstack/template-app-vanilla-ts/package.json +++ b/packages/create-rstack/template-app-vanilla-ts/package.json @@ -18,7 +18,7 @@ "@testing-library/jest-dom": "^7.0.1", "@types/node": "^24.13.3", "happy-dom": "^20.11.2", - "rstack": "^0.6.2", + "rstack": "^0.6.3", "typescript": "^7.0.2" } } diff --git a/packages/create-rstack/template-app-vanilla/package.json b/packages/create-rstack/template-app-vanilla/package.json index 299c97d1..7185e366 100644 --- a/packages/create-rstack/template-app-vanilla/package.json +++ b/packages/create-rstack/template-app-vanilla/package.json @@ -17,6 +17,6 @@ "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^7.0.1", "happy-dom": "^20.11.2", - "rstack": "^0.6.2" + "rstack": "^0.6.3" } } diff --git a/packages/create-rstack/template-app-vue-ts/package.json b/packages/create-rstack/template-app-vue-ts/package.json index bf87341d..c6f81f88 100644 --- a/packages/create-rstack/template-app-vue-ts/package.json +++ b/packages/create-rstack/template-app-vue-ts/package.json @@ -22,7 +22,7 @@ "@types/node": "^24.13.3", "@vue/test-utils": "^2.4.11", "happy-dom": "^20.11.2", - "rstack": "^0.6.2", + "rstack": "^0.6.3", "typescript": "^6.0.3", "vue-tsc": "^3.3.10" } diff --git a/packages/create-rstack/template-app-vue/package.json b/packages/create-rstack/template-app-vue/package.json index bcad8242..559baa2d 100644 --- a/packages/create-rstack/template-app-vue/package.json +++ b/packages/create-rstack/template-app-vue/package.json @@ -21,6 +21,6 @@ "@testing-library/jest-dom": "^7.0.1", "@vue/test-utils": "^2.4.11", "happy-dom": "^20.11.2", - "rstack": "^0.6.2" + "rstack": "^0.6.3" } } diff --git a/packages/create-rstack/template-doc-i18n/package.json b/packages/create-rstack/template-doc-i18n/package.json index 1465a69e..d15b4c05 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.6.2", + "rstack": "^0.6.3", "typescript": "^7.0.2" } } diff --git a/packages/create-rstack/template-doc/package.json b/packages/create-rstack/template-doc/package.json index 926b4b36..526c8535 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.6.2", + "rstack": "^0.6.3", "typescript": "^7.0.2" } } diff --git a/packages/create-rstack/template-lib-node-ts/package.json b/packages/create-rstack/template-lib-node-ts/package.json index 3fcccd5a..bb50a0bf 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.6.2", + "rstack": "^0.6.3", "typescript": "^7.0.2" }, "engines": { diff --git a/packages/create-rstack/template-lib-node/package.json b/packages/create-rstack/template-lib-node/package.json index 5fcda629..75df9220 100644 --- a/packages/create-rstack/template-lib-node/package.json +++ b/packages/create-rstack/template-lib-node/package.json @@ -18,7 +18,7 @@ "test:watch": "rs test --watch" }, "devDependencies": { - "rstack": "^0.6.2" + "rstack": "^0.6.3" }, "engines": { "node": ">=22.12.0" diff --git a/packages/create-rstack/template-lib-react-ts/package.json b/packages/create-rstack/template-lib-react-ts/package.json index af6269d5..96801542 100644 --- a/packages/create-rstack/template-lib-react-ts/package.json +++ b/packages/create-rstack/template-lib-react-ts/package.json @@ -33,7 +33,7 @@ "happy-dom": "^20.11.2", "react": "^19.2.8", "react-dom": "^19.2.8", - "rstack": "^0.6.2", + "rstack": "^0.6.3", "typescript": "^7.0.2" }, "peerDependencies": { diff --git a/packages/create-rstack/template-lib-react/package.json b/packages/create-rstack/template-lib-react/package.json index 45a32b13..beb4e88b 100644 --- a/packages/create-rstack/template-lib-react/package.json +++ b/packages/create-rstack/template-lib-react/package.json @@ -25,7 +25,7 @@ "happy-dom": "^20.11.2", "react": "^19.2.8", "react-dom": "^19.2.8", - "rstack": "^0.6.2" + "rstack": "^0.6.3" }, "peerDependencies": { "react": ">=18.0.0", diff --git a/packages/create-rstack/template-lib-solid-ts/package.json b/packages/create-rstack/template-lib-solid-ts/package.json index 435fcae8..66909ca2 100644 --- a/packages/create-rstack/template-lib-solid-ts/package.json +++ b/packages/create-rstack/template-lib-solid-ts/package.json @@ -30,7 +30,7 @@ "@testing-library/jest-dom": "^7.0.1", "@types/node": "^24.13.3", "happy-dom": "^20.11.2", - "rstack": "^0.6.2", + "rstack": "^0.6.3", "solid-js": "^1.9.15", "typescript": "^7.0.2" }, diff --git a/packages/create-rstack/template-lib-solid/package.json b/packages/create-rstack/template-lib-solid/package.json index 84a062de..ccd84f93 100644 --- a/packages/create-rstack/template-lib-solid/package.json +++ b/packages/create-rstack/template-lib-solid/package.json @@ -27,7 +27,7 @@ "@solidjs/testing-library": "^0.8.10", "@testing-library/jest-dom": "^7.0.1", "happy-dom": "^20.11.2", - "rstack": "^0.6.2", + "rstack": "^0.6.3", "solid-js": "^1.9.15" }, "peerDependencies": { diff --git a/packages/create-rstack/template-lib-svelte-ts/package.json b/packages/create-rstack/template-lib-svelte-ts/package.json index e2a0ec2c..11670082 100644 --- a/packages/create-rstack/template-lib-svelte-ts/package.json +++ b/packages/create-rstack/template-lib-svelte-ts/package.json @@ -6,8 +6,7 @@ ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" - }, - "./style.css": "./dist/index.css" + } }, "types": "./dist/index.d.ts", "files": [ @@ -28,7 +27,7 @@ "@types/node": "^24.13.3", "happy-dom": "^20.11.2", "prettier-plugin-svelte": "^4.1.1", - "rstack": "^0.6.2", + "rstack": "^0.6.3", "svelte": "^5.56.9", "svelte-check": "^4.7.6", "svelte2tsx": "^0.7.61", 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 2fb3b6f8..7534fb87 100644 --- a/packages/create-rstack/template-lib-svelte-ts/rstack.config.ts +++ b/packages/create-rstack/template-lib-svelte-ts/rstack.config.ts @@ -5,6 +5,7 @@ import { svelteDtsPlugin } from './scripts/rslib-plugin-svelte-dts.ts'; define.lib(async () => { const { pluginSvelte } = await import('@rsbuild/plugin-svelte'); return { + bundle: false, output: { target: 'web', }, diff --git a/packages/create-rstack/template-lib-svelte/package.json b/packages/create-rstack/template-lib-svelte/package.json index 2472ccb2..461f55b1 100644 --- a/packages/create-rstack/template-lib-svelte/package.json +++ b/packages/create-rstack/template-lib-svelte/package.json @@ -2,10 +2,7 @@ "name": "rstack-lib-svelte", "version": "0.0.0", "type": "module", - "exports": { - ".": "./dist/index.js", - "./style.css": "./dist/index.css" - }, + "exports": "./dist/index.js", "files": [ "dist", "README.md" @@ -23,7 +20,7 @@ "@rsbuild/plugin-svelte": "^2.0.1", "happy-dom": "^20.11.2", "prettier-plugin-svelte": "^4.1.1", - "rstack": "^0.6.2", + "rstack": "^0.6.3", "svelte": "^5.56.9" }, "peerDependencies": { diff --git a/packages/create-rstack/template-lib-svelte/rstack.config.js b/packages/create-rstack/template-lib-svelte/rstack.config.js index b0573e3b..ce991beb 100644 --- a/packages/create-rstack/template-lib-svelte/rstack.config.js +++ b/packages/create-rstack/template-lib-svelte/rstack.config.js @@ -5,6 +5,7 @@ import { define } from 'rstack'; define.lib(async () => { const { pluginSvelte } = await import('@rsbuild/plugin-svelte'); return { + bundle: false, output: { target: 'web', }, diff --git a/packages/create-rstack/template-lib-vue-ts/package.json b/packages/create-rstack/template-lib-vue-ts/package.json index d6fe0aab..3759973f 100644 --- a/packages/create-rstack/template-lib-vue-ts/package.json +++ b/packages/create-rstack/template-lib-vue-ts/package.json @@ -28,7 +28,7 @@ "@types/node": "^24.13.3", "@vue/test-utils": "^2.4.11", "happy-dom": "^20.11.2", - "rstack": "^0.6.2", + "rstack": "^0.6.3", "typescript": "^6.0.3", "vue": "^3.5.41", "vue-tsc": "^3.3.10" diff --git a/packages/create-rstack/template-lib-vue/package.json b/packages/create-rstack/template-lib-vue/package.json index 2a7103f0..949ac1b8 100644 --- a/packages/create-rstack/template-lib-vue/package.json +++ b/packages/create-rstack/template-lib-vue/package.json @@ -21,7 +21,7 @@ "@testing-library/jest-dom": "^7.0.1", "@vue/test-utils": "^2.4.11", "happy-dom": "^20.11.2", - "rstack": "^0.6.2", + "rstack": "^0.6.3", "vue": "^3.5.41" }, "peerDependencies": { diff --git a/packages/rstack/binding.cjs b/packages/rstack/binding.cjs index 8b3d2189..77c7626e 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.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.`) + if (bindingPackageVersion !== '0.6.3' && 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.3 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.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.`) + if (bindingPackageVersion !== '0.6.3' && 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.3 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.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.`) + if (bindingPackageVersion !== '0.6.3' && 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.3 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.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.`) + if (bindingPackageVersion !== '0.6.3' && 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.3 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.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.`) + if (bindingPackageVersion !== '0.6.3' && 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.3 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.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.`) + if (bindingPackageVersion !== '0.6.3' && 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.3 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.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.`) + if (bindingPackageVersion !== '0.6.3' && 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.3 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.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.`) + if (bindingPackageVersion !== '0.6.3' && 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.3 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.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.`) + if (bindingPackageVersion !== '0.6.3' && 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.3 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.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.`) + if (bindingPackageVersion !== '0.6.3' && 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.3 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.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.`) + if (bindingPackageVersion !== '0.6.3' && 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.3 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.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.`) + if (bindingPackageVersion !== '0.6.3' && 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.3 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.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.`) + if (bindingPackageVersion !== '0.6.3' && 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.3 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.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.`) + if (bindingPackageVersion !== '0.6.3' && 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.3 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.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.`) + if (bindingPackageVersion !== '0.6.3' && 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.3 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.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.`) + if (bindingPackageVersion !== '0.6.3' && 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.3 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.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.`) + if (bindingPackageVersion !== '0.6.3' && 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.3 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.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.`) + if (bindingPackageVersion !== '0.6.3' && 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.3 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.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.`) + if (bindingPackageVersion !== '0.6.3' && 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.3 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.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.`) + if (bindingPackageVersion !== '0.6.3' && 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.3 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.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.`) + if (bindingPackageVersion !== '0.6.3' && 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.3 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.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.`) + if (bindingPackageVersion !== '0.6.3' && 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.3 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.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.`) + if (bindingPackageVersion !== '0.6.3' && 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.3 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.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.`) + if (bindingPackageVersion !== '0.6.3' && 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.3 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.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.`) + if (bindingPackageVersion !== '0.6.3' && 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.3 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.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.`) + if (bindingPackageVersion !== '0.6.3' && 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.3 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.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.`) + if (bindingPackageVersion !== '0.6.3') { + throw new Error(`WASI binding package version mismatch, expected 0.6.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } } wasiBinding = require('@rstackjs/cli-wasm32-wasi') diff --git a/packages/rstack/binding.d.cts b/packages/rstack/binding.d.cts index fdbfacfc..d5fb7349 100644 --- a/packages/rstack/binding.d.cts +++ b/packages/rstack/binding.d.cts @@ -22,6 +22,12 @@ export declare class IgnoreMatcher { constructor(sources: Array) /** Returns whether a file or directory is ignored by any source. */ isIgnored(filePath: string, isDirectory: boolean): boolean + /** Matches children with nonzero candidate flags and returns one byte per input name. */ + isIgnoredBatch(parentPath: string, names: Array, directoryFlags: Uint8Array, candidateFlags: Uint8Array): Uint8Array + /** Matches up to 32 candidate-selected children and returns an ignored-entry bit mask. */ + isIgnoredBatchMask(parentPath: string, names: Array, directoryMask: number, candidateMask: number): number + /** Matches a single child without constructing an intermediate names array. */ + isIgnoredChild(parentPath: string, name: string, isDirectory: boolean): boolean } /** A Gitignore-compatible pattern source received from JavaScript. */ diff --git a/packages/rstack/package.json b/packages/rstack/package.json index b7a85a00..073f88c8 100644 --- a/packages/rstack/package.json +++ b/packages/rstack/package.json @@ -1,6 +1,6 @@ { "name": "rstack", - "version": "0.6.2", + "version": "0.6.3", "description": "One CLI for JavaScript development, powered by Rstack.", "homepage": "https://rstack.rs", "bugs": { diff --git a/packages/rstack/src/cli/commandHelp.ts b/packages/rstack/src/cli/commandHelp.ts index 2b6398e5..a844a1b4 100644 --- a/packages/rstack/src/cli/commandHelp.ts +++ b/packages/rstack/src/cli/commandHelp.ts @@ -498,6 +498,7 @@ const HELP_DEFINITIONS = { { title: 'Options', items: [ + ['-f, --force', 'Install despite an existing Git hooks setup'], [ '--hooks-dir ', 'Specify hooks directory relative to the Git repository root', diff --git a/packages/rstack/src/cli/commands.ts b/packages/rstack/src/cli/commands.ts index 4368dbea..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'; @@ -138,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'); @@ -146,11 +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'); @@ -177,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 { diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index 7b13f498..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,6 +29,11 @@ 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; @@ -255,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, @@ -265,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(); @@ -337,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, diff --git a/packages/rstack/src/fmt/discoverPaths.ts b/packages/rstack/src/fmt/discoverPaths.ts index 32e94431..9381d275 100644 --- a/packages/rstack/src/fmt/discoverPaths.ts +++ b/packages/rstack/src/fmt/discoverPaths.ts @@ -5,6 +5,11 @@ import micromatch from 'micromatch'; import readdir, { type Dirent, type DirentLike } from 'tiny-readdir'; import type { GitIgnoreMatcher as NativeGitIgnoreMatcher } from '../../binding.cjs'; import { loadNativeBinding } from '../native/index.ts'; +import type { + BatchIgnoreContext, + IgnoreMatcher, + IgnorePredicate, +} from './ignore.ts'; import { createRelativePathResolver, toPosixPath, @@ -21,8 +26,18 @@ const defaultIgnoredDirNames = new Set([ 'node_modules', ]); -const gitIgnored = Symbol('gitIgnored'); -type GitIgnoreDirent = Dirent & { [gitIgnored]?: true }; +const ignored = Symbol('ignored'); +type IgnoredDirent = Dirent & { [ignored]?: true }; +type TraversalIgnorePredicate = (( + filePath: string, + isDirectory: boolean, +) => boolean) & + Pick; + +interface GitIgnoreBatchContext { + readonly matcher: NativeGitIgnoreMatcher; + readonly relativeParent: string; +} interface DiscoverFmtPathsOptions { /** Absolute directory used to resolve input paths. */ @@ -31,7 +46,7 @@ interface DiscoverFmtPathsOptions { /** Whether files inside node_modules may be discovered. */ withNodeModules?: boolean; /** Returns whether a candidate path should be excluded. */ - isIgnored?: (filePath: string, isDirectory: boolean) => boolean; + isIgnored?: TraversalIgnorePredicate; } const isErrnoException = (error: unknown): error is NodeJS.ErrnoException => @@ -141,55 +156,20 @@ class GitIgnoreFiles { return this.#matcher!.isIgnored(toPosixPath(relativePath), isDirectory); } - /** Matches one directory's entries in a single native call. */ - matchDirents( - parentPath: string, - dirents: Dirent[], - ): boolean | number | Uint8Array | undefined { - if (!this.#hasRules || dirents.length === 0) { + resolveBatchContext(parentPath: string): GitIgnoreBatchContext | undefined { + if (!this.#hasRules) { return; } - const relativeParentPath = this.#resolveRelativePath(parentPath); - if (!isRelativePathInside(relativeParentPath)) { + const relativeParent = this.#resolveRelativePath(parentPath); + if (!isRelativePathInside(relativeParent)) { return; } - const relativeParent = toPosixPath(relativeParentPath); - - if (dirents.length === 1) { - const dirent = dirents[0]; - return this.#matcher!.isIgnoredChild( - relativeParent, - dirent.name, - dirent.isDirectory(), - ); - } - - const names = new Array(dirents.length); - - if (dirents.length <= 32) { - let directoryMask = 0; - for (let index = 0; index < dirents.length; index++) { - const dirent = dirents[index]; - names[index] = dirent.name; - directoryMask |= Number(dirent.isDirectory()) << index; - } - return this.#matcher!.isIgnoredBatchMask( - relativeParent, - names, - directoryMask >>> 0, - ); - } - - const directoryFlags = new Uint8Array(dirents.length); - for (let index = 0; index < dirents.length; index++) { - const dirent = dirents[index]; - names[index] = dirent.name; - directoryFlags[index] = Number(dirent.isDirectory()); - } - - return this.#matcher!.isIgnoredBatch(relativeParent, names, directoryFlags); + return { + matcher: this.#matcher!, + relativeParent: toPosixPath(relativeParent), + }; } #load(directoryPath: string): Promise { @@ -218,29 +198,219 @@ class GitIgnoreFiles { } } +const isIgnoredBeforeNative = ( + parentPath: string, + dirent: Dirent, + ignoredDirNames: ReadonlySet, + isIncluded: ((filePath: string) => boolean) | undefined, + precheck: IgnorePredicate | undefined, +): boolean => { + if (ignoredDirNames.has(dirent.name)) { + return true; + } + + const isDirectory = dirent.isDirectory(); + let targetPath: string | undefined; + if (!isDirectory && isIncluded) { + targetPath = path.join(parentPath, dirent.name); + if (!isIncluded(targetPath)) { + return true; + } + } + if (!isDirectory && isBinaryPath(dirent.name)) { + return true; + } + + return ( + precheck?.( + targetPath ?? path.join(parentPath, dirent.name), + isDirectory, + ) === true + ); +}; + +/** Matches one directory after earlier traversal rules have removed candidates. */ +const markIgnoredDirents = ( + parentPath: string, + dirents: Dirent[], + gitIgnore: GitIgnoreFiles, + ignoredDirNames: ReadonlySet, + isIncluded: ((filePath: string) => boolean) | undefined, + batchIgnore?: BatchIgnoreContext, +): void => { + const gitIgnoreContext = gitIgnore.resolveBatchContext(parentPath); + + if (dirents.length === 1) { + const dirent = dirents[0]; + if ( + gitIgnoreContext?.matcher.isIgnoredChild( + gitIgnoreContext.relativeParent, + dirent.name, + dirent.isDirectory(), + ) === true || + isIgnoredBeforeNative( + parentPath, + dirent, + ignoredDirNames, + isIncluded, + batchIgnore?.precheck, + ) || + batchIgnore?.matcher.isIgnoredChild( + parentPath, + dirent.name, + dirent.isDirectory(), + ) === true + ) { + (dirent as IgnoredDirent)[ignored] = true; + } + return; + } + + const names = new Array(dirents.length); + + if (dirents.length <= 32) { + let directoryMask = 0; + for (let index = 0; index < dirents.length; index++) { + const dirent = dirents[index]; + names[index] = dirent.name; + directoryMask |= Number(dirent.isDirectory()) << index; + } + + let ignoredMask = gitIgnoreContext + ? gitIgnoreContext.matcher.isIgnoredBatchMask( + gitIgnoreContext.relativeParent, + names, + directoryMask >>> 0, + ) + : 0; + + if (batchIgnore) { + for (let index = 0; index < dirents.length; index++) { + const entryMask = 1 << index; + if ( + (ignoredMask & entryMask) === 0 && + isIgnoredBeforeNative( + parentPath, + dirents[index], + ignoredDirNames, + isIncluded, + batchIgnore.precheck, + ) + ) { + ignoredMask |= entryMask; + } + } + + const validMask = 0xffffffff >>> (32 - dirents.length); + const candidateMask = (validMask & ~ignoredMask) >>> 0; + if (candidateMask !== 0) { + ignoredMask = + (ignoredMask | + batchIgnore.matcher.isIgnoredBatchMask( + parentPath, + names, + directoryMask >>> 0, + candidateMask, + )) >>> + 0; + } + } + + for (let index = 0; index < dirents.length; index++) { + if ((ignoredMask & (1 << index)) !== 0) { + (dirents[index] as IgnoredDirent)[ignored] = true; + } + } + return; + } + + const directoryFlags = new Uint8Array(dirents.length); + for (let index = 0; index < dirents.length; index++) { + const dirent = dirents[index]; + names[index] = dirent.name; + directoryFlags[index] = Number(dirent.isDirectory()); + } + + const ignoredFlags = gitIgnoreContext + ? gitIgnoreContext.matcher.isIgnoredBatch( + gitIgnoreContext.relativeParent, + names, + directoryFlags, + ) + : new Uint8Array(dirents.length); + + if (batchIgnore) { + const candidateFlags = new Uint8Array(dirents.length); + let candidateCount = 0; + for (let index = 0; index < dirents.length; index++) { + if (ignoredFlags[index] === 0) { + if ( + isIgnoredBeforeNative( + parentPath, + dirents[index], + ignoredDirNames, + isIncluded, + batchIgnore.precheck, + ) + ) { + ignoredFlags[index] = 1; + } else { + candidateFlags[index] = 1; + candidateCount++; + } + } + } + + if (candidateCount !== 0) { + const nextIgnored = batchIgnore.matcher.isIgnoredBatch( + parentPath, + names, + directoryFlags, + candidateFlags, + ); + for (let index = 0; index < dirents.length; index++) { + ignoredFlags[index] |= nextIgnored[index]; + } + } + } + + for (let index = 0; index < dirents.length; index++) { + if (ignoredFlags[index] !== 0) { + (dirents[index] as IgnoredDirent)[ignored] = true; + } + } +}; + const createTraversalOptions = ( gitIgnore: GitIgnoreFiles, ignoredDirNames: ReadonlySet, signal: { aborted: boolean }, onError: (error: unknown) => void, isIncluded?: (filePath: string) => boolean, - isIgnored?: (filePath: string, isDirectory: boolean) => boolean, + isIgnored?: TraversalIgnorePredicate, ) => { + const batchIgnore = isIgnored?.batch; + const scalarIgnore = batchIgnore ? undefined : isIgnored; + return { followSymlinks: false, signal, ignore: (targetPath: string, targetContext: DirentLike) => { // With symlink following disabled, tiny-readdir always provides a Dirent here. const dirent = targetContext as Dirent; - if (ignoredDirNames.has(dirent.name)) { + if ( + (dirent as IgnoredDirent)[ignored] === true || + ignoredDirNames.has(dirent.name) + ) { return true; } + if (batchIgnore) { + return false; + } + if (dirent.isDirectory()) { - return ( - (dirent as GitIgnoreDirent)[gitIgnored] === true || - isIgnored?.(targetPath, true) === true - ); + return scalarIgnore?.(targetPath, true) === true; } if (isIncluded !== undefined && !isIncluded(targetPath)) { @@ -248,9 +418,7 @@ const createTraversalOptions = ( } return ( - isIgnored?.(targetPath, false) === true || - isBinaryPath(targetPath) || - (dirent as GitIgnoreDirent)[gitIgnored] === true + scalarIgnore?.(targetPath, false) === true || isBinaryPath(targetPath) ); }, onDirents: async (dirents: Dirent[]) => { @@ -268,24 +436,14 @@ const createTraversalOptions = ( await gitIgnore.load(parentPath); } - const ignored = gitIgnore.matchDirents(parentPath, dirents); - if (typeof ignored === 'boolean') { - if (ignored) { - (dirents[0] as GitIgnoreDirent)[gitIgnored] = true; - } - } else if (typeof ignored === 'number') { - for (let index = 0; index < dirents.length; index++) { - if (ignored & (1 << index)) { - (dirents[index] as GitIgnoreDirent)[gitIgnored] = true; - } - } - } else if (ignored) { - for (let index = 0; index < ignored.length; index++) { - if (ignored[index] === 1) { - (dirents[index] as GitIgnoreDirent)[gitIgnored] = true; - } - } - } + markIgnoredDirents( + parentPath, + dirents, + gitIgnore, + ignoredDirNames, + isIncluded, + batchIgnore, + ); } catch (error) { onError(error); } @@ -300,7 +458,7 @@ const discoverDirectoryFiles = async ( gitIgnore: GitIgnoreFiles, ignoredDirNames: ReadonlySet, isIncluded?: (filePath: string) => boolean, - isIgnored?: (filePath: string, isDirectory: boolean) => boolean, + isIgnored?: TraversalIgnorePredicate, ): Promise => { let failed = false; let failure: unknown; diff --git a/packages/rstack/src/fmt/discovery.ts b/packages/rstack/src/fmt/discovery.ts index 8e238de1..5e738a25 100644 --- a/packages/rstack/src/fmt/discovery.ts +++ b/packages/rstack/src/fmt/discovery.ts @@ -18,19 +18,20 @@ const discoverFmtFiles = async ({ withNodeModules, config, }: DiscoverFmtFilesOptions): Promise => { - const isIgnored = await createIgnoreMatcher({ config, cwd, ignorePaths }); const isExcluded = excludedDirPath ? createDirMatcher(excludedDirPath) : undefined; - const shouldIgnore = isExcluded - ? (filePath: string, isDirectory = false) => - isExcluded(filePath) || isIgnored(filePath, isDirectory) - : isIgnored; + const isIgnored = await createIgnoreMatcher({ + config, + cwd, + ignorePaths, + precheck: isExcluded, + }); const filePaths = await discoverFmtPaths({ cwd, patterns, withNodeModules, - isIgnored: shouldIgnore, + isIgnored, }); if (filePaths.length === 0) { return []; diff --git a/packages/rstack/src/fmt/ignore.ts b/packages/rstack/src/fmt/ignore.ts index 48e52105..4eb58003 100644 --- a/packages/rstack/src/fmt/ignore.ts +++ b/packages/rstack/src/fmt/ignore.ts @@ -1,6 +1,9 @@ import { readFile } from 'node:fs/promises'; import path from 'node:path'; -import type { IgnoreSource } from '../../binding.cjs'; +import type { + IgnoreMatcher as NativeIgnoreMatcher, + IgnoreSource, +} from '../../binding.cjs'; import { loadNativeBinding } from '../native/index.ts'; import type { ResolvedFmtConfig } from './types.ts'; @@ -14,23 +17,59 @@ const defaultIgnoreNames = ['package-lock.json', 'pnpm-lock.yaml']; type IgnorePredicate = (filePath: string, isDirectory?: boolean) => boolean; +type BatchIgnoreMatcher = Pick< + NativeIgnoreMatcher, + 'isIgnoredBatch' | 'isIgnoredBatchMask' | 'isIgnoredChild' +>; + +interface BatchIgnoreContext { + readonly matcher: BatchIgnoreMatcher; + /** Cheap JavaScript checks applied before crossing into the native matcher. */ + readonly precheck?: IgnorePredicate; +} + +type IgnoreMatcher = IgnorePredicate & { + readonly batch?: BatchIgnoreContext; +}; + interface CreateIgnoreMatcherOptions { config: ResolvedFmtConfig; /** Base directory for relative ignore paths. */ cwd: string; ignorePaths?: string[]; + precheck?: IgnorePredicate; } -const createDefaultMatcher = (): IgnorePredicate => { +const createDefaultMatcher = (): IgnoreMatcher => { const suffixes = defaultIgnoreNames.map((name) => `${path.sep}${name}`); return (filePath) => suffixes.some((suffix) => filePath.endsWith(suffix)); }; -const createSourceMatcher = (sources: IgnoreSource[]): IgnorePredicate => { - const matcher = new (loadNativeBinding().IgnoreMatcher)(sources); +const combineIgnorePredicates = ( + first: IgnorePredicate, + second: IgnorePredicate, +): IgnorePredicate => { return (filePath, isDirectory = false) => - matcher.isIgnored(filePath, isDirectory); + first(filePath, isDirectory) || second(filePath, isDirectory); +}; + +const createSourceMatcher = ( + sources: IgnoreSource[], + precheck?: IgnorePredicate, +): IgnoreMatcher => { + const matcher = new (loadNativeBinding().IgnoreMatcher)(sources); + const isIgnored: IgnorePredicate = precheck + ? (filePath, isDirectory = false) => + precheck(filePath, isDirectory) || + matcher.isIgnored(filePath, isDirectory) + : (filePath, isDirectory = false) => + matcher.isIgnored(filePath, isDirectory); + + return Object.assign( + isIgnored, + precheck ? { batch: { matcher, precheck } } : { batch: { matcher } }, + ); }; const loadIgnoreSource = async ( @@ -59,29 +98,36 @@ const createIgnoreMatcher = async ({ config, cwd, ignorePaths = [], -}: CreateIgnoreMatcherOptions): Promise => { + precheck, +}: CreateIgnoreMatcherOptions): Promise => { const ignoreFileSources = await Promise.all( ignorePaths.map((ignorePath) => loadIgnoreSource(cwd, ignorePath)), ); if (config.ignorePatterns.length) { - return createSourceMatcher([ - { - rootPath: config.rootPath, - patterns: [...defaultIgnoreNames, ...config.ignorePatterns].join('\n'), - }, - ...ignoreFileSources, - ]); + return createSourceMatcher( + [ + { + rootPath: config.rootPath, + patterns: [...defaultIgnoreNames, ...config.ignorePatterns].join( + '\n', + ), + }, + ...ignoreFileSources, + ], + precheck, + ); } const defaultMatcher = createDefaultMatcher(); + const matcher = precheck + ? combineIgnorePredicates(precheck, defaultMatcher) + : defaultMatcher; if (ignoreFileSources.length === 0) { - return defaultMatcher; + return matcher; } - const cliMatcher = createSourceMatcher(ignoreFileSources); - return (filePath, isDirectory = false) => - defaultMatcher(filePath, isDirectory) || cliMatcher(filePath, isDirectory); + return createSourceMatcher(ignoreFileSources, matcher); }; export { createIgnoreMatcher }; -export type { IgnorePredicate }; +export type { BatchIgnoreContext, IgnoreMatcher, IgnorePredicate }; diff --git a/packages/rstack/src/fmt/runner.ts b/packages/rstack/src/fmt/runner.ts index 89ad3a9a..c7ebbd8a 100644 --- a/packages/rstack/src/fmt/runner.ts +++ b/packages/rstack/src/fmt/runner.ts @@ -39,7 +39,7 @@ interface RunCache { hashOptions: ReturnType; } -interface FmtWorkerPoolResult { +interface FmtFilesResult { files: FmtFileResult[]; processedFileCount: number; } @@ -193,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), @@ -209,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); @@ -221,24 +259,7 @@ const runWithWorkers = async ( 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(); } @@ -284,7 +305,7 @@ 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 { diff --git a/packages/rstack/src/fmt/worker.ts b/packages/rstack/src/fmt/worker.ts index 35d47f49..40d7a764 100644 --- a/packages/rstack/src/fmt/worker.ts +++ b/packages/rstack/src/fmt/worker.ts @@ -16,8 +16,8 @@ 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, diff --git a/packages/rstack/src/fmt/yukuPlugin.ts b/packages/rstack/src/fmt/yukuPlugin.ts index 824af9fb..f704684d 100644 --- a/packages/rstack/src/fmt/yukuPlugin.ts +++ b/packages/rstack/src/fmt/yukuPlugin.ts @@ -260,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; @@ -362,12 +385,14 @@ 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 && diff --git a/packages/rstack/src/rslintConfig.ts b/packages/rstack/src/rslintConfig.ts index 50f13c20..2a0cb25f 100644 --- a/packages/rstack/src/rslintConfig.ts +++ b/packages/rstack/src/rslintConfig.ts @@ -1,7 +1,10 @@ -import { loadRstackConfig } from './config.ts'; +import { loadRstackConfig, type LoadedRstackConfig } from './config.ts'; import type { RslintConfig } from '@rslint/core'; -const { configs } = await loadRstackConfig(); +// 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; diff --git a/packages/rstack/src/setup/index.ts b/packages/rstack/src/setup/index.ts index ef597323..5b37d61d 100644 --- a/packages/rstack/src/setup/index.ts +++ b/packages/rstack/src/setup/index.ts @@ -7,6 +7,7 @@ export const runSetupCLI = async (args: string[]): Promise => { const { values } = parseArgs({ args, options: { + force: { type: 'boolean', short: 'f' }, help: { type: 'boolean', short: 'h' }, 'hooks-dir': { type: 'string', multiple: true }, }, @@ -28,15 +29,47 @@ export const runSetupCLI = async (args: string[]): Promise => { return; } - const result = installHooks({ hooksDir }); + const result = installHooks({ force: values.force, hooksDir }); - if (result.status === 'installed' || result.status === 'unchanged') { + if (result.status === 'installed') { + // Warn when `--force` preserves an existing hooks setup but makes it inactive. + if (result.inactiveHooks) { + const { hooks, path, restore } = result.inactiveHooks; + const hooksMessage = hooks.length + ? `: ${color.yellow(hooks.join(', '))}` + : ''; + logger.warn( + `The previous Git hooks path "${color.yellow(path)}" is now inactive${hooksMessage}.`, + ); + + if (restore === 'unset') { + logger.info( + `The existing files were preserved and will become active again if ${color.yellow('core.hooksPath')} is unset.`, + ); + } else { + logger.info( + `The existing files were preserved. Set ${color.yellow('core.hooksPath')} back to this path to use them again.`, + ); + } + } + return; + } + + if (result.status === 'unchanged') { return; } if (result.status === 'skipped') { if (result.message) { logger.warn(`Git hooks setup skipped: ${color.yellow(result.message)}.`); + if ( + result.reason === 'existing-git-hooks' || + result.reason === 'hooks-path-conflict' + ) { + logger.info( + `To continue, run ${color.yellow('rs setup --force')}. Existing hook files will be preserved but become inactive.`, + ); + } return; } diff --git a/packages/rstack/src/setup/install.ts b/packages/rstack/src/setup/install.ts index eae7c186..a0dabd99 100644 --- a/packages/rstack/src/setup/install.ts +++ b/packages/rstack/src/setup/install.ts @@ -17,9 +17,16 @@ const gitignore = '*\n'; type InstallHooksOptions = { cwd?: string; + force?: boolean; hooksDir?: string; }; +type InactiveHooks = { + hooks: string[]; + path: string; + restore: 'configure' | 'unset'; +}; + type FailedInstallResult = { status: 'failed'; reason: string; @@ -33,7 +40,7 @@ type SkippedInstallResult = { }; type InstallResult = - | { status: 'installed'; hooksPath: string } + | { status: 'installed'; hooksPath: string; inactiveHooks?: InactiveHooks } | { status: 'unchanged'; hooksPath: string } | SkippedInstallResult | FailedInstallResult; @@ -45,6 +52,8 @@ type GitContext = { projectPath: string; }; +type GitConfigScopeOption = '--local' | '--worktree'; + const fail = (reason: string, message: string): FailedInstallResult => ({ status: 'failed', reason, @@ -104,9 +113,78 @@ const gitFailure = ( ); }; +const resolveHooksPathScope = ( + cwd: string, +): GitConfigScopeOption | FailedInstallResult => { + const configured = runGit(cwd, [ + 'config', + '--show-scope', + '--get', + 'core.hooksPath', + ]); + if (configured.error || configured.status === null) { + return gitFailure(configured.error, configured.stderr); + } + + // Exit status 1 means core.hooksPath is not configured yet. + if (configured.status === 1) { + return '--local'; + } + if (configured.status !== 0) { + return fail( + 'git-config-failed', + `Failed to resolve the core.hooksPath scope: ${configured.stderr.trim()}`, + ); + } + + const separator = configured.stdout.indexOf('\t'); + const scope = separator === -1 ? '' : configured.stdout.slice(0, separator); + if (scope === 'worktree') { + return '--worktree'; + } + if (scope === 'command') { + return fail( + 'hooks-path-command-scope', + "Cannot configure core.hooksPath because it is set in Git's command scope. Remove the command-scoped override and rerun rs setup.", + ); + } + if (scope === 'system' || scope === 'global' || scope === 'local') { + return '--local'; + } + + return fail( + 'git-config-failed', + 'Failed to resolve the core.hooksPath scope.', + ); +}; + +const resolveGitHooksPath = (cwd: string): string | FailedInstallResult => { + const hooksDirectory = runGit(cwd, [ + 'rev-parse', + '--path-format=absolute', + '--git-path', + 'hooks', + ]); + if (hooksDirectory.error || hooksDirectory.status === null) { + return gitFailure(hooksDirectory.error, hooksDirectory.stderr); + } + if (hooksDirectory.status !== 0) { + return fail( + 'git-command-failed', + `Failed to resolve the Git hooks path: ${hooksDirectory.stderr.trim()}`, + ); + } + + const resolvedDirectory = removeLineEnding(hooksDirectory.stdout); + if (!resolvedDirectory) { + return fail('git-command-failed', 'Failed to resolve the Git hooks path.'); + } + return resolvedDirectory; +}; + const resolveGitContext = (cwd: string): GitContext | InstallResult => { // Resolve every repository path in one Git process. `--git-path hooks` - // accounts for an existing local or global core.hooksPath configuration. + // accounts for the effective core.hooksPath configuration across Git scopes. const repository = runGit(cwd, [ 'rev-parse', '--is-inside-work-tree', @@ -252,6 +330,7 @@ const findExistingHooks = (directory: string): string[] => export const installHooks = ({ cwd = process.cwd(), + force = false, hooksDir = defaultHooksDir, }: InstallHooksOptions = {}): InstallResult => { if (process.env.RSTACK_HOOKS === '0') { @@ -282,16 +361,24 @@ export const installHooks = ({ effectiveHooksDirectory, defaultHooksDirectory, ); + let inactiveHooks: InactiveHooks | undefined; if (!hooksPathMatches && !usesDefaultHooks) { const activeOwner = readOwner(effectiveHooksDirectory); if (!activeOwner) { - return skip( - 'hooks-path-conflict', - `Git hooks are already configured at "${displayPath(gitRoot, effectiveHooksDirectory)}"`, - ); - } - if (activeOwner !== projectPath) { + if (!force) { + return skip( + 'hooks-path-conflict', + `Git hooks are already configured at "${displayPath(gitRoot, effectiveHooksDirectory)}"`, + ); + } + + inactiveHooks = { + hooks: findExistingHooks(effectiveHooksDirectory), + path: displayPath(gitRoot, effectiveHooksDirectory), + restore: 'configure', + }; + } else if (activeOwner !== projectPath) { return ownerConflict(activeOwner); } } @@ -299,13 +386,27 @@ export const installHooks = ({ if (usesDefaultHooks) { const existingHooks = findExistingHooks(defaultHooksDirectory); if (existingHooks.length > 0) { - return skip( - 'existing-git-hooks', - `existing Git hooks were found: ${existingHooks.join(', ')}`, - ); + if (!force) { + return skip( + 'existing-git-hooks', + `existing Git hooks were found: ${existingHooks.join(', ')}`, + ); + } + + inactiveHooks = { + hooks: existingHooks, + path: displayPath(gitRoot, defaultHooksDirectory), + restore: 'unset', + }; } } + // Preserve a worktree-scoped override instead of writing a shadowed local value. + const configScope = hooksPathMatches ? '--local' : resolveHooksPathScope(cwd); + if (typeof configScope !== 'string') { + return configScope; + } + const files = Object.entries(createHookFiles()); try { mkdirSync(directory, { recursive: true }); @@ -346,7 +447,7 @@ export const installHooks = ({ // Point Git at the generated directory only after every runtime file is ready. const configured = runGit(cwd, [ 'config', - '--local', + configScope, 'core.hooksPath', hooksPath, ]); @@ -360,5 +461,20 @@ export const installHooks = ({ ); } - return { status: 'installed', hooksPath }; + const configuredHooksPath = resolveGitHooksPath(cwd); + if (typeof configuredHooksPath !== 'string') { + return configuredHooksPath; + } + if (!isSamePath(configuredHooksPath, directory)) { + return fail( + 'git-config-failed', + `Failed to activate Rstack Git hooks: core.hooksPath resolves to "${displayPath(gitRoot, configuredHooksPath)}" instead of "${hooksPath}".`, + ); + } + + return { + status: 'installed', + hooksPath, + ...(inactiveHooks ? { inactiveHooks } : {}), + }; }; diff --git a/packages/rstack/tests/cli/setup/__snapshots__/index.test.ts.snap b/packages/rstack/tests/cli/setup/__snapshots__/index.test.ts.snap index d238e172..71351935 100644 --- a/packages/rstack/tests/cli/setup/__snapshots__/index.test.ts.snap +++ b/packages/rstack/tests/cli/setup/__snapshots__/index.test.ts.snap @@ -9,6 +9,7 @@ Usage: Install Git hooks in the current repository Options: + -f, --force Install despite an existing Git hooks setup --hooks-dir Specify hooks directory relative to the Git repository root -h, --help Display this help message " diff --git a/packages/rstack/tests/cli/setup/index.test.ts b/packages/rstack/tests/cli/setup/index.test.ts index 0780fd4a..93bda958 100644 --- a/packages/rstack/tests/cli/setup/index.test.ts +++ b/packages/rstack/tests/cli/setup/index.test.ts @@ -1,5 +1,6 @@ import { spawnSync } from 'node:child_process'; import { + chmodSync, existsSync, mkdirSync, mkdtempSync, @@ -34,6 +35,16 @@ const runSetup = (args: string[], runCwd: string = cwd) => env, }); +const runSetupSuccessfully = (args: string[], runCwd: string = cwd): string => { + const result = runSetup(args, runCwd); + if (result.status !== 0) { + throw new Error( + result.stderr || result.error?.message || `Exited with ${result.status}`, + ); + } + return `${result.stdout}${result.stderr}`; +}; + beforeEach(() => { cwd = mkdtempSync(path.join(import.meta.dirname, 'test-temp-rstack setup ')); env = { @@ -108,6 +119,65 @@ test('installs hooks silently without loading Rstack config', ({ expect(execCli('setup', { cwd, env })).toBe(''); }); +test('guides and forces setup while preserving existing hooks', ({ + expect, +}) => { + initRepository(); + const existingHook = path.join(cwd, '.git', 'hooks', 'pre-commit'); + writeFileSync( + existingHook, + "#!/usr/bin/env sh\nprintf 'ran\\n' > old-hook-ran\n", + ); + chmodSync(existingHook, 0o755); + + const skippedOutput = runSetupSuccessfully([]); + expect(skippedOutput).toContain( + 'Git hooks setup skipped: existing Git hooks were found: pre-commit.', + ); + expect(skippedOutput).toContain( + 'To continue, run rs setup --force. Existing hook files will be preserved but become inactive.', + ); + + const forcedOutput = runSetupSuccessfully(['--force']); + expect(forcedOutput).toContain( + 'The previous Git hooks path ".git/hooks" is now inactive: pre-commit.', + ); + expect(forcedOutput).toContain( + 'The existing files were preserved and will become active again if core.hooksPath is unset.', + ); + expect(git(['config', '--local', '--get', 'core.hooksPath'])).toBe(hooksPath); + + git(['hook', 'run', 'pre-commit']); + expect(existsSync(path.join(cwd, 'old-hook-ran'))).toBe(false); + + git(['config', '--local', '--unset', 'core.hooksPath']); + git(['hook', 'run', 'pre-commit']); + expect(existsSync(path.join(cwd, 'old-hook-ran'))).toBe(true); + + expect(runSetupSuccessfully(['-f'])).toContain( + 'The previous Git hooks path ".git/hooks" is now inactive: pre-commit.', + ); +}); + +test('reports how to restore a replaced hooks path', ({ expect }) => { + initRepository(); + const existingDirectory = path.join(cwd, '.husky', '_'); + mkdirSync(existingDirectory, { recursive: true }); + writeFileSync( + path.join(existingDirectory, 'pre-commit'), + '#!/usr/bin/env sh\n', + ); + git(['config', '--local', 'core.hooksPath', '.husky/_']); + + const output = runSetupSuccessfully(['--force']); + expect(output).toContain( + 'The previous Git hooks path ".husky/_" is now inactive: pre-commit.', + ); + expect(output).toContain( + 'The existing files were preserved. Set core.hooksPath back to this path to use them again.', + ); +}); + test('installs root-relative hooks and reports owner conflicts', ({ execCli, expect, @@ -126,9 +196,7 @@ test('installs root-relative hooks and reports owner conflicts', ({ ); expect(existsSync(path.join(cwd, 'custom hooks', '_', 'runner'))).toBe(true); - const conflict = runSetup(['--hooks-dir', 'custom hooks'], docs); - expect(conflict.status).toBe(0); - expect(`${conflict.stdout}${conflict.stderr}`).toContain( + expect(runSetupSuccessfully(['--hooks-dir', 'custom hooks'], docs)).toContain( 'Git hooks are already managed by Rstack project "frontend"', ); }); diff --git a/packages/rstack/tests/fmt/discoverPaths.test.ts b/packages/rstack/tests/fmt/discoverPaths.test.ts index 6cad49f9..7de788c1 100644 --- a/packages/rstack/tests/fmt/discoverPaths.test.ts +++ b/packages/rstack/tests/fmt/discoverPaths.test.ts @@ -321,6 +321,102 @@ test('applies an external ignore matcher to traversed and explicit paths', async }); }); +test('batches external ignore matching after gitignore short-circuiting', async () => { + await withTempProject(async (rootPath) => { + writeProjectFile(rootPath, '.gitignore', 'git-ignored.ts\n'); + writeProjectFile(rootPath, 'config-ignored.ts'); + writeProjectFile(rootPath, 'git-ignored.ts'); + writeProjectFile(rootPath, 'image.png'); + writeProjectFile(rootPath, 'not-included.js'); + writeProjectFile(rootPath, 'visible.ts'); + + const scalarIgnore = rs.fn(() => false); + const maskIgnore = rs.fn( + ( + _parentPath: string, + names: string[], + _directoryMask: number, + candidateMask: number, + ): number => { + const index = names.indexOf('config-ignored.ts'); + return candidateMask & (1 << index); + }, + ); + const isIgnored = Object.assign(scalarIgnore, { + batch: { + matcher: { + isIgnoredBatch: rs.fn(() => new Uint8Array()), + isIgnoredBatchMask: maskIgnore, + isIgnoredChild: rs.fn(() => false), + }, + }, + }); + + const files = await discoverFmtPaths({ + cwd: rootPath, + patterns: ['*.ts'], + isIgnored, + }); + + expect(relativePaths(rootPath, files)).toEqual(['visible.ts']); + expect(scalarIgnore).toHaveBeenCalledTimes(1); + expect(scalarIgnore).toHaveBeenCalledWith(rootPath, true); + expect(maskIgnore).toHaveBeenCalledTimes(1); + + const names = maskIgnore.mock.calls[0][1]; + const candidateMask = maskIgnore.mock.calls[0][3]; + expect(candidateMask & (1 << names.indexOf('.git'))).toBe(0); + expect(candidateMask & (1 << names.indexOf('git-ignored.ts'))).toBe(0); + expect(candidateMask & (1 << names.indexOf('image.png'))).toBe(0); + expect(candidateMask & (1 << names.indexOf('not-included.js'))).toBe(0); + expect(candidateMask & (1 << names.indexOf('visible.ts'))).not.toBe(0); + }); +}); + +test('uses array batches for directories with more than 32 entries', async () => { + await withTempProject(async (rootPath) => { + for (let index = 0; index < 33; index++) { + writeProjectFile(rootPath, `${index}.ts`); + } + + const arrayIgnore = rs.fn( + ( + _parentPath: string, + names: string[], + _directoryFlags: Uint8Array, + candidateFlags: Uint8Array, + ): Uint8Array => { + const ignored = new Uint8Array(names.length); + const index = names.indexOf('32.ts'); + ignored[index] = candidateFlags[index]; + return ignored; + }, + ); + const isIgnored = Object.assign( + rs.fn(() => false), + { + batch: { + matcher: { + isIgnoredBatch: arrayIgnore, + isIgnoredBatchMask: rs.fn(() => 0), + isIgnoredChild: rs.fn(() => false), + }, + }, + }, + ); + + const files = await discoverFmtPaths({ cwd: rootPath, isIgnored }); + + expect(files).toHaveLength(32); + expect(files).not.toContain(path.join(rootPath, '32.ts')); + expect(arrayIgnore).toHaveBeenCalledTimes(1); + const names = arrayIgnore.mock.calls[0][1]; + const candidateFlags = arrayIgnore.mock.calls[0][3]; + expect(candidateFlags[names.indexOf('.git')]).toBe(0); + expect(candidateFlags[names.indexOf('0.ts')]).toBe(1); + }); +}); + test.runIf(process.platform !== 'win32')( 'does not follow file or directory symlinks', async () => { diff --git a/packages/rstack/tests/fmt/discovery.test.ts b/packages/rstack/tests/fmt/discovery.test.ts index 0e062435..e55635e2 100644 --- a/packages/rstack/tests/fmt/discovery.test.ts +++ b/packages/rstack/tests/fmt/discovery.test.ts @@ -1,8 +1,9 @@ import { mkdirSync } from 'node:fs'; import path from 'node:path'; -import { expect, test } from 'rstack/test'; +import { expect, rs, test } from 'rstack/test'; import { normalizeFmtConfig } from '../../src/fmt/config.ts'; import { discoverFmtFiles } from '../../src/fmt/discovery.ts'; +import { loadNativeBinding } from '../../src/native/index.ts'; import type { FmtConfig } from '../../src/fmt/types.ts'; import { withTempProject, writeProjectFile } from './helpers.ts'; @@ -47,6 +48,48 @@ test('applies config ignore patterns to discovered and explicit files', async () }); }); +test('uses the native batch matcher during directory discovery', async () => { + await withTempProject(async (rootPath) => { + writeProjectFile(rootPath, 'generated/blocked.ts'); + writeProjectFile(rootPath, 'generated/keep.ts'); + writeProjectFile(rootPath, 'single/blocked.ts'); + writeProjectFile(rootPath, 'src/index.ts'); + const NativeIgnoreMatcher = loadNativeBinding().IgnoreMatcher; + const batchMatch = rs.spyOn( + NativeIgnoreMatcher.prototype, + 'isIgnoredBatchMask', + ); + const childMatch = rs.spyOn( + NativeIgnoreMatcher.prototype, + 'isIgnoredChild', + ); + const scalarMatch = rs.spyOn(NativeIgnoreMatcher.prototype, 'isIgnored'); + + try { + const files = await discover(rootPath, undefined, { + ignorePatterns: ['generated/blocked.ts', 'single/blocked.ts'], + }); + + expect(relativePaths(rootPath, files)).toEqual([ + path.join('generated', 'keep.ts'), + path.join('src', 'index.ts'), + ]); + expect(batchMatch).toHaveBeenCalled(); + expect(childMatch).toHaveBeenCalledWith( + path.join(rootPath, 'single'), + 'blocked.ts', + false, + ); + expect(scalarMatch).toHaveBeenCalledTimes(1); + expect(scalarMatch).toHaveBeenCalledWith(rootPath, true); + } finally { + batchMatch.mockRestore(); + childMatch.mockRestore(); + scalarMatch.mockRestore(); + } + }); +}); + test('applies config ignore patterns outside the config root', async () => { await withTempProject(async (rootPath) => { const configRoot = path.join(rootPath, 'project'); @@ -81,29 +124,44 @@ test('excludes .rstack from discovery', async () => { }); }); -test('excludes a custom cache directory', async () => { - await withTempProject(async (rootPath) => { - const cacheDir = path.join(rootPath, 'custom-cache'); - const cacheFile = writeProjectFile(rootPath, 'custom-cache/v1.json', '{}'); - writeProjectFile(rootPath, 'custom-cache/nested/ignored.ts'); - writeProjectFile(rootPath, 'index.ts'); - - const discoveredFiles = await discoverFmtFiles({ - cwd: rootPath, - excludedDirPath: cacheDir, - config: normalizeFmtConfig(undefined, rootPath), - }); - const explicitFile = await discoverFmtFiles({ - cwd: rootPath, - excludedDirPath: cacheDir, - patterns: [cacheFile], - config: normalizeFmtConfig(undefined, rootPath), +const customCacheCases: { config?: FmtConfig; name: string }[] = [ + { name: 'with scalar matching' }, + { + name: 'before native matching', + config: { ignorePatterns: ['generated/'] }, + }, +]; + +for (const { config, name } of customCacheCases) { + test(`excludes a custom cache directory ${name}`, async () => { + await withTempProject(async (rootPath) => { + const cacheDir = path.join(rootPath, 'custom-cache'); + const cacheFile = writeProjectFile( + rootPath, + 'custom-cache/v1.json', + '{}', + ); + writeProjectFile(rootPath, 'custom-cache/nested/ignored.ts'); + writeProjectFile(rootPath, 'index.ts'); + const normalizedConfig = normalizeFmtConfig(config, rootPath); + + const discoveredFiles = await discoverFmtFiles({ + cwd: rootPath, + excludedDirPath: cacheDir, + config: normalizedConfig, + }); + const explicitFile = await discoverFmtFiles({ + cwd: rootPath, + excludedDirPath: cacheDir, + patterns: [cacheFile], + config: normalizedConfig, + }); + + expect(relativePaths(rootPath, discoveredFiles)).toEqual(['index.ts']); + expect(explicitFile).toEqual([]); }); - - expect(relativePaths(rootPath, discoveredFiles)).toEqual(['index.ts']); - expect(explicitFile).toEqual([]); }); -}); +} test('keeps files re-included by a CLI ignore file during directory traversal', async () => { await withTempProject(async (rootPath) => { diff --git a/packages/rstack/tests/fmt/nativeIgnore.test.ts b/packages/rstack/tests/fmt/nativeIgnore.test.ts new file mode 100644 index 00000000..69d72ae6 --- /dev/null +++ b/packages/rstack/tests/fmt/nativeIgnore.test.ts @@ -0,0 +1,77 @@ +import path from 'node:path'; +import { expect, test } from 'rstack/test'; +import { loadNativeBinding } from '../../src/native/index.ts'; + +const rootPath = path.join(import.meta.dirname, 'project'); + +const createMatcher = () => + new (loadNativeBinding().IgnoreMatcher)([ + { + rootPath, + patterns: '*.js\n!keep.js\ndist/', + }, + { + rootPath, + patterns: 'keep.js', + }, + ]); + +test('matches selected config ignore entries in native batches', () => { + const matcher = createMatcher(); + const names = ['drop.js', 'keep.js', 'keep.ts', 'dist', 'skipped.js']; + + expect( + Array.from( + matcher.isIgnoredBatch( + rootPath, + names, + new Uint8Array([0, 0, 0, 1, 0]), + new Uint8Array([1, 1, 1, 1, 0]), + ), + ), + ).toEqual([1, 1, 0, 1, 0]); + expect(matcher.isIgnoredBatchMask(rootPath, names, 0b01000, 0b01111)).toBe( + 0b01011, + ); + expect(matcher.isIgnoredChild(rootPath, 'drop.js', false)).toBe(true); + expect(matcher.isIgnoredChild(rootPath, 'keep.ts', false)).toBe(false); +}); + +test('preserves the high bit in native config ignore batch masks', () => { + const matcher = createMatcher(); + const names = Array.from({ length: 32 }, (_, index) => `${index}.ts`); + names[31] = 'drop.js'; + + expect(matcher.isIgnoredBatchMask(rootPath, names, 0, 0x80000000)).toBe( + 0x80000000, + ); +}); + +test('validates native config ignore batch inputs', () => { + const matcher = createMatcher(); + + expect(() => + matcher.isIgnoredBatch( + rootPath, + ['index.js'], + new Uint8Array(), + new Uint8Array([1]), + ), + ).toThrow('Name and directory flag counts must match.'); + expect(() => + matcher.isIgnoredBatch( + rootPath, + ['index.js'], + new Uint8Array([0]), + new Uint8Array(), + ), + ).toThrow('Name and candidate flag counts must match.'); + expect(() => + matcher.isIgnoredBatchMask( + rootPath, + Array.from({ length: 33 }, (_, index) => `${index}.js`), + 0, + 0xffffffff, + ), + ).toThrow('A bit-mask batch cannot contain more than 32 names.'); +}); diff --git a/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts b/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts index cb3dec45..4f1aeba5 100644 --- a/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts +++ b/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts @@ -68,12 +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', ); + writeProjectFile(rootPath, 'script', '#!/usr/bin/env node\nconst value=1'); await expect( runFmtFiles({ @@ -81,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/setup/install.test.ts b/packages/rstack/tests/setup/install.test.ts index f91b2b45..34183036 100644 --- a/packages/rstack/tests/setup/install.test.ts +++ b/packages/rstack/tests/setup/install.test.ts @@ -137,8 +137,12 @@ test('reports Git configuration failures without changing hooksPath', () => { }); }); -test('does not replace another Git hooks path', () => { +test('requires force to replace another Git hooks path', () => { withRepository((cwd) => { + const existingDirectory = path.join(cwd, '.husky', '_'); + const existingHook = path.join(existingDirectory, 'pre-commit'); + mkdirSync(existingDirectory, { recursive: true }); + writeFileSync(existingHook, '#!/usr/bin/env sh\n'); runGit(cwd, ['config', '--local', 'core.hooksPath', '.husky/_']); expect(installHooks({ cwd })).toMatchObject({ @@ -149,10 +153,74 @@ test('does not replace another Git hooks path', () => { '.husky/_', ); expect(existsSync(path.join(cwd, hooksPath))).toBe(false); + + expect(installHooks({ cwd, force: true })).toEqual({ + status: 'installed', + hooksPath, + inactiveHooks: { + hooks: ['pre-commit'], + path: '.husky/_', + restore: 'configure', + }, + }); + expect(runGit(cwd, ['config', '--local', '--get', 'core.hooksPath'])).toBe( + hooksPath, + ); + expect(readFileSync(existingHook, 'utf8')).toBe('#!/usr/bin/env sh\n'); + }); +}); + +test('replaces a worktree-scoped hooks path at the same scope', () => { + withRepository((cwd) => { + runGit(cwd, ['config', '--local', 'extensions.worktreeConfig', 'true']); + runGit(cwd, ['config', '--worktree', 'core.hooksPath', '.husky/_']); + + expect(installHooks({ cwd, force: true }).status).toBe('installed'); + expect( + runGit(cwd, ['config', '--show-scope', '--get', 'core.hooksPath']), + ).toBe(`worktree\t${hooksPath}`); + expect( + git(cwd, ['config', '--local', '--get', 'core.hooksPath']).status, + ).toBe(1); + }); +}); + +test('rejects a command-scoped hooks path override', () => { + withRepository((cwd) => { + const originalCount = process.env.GIT_CONFIG_COUNT; + const originalKey = process.env.GIT_CONFIG_KEY_0; + const originalValue = process.env.GIT_CONFIG_VALUE_0; + process.env.GIT_CONFIG_COUNT = '1'; + process.env.GIT_CONFIG_KEY_0 = 'core.hooksPath'; + process.env.GIT_CONFIG_VALUE_0 = '.husky/_'; + + try { + expect(installHooks({ cwd, force: true })).toMatchObject({ + status: 'failed', + reason: 'hooks-path-command-scope', + }); + } finally { + restoreEnv('GIT_CONFIG_COUNT', originalCount); + restoreEnv('GIT_CONFIG_KEY_0', originalKey); + restoreEnv('GIT_CONFIG_VALUE_0', originalValue); + } + }); +}); + +test('verifies the effective hooks path after configuring Git', () => { + withRepository((cwd) => { + const includedConfig = path.join(cwd, 'included.gitconfig'); + writeFileSync(includedConfig, '[core]\n\thooksPath = .husky/_\n'); + runGit(cwd, ['config', '--local', 'include.path', includedConfig]); + + expect(installHooks({ cwd, force: true })).toMatchObject({ + status: 'failed', + reason: 'git-config-failed', + }); }); }); -test('does not bypass existing Git hooks', () => { +test('requires force to bypass existing Git hooks', () => { withRepository((cwd) => { const existingHook = path.join(cwd, '.git', 'hooks', 'pre-commit'); writeFileSync(existingHook, '#!/usr/bin/env sh\n'); @@ -165,6 +233,54 @@ test('does not bypass existing Git hooks', () => { expect( git(cwd, ['config', '--local', '--get', 'core.hooksPath']).status, ).toBe(1); + + expect(installHooks({ cwd, force: true })).toEqual({ + status: 'installed', + hooksPath, + inactiveHooks: { + hooks: ['pre-commit'], + path: '.git/hooks', + restore: 'unset', + }, + }); + expect(runGit(cwd, ['config', '--local', '--get', 'core.hooksPath'])).toBe( + hooksPath, + ); expect(readFileSync(existingHook, 'utf8')).toBe('#!/usr/bin/env sh\n'); }); }); + +test('force does not replace hooks owned by another Rstack project', () => { + withRepository((cwd) => { + const frontend = path.join(cwd, 'frontend'); + const docs = path.join(cwd, 'docs'); + mkdirSync(frontend); + mkdirSync(docs); + + expect(installHooks({ cwd: frontend }).status).toBe('installed'); + expect(installHooks({ cwd: docs, force: true })).toMatchObject({ + status: 'skipped', + reason: 'owned-by-another-project', + }); + expect(readFileSync(path.join(cwd, hooksPath, '.owner'), 'utf8')).toBe( + 'frontend\n', + ); + }); +}); + +test('force does not replace an invalid Rstack hooks directory', () => { + withRepository((cwd) => { + const directory = path.join(cwd, hooksPath); + mkdirSync(directory, { recursive: true }); + writeFileSync(path.join(directory, '.owner'), 'invalid'); + + expect(installHooks({ cwd, force: true })).toMatchObject({ + status: 'skipped', + reason: 'hooks-directory-conflict', + }); + expect( + git(cwd, ['config', '--local', '--get', 'core.hooksPath']).status, + ).toBe(1); + expect(existsSync(path.join(directory, 'runner'))).toBe(false); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 136751c0..9a78c2cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,8 +20,8 @@ catalogs: specifier: ^2.0.1 version: 2.0.1 '@rslib/core': - specifier: ~1.0.0-beta.3 - version: 1.0.0-beta.3 + specifier: ~1.0.0-rc.1 + version: 1.0.0-rc.1 '@rslint/core': specifier: ~0.8.1 version: 0.8.1 @@ -363,7 +363,7 @@ importers: version: 2.1.13 '@rslib/core': specifier: 'catalog:' - version: 1.0.0-beta.3(typescript@7.0.2) + version: 1.0.0-rc.1(typescript@7.0.2) '@rslint/core': specifier: 'catalog:' version: 0.8.1 @@ -397,7 +397,7 @@ importers: version: 0.11.8(@rsbuild/core@2.1.13)(@rstest/core@0.11.8) '@rstest/adapter-rslib': specifier: 'catalog:' - version: 0.11.8(@rslib/core@1.0.0-beta.3)(@rstest/core@0.11.8)(typescript@7.0.2) + version: 0.11.8(@rslib/core@1.0.0-rc.1)(@rstest/core@0.11.8)(typescript@7.0.2) '@types/micromatch': specifier: 'catalog:' version: 4.0.10 @@ -497,66 +497,66 @@ packages: '@adobe/css-tools@4.5.0': resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} - '@ast-grep/napi-darwin-arm64@0.37.0': - resolution: {integrity: sha512-QAiIiaAbLvMEg/yBbyKn+p1gX2/FuaC0SMf7D7capm/oG4xGMzdeaQIcSosF4TCxxV+hIH4Bz9e4/u7w6Bnk3Q==} + '@ast-grep/napi-darwin-arm64@0.45.1': + resolution: {integrity: sha512-CthYcA0H3z5A2EHKH4rhSuFzpYUtxqUMnohmejxtMS6wiAgriKhOCopxN8XKclspYMtQyX2GC/PbvcU3dIbQHw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@ast-grep/napi-darwin-x64@0.37.0': - resolution: {integrity: sha512-zvcvdgekd4ySV3zUbUp8HF5nk5zqwiMXTuVzTUdl/w08O7JjM6XPOIVT+d2o/MqwM9rsXdzdergY5oY2RdhSPA==} + '@ast-grep/napi-darwin-x64@0.45.1': + resolution: {integrity: sha512-PhlXMDEEH5fMjXjYcrbeNgGqrojj4zN8C7eMQC6M4pVkuW74uPhrblD4KUbNunp5I2LkWCdKOs36jhBaZe1iPw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@ast-grep/napi-linux-arm64-gnu@0.37.0': - resolution: {integrity: sha512-L7Sj0lXy8X+BqSMgr1LB8cCoWk0rericdeu+dC8/c8zpsav5Oo2IQKY1PmiZ7H8IHoFBbURLf8iklY9wsD+cyA==} + '@ast-grep/napi-linux-arm64-gnu@0.45.1': + resolution: {integrity: sha512-XScjs95/SrsV6kLl9MnF2qbqwKU79bcBA3JHi6xUu+hf0uZ0lc6MsZ595cEy5yw9PYRhgqwrT3wta9p4qU4jpg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@ast-grep/napi-linux-arm64-musl@0.37.0': - resolution: {integrity: sha512-LF9sAvYy6es/OdyJDO3RwkX3I82Vkfsng1sqUBcoWC1jVb1wX5YVzHtpQox9JrEhGl+bNp7FYxB4Qba9OdA5GA==} + '@ast-grep/napi-linux-arm64-musl@0.45.1': + resolution: {integrity: sha512-qjH5CN5KIUdJW2dHfp8W57QsP6H0mA6E/rboKEzAxw6Ant1q2ZKqE3bLHUZMDoZXOBGdCODSZm0bTDcctiP49g==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@ast-grep/napi-linux-x64-gnu@0.37.0': - resolution: {integrity: sha512-TViz5/klqre6aSmJzswEIjApnGjJzstG/SE8VDWsrftMBMYt2PTu3MeluZVwzSqDao8doT/P+6U11dU05UOgxw==} + '@ast-grep/napi-linux-x64-gnu@0.45.1': + resolution: {integrity: sha512-nPP0y5EnehCgmYqvVDKSvH4vHbEFdAi8L+Kq2Sz94vK32yv4eOWf9vWhgoPscWpw76GWuwhpDvME8SZARnS3DQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@ast-grep/napi-linux-x64-musl@0.37.0': - resolution: {integrity: sha512-/BcCH33S9E3ovOAEoxYngUNXgb+JLg991sdyiNP2bSoYd30a9RHrG7CYwW6fMgua3ijQ474eV6cq9yZO1bCpXg==} + '@ast-grep/napi-linux-x64-musl@0.45.1': + resolution: {integrity: sha512-sg50LKH7TskWd/boWVFp9gwKc3Jd8sg0f8P6T2VQemX/EOj+OFaMJ2+y7Ok17WI/dODkrZgAPUwq7RAvMzLtqg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@ast-grep/napi-win32-arm64-msvc@0.37.0': - resolution: {integrity: sha512-TjQA4cFoIEW2bgjLkaL9yqT4XWuuLa5MCNd0VCDhGRDMNQ9+rhwi9eLOWRaap3xzT7g+nlbcEHL3AkVCD2+b3A==} + '@ast-grep/napi-win32-arm64-msvc@0.45.1': + resolution: {integrity: sha512-wnTmD34OD9bUpdBVTb58P0y+YPb/2C2g07zj96LSk5R4pdcEMrCFsrWZlaNwtEV4q7L9BfLgrxq3MBGI8c6doA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@ast-grep/napi-win32-ia32-msvc@0.37.0': - resolution: {integrity: sha512-uNmVka8fJCdYsyOlF9aZqQMLTatEYBynjChVTzUfFMDfmZ0bihs/YTqJVbkSm8TZM7CUX82apvn50z/dX5iWRA==} + '@ast-grep/napi-win32-ia32-msvc@0.45.1': + resolution: {integrity: sha512-gRyJwRPY1mWRtnJ6AMncV2pNU+B2indjLCb9z/+aZrUN5u/gOxYryEzvRatyC9rVUMeQGJD+k0htpsqIwgXVbA==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@ast-grep/napi-win32-x64-msvc@0.37.0': - resolution: {integrity: sha512-vCiFOT3hSCQuHHfZ933GAwnPzmL0G04JxQEsBRfqONywyT8bSdDc/ECpAfr3S9VcS4JZ9/F6tkePKW/Om2Dq2g==} + '@ast-grep/napi-win32-x64-msvc@0.45.1': + resolution: {integrity: sha512-mzfMmCO7tSNks+NGkGkSnDBKxBjHoOLAMJt2IbAkMYATxHC0RqfBoN7Qtd02VGhHWEfwWLPv/wU6MrvweZ3jdQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@ast-grep/napi@0.37.0': - resolution: {integrity: sha512-Hb4o6h1Pf6yRUAX07DR4JVY7dmQw+RVQMW5/m55GoiAT/VRoKCWBtIUPPOnqDVhbx1Cjfil9b6EDrgJsUAujEQ==} + '@ast-grep/napi@0.45.1': + resolution: {integrity: sha512-4cmGQEHoP9GLHEhGvd1vgSzO3Y6KF7FczDk4+q/VfXV9/9sNxNVgNHC8t3BglhIADDcoHrCMc9aw4lHG+M240A==} engines: {node: '>= 10'} '@babel/code-frame@7.29.7': @@ -1295,6 +1295,16 @@ packages: core-js: optional: true + '@rsbuild/core@2.2.0-rc.0': + resolution: {integrity: sha512-f6orjv+wOR1u7KchE/vANGP0Eg7AP0UaiM0qn4n+5I0HOUdkVVbNCA1eZSpyX+TJ9bIdZtkbR6T47vBdoFr1MA==} + 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: @@ -1311,8 +1321,8 @@ packages: '@rsbuild/core': optional: true - '@rslib/core@1.0.0-beta.3': - resolution: {integrity: sha512-OtfmaBoGlHo1KYvQ3+B0ZLy3zUsAdoRZfWieaxoJMJ9uKAws2//afFgvUqeSDkkSJDlp8TDdgt4hib+QaFOaGQ==} + '@rslib/core@1.0.0-rc.1': + resolution: {integrity: sha512-xKK79mKdxhqi+tNpbIaJgAUWv1RkH5En+W6eBflXUJGhoSacx97iCoLLQl9mAIti0c7l3zpGlvOlnwxTP2llaQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -1387,6 +1397,11 @@ packages: cpu: [arm64] os: [darwin] + '@rspack/binding-darwin-arm64@2.2.0-rc.0': + resolution: {integrity: sha512-Uvy3YnN1pCLe+2/8hF06KiCPegf8ztOuM3VE/p0MJKRitkL5DPoZVHyc40zl6e+O5WB/9tJ5tnEgRG9pDWxFng==} + cpu: [arm64] + os: [darwin] + '@rspack/binding-darwin-x64@2.1.10': resolution: {integrity: sha512-my/0h2LwxCRT6cg3oDDC2e0ZOxQLVajAdIcv0fqnQk5JRNvVuL89PuTutitnSqie1A0/JSL8OQz5XHwmoS3kow==} cpu: [x64] @@ -1397,6 +1412,11 @@ packages: cpu: [x64] os: [darwin] + '@rspack/binding-darwin-x64@2.2.0-rc.0': + resolution: {integrity: sha512-UQO0PgLarsA0lv96uqCTAH1eAnAIPHb+qhMfds5ubWKZgKlr0taGQl253C3DN5pfzbHWfNQgAfVUaVMydm9czw==} + cpu: [x64] + os: [darwin] + '@rspack/binding-linux-arm64-gnu@2.1.10': resolution: {integrity: sha512-laevn9g+E5PAUEGqiKe6Ju5KApsuQYp+bPI17XS3Lkl8eqL5pS/BmHYU7QMlst4GzV8+wlruVTMh//+st6Vqzg==} cpu: [arm64] @@ -1409,6 +1429,12 @@ packages: os: [linux] libc: [glibc] + '@rspack/binding-linux-arm64-gnu@2.2.0-rc.0': + resolution: {integrity: sha512-ZggL6W9ckpvhqIPi7K3zDRiEaQd5Co2qRnN5J8OMmBkQlvYQek0CE/SZHFcZsDM0//6+0mkI+VjaTeWdvqJsaQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@rspack/binding-linux-arm64-musl@2.1.10': resolution: {integrity: sha512-V71+Qz5G72+ROZXrJn5zxOszdG1AEbO8pcC/itXXtf4yRR6a3bVHKNKGhipBNxb8eI6cnD/01FH1h3ZG655jLw==} cpu: [arm64] @@ -1421,12 +1447,24 @@ packages: os: [linux] libc: [musl] + '@rspack/binding-linux-arm64-musl@2.2.0-rc.0': + resolution: {integrity: sha512-MfDTg9fn/17lRtGMsHLK8BQJs+AEtTsVMWOg1CMou/ls8IrucXoFZ9Ux0i2Jq1ph1ZiKgr4l7pMzdk3SXpNiQg==} + 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-ppc64-gnu@2.2.0-rc.0': + resolution: {integrity: sha512-n84s99eIMr/4xQ1XCctxtu4AnchukynuyJ4DaJ4vlcRKVdFAnPSpUH669rAxztsby5xa3ftAASmxwXhMUFzMsg==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + '@rspack/binding-linux-riscv64-gnu@2.1.10': resolution: {integrity: sha512-GMGTJpy9/ecE+5F5IfxZH4bXv0Wx/b2TiehTlCbTksbL+pKpLHYy0rwGdjWDKbmBkhxMMqPiC7PDnn9LbdnnLA==} cpu: [riscv64] @@ -1439,6 +1477,12 @@ packages: os: [linux] libc: [glibc] + '@rspack/binding-linux-riscv64-gnu@2.2.0-rc.0': + resolution: {integrity: sha512-s5bg/LlwnMSVXZfR16KGO3jVWUpobbPp90qE2DXwfaFpcLmMweUnANERM2mCpIiW3MuVnTAXTEjvEcxfB4mXEw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + '@rspack/binding-linux-riscv64-musl@2.1.10': resolution: {integrity: sha512-rkurnAWc04vIbzG1QCrPBWSJadZvaOt1mazFH3EdiJO8VUiu0I1T9zdiwuDOPrd50lOKIZlcTXbd5aaAkWEnvQ==} cpu: [riscv64] @@ -1451,12 +1495,24 @@ packages: os: [linux] libc: [musl] + '@rspack/binding-linux-riscv64-musl@2.2.0-rc.0': + resolution: {integrity: sha512-XNjEnkrNv67CZ4/IhkPQVfNkz9JHevelgZspzuuJOOyn+Z9b1m8JN+shv5Kq06sSudN9aQpLLa6slbHlKGv9lA==} + 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-s390x-gnu@2.2.0-rc.0': + resolution: {integrity: sha512-OQpj1j6Jfy+YBYDSGwJisZZEXS3g0Y/M5xlb26yqlZBKA/7kamCMDccUPTKNpuJaRQXK1DUerBsA+AK8TELG3g==} + cpu: [s390x] + os: [linux] + libc: [glibc] + '@rspack/binding-linux-x64-gnu@2.1.10': resolution: {integrity: sha512-Fat09V6jUuyo9qG7Wyj9cQ31VDfLmokXyBtGqKxY5OvSWHereB7QUub5btbPXHwbp6Iq4aAQyUbbLTzvR1YaBw==} cpu: [x64] @@ -1469,6 +1525,12 @@ packages: os: [linux] libc: [glibc] + '@rspack/binding-linux-x64-gnu@2.2.0-rc.0': + resolution: {integrity: sha512-1QoW+7zjApCgL4v5P14AmnxfvOMCk131abSBCl/+u6h/aIainSwpf+O3ar0FqvkQaSPPOQJrg03ys57WyEwlAQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + '@rspack/binding-linux-x64-musl@2.1.10': resolution: {integrity: sha512-lhHOnIJ4ClpIlA1f1L8aoxEZivYLjnjq5A6jKKz7BKsm+cHK8kqqEm6lO5KqA5xQT0Lonq1o28bmKHEj6JHInw==} cpu: [x64] @@ -1481,6 +1543,12 @@ packages: os: [linux] libc: [musl] + '@rspack/binding-linux-x64-musl@2.2.0-rc.0': + resolution: {integrity: sha512-KLN4bIC3M1dSEuPBX1SPOEkBRyZnnIx8vBjfIFFpKa1bw/CsOHDF7Y6IAAsf9hir7dH3cUd4vaPSCVRM4KiTDw==} + cpu: [x64] + os: [linux] + libc: [musl] + '@rspack/binding-wasm32-wasi@2.1.10': resolution: {integrity: sha512-KY5YbWbuvYcoaLXnV+vzZOvGRCeb6jt4EpVpKdph1h1IJjwX/ju15EQ+GOe3iecZEdf0OttQcNVcwBkLkFT9ag==} cpu: [wasm32] @@ -1489,6 +1557,10 @@ packages: resolution: {integrity: sha512-om7GAKWAU3lcSvbCon2m7mzw8v9OTrO2LW2MZ1lGe/uVJJmwGGkl9HVoXFyWFLrN6YVFyx8iP+AkN4owDWB9Cw==} cpu: [wasm32] + '@rspack/binding-wasm32-wasi@2.2.0-rc.0': + resolution: {integrity: sha512-2Vvtckz5DNghQKQKwghfM4j30MOu7t9J7mbUCCi7mTUccpy7Cnr0HPmSV9Jx4sUj6vgQEV+8lIAIxNDNtr0Vhg==} + cpu: [wasm32] + '@rspack/binding-win32-arm64-msvc@2.1.10': resolution: {integrity: sha512-z4GWzMLofaDGpAt9Z+MlN88LlUBDm+zM6R2GdOOPM6/4g/h3/+47OP7casmSL3AwTGYBEJqogwt08sRSosB6Cg==} cpu: [arm64] @@ -1499,6 +1571,11 @@ packages: cpu: [arm64] os: [win32] + '@rspack/binding-win32-arm64-msvc@2.2.0-rc.0': + resolution: {integrity: sha512-enPqTT2sdt6HgItyk6SA6ubvZ2UXkFIpwhsCuoL83z9vHoDw/Y8obC8L92nIuK6FDP17tokm/r1SFPhDZJIYcg==} + cpu: [arm64] + os: [win32] + '@rspack/binding-win32-ia32-msvc@2.1.10': resolution: {integrity: sha512-7qcWdsZ+GuGtzKjqgy7wTN7Dso/ezIY8yhx1r2yIbcczdmXj4FhaEampMDp/25HwtKwIGBBoh6HHSt3JWxpTUg==} cpu: [ia32] @@ -1509,6 +1586,11 @@ packages: cpu: [ia32] os: [win32] + '@rspack/binding-win32-ia32-msvc@2.2.0-rc.0': + resolution: {integrity: sha512-qYeZLJDfboqkWmNcqazjUcld2QegyErDJVaCfAPm2mnneMmKhQWsOs/DmlqRfC4ePaQN9uOtu26iLwKy2o1sgw==} + cpu: [ia32] + os: [win32] + '@rspack/binding-win32-x64-msvc@2.1.10': resolution: {integrity: sha512-pgp23pLrzfhGnKycxzr7ifP17lAbWZEfnx1bX8gXtYrnpJ66DRNyTKSzxB6sa/HBWjS1L8PX5TjMZ44WfPydqQ==} cpu: [x64] @@ -1519,12 +1601,20 @@ packages: cpu: [x64] os: [win32] + '@rspack/binding-win32-x64-msvc@2.2.0-rc.0': + resolution: {integrity: sha512-bvzp27ZvpZW1vcCG/srk/K/6cffcR/kqDUcW8dWEMFlntPSBTml9lOC4ouSBpU7n/qIwG6hKE56FaTWWapc3Lw==} + 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/binding@2.2.0-rc.0': + resolution: {integrity: sha512-/Q9ysTd5ajEbIS+Sv61trElR7pWAa5LvcWNrYT+AKI9APsVwy4Y3CIPjEkd7jYeNHl1PJWSLCOUy+BzRUICDUg==} + '@rspack/core@2.1.10': resolution: {integrity: sha512-YSS2/Xxz8uiG/KXDkqOoA3dTetNo/vysk7bAexQOrU8iuq7JuzDTTAwLKvWZnwmvME8M8m5wcM4YvfIwYmidHA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1549,6 +1639,18 @@ packages: '@swc/helpers': optional: true + '@rspack/core@2.2.0-rc.0': + resolution: {integrity: sha512-2LAdAFF/ZdnBRltI+IievGhysby9LESxvELxS1ZVkPc1F6ZAJ1skIcCNOLmfZeoYwQ5nXZjdUCbCf9MFDMWJjg==} + 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/plugin-react-refresh@2.0.2': resolution: {integrity: sha512-dGNZiCxQxgAUI9sah7gd8u+O7OJZRCmqtEJNDOd8xW5RqcieC86F7p5qcShyw6onH5pKf57evpr2VjGbaFGkZg==} peerDependencies: @@ -2819,8 +2921,8 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} - rsbuild-plugin-dts@1.0.0-beta.3: - resolution: {integrity: sha512-Q8x/yyOsy8sNR8sHn0xuZsul7ErT5kXkTmDwCIxQ8esiMCW7QKjfyXDa4kvTxWn7oSQ5NP/O6qZM2vEA07thKw==} + rsbuild-plugin-dts@1.0.0-rc.1: + resolution: {integrity: sha512-erD9AHYfEr5I7yCI9z2qR3NJTnwl1JgmFErjiVeOo7cKjItzaG2QrOqmlYM0DAfzPTktTznPaqpmqkr1kI6pqA==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: '@microsoft/api-extractor': ^7 @@ -3197,44 +3299,44 @@ snapshots: '@adobe/css-tools@4.5.0': {} - '@ast-grep/napi-darwin-arm64@0.37.0': + '@ast-grep/napi-darwin-arm64@0.45.1': optional: true - '@ast-grep/napi-darwin-x64@0.37.0': + '@ast-grep/napi-darwin-x64@0.45.1': optional: true - '@ast-grep/napi-linux-arm64-gnu@0.37.0': + '@ast-grep/napi-linux-arm64-gnu@0.45.1': optional: true - '@ast-grep/napi-linux-arm64-musl@0.37.0': + '@ast-grep/napi-linux-arm64-musl@0.45.1': optional: true - '@ast-grep/napi-linux-x64-gnu@0.37.0': + '@ast-grep/napi-linux-x64-gnu@0.45.1': optional: true - '@ast-grep/napi-linux-x64-musl@0.37.0': + '@ast-grep/napi-linux-x64-musl@0.45.1': optional: true - '@ast-grep/napi-win32-arm64-msvc@0.37.0': + '@ast-grep/napi-win32-arm64-msvc@0.45.1': optional: true - '@ast-grep/napi-win32-ia32-msvc@0.37.0': + '@ast-grep/napi-win32-ia32-msvc@0.45.1': optional: true - '@ast-grep/napi-win32-x64-msvc@0.37.0': + '@ast-grep/napi-win32-x64-msvc@0.45.1': optional: true - '@ast-grep/napi@0.37.0': + '@ast-grep/napi@0.45.1': optionalDependencies: - '@ast-grep/napi-darwin-arm64': 0.37.0 - '@ast-grep/napi-darwin-x64': 0.37.0 - '@ast-grep/napi-linux-arm64-gnu': 0.37.0 - '@ast-grep/napi-linux-arm64-musl': 0.37.0 - '@ast-grep/napi-linux-x64-gnu': 0.37.0 - '@ast-grep/napi-linux-x64-musl': 0.37.0 - '@ast-grep/napi-win32-arm64-msvc': 0.37.0 - '@ast-grep/napi-win32-ia32-msvc': 0.37.0 - '@ast-grep/napi-win32-x64-msvc': 0.37.0 + '@ast-grep/napi-darwin-arm64': 0.45.1 + '@ast-grep/napi-darwin-x64': 0.45.1 + '@ast-grep/napi-linux-arm64-gnu': 0.45.1 + '@ast-grep/napi-linux-arm64-musl': 0.45.1 + '@ast-grep/napi-linux-x64-gnu': 0.45.1 + '@ast-grep/napi-linux-x64-musl': 0.45.1 + '@ast-grep/napi-win32-arm64-msvc': 0.45.1 + '@ast-grep/napi-win32-ia32-msvc': 0.45.1 + '@ast-grep/napi-win32-x64-msvc': 0.45.1 '@babel/code-frame@7.29.7': dependencies: @@ -3886,6 +3988,13 @@ snapshots: transitivePeerDependencies: - '@module-federation/runtime-tools' + '@rsbuild/core@2.2.0-rc.0': + dependencies: + '@rspack/core': 2.2.0-rc.0(@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) @@ -3923,10 +4032,10 @@ snapshots: optionalDependencies: '@rsbuild/core': 2.1.10 - '@rslib/core@1.0.0-beta.3(typescript@7.0.2)': + '@rslib/core@1.0.0-rc.1(typescript@7.0.2)': dependencies: - '@rsbuild/core': 2.1.13 - rsbuild-plugin-dts: 1.0.0-beta.3(@rsbuild/core@2.1.13)(typescript@7.0.2) + '@rsbuild/core': 2.2.0-rc.0 + rsbuild-plugin-dts: 1.0.0-rc.1(@rsbuild/core@2.2.0-rc.0)(typescript@7.0.2) optionalDependencies: typescript: 7.0.2 transitivePeerDependencies: @@ -3976,54 +4085,84 @@ snapshots: '@rspack/binding-darwin-arm64@2.1.8': optional: true + '@rspack/binding-darwin-arm64@2.2.0-rc.0': + optional: true + '@rspack/binding-darwin-x64@2.1.10': optional: true '@rspack/binding-darwin-x64@2.1.8': optional: true + '@rspack/binding-darwin-x64@2.2.0-rc.0': + 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-gnu@2.2.0-rc.0': + 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-arm64-musl@2.2.0-rc.0': + optional: true + '@rspack/binding-linux-ppc64-gnu@2.1.10': optional: true + '@rspack/binding-linux-ppc64-gnu@2.2.0-rc.0': + 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-gnu@2.2.0-rc.0': + 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-riscv64-musl@2.2.0-rc.0': + optional: true + '@rspack/binding-linux-s390x-gnu@2.1.10': optional: true + '@rspack/binding-linux-s390x-gnu@2.2.0-rc.0': + 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-gnu@2.2.0-rc.0': + optional: true + '@rspack/binding-linux-x64-musl@2.1.10': optional: true '@rspack/binding-linux-x64-musl@2.1.8': optional: true + '@rspack/binding-linux-x64-musl@2.2.0-rc.0': + optional: true + '@rspack/binding-wasm32-wasi@2.1.10': dependencies: '@emnapi/core': 1.11.3 @@ -4038,24 +4177,40 @@ snapshots: '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) optional: true + '@rspack/binding-wasm32-wasi@2.2.0-rc.0': + 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-win32-arm64-msvc@2.1.10': optional: true '@rspack/binding-win32-arm64-msvc@2.1.8': optional: true + '@rspack/binding-win32-arm64-msvc@2.2.0-rc.0': + 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-ia32-msvc@2.2.0-rc.0': + optional: true + '@rspack/binding-win32-x64-msvc@2.1.10': optional: true '@rspack/binding-win32-x64-msvc@2.1.8': optional: true + '@rspack/binding-win32-x64-msvc@2.2.0-rc.0': + optional: true + '@rspack/binding@2.1.10': optionalDependencies: '@rspack/binding-darwin-arm64': 2.1.10 @@ -4088,6 +4243,23 @@ snapshots: '@rspack/binding-win32-ia32-msvc': 2.1.8 '@rspack/binding-win32-x64-msvc': 2.1.8 + '@rspack/binding@2.2.0-rc.0': + optionalDependencies: + '@rspack/binding-darwin-arm64': 2.2.0-rc.0 + '@rspack/binding-darwin-x64': 2.2.0-rc.0 + '@rspack/binding-linux-arm64-gnu': 2.2.0-rc.0 + '@rspack/binding-linux-arm64-musl': 2.2.0-rc.0 + '@rspack/binding-linux-ppc64-gnu': 2.2.0-rc.0 + '@rspack/binding-linux-riscv64-gnu': 2.2.0-rc.0 + '@rspack/binding-linux-riscv64-musl': 2.2.0-rc.0 + '@rspack/binding-linux-s390x-gnu': 2.2.0-rc.0 + '@rspack/binding-linux-x64-gnu': 2.2.0-rc.0 + '@rspack/binding-linux-x64-musl': 2.2.0-rc.0 + '@rspack/binding-wasm32-wasi': 2.2.0-rc.0 + '@rspack/binding-win32-arm64-msvc': 2.2.0-rc.0 + '@rspack/binding-win32-ia32-msvc': 2.2.0-rc.0 + '@rspack/binding-win32-x64-msvc': 2.2.0-rc.0 + '@rspack/core@2.1.10(@swc/helpers@0.5.23)': dependencies: '@rspack/binding': 2.1.10 @@ -4100,6 +4272,12 @@ snapshots: optionalDependencies: '@swc/helpers': 0.5.23 + '@rspack/core@2.2.0-rc.0(@swc/helpers@0.5.23)': + dependencies: + '@rspack/binding': 2.2.0-rc.0 + optionalDependencies: + '@swc/helpers': 0.5.23 + '@rspack/plugin-react-refresh@2.0.2(@rspack/core@2.1.10)(react-refresh@0.18.0)': dependencies: react-refresh: 0.18.0 @@ -4189,9 +4367,9 @@ snapshots: '@rsbuild/core': 2.1.13 '@rstest/core': 0.11.8(happy-dom@20.11.2) - '@rstest/adapter-rslib@0.11.8(@rslib/core@1.0.0-beta.3)(@rstest/core@0.11.8)(typescript@7.0.2)': + '@rstest/adapter-rslib@0.11.8(@rslib/core@1.0.0-rc.1)(@rstest/core@0.11.8)(typescript@7.0.2)': dependencies: - '@rslib/core': 1.0.0-beta.3(typescript@7.0.2) + '@rslib/core': 1.0.0-rc.1(typescript@7.0.2) '@rstest/core': 0.11.8(happy-dom@20.11.2) optionalDependencies: typescript: 7.0.2 @@ -5659,10 +5837,10 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 - rsbuild-plugin-dts@1.0.0-beta.3(@rsbuild/core@2.1.13)(typescript@7.0.2): + rsbuild-plugin-dts@1.0.0-rc.1(@rsbuild/core@2.2.0-rc.0)(typescript@7.0.2): dependencies: - '@ast-grep/napi': 0.37.0 - '@rsbuild/core': 2.1.13 + '@ast-grep/napi': 0.45.1 + '@rsbuild/core': 2.2.0-rc.0 optionalDependencies: typescript: 7.0.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4d7e2214..d386e1d9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -16,7 +16,7 @@ catalog: '@rsbuild/core': '~2.1.13' '@rsbuild/plugin-react': '^2.1.0' '@rsbuild/plugin-sass': '^2.0.1' - '@rslib/core': '~1.0.0-beta.3' + '@rslib/core': '~1.0.0-rc.1' '@rslint/core': '~0.8.1' '@rspress/core': '^2.0.19' '@rspress/plugin-client-redirects': '^2.0.19' diff --git a/scripts/dictionary.txt b/scripts/dictionary.txt index 06b17167..8c07ec80 100644 --- a/scripts/dictionary.txt +++ b/scripts/dictionary.txt @@ -6,17 +6,20 @@ dirents editmsg errexit esac +esbenp extglob fnames huskyrc indentable jsonline llms +MJML napi noformat noprettier nosystem oxfmt +precheck quasis rsbuild rslib @@ -30,6 +33,7 @@ rstest shiki shikijs solidjs +Trae turborepo typicode worktank 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/cli/setup.mdx b/website/docs/en/guide/cli/setup.mdx index 2ea8bbe9..6a960433 100644 --- a/website/docs/en/guide/cli/setup.mdx +++ b/website/docs/en/guide/cli/setup.mdx @@ -41,12 +41,26 @@ rs staged :::warning Existing Git hook managers -`rs setup` updates the repository's [`core.hooksPath`](https://git-scm.com/docs/git-config#Documentation/git-config.txt-corehooksPath). It skips installation when another hooks path or existing Git hook is detected. Migrate the required hooks and remove the existing hooks configuration before running the command. +`rs setup` updates the repository's [`core.hooksPath`](https://git-scm.com/docs/git-config#Documentation/git-config.txt-corehooksPath). It skips installation when another hooks path or existing Git hook is detected. Run `rs setup --force` to install Rstack hooks anyway. ::: ## Options +### `--force` + +`--force` (or `-f`) installs Rstack hooks even when an existing Git hooks setup is detected: + +```bash +rs setup --force +``` + +Rstack preserves the existing hook files and sets `core.hooksPath` to its generated hooks directory. While this setting is active, Git no longer runs hooks from the previous location. + +:::tip +Run `rs setup --force` only once. Use `rs setup` without `--force` in the `prepare` script. +::: + ### `--hooks-dir` Sets the directory for hook scripts, relative to the Git repository root. @@ -171,23 +185,37 @@ To change the owner, remove `rs setup` from the previous project's `prepare` scr To remove hooks managed by Rstack CLI: 1. Remove `rs setup` from the `prepare` script. -2. Unset the repository's hooks path: +2. Check which Git configuration scope defines the active hooks path: + + ```bash + git config --show-scope --get core.hooksPath + ``` + +3. Unset the hooks path in the reported scope. For `local`, run: ```bash git config --local --unset core.hooksPath ``` -3. Delete `.rstack/hooks/`, or the directory passed to `--hooks-dir`. + For `worktree`, run: + + ```bash + git config --worktree --unset core.hooksPath + ``` + + If `--force` previously preserved hooks under `.git/hooks`, unsetting the path reactivates those files. Delete any obsolete files first if you do not want them to run. + +4. Delete `.rstack/hooks/`, or the directory passed to `--hooks-dir`. ## Troubleshooting ### Hook does not run - Check that the hook script has a [supported name](#supported-hooks) and is next to the `_` directory. -- Run `git config --local --get core.hooksPath` and verify the configured path. +- Run `git config --show-scope --get core.hooksPath` and verify the effective scope and path. - 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 hooks setup is reported, run `rs setup --force`. - 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 CLI runs them with `sh`. diff --git a/website/docs/en/guide/cli/staged.mdx b/website/docs/en/guide/cli/staged.mdx index 0ff122f8..d3938e6b 100644 --- a/website/docs/en/guide/cli/staged.mdx +++ b/website/docs/en/guide/cli/staged.mdx @@ -26,12 +26,36 @@ rs staged --allow-empty ### `--concurrent` -`--concurrent` (or `-p`) sets how many tasks run concurrently; use `false` to run them serially. +`--concurrent` controls how many tasks may run at the same time. It accepts `true`, `false`, or a positive integer. + +When the option is omitted, it defaults to `true`, so `rs staged` runs tasks concurrently without a fixed limit. Set it to `false` to run one task at a time, or use a positive integer to limit the number of concurrent tasks. ```bash +# Use the default: run tasks concurrently without a fixed limit +rs staged +rs staged --concurrent true + +# Run one task at a time rs staged --concurrent false + +# Run at most four tasks at a time +rs staged --concurrent 4 + +# -p is shorthand for --concurrent +rs staged -p 4 +``` + +For example: + +```ts +define.staged({ + '*.ts': ['rs lint --fix', 'rs fmt'], + '*.md': 'rs fmt', +}); ``` +With concurrency enabled, `rs fmt` for Markdown files may run while TypeScript files are being linted. For TypeScript files, `rs fmt` starts only after `rs lint --fix` finishes. + ### `--cwd` `--cwd` sets the working directory used to run all tasks. @@ -96,7 +120,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/formatting.mdx b/website/docs/en/guide/formatting.mdx index c1c3a1ce..d77d469d 100644 --- a/website/docs/en/guide/formatting.mdx +++ b/website/docs/en/guide/formatting.mdx @@ -54,6 +54,19 @@ In addition to Prettier options and `overrides`, Rstack CLI 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: diff --git a/website/docs/en/guide/git-hooks.mdx b/website/docs/en/guide/git-hooks.mdx new file mode 100644 index 00000000..800f9016 --- /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 --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. Run `rs setup --force` to install Rstack hooks anyway. See the [`rs setup` guide](./cli/setup) 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..5df4a28a 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 wrapCode +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. +```text wrapCode +Use the migrate-to-rstack-cli Skill to migrate Rslint to Rstack CLI, leaving all other tools unchanged. ``` +Once you have reviewed and validated the changes, move on to Rstest: + +```text wrapCode +Use the migrate-to-rstack-cli Skill to migrate Rstest 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/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/cli/setup.mdx b/website/docs/zh/guide/cli/setup.mdx index ea39d804..cdef8ab9 100644 --- a/website/docs/zh/guide/cli/setup.mdx +++ b/website/docs/zh/guide/cli/setup.mdx @@ -41,12 +41,26 @@ rs staged :::warning 已有 Git hook 管理工具 -`rs setup` 会更新仓库的 [`core.hooksPath`](https://git-scm.com/docs/git-config#Documentation/git-config.txt-corehooksPath)。检测到其他 hooks 路径或已有 Git hook 时,命令会跳过安装。请先迁移所需的 hooks 并移除已有 hooks 配置,再运行该命令。 +`rs setup` 会更新仓库的 [`core.hooksPath`](https://git-scm.com/docs/git-config#Documentation/git-config.txt-corehooksPath)。检测到其他 hooks 路径或已有 Git hook 时,命令会跳过安装。可运行 `rs setup --force` 强制安装 Rstack hooks。 ::: ## 选项 \{#options} +### `--force` + +检测到已有 Git hooks 配置时,可使用 `--force`(或 `-f`)强制安装 Rstack hooks: + +```bash +rs setup --force +``` + +Rstack 会保留原有 hook 文件,并将 `core.hooksPath` 指向 Rstack 生成的 hooks 目录。该配置生效期间,Git 不再执行原路径下的 hooks。 + +:::tip +`rs setup --force` 只需运行一次。`prepare` 脚本中应使用不带 `--force` 的 `rs setup`。 +::: + ### `--hooks-dir` 设置 hook 脚本的存放目录,路径相对于 Git 仓库根目录。 @@ -171,23 +185,37 @@ rs staged 如需移除由 Rstack CLI 管理的 hooks: 1. 从 `prepare` 脚本中移除 `rs setup`。 -2. 删除仓库的 hooks 路径配置: +2. 检查当前生效的 hooks 路径来自哪个 Git 配置作用域: + + ```bash + git config --show-scope --get core.hooksPath + ``` + +3. 根据输出,在对应作用域中取消 hooks 路径配置。作用域为 `local` 时运行: ```bash git config --local --unset core.hooksPath ``` -3. 删除 `.rstack/hooks/` 或通过 `--hooks-dir` 指定的目录。 + 作用域为 `worktree` 时运行: + + ```bash + git config --worktree --unset core.hooksPath + ``` + + 如果之前通过 `--force` 保留了 `.git/hooks` 下的 hooks,取消路径配置会重新启用这些文件。如果不希望它们运行,请先删除不再需要的文件。 + +4. 删除 `.rstack/hooks/` 或通过 `--hooks-dir` 指定的目录。 ## 故障排查 \{#troubleshooting} ### Hook 未运行 \{#hook-does-not-run} - 确认 hook 脚本使用[支持的名称](#supported-hooks),并与 `_` 目录同级。 -- 运行 `git config --local --get core.hooksPath`,检查配置的路径。 +- 运行 `git config --show-scope --get core.hooksPath`,检查当前生效的配置作用域和路径。 - 重新运行 `rs setup`,恢复生成文件及其可执行权限。 - 检查环境变量或初始化文件中是否设置了 `RSTACK_HOOKS=0`。 -- 如果命令提示存在其他 hooks 配置,请先迁移或移除冲突配置,再重新运行该命令。 +- 如果命令提示存在其他 hooks 配置,请运行 `rs setup --force`。 - 如果命令提示其他项目是 hooks owner,请按照 [Monorepo](#monorepo) 中的步骤转移 owner。 hook 脚本不需要可执行权限,因为 Rstack CLI 会使用 `sh` 运行它。 diff --git a/website/docs/zh/guide/cli/staged.mdx b/website/docs/zh/guide/cli/staged.mdx index 6191a678..4821c3ca 100644 --- a/website/docs/zh/guide/cli/staged.mdx +++ b/website/docs/zh/guide/cli/staged.mdx @@ -26,12 +26,36 @@ rs staged --allow-empty ### `--concurrent` -`--concurrent`(或 `-p`)用于设置并发运行的任务数量;设为 `false` 时串行运行。 +`--concurrent` 用于控制同时运行的任务数量,可设置为 `true`、`false` 或正整数。 + +省略该选项时,默认值为 `true`,`rs staged` 会并发运行任务且不设置固定的并发上限。设为 `false` 时会逐个运行任务;设为正整数时,会限制同时运行的任务数量。 ```bash +# 使用默认行为:并发运行任务,不设置固定的并发上限 +rs staged +rs staged --concurrent true + +# 每次运行一个任务 rs staged --concurrent false + +# 最多同时运行四个任务 +rs staged --concurrent 4 + +# -p 是 --concurrent 的缩写 +rs staged -p 4 +``` + +例如: + +```ts +define.staged({ + '*.ts': ['rs lint --fix', 'rs fmt'], + '*.md': 'rs fmt', +}); ``` +启用并发时,Markdown 文件的 `rs fmt` 可以在 TypeScript 文件执行 `rs lint --fix` 的同时运行;对于 TypeScript 文件,只有 `rs lint --fix` 完成后才会运行 `rs fmt`。 + ### `--cwd` `--cwd` 用于设置运行所有任务时使用的工作目录。 @@ -96,7 +120,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/formatting.mdx b/website/docs/zh/guide/formatting.mdx index 157bd54c..31a6cd19 100644 --- a/website/docs/zh/guide/formatting.mdx +++ b/website/docs/zh/guide/formatting.mdx @@ -54,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` 根据命令行中传入的路径确定格式化范围。以下输入可以组合使用: diff --git a/website/docs/zh/guide/git-hooks.mdx b/website/docs/zh/guide/git-hooks.mdx new file mode 100644 index 00000000..3a04d20f --- /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 --get core.hooksPath +# .rstack/hooks/_ +``` + +:::tip + +- `_` 目录由命令动态生成,且默认被 Git 忽略。 +- 如果检测到其他 hooks 路径或已有 Git hooks,`rs setup` 会跳过安装。可运行 `rs setup --force` 强制安装 Rstack hooks。详细说明请参考 [`rs setup` 指南](./cli/setup)。 + +::: + +## 提交前检查 \{#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..d504ae68 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 wrapCode +运行 `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。 +```text wrapCode +使用 migrate-to-rstack-cli Skill 将 Rslint 迁移到 Rstack CLI,并保持其他工具不变。 ``` +审查并验证本次改动后,再迁移 Rstest: + +```text wrapCode +使用 migrate-to-rstack-cli Skill 将 Rstest 迁移到 Rstack CLI,并保持其他工具不变。 +``` + +其余工具也可以按此方式依次迁移。 + ## 支持的工具 \{#supported-tools} 该 Skill 可以直接迁移以下独立工具: diff --git a/website/i18n.json b/website/i18n.json index f11e6b2c..f9e061fd 100644 --- a/website/i18n.json +++ b/website/i18n.json @@ -11,6 +11,10 @@ "en": "GitHub", "zh": "GitHub" }, + "latestRelease": { + "en": "Latest release", + "zh": "最新版本" + }, "title": { "en": "Unified Toolchain for", "zh": "统一工具链" diff --git a/website/theme/components/Hero.module.scss b/website/theme/components/Hero.module.scss index 2b296bb4..1ee413ae 100644 --- a/website/theme/components/Hero.module.scss +++ b/website/theme/components/Hero.module.scss @@ -1,7 +1,6 @@ .hero { --hero-title: #111214; - --hero-title-muted: #747474; - --hero-text: #707174; + --hero-text: #848484; --hero-primary-bg: #111214; --hero-primary-bg-hover: #383838; --hero-primary-text: #fff; @@ -15,16 +14,15 @@ align-items: center; justify-content: center; box-sizing: border-box; - min-height: calc(100svh - var(--rp-nav-height)); - padding: clamp(3.75rem, 7.5vh, 6rem) 2rem; + min-height: calc(80svh - var(--rp-nav-height)); + padding: clamp(3.75rem, 7.5vh, 6rem) 2rem 0; overflow: hidden; background: var(--rp-c-bg); } :global(.dark) .hero { --hero-title: #f5f5f5; - --hero-title-muted: #9a9a9a; - --hero-text: #a1a3a6; + --hero-text: #9a9a9a; --hero-primary-bg: #f2f2f2; --hero-primary-bg-hover: #fff; --hero-primary-text: #111214; @@ -44,10 +42,37 @@ transform: translateY(-1.5rem); } +.releaseLink { + display: inline-flex; + align-items: center; + margin-bottom: clamp(1.5rem, 3vh, 2.5rem); + color: var(--hero-text); + font-size: 0.875rem; + font-weight: 400; + line-height: 1.5; + text-decoration-line: underline; + text-decoration-color: transparent; + text-decoration-thickness: 1px; + text-underline-offset: 0.25em; + transition: + color 0.2s ease, + text-decoration-color 0.2s ease; + + &:hover { + color: var(--hero-title); + text-decoration-color: currentcolor; + } + + &:focus-visible { + outline: 2px solid var(--rp-c-brand); + outline-offset: 3px; + } +} + .title { margin: 0; color: var(--hero-title); - font-size: clamp(3rem, 4.5vw, 4rem); + font-size: clamp(3rem, 7vw, 4rem); font-weight: 600; line-height: 1.1; letter-spacing: -0.055em; @@ -59,16 +84,16 @@ } .subtitle { - color: var(--hero-title-muted); + color: var(--hero-text); } .description { max-width: 42rem; margin: clamp(2.5rem, 5vh, 3.75rem) 0 0; color: var(--hero-text); - font-size: clamp(1rem, 1.25vw, 1.25rem); + font-size: clamp(1rem, 2vw, 1.2rem); font-weight: 400; - line-height: 1.5; + line-height: 1.6; letter-spacing: -0.02em; text-wrap: balance; } @@ -125,15 +150,15 @@ &:hover { color: var(--hero-secondary-text); - border-color: var(--hero-title-muted); + border-color: var(--hero-text); background: var(--hero-secondary-bg-hover); } } @media (max-width: 640px) { .hero { - min-height: calc(100svh - var(--rp-nav-height)); - padding: 3.25rem 1.25rem; + min-height: calc(80svh - var(--rp-nav-height)); + padding: 3.25rem 1.25rem 0; } .title { @@ -142,8 +167,13 @@ letter-spacing: -0.055em; } + .releaseLink { + margin-bottom: 1.5rem; + font-size: 0.8125rem; + } + .description { - max-width: 28rem; + max-width: 30rem; margin-top: 2rem; font-size: 0.9375rem; line-height: 1.55; @@ -162,7 +192,8 @@ } @media (prefers-reduced-motion: reduce) { - .link { + .link, + .releaseLink { transition: none; } } diff --git a/website/theme/components/Hero.tsx b/website/theme/components/Hero.tsx index 00b18e29..7af8abb1 100644 --- a/website/theme/components/Hero.tsx +++ b/website/theme/components/Hero.tsx @@ -1,9 +1,24 @@ import { useI18n } from '@rspress/core/runtime'; import { Link } from '@rspress/core/theme-original'; +import rstackPackage from 'rstack/package.json'; import { useI18nUrl } from './utils'; import styles from './Hero.module.scss'; const githubUrl = 'https://github.com/rstackjs/rstack-cli'; +const releasesUrl = `${githubUrl}/releases`; + +function ReleaseLink({ label }: { label: string }) { + return ( + + {label} v{rstackPackage.version} + + ); +} export function Hero() { const tUrl = useI18nUrl(); @@ -12,6 +27,8 @@ export function Hero() { return (
+ +

{t('title')} {t('subtitle')}