diff --git a/.agents/skills/migrate-to-rstack-cli/SKILL.md b/.agents/skills/migrate-to-rstack-cli/SKILL.md index 308303c6..917de99c 100644 --- a/.agents/skills/migrate-to-rstack-cli/SKILL.md +++ b/.agents/skills/migrate-to-rstack-cli/SKILL.md @@ -24,15 +24,15 @@ Read every matching reference before editing. Load only the tools present in the 1. Inspect manifests, workspace catalogs, lock files, scripts, standalone configs, ignore files, Git hooks, TypeScript `types`, and source imports. 2. Read the matching references and inventory behavior that must survive: config functions, CLI arguments, plugins, presets, adapters, custom config paths, and chained commands. -3. Check the latest `rstack` version and inspect its Node.js engine and underlying tool versions. Resolve plugin and adapter peer ranges first; upgrade incompatible extensions or stop when no compatible version exists. Add `rstack` using the repository's existing package manager and version convention, usually as a development dependency. +3. Check the latest `rstack` release, Node.js engine, underlying tool versions, and relevant peer ranges. Upgrade incompatible plugins or adapters; stop if no compatible version exists. Ensure development and CI use supported Node.js versions, but do not narrow a published package's runtime `engines` solely to satisfy Rstack. Add `rstack` as a development dependency with the existing package manager. 4. If a matching reference uses a `define.*` registration, create `rstack.config.ts` and move the standalone configuration into it. 5. Rewrite commands and imports as directed by the references. -6. Search again for old direct imports, binaries, config paths, manifest entries, and type references. Remove only entries with no remaining direct or runtime use and no unresolved peer compatibility requirement. +6. Search again for old imports, binaries, config paths, manifest entries, package-manager metadata, and type references. Remove an item only after ruling out direct or runtime use and unresolved peer constraints. 7. Delete a standalone config only after its behavior is represented in `rstack.config.*`. 8. Refresh the lockfile with the repository's package manager. Confirm the expected tool version changes and resolve peer dependency warnings. -9. Run the repository's existing migrated scripts and required checks. Compare generated artifacts or runtime behavior where relevant. +9. Run migrated scripts and required repository checks. Compare generated artifacts or runtime behavior where relevant. After any follow-up changes, rerun the relevant checks against the final code. -The underlying Rsbuild, Rslib, Rstest, Rslint, and Prettier packages remain transitive dependencies of `rstack`. Do not require their names to disappear from the lockfile; require obsolete direct manifest entries and imports to disappear. +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 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 811517b7..09a54143 100644 --- a/.agents/skills/migrate-to-rstack-cli/references/git-hooks.md +++ b/.agents/skills/migrate-to-rstack-cli/references/git-hooks.md @@ -10,6 +10,8 @@ Migrate [Husky](https://typicode.github.io/husky/) or [simple-git-hooks](https:/ 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. +`rs setup` creates `.rstack/hooks/_/.gitignore`. Do not list `.rstack/hooks/_` in the root `.gitignore`. + ## Husky 1. Locate the source hooks: @@ -42,7 +44,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, configuration, and installer command. Remove old generated hook files only after confirming their ownership and exact paths. +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. For example, migrate: diff --git a/.agents/skills/migrate-to-rstack-cli/references/lint-staged.md b/.agents/skills/migrate-to-rstack-cli/references/lint-staged.md index 7cd80805..b98d479d 100644 --- a/.agents/skills/migrate-to-rstack-cli/references/lint-staged.md +++ b/.agents/skills/migrate-to-rstack-cli/references/lint-staged.md @@ -10,8 +10,9 @@ If staged tasks invoke Prettier, also read [prettier.md](prettier.md). 1. Replace staged-file script invocations with `rs staged`. 2. Move the staged-file config into `define.staged` in `rstack.config.*`. -3. Remove the old manifest key or config file. -4. Remove the direct staged-file dependency only when no script, config, or programmatic API still uses it. +3. Preserve previous behavior. Separate code tasks that lint and format from format-only tasks. +4. Remove the old manifest key or config file. +5. Remove the direct staged-file dependency only when no script, config, or programmatic API still uses it. ## Config Pattern @@ -19,8 +20,8 @@ If staged tasks invoke Prettier, also read [prettier.md](prettier.md). import { define } from 'rstack'; define.staged({ - '*.{ts,tsx,js,jsx}': ['rs lint --fix', 'rs fmt'], - '*.{json,md}': 'rs fmt', + '*.{js,jsx,ts,tsx,mjs,cjs}': ['rs lint', 'rs fmt'], + '*.{json,jsonc,md,mdx,css,html,yml,yaml}': 'rs fmt', }); ``` diff --git a/.agents/skills/migrate-to-rstack-cli/references/prettier.md b/.agents/skills/migrate-to-rstack-cli/references/prettier.md index 8303012d..280cadbf 100644 --- a/.agents/skills/migrate-to-rstack-cli/references/prettier.md +++ b/.agents/skills/migrate-to-rstack-cli/references/prettier.md @@ -17,6 +17,8 @@ Read this reference when the project uses the `prettier` CLI or API, `package.js `rs fmt` ignores `package-lock.json` and `pnpm-lock.yaml` by default. Drop redundant ignore entries during migration, but keep intentional negations. +Rstack creates `.rstack/cache/.gitignore` by default. Do not list `.rstack/cache` in the root `.gitignore`; add explicit rules only for custom cache paths. + `rs fmt` does not read Prettier configuration files, `.prettierignore`, or `.editorconfig`. Keep `.editorconfig` when editors or other tools use it. Keep Prettier when application code uses APIs such as `prettier.format()`; `rs fmt` is not a drop-in replacement for the programmatic API. diff --git a/.agents/skills/migrate-to-rstack-cli/references/rslint.md b/.agents/skills/migrate-to-rstack-cli/references/rslint.md index 2fd8d651..96b31d59 100644 --- a/.agents/skills/migrate-to-rstack-cli/references/rslint.md +++ b/.agents/skills/migrate-to-rstack-cli/references/rslint.md @@ -36,4 +36,4 @@ If a script also runs Prettier, migrate its formatting command as described in [ ## Validate -Run the non-writing lint script. +Run lint without writes. If Rstack upgrades Rslint, preserve the pre-migration lint baseline: disable newly enabled rules instead of changing source code, unless code changes are requested. diff --git a/.agents/skills/release-rstack/SKILL.md b/.agents/skills/release-rstack/SKILL.md index aee27778..c78e9453 100644 --- a/.agents/skills/release-rstack/SKILL.md +++ b/.agents/skills/release-rstack/SKILL.md @@ -1,6 +1,6 @@ --- name: release-rstack -description: Create a release pull request for the `rstack` npm package at a specific version. Use when asked to prepare, create, or open an rstack package release PR. +description: Create a coordinated release pull request for the `rstack` and `create-rstack` npm packages. Use when asked to prepare, create, or open an rstack package release PR. --- # Release Rstack @@ -11,6 +11,16 @@ description: Create a release pull request for the `rstack` npm package at a spe If the version is missing, ask for it before making changes. +## Version rules + +- Read both package versions before editing. Require the rstack target to be a valid, increasing SemVer version. +- Keep the package version lines independent. Apply the rstack bump type to the current `create-rstack` version: + - Patch: increment the patch version. + - Minor: increment the minor version and reset patch to `0`. + - Prerelease: use the rstack target's identifier. From a stable version, increment patch and append `-.0`; otherwise increment the final prerelease number. + - Prerelease to stable with the same core version: remove the `create-rstack` prerelease suffix. +- Stop and ask the user for major or ambiguous changes. + ## Steps 1. Check the worktree with `git status --short`. If there are uncommitted changes or untracked files, stop and ask the user how to proceed. Do not stash, discard, or include them. @@ -19,14 +29,18 @@ If the version is missing, ask for it before making changes. 3. Create and switch to `release/v` from the clean default-branch HEAD. -4. Update only the `version` field in `packages/rstack/package.json` to ``. +4. Update the `version` field in `packages/rstack/package.json` to `` and the `version` field in `packages/create-rstack/package.json` to the derived `create-rstack` version. + +5. In every `packages/create-rstack/template-*/package.json`, set the `rstack` dependency to `^`. Update only that dependency entry and verify every template package manifest uses the same target version. + +6. Run `pnpm --filter rstack build:native` to regenerate `packages/rstack/binding.cjs` and `packages/rstack/binding.d.cts` for the new version. Do not edit generated binding files manually. -5. Review the diff and confirm it contains exactly the one version-field change above. +7. Review the diff and confirm it contains only both package version changes, the template `rstack` dependency updates, and the regenerated binding files above. Verify the two package version changes use the intended matching bump type and no template retains an older rstack version. -6. Create a commit with this exact message: `release: v`. +8. Create a commit with this exact message: `release: v`. -7. Push the branch to `origin`. Recheck that the branch being pushed is `release/v` and never push the default branch directly. +9. Push the branch to `origin`. Recheck that the branch being pushed is `release/v` and never push the default branch directly. -8. Create a pull request against the default branch. In Codex, use the GitHub connector/plugin; use another available GitHub workflow only when the connector is unavailable. Use `release: v` as the PR title. +10. Create a pull request against the default branch. In Codex, use the GitHub connector/plugin; use another available GitHub workflow only when the connector is unavailable. Use `release: v` as the PR title. Return the pull request URL. diff --git a/.agents/skills/rstack-cli-best-practices/SKILL.md b/.agents/skills/rstack-cli-best-practices/SKILL.md index 21940ca7..b614601f 100644 --- a/.agents/skills/rstack-cli-best-practices/SKILL.md +++ b/.agents/skills/rstack-cli-best-practices/SKILL.md @@ -16,7 +16,7 @@ Before any Rstack work, find and read the relevant Markdown documentation shippe Model knowledge can be outdated; the installed documentation is the source of truth for the project's Rstack version. -1. Start with `node_modules/rstack/dist/docs/llms.txt`, then read only the linked pages relevant to the task before proposing or making changes. +1. Start with `node_modules/rstack/docs/llms.txt`, then read only the linked pages relevant to the task before proposing or making changes. 2. For exact CLI flags and behavior, also run `rs -h` or `rs -h` when supported. diff --git a/.cargo/release.toml b/.cargo/release.toml new file mode 100644 index 00000000..bd3a5760 --- /dev/null +++ b/.cargo/release.toml @@ -0,0 +1,3 @@ +# Remove source locations from release panic metadata to reduce binary size. +[target.'cfg(all())'] +rustflags = ["-Zlocation-detail=none"] diff --git a/.gitattributes b/.gitattributes index 5473728b..48aa62aa 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,6 @@ # Let GitHub languages ignores MDX files *.mdx linguist-documentation + +# NAPI-RS owns these generated files; keep regeneration deterministic on Windows. +packages/rstack/binding.cjs text eol=lf +packages/rstack/binding.d.cts text eol=lf diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0cf85e28..795132ce 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -26,22 +26,28 @@ jobs: - name: Setup Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: - node-version: 24.18.0 + node-version: 24.18.1 package-manager-cache: false - name: Install Pnpm - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 with: run_install: true - name: Build Packages run: node --run build - - name: Lint - run: node --run lint + - name: Check Rust Format + run: cargo fmt --all -- --check - - name: Check Format - run: node --run check:format + - name: Lint Rust + run: cargo clippy --profile ci --workspace --all-targets --locked -- -D warnings + + - name: Build Native Binding + run: pnpm --filter rstack build:native:ci + + - name: Check + run: node --run check - name: Check Spell run: node --run check:spell diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0f039d57..b0e43c23 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,11 +24,6 @@ on: - rc - canary - latest - branch: - description: 'Release Branch (confirm release branch)' - required: true - default: 'main' - permissions: {} jobs: @@ -50,10 +45,17 @@ jobs: fi echo "npm_tag validation passed: $NPM_TAG" + native: + name: Build native packages + needs: validate_inputs + permissions: + contents: read + uses: ./.github/workflows/reusable-native-release.yml + release: name: Release if: github.repository == 'rstackjs/rstack-cli' && github.event_name == 'workflow_dispatch' - needs: validate_inputs + needs: native runs-on: ubuntu-latest environment: npm permissions: @@ -65,16 +67,15 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 1 - ref: ${{ github.event.inputs.branch }} - name: Setup Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 24.18.0 + node-version: 24.18.1 package-manager-cache: false - name: Setup Pnpm - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 with: run_install: true @@ -84,6 +85,18 @@ jobs: - name: Prepare release run: node --run release:prepare + - name: Download native packages + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: native-packages + path: packages/rstack/npm + + - name: Prepare native packages + run: pnpm --dir packages/rstack exec napi pre-publish --config-path napi.json --skip-optional-publish --no-gh-release + - name: Publish to npm + env: + NPM_TAG: ${{ inputs.npm_tag }} run: | - pnpm --filter './packages/*' -r stage publish --tag ${{ github.event.inputs.npm_tag }} --no-git-checks + pnpm --dir packages/rstack/npm -r publish --tag "$NPM_TAG" --no-git-checks + pnpm --filter './packages/*' -r stage publish --tag "$NPM_TAG" --no-git-checks diff --git a/.github/workflows/reusable-native-build.yml b/.github/workflows/reusable-native-build.yml new file mode 100644 index 00000000..d06dbd48 --- /dev/null +++ b/.github/workflows/reusable-native-build.yml @@ -0,0 +1,122 @@ +name: Reusable Native Build + +on: + workflow_call: + inputs: + target: + description: Rust target triple to build + required: true + type: string + runner: + description: GitHub-hosted runner label + required: true + type: string + filename: + description: Expected NAPI-RS binary filename + required: true + type: string + +permissions: + contents: read + +env: + CARGO_INCREMENTAL: 0 + +jobs: + build: + name: Build ${{ inputs.target }} + runs-on: ${{ inputs.runner }} + env: + RUST_TARGET: ${{ inputs.target }} + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version: 24.18.1 + package-manager-cache: false + + - name: Install Pnpm + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + with: + run_install: true + + - name: Install Rust target + shell: bash + run: rustup target add "$RUST_TARGET" + + - name: Setup Zig + if: contains(inputs.target, 'musl') + uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 + with: + version: 0.14.1 + + - name: Install cargo-zigbuild + if: contains(inputs.target, 'musl') + uses: taiki-e/install-action@6c6fd71fe4fb72c3697d269963d0e15df8adedad # v2.85.10 + with: + tool: cargo-zigbuild@0.23.0 + + - name: Install RISC-V GNU toolchain + if: inputs.target == 'riscv64gc-unknown-linux-gnu' + shell: bash + run: | + sudo apt-get update + sudo apt-get install --yes gcc-riscv64-linux-gnu + + - name: Build native binding + shell: bash + working-directory: packages/rstack + run: | + args=( + build + --config-path napi.json + --platform + --release + --target "$RUST_TARGET" + --manifest-path ../../Cargo.toml + --package rstack-binding + --output-dir . + --js binding.cjs + --dts binding.d.cts + ) + + case "$RUST_TARGET" in + x86_64-unknown-linux-gnu | aarch64-unknown-linux-gnu | \ + powerpc64le-unknown-linux-gnu | s390x-unknown-linux-gnu) + args+=(--use-napi-cross) + ;; + x86_64-unknown-linux-musl | aarch64-unknown-linux-musl | \ + riscv64gc-unknown-linux-musl) + args+=(--cross-compile) + ;; + aarch64-apple-darwin | x86_64-apple-darwin) + export MACOSX_DEPLOYMENT_TARGET=11.0 + ;; + i686-pc-windows-msvc) + # Reduce peak link time and memory usage for the constrained 32-bit target. + export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=32 + export CARGO_PROFILE_RELEASE_LTO=false + ;; + riscv64gc-unknown-linux-gnu | x86_64-pc-windows-msvc | \ + aarch64-pc-windows-msvc) + ;; + *) + echo "Unsupported native target: $RUST_TARGET" >&2 + exit 1 + ;; + esac + + # Omit panic source locations to reduce the release binary size. + args+=(-- --config ../../.cargo/release.toml) + pnpm exec napi "${args[@]}" + + - name: Upload native binding + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: native-${{ inputs.target }} + path: packages/rstack/${{ inputs.filename }} + if-no-files-found: error + retention-days: 1 diff --git a/.github/workflows/reusable-native-release.yml b/.github/workflows/reusable-native-release.yml new file mode 100644 index 00000000..920a686e --- /dev/null +++ b/.github/workflows/reusable-native-release.yml @@ -0,0 +1,101 @@ +name: Reusable Native Release + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + name: Build ${{ matrix.target }} + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-unknown-linux-gnu + runner: ubuntu-latest + filename: rstack.linux-x64-gnu.node + - target: aarch64-unknown-linux-gnu + runner: ubuntu-latest + filename: rstack.linux-arm64-gnu.node + - target: x86_64-pc-windows-msvc + runner: windows-latest + filename: rstack.win32-x64-msvc.node + - target: aarch64-apple-darwin + runner: macos-latest + filename: rstack.darwin-arm64.node + - target: powerpc64le-unknown-linux-gnu + runner: ubuntu-latest + filename: rstack.linux-ppc64-gnu.node + - target: s390x-unknown-linux-gnu + runner: ubuntu-latest + filename: rstack.linux-s390x-gnu.node + - target: aarch64-pc-windows-msvc + runner: windows-latest + filename: rstack.win32-arm64-msvc.node + - target: x86_64-apple-darwin + runner: macos-latest + filename: rstack.darwin-x64.node + - target: x86_64-unknown-linux-musl + runner: ubuntu-latest + filename: rstack.linux-x64-musl.node + - target: riscv64gc-unknown-linux-gnu + runner: ubuntu-latest + filename: rstack.linux-riscv64-gnu.node + - target: aarch64-unknown-linux-musl + runner: ubuntu-latest + filename: rstack.linux-arm64-musl.node + - target: riscv64gc-unknown-linux-musl + runner: ubuntu-latest + filename: rstack.linux-riscv64-musl.node + - target: i686-pc-windows-msvc + runner: windows-latest + filename: rstack.win32-ia32-msvc.node + uses: ./.github/workflows/reusable-native-build.yml + with: + target: ${{ matrix.target }} + runner: ${{ matrix.runner }} + filename: ${{ matrix.filename }} + + assemble: + name: Assemble native packages + needs: build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version: 24.18.1 + package-manager-cache: false + + - name: Install Pnpm + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + with: + run_install: true + + - name: Download native bindings + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: native-* + path: artifacts + + - name: Assemble native packages + run: | + pnpm --dir packages/rstack package:native + pnpm --dir packages/rstack exec napi artifacts --config-path napi.json --output-dir ../../artifacts + + - name: Check native packages + run: pnpm --dir packages/rstack exec napi pre-publish --config-path napi.json --dry-run --no-gh-release + + - name: Upload native packages + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: native-packages + path: packages/rstack/npm + if-no-files-found: error + retention-days: 1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 99955d61..3062f9f7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, windows-latest] + os: [ubuntu-latest, windows-latest, macos-latest] # Steps represent a sequence of tasks that will be executed as part of the job steps: @@ -29,16 +29,25 @@ jobs: - name: Setup Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: - node-version: 24.18.0 + node-version: 24.18.1 package-manager-cache: false - name: Install Pnpm - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 with: run_install: true - name: Build Packages run: node --run build + - name: Run Rust Tests + run: cargo test --profile ci --workspace --locked + + - name: Build Native Binding + run: pnpm --filter rstack build:native:ci + + - name: Check Generated Native Files + run: git diff --exit-code -- packages/rstack/binding.cjs packages/rstack/binding.d.cts + - name: Run Test run: node --run test diff --git a/.gitignore b/.gitignore index c584b861..ef2bf49b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,20 @@ node_modules dist/ dist-*/ +/packages/rstack/docs/ test-results doc_build +# Rust / NAPI-RS +/target/ +/artifacts/ +/packages/rstack/*.node + +# Native packages are generated +/packages/rstack/npm/* +# Keep only the publish workspace manifest +!/packages/rstack/npm/pnpm-workspace.yaml + # Temp files test-temp-* TODO.md diff --git a/AGENTS.md b/AGENTS.md index 8a2d17ec..e8e3b4c5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,6 +5,7 @@ - Use repo Node.js/pnpm versions (`package.json`) - `pnpm` workspace; shared deps in `pnpm-workspace.yaml` catalogs - TypeScript, Rsbuild/Rslib/Rstest/Rslint, Prettier +- Rust/Cargo workspace with a NAPI-RS binding under `crates/` ## Commands @@ -13,23 +14,29 @@ corepack enable && pnpm install # dev checks -pnpm lint +pnpm check pnpm test # build / format / spelling pnpm build pnpm format -pnpm check:format pnpm check:spell # focused work pnpm --filter rstack build +pnpm --filter rstack build:native pnpm --filter rstack test ``` ## Testing -- Run `pnpm build` once before `pnpm test` command +- Run `pnpm build` and `pnpm --filter rstack build:native` before `pnpm test` + +## Native + +- Follow `crates/AGENTS.md` for Rust changes +- Keep the JS bridge lazy; use the generated loader directly +- Regenerate binding files with `build:native` ## Documentation @@ -39,7 +46,8 @@ pnpm --filter rstack test ## Project structure ```text -packages/rstack/ # CLI package -examples/* # example projects -scripts/ # repo tooling +crates/ # Rust crates +packages/rstack/ # CLI package and private native bridge +examples/* # example projects +scripts/ # repo tooling ``` diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..345a8ab6 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,456 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "serde_core", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "convert_case" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "ctor" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d83cb7e7a873830708d6b02a78cd36a592c6fa14bf267b68725103b85c0d77f" + +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "globset" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "ignore" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b69833ed729dc5aa7d19541d96d6cf8e9137194207a04916d658e43168402f" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "libloading" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "napi" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f71d6bc097c4a6eb853c3f24991ab8c9f50f57d1f719e305175541482217e36" +dependencies = [ + "bitflags", + "ctor", + "futures", + "napi-build", + "napi-sys", + "nohash-hasher", + "rustc-hash", +] + +[[package]] +name = "napi-build" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5282704fbe8d49b0cf8b08e3f33233416a528658f205c7e5ace63b582de0b11c" + +[[package]] +name = "napi-derive" +version = "3.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9002b2940f0184444754546e0fcd15182f56948e6f381968b019d549387c42" +dependencies = [ + "convert_case", + "ctor", + "napi-derive-backend", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "napi-derive-backend" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d60b5d773ad46c698c8cc2cd9fde0b283d39cbb7f71c04bee633c7bdba4423bd" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "semver", + "syn 2.0.119", +] + +[[package]] +name = "napi-sys" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85fbf1fa9f1babfe396d74bbbf52b3643770243e8f5b0b46715d4caf7f0dfc9a" +dependencies = [ + "libloading", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rstack-binding" +version = "0.1.0" +dependencies = [ + "napi", + "napi-build", + "napi-derive", + "rstack-ignore", +] + +[[package]] +name = "rstack-ignore" +version = "0.1.0" +dependencies = [ + "ignore", + "pathdiff", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..4ce85357 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,49 @@ +[workspace] +members = ["crates/rstack-binding", "crates/rstack-ignore"] +resolver = "2" + +[workspace.package] +edition = "2021" +license = "MIT" +repository = "https://github.com/rstackjs/rstack-cli" +rust-version = "1.88" + +[workspace.dependencies] +ignore = { version = "0.4.33", default-features = false } +napi = { version = "3.12.0", default-features = false, features = ["napi9"] } +napi-build = "2.4.0" +napi-derive = "3.6.2" +pathdiff = "0.2.3" +rstack-ignore = { path = "crates/rstack-ignore" } + +# Local development: 16 codegen units are sufficient for this small workspace while preserving +# parallel compilation and full debugging support. +[profile.dev] +codegen-units = 16 +debug = 2 +incremental = true +panic = "unwind" +split-debuginfo = "unpacked" + +# CI: 256 codegen units favor clean-build parallelism, while disabling cross-crate LTO avoids the +# release-only linking cost. +[profile.ci] +codegen-units = 256 +debug = false +incremental = false +inherits = "release" +lto = false +opt-level = 2 +# Cargo tests require unwinding, so keep the CI native build consistent. +panic = "unwind" +strip = false + +# Release: one codegen unit and fat LTO maximize optimization; abort prevents unwinding across the +# NAPI FFI boundary. +[profile.release] +codegen-units = 1 +debug = false +lto = "fat" +opt-level = 3 +panic = "abort" +strip = true diff --git a/README.md b/README.md index 7edb06d2..6285e4c8 100644 --- a/README.md +++ b/README.md @@ -12,18 +12,19 @@ Rstack CLI brings the Rstack toolchain together for JavaScript development, with It also covers local development needs outside Rstack's scope, with Prettier formatting and lint-staged commands. -| Command | Description | Powered by | -| ------------ | -------------------------------- | ------------------------------------------------------------------------------------------------- | -| `rs dev` | Run the app dev server | [Rsbuild](https://github.com/web-infra-dev/rsbuild) | -| `rs build` | Build the app for production | [Rsbuild](https://github.com/web-infra-dev/rsbuild) | -| `rs preview` | Preview the app production build | [Rsbuild](https://github.com/web-infra-dev/rsbuild) | -| `rs test` | Run tests | [Rstest](https://github.com/web-infra-dev/rstest) | -| `rs lint` | Lint code | [Rslint](https://github.com/web-infra-dev/rslint) | -| `rs lib` | Build library | [Rslib](https://github.com/web-infra-dev/rslib) | -| `rs doc` | Serve or build docs | [Rspress](https://github.com/web-infra-dev/rspress) | -| `rs fmt` | Format code | [Prettier](https://github.com/prettier/prettier) + [Yuku](https://github.com/yuku-toolchain/yuku) | -| `rs setup` | Install Git hooks | - | -| `rs staged` | Run tasks on staged Git files | [lint-staged](https://github.com/lint-staged/lint-staged) | +| Command | Description | +| --------------------------------------------------- | -------------------------------------------- | +| [`rs dev`](https://rstack.rs/guide/cli/dev) | Run the app dev server | +| [`rs build`](https://rstack.rs/guide/cli/build) | Build the app for production | +| [`rs preview`](https://rstack.rs/guide/cli/preview) | Preview the app production build | +| [`rs test`](https://rstack.rs/guide/cli/test) | Run tests | +| [`rs lint`](https://rstack.rs/guide/cli/lint) | Lint code | +| [`rs fmt`](https://rstack.rs/guide/cli/fmt) | Format code | +| [`rs check`](https://rstack.rs/guide/cli/check) | Run static checks, including lint and format | +| [`rs lib`](https://rstack.rs/guide/cli/lib) | Build library | +| [`rs doc`](https://rstack.rs/guide/cli/doc) | Serve or build docs | +| [`rs setup`](https://rstack.rs/guide/cli/setup) | Install Git hooks | +| [`rs staged`](https://rstack.rs/guide/cli/staged) | Run tasks on staged Git files | Rstack CLI fits into your existing project workflow. It does not replace your runtime, package manager, or task runner, such as [pnpm](https://github.com/pnpm/pnpm), [Bun](https://github.com/oven-sh/bun), [Turborepo](https://github.com/vercel/turborepo), [Nx](https://github.com/nrwl/nx), and [Nub](https://github.com/nubjs/nub). @@ -57,6 +58,7 @@ bun add -d rstack "build": "rs build", "preview": "rs preview", "test": "rs test", + "check": "rs check --type-check", "lint": "rs lint", "lib": "rs lib", "doc": "rs doc", @@ -73,6 +75,7 @@ pnpm dev pnpm build pnpm preview pnpm test +pnpm check pnpm lint pnpm lib pnpm doc diff --git a/crates/AGENTS.md b/crates/AGENTS.md new file mode 100644 index 00000000..b7178eac --- /dev/null +++ b/crates/AGENTS.md @@ -0,0 +1,18 @@ +# AGENTS.md + +## Architecture + +- Keep `rstack-binding` a thin Node/NAPI adapter +- Put reusable or domain logic in separate crates +- Coordinate binding changes with the private JS loader and its Rstest coverage + +## Checks + +Run from the repository root: + +```bash +cargo fmt --all -- --check +cargo clippy --workspace --all-targets --locked -- -D warnings +cargo test --workspace --locked +pnpm --filter rstack build:native +``` diff --git a/crates/rstack-binding/Cargo.toml b/crates/rstack-binding/Cargo.toml new file mode 100644 index 00000000..74dfbcbe --- /dev/null +++ b/crates/rstack-binding/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "rstack-binding" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true +publish = false + +[lib] +crate-type = ["cdylib"] +test = false + +[dependencies] +napi.workspace = true +napi-derive.workspace = true +rstack-ignore.workspace = true + +[build-dependencies] +napi-build.workspace = true diff --git a/crates/rstack-binding/build.rs b/crates/rstack-binding/build.rs new file mode 100644 index 00000000..0f1b0100 --- /dev/null +++ b/crates/rstack-binding/build.rs @@ -0,0 +1,3 @@ +fn main() { + napi_build::setup(); +} diff --git a/crates/rstack-binding/src/lib.rs b/crates/rstack-binding/src/lib.rs new file mode 100644 index 00000000..da3a361d --- /dev/null +++ b/crates/rstack-binding/src/lib.rs @@ -0,0 +1,139 @@ +#![deny(clippy::all)] + +use std::path::Path; + +use napi::{bindgen_prelude::Uint8Array, Error, Status}; +use napi_derive::napi; +use rstack_ignore::{ + GitIgnoreMatcher as CoreGitIgnoreMatcher, IgnoreMatcher as CoreIgnoreMatcher, + IgnoreSource as CoreIgnoreSource, +}; + +/// A Gitignore-compatible pattern source received from JavaScript. +#[napi(object, object_to_js = false)] +pub struct IgnoreSource { + /// Directory that patterns are resolved from. + pub root_path: String, + /// Newline-delimited Gitignore patterns. + pub patterns: String, +} + +/// JavaScript-facing wrapper around the compiled Rust matcher. +#[napi] +pub struct IgnoreMatcher { + inner: CoreIgnoreMatcher, +} + +#[napi] +impl IgnoreMatcher { + /// Compiles all pattern sources once and keeps the result for repeated path checks. + #[napi(constructor)] + pub fn new(sources: Vec) -> napi::Result { + let sources = sources + .into_iter() + .map(|source| CoreIgnoreSource::new(source.root_path, source.patterns)); + let inner = CoreIgnoreMatcher::new(sources).map_err(|error| { + Error::from_reason(format!("Failed to compile ignore patterns: {error}")) + })?; + + Ok(Self { inner }) + } + + /// Returns whether a file or directory is ignored by any source. + #[napi] + pub fn is_ignored(&mut self, file_path: String, is_directory: bool) -> bool { + self.inner.is_ignored(Path::new(&file_path), is_directory) + } +} + +/// JavaScript-facing hierarchy for repository `.gitignore` files. +#[napi] +pub struct GitIgnoreMatcher { + inner: CoreGitIgnoreMatcher, +} + +impl Default for GitIgnoreMatcher { + fn default() -> Self { + Self { + inner: CoreGitIgnoreMatcher::new(), + } + } +} + +#[napi] +impl GitIgnoreMatcher { + /// Creates an empty matcher whose sources can be added during directory traversal. + #[napi(constructor)] + pub fn new() -> Self { + Self::default() + } + + /// Compiles or replaces rules rooted at a repository-relative POSIX directory. + #[napi] + pub fn add_source(&mut self, relative_root: String, patterns: String) -> napi::Result { + self.inner + .add_source(&relative_root, &patterns) + .map_err(|error| { + Error::from_reason(format!("Failed to compile .gitignore patterns: {error}")) + }) + } + + /// Returns whether one repository-relative POSIX path is ignored. + #[napi] + pub fn is_ignored(&mut self, relative_path: String, is_directory: bool) -> bool { + self.inner.is_ignored(&relative_path, is_directory) + } + + /// Matches one directory's entries in a native call and returns one byte per name. + #[napi] + pub fn is_ignored_batch( + &mut self, + relative_parent: String, + names: Vec, + directory_flags: Uint8Array, + ) -> napi::Result { + if names.len() != directory_flags.len() { + return Err(Error::new( + Status::InvalidArg, + "Name and directory flag counts must match.", + )); + } + + Ok(self + .inner + .is_ignored_batch(&relative_parent, &names, directory_flags.as_ref()) + .into()) + } + + /// Matches up to 32 entries while avoiding per-directory typed-array allocation. + #[napi] + pub fn is_ignored_batch_mask( + &mut self, + relative_parent: String, + names: Vec, + directory_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(&relative_parent, &names, directory_mask)) + } + + /// Matches a single directory entry without constructing a JavaScript array. + #[napi] + pub fn is_ignored_child( + &mut self, + relative_parent: String, + name: String, + is_directory: bool, + ) -> bool { + self.inner + .is_ignored_child(&relative_parent, &name, is_directory) + } +} diff --git a/crates/rstack-ignore/Cargo.toml b/crates/rstack-ignore/Cargo.toml new file mode 100644 index 00000000..fe80d3c6 --- /dev/null +++ b/crates/rstack-ignore/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "rstack-ignore" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true +publish = false + +[dependencies] +ignore.workspace = true +pathdiff.workspace = true diff --git a/crates/rstack-ignore/src/lib.rs b/crates/rstack-ignore/src/lib.rs new file mode 100644 index 00000000..c0944109 --- /dev/null +++ b/crates/rstack-ignore/src/lib.rs @@ -0,0 +1,507 @@ +#![deny(clippy::all)] + +use std::{borrow::Cow, collections::HashMap, path::Path, path::PathBuf}; + +use ignore::{ + gitignore::{Gitignore, GitignoreBuilder}, + Match, +}; + +fn compile_patterns(patterns: &str) -> Result { + // Paths are made relative to the source root before matching, so the builder uses a + // synthetic root instead of tying compiled patterns to an absolute path. + let mut builder = GitignoreBuilder::new("."); + // Match the case-insensitive default used by the previous JavaScript matcher. + builder.case_insensitive(true)?; + + // Accept ignore files with CRLF line endings or a UTF-8 byte-order mark. + for line in patterns.split('\n') { + let line = line.strip_suffix('\r').unwrap_or(line); + let line = line.strip_prefix('\u{feff}').unwrap_or(line); + // Gitignore files and the previous JavaScript matcher treat malformed lines as + // nonmatching, while continuing to apply the remaining valid rules. + let _ = builder.add_line(None, line); + } + + builder.build() +} + +/// Raw Gitignore patterns anchored to a base directory. +pub struct IgnoreSource { + root_path: PathBuf, + patterns: String, +} + +impl IgnoreSource { + /// Creates a pattern source whose rules are resolved from `root_path`. + pub fn new(root_path: impl Into, patterns: impl Into) -> Self { + Self { + root_path: root_path.into(), + patterns: patterns.into(), + } + } +} + +/// A reusable matcher compiled from one or more independent pattern sources. +pub struct IgnoreMatcher { + sources: Vec, +} + +impl IgnoreMatcher { + /// Compiles every source while preserving source-level ignore isolation. + pub fn new(sources: impl IntoIterator) -> Result { + let sources = sources + .into_iter() + .map(SourceMatcher::new) + .collect::>()?; + + Ok(Self { sources }) + } + + /// Returns whether a file or directory is ignored by any source. + pub fn is_ignored(&mut self, file_path: &Path, is_directory: bool) -> bool { + // Sources are independent: a negation in one source cannot re-include a path ignored by + // another source. + self.sources + .iter_mut() + .any(|source| source.is_ignored(file_path, is_directory)) + } +} + +/// A hierarchy of repository `.gitignore` files keyed by their root-relative directories. +#[derive(Default)] +pub struct GitIgnoreMatcher { + matchers: HashMap, GitIgnoreSourceMatcher>, + // Traversal checks every file's parent, so cache both ignored and included directories. + ignored_directories: HashMap, bool>, +} + +impl GitIgnoreMatcher { + /// Creates an empty hierarchy. Sources can be added as traversal discovers them. + pub fn new() -> Self { + Self::default() + } + + /// Adds or replaces the patterns rooted at a POSIX, repository-relative directory. + /// + /// Returns whether the hierarchy contains any effective rules after the update. + pub fn add_source( + &mut self, + relative_root: &str, + patterns: &str, + ) -> Result { + let relative_root = normalize_relative_path(relative_root); + let matcher = compile_patterns(patterns)?; + + self.invalidate_directory_cache(relative_root); + if matcher.is_empty() { + self.matchers.remove(relative_root); + } else { + self.matchers + .insert(relative_root.into(), GitIgnoreSourceMatcher::new(matcher)); + } + Ok(!self.matchers.is_empty()) + } + + /// Returns whether a repository-relative POSIX path is ignored by its applicable hierarchy. + pub fn is_ignored(&mut self, relative_path: &str, is_directory: bool) -> bool { + let relative_path = normalize_relative_path(relative_path); + if relative_path.is_empty() || self.matchers.is_empty() { + return false; + } + + if is_directory { + return self.is_directory_ignored(relative_path); + } + + // Git cannot re-include a path below an ignored directory, so parent state wins. + parent_directory(relative_path).is_some_and(|parent| self.is_directory_ignored(parent)) + || self.matches(relative_path, false) + } + + /// Matches one directory's child names without crossing the native boundary per entry. + pub fn is_ignored_batch( + &mut self, + relative_parent: &str, + names: &[String], + directory_flags: &[u8], + ) -> Vec { + let relative_parent = normalize_relative_path(relative_parent); + let separator = usize::from(!relative_parent.is_empty()); + let name_capacity = names.iter().map(String::len).max().unwrap_or(0); + let mut relative_path = + String::with_capacity(relative_parent.len() + separator + name_capacity); + let mut ignored = Vec::with_capacity(names.len()); + + for (name, is_directory) in names.iter().zip(directory_flags) { + relative_path.clear(); + if !relative_parent.is_empty() { + relative_path.push_str(relative_parent); + relative_path.push('/'); + } + relative_path.push_str(name); + ignored.push(u8::from( + self.is_ignored(&relative_path, *is_directory != 0), + )); + } + + ignored + } + + /// Matches one child without allocating an intermediate names array. + pub fn is_ignored_child( + &mut self, + relative_parent: &str, + name: &str, + is_directory: bool, + ) -> bool { + let relative_parent = normalize_relative_path(relative_parent); + if relative_parent.is_empty() { + return self.is_ignored(name, is_directory); + } + + let mut relative_path = String::with_capacity(relative_parent.len() + 1 + name.len()); + relative_path.push_str(relative_parent); + relative_path.push('/'); + relative_path.push_str(name); + self.is_ignored(&relative_path, is_directory) + } + + /// Matches up to 32 child names and packs both input types and results into bit masks. + pub fn is_ignored_batch_mask( + &mut self, + relative_parent: &str, + names: &[String], + directory_mask: u32, + ) -> u32 { + debug_assert!(names.len() <= u32::BITS as usize); + + let relative_parent = normalize_relative_path(relative_parent); + let separator = usize::from(!relative_parent.is_empty()); + let name_capacity = names.iter().map(String::len).max().unwrap_or(0); + let mut relative_path = + String::with_capacity(relative_parent.len() + separator + name_capacity); + let mut ignored_mask = 0; + + for (index, name) in names.iter().enumerate() { + relative_path.clear(); + if !relative_parent.is_empty() { + relative_path.push_str(relative_parent); + relative_path.push('/'); + } + relative_path.push_str(name); + if self.is_ignored(&relative_path, directory_mask & (1 << index) != 0) { + ignored_mask |= 1 << index; + } + } + + ignored_mask + } + + fn invalidate_directory_cache(&mut self, relative_root: &str) { + if relative_root.is_empty() { + self.ignored_directories.clear(); + return; + } + + let descendant_prefix = format!("{relative_root}/"); + self.ignored_directories + .retain(|relative_path, _| !relative_path.starts_with(&descendant_prefix)); + } + + fn is_directory_ignored(&mut self, relative_path: &str) -> bool { + let relative_path = relative_path.trim_end_matches('/'); + if relative_path.is_empty() { + return false; + } + + if let Some(ignored) = self.ignored_directories.get(relative_path) { + return *ignored; + } + + let ignored = parent_directory(relative_path) + .is_some_and(|parent| self.is_directory_ignored(parent)) + || self.matches(relative_path, true); + self.ignored_directories + .insert(relative_path.into(), ignored); + ignored + } + + fn matches(&mut self, relative_path: &str, is_directory: bool) -> bool { + // Most repositories only use a root `.gitignore`. Avoid looking up every path segment + // when no nested matcher can override the root result. + if self.matchers.len() == 1 { + if let Some(root_matcher) = self.matchers.get_mut("") { + return root_matcher + .match_path(relative_path, is_directory) + .unwrap_or(false); + } + } + + let mut ignored = false; + let mut matcher_root_end = 0; + let mut path_from_matcher_start = 0; + + for segment in relative_path.split('/') { + let matcher_root = &relative_path[..matcher_root_end]; + if let Some(matcher) = self.matchers.get_mut(matcher_root) { + let path_from_matcher = &relative_path[path_from_matcher_start..]; + if let Some(state) = matcher.match_path(path_from_matcher, is_directory) { + ignored = state; + } + } + + matcher_root_end = path_from_matcher_start + segment.len(); + path_from_matcher_start = (matcher_root_end + 1).min(relative_path.len()); + } + + ignored + } +} + +/// One `.gitignore` source with the directory state needed to reproduce +/// `ignore.test(path)` without re-walking ancestors for every file. +struct GitIgnoreSourceMatcher { + matcher: Gitignore, + directory_states: HashMap, Option>, +} + +impl GitIgnoreSourceMatcher { + fn new(matcher: Gitignore) -> Self { + Self { + matcher, + directory_states: HashMap::new(), + } + } + + fn match_path(&mut self, relative_path: &str, is_directory: bool) -> Option { + if is_directory { + return self.match_directory(relative_path); + } + + match self.matcher.matched(relative_path, false) { + Match::Ignore(_) => Some(true), + Match::Whitelist(_) => Some(false), + Match::None => parent_directory(relative_path) + .is_some_and(|parent| self.is_directory_ignored(parent)) + .then_some(true), + } + } + + fn match_directory(&mut self, relative_path: &str) -> Option { + if let Some(state) = self.directory_states.get(relative_path) { + return *state; + } + + let matched = self.matcher.matched(relative_path, true); + let state = match matched { + Match::Ignore(_) => Some(true), + Match::Whitelist(_) => Some(false), + Match::None => parent_directory(relative_path) + .is_some_and(|parent| self.is_directory_ignored(parent)) + .then_some(true), + }; + self.directory_states.insert(relative_path.into(), state); + state + } + + fn is_directory_ignored(&mut self, relative_path: &str) -> bool { + self.match_directory(relative_path) == Some(true) + } +} + +struct SourceMatcher { + root_path: PathBuf, + matcher: Gitignore, + // File checks repeatedly consult their parents, so cache both ignored and included directories. + ignored_directories: HashMap, bool>, +} + +impl SourceMatcher { + fn new(source: IgnoreSource) -> Result { + Ok(Self { + root_path: source.root_path, + matcher: compile_patterns(&source.patterns)?, + ignored_directories: HashMap::new(), + }) + } + + fn is_ignored(&mut self, file_path: &Path, is_directory: bool) -> bool { + let relative_path = self.relative_path(file_path); + if relative_path.as_os_str().is_empty() { + return false; + } + + let relative_path = to_posix_path(&relative_path); + if is_directory { + return self.is_directory_ignored(&relative_path); + } + + // Gitignore cannot re-include a path below an ignored directory, so parent state wins. + parent_directory(&relative_path).is_some_and(|parent| self.is_directory_ignored(parent)) + || is_ignore_match(self.matcher.matched(relative_path.as_ref(), false)) + } + + fn relative_path<'path>(&self, file_path: &'path Path) -> Cow<'path, Path> { + if let Ok(relative_path) = file_path.strip_prefix(&self.root_path) { + return Cow::Borrowed(relative_path); + } + + // Patterns may intentionally target paths outside the source root with `../` segments. + Cow::Owned( + pathdiff::diff_paths(file_path, &self.root_path) + .unwrap_or_else(|| file_path.to_path_buf()), + ) + } + + fn is_directory_ignored(&mut self, relative_path: &str) -> bool { + let relative_path = relative_path.trim_end_matches('/'); + if relative_path.is_empty() { + return false; + } + + if let Some(ignored) = self.ignored_directories.get(relative_path) { + return *ignored; + } + + let ignored = parent_directory(relative_path) + .is_some_and(|parent| self.is_directory_ignored(parent)) + || is_ignore_match(self.matcher.matched(relative_path, true)); + self.ignored_directories + .insert(relative_path.into(), ignored); + ignored + } +} + +fn parent_directory(relative_path: &str) -> Option<&str> { + let separator = relative_path.rfind('/')?; + (separator > 0).then_some(&relative_path[..separator]) +} + +fn is_ignore_match(matched: Match<&ignore::gitignore::Glob>) -> bool { + matches!(matched, Match::Ignore(_)) +} + +fn normalize_relative_path(path: &str) -> &str { + let path = path.trim_matches('/'); + if path == "." { + "" + } else { + path.strip_prefix("./").unwrap_or(path) + } +} + +fn to_posix_path(path: &Path) -> Cow<'_, str> { + let path = path.to_string_lossy(); + + #[cfg(windows)] + { + Cow::Owned(path.replace('\\', "/")) + } + + #[cfg(not(windows))] + { + path + } +} + +#[cfg(test)] +mod tests { + use super::{GitIgnoreMatcher, IgnoreMatcher, IgnoreSource}; + use std::path::Path; + + #[test] + fn keeps_independent_ignore_sources_isolated() { + let mut matcher = IgnoreMatcher::new([ + IgnoreSource::new("project", "*.js\n!keep.js"), + IgnoreSource::new("project", "keep.js"), + ]) + .unwrap(); + + assert!(matcher.is_ignored(Path::new("project/keep.js"), false)); + assert!(matcher.is_ignored(Path::new("project/drop.js"), false)); + assert!(!matcher.is_ignored(Path::new("project/keep.ts"), false)); + } + + #[test] + fn applies_nested_sources_and_child_negation() { + let mut matcher = GitIgnoreMatcher::new(); + matcher.add_source("", "*.js\ndist/\n").unwrap(); + matcher.add_source("src", "!keep.js\n").unwrap(); + matcher.add_source("dist", "!keep.js\n").unwrap(); + + assert!(!matcher.is_ignored("src/keep.js", false)); + assert!(matcher.is_ignored("src/drop.js", false)); + assert!(matcher.is_ignored("dist", true)); + assert!(matcher.is_ignored("dist/keep.js", false)); + assert!(!matcher.is_ignored("visible.ts", false)); + } + + #[test] + fn does_not_propagate_an_ancestor_unignore_across_sources() { + let mut matcher = GitIgnoreMatcher::new(); + matcher.add_source("", "debug/\n").unwrap(); + matcher.add_source("scripts", "!debug\n").unwrap(); + + assert!(!matcher.is_ignored("scripts/debug", true)); + assert!(matcher.is_ignored("scripts/debug/launch.mjs", false)); + } + + #[test] + fn applies_a_nested_source_without_root_rules() { + let mut matcher = GitIgnoreMatcher::new(); + matcher.add_source("src", "*.js\n").unwrap(); + + assert!(!matcher.is_ignored("root.js", false)); + assert!(matcher.is_ignored("src/drop.js", false)); + assert!(!matcher.is_ignored("src/keep.ts", false)); + } + + #[test] + fn preserves_valid_rules_around_malformed_and_normalized_lines() { + let mut matcher = GitIgnoreMatcher::new(); + matcher + .add_source("", "\u{feff}ignored.js\r\nmalformed\\\n*.snap\n") + .unwrap(); + + assert!(matcher.is_ignored("IGNORED.JS", false)); + assert!(matcher.is_ignored("nested/value.snap", false)); + assert!(!matcher.is_ignored("nested/value.ts", false)); + } + + #[test] + fn invalidates_descendant_directory_cache_when_adding_a_source() { + let mut matcher = GitIgnoreMatcher::new(); + matcher.add_source("", "generated/keep/**\n").unwrap(); + + assert!(matcher.is_ignored("generated/keep/nested", true)); + matcher.add_source("generated/keep", "!nested/\n").unwrap(); + + assert!(!matcher.is_ignored("generated/keep/nested", true)); + } + + #[test] + fn matches_batches_in_input_order() { + let mut matcher = GitIgnoreMatcher::new(); + matcher.add_source("", "*.js\ndist/\n").unwrap(); + let names = vec!["index.js".into(), "index.ts".into(), "dist".into()]; + + assert_eq!( + matcher.is_ignored_batch("nested", &names, &[0, 0, 1]), + vec![1, 0, 1] + ); + assert_eq!( + matcher.is_ignored_batch_mask("nested", &names, 0b100), + 0b101 + ); + assert!(matcher.is_ignored_child("nested", "index.js", false)); + } + + #[test] + fn omits_empty_sources() { + let mut matcher = GitIgnoreMatcher::new(); + + assert!(!matcher.add_source("", "# comment only\n").unwrap()); + assert!(!matcher.is_ignored("index.js", false)); + } +} diff --git a/cspell.config.js b/cspell.config.js index 5e8e78e2..46db6ca7 100644 --- a/cspell.config.js +++ b/cspell.config.js @@ -19,6 +19,7 @@ export default { 'coverage', 'doc_build', 'node_modules', + 'packages/rstack/binding.cjs', 'packages/rstack/THIRD_PARTY_NOTICES.md', 'pnpm-lock.yaml', ], diff --git a/package.json b/package.json index 4c40375e..7d2aa3e5 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "license": "MIT", "type": "module", "scripts": { - "build": "pnpm --filter './packages/**' build", - "check:format": "rs fmt --check", + "build": "pnpm --filter \"./packages/**\" build", + "check": "rs check --type-check", "check:spell": "pnpm dlx cspell && heading-case", "doc": "pnpm --dir website dev", "doc:build": "node --run build && pnpm --dir website build", @@ -14,7 +14,7 @@ "lint": "rs lint --type-check", "prepare": "node scripts/rs.js setup", "release:prepare": "node scripts/prepare-release.js", - "test": "pnpm --filter './packages/**' test" + "test": "pnpm --parallel --filter \"./packages/**\" test" }, "devDependencies": { "@types/node": "catalog:", @@ -24,5 +24,5 @@ "rstack": "workspace:*", "typescript": "catalog:" }, - "packageManager": "pnpm@11.20.0" + "packageManager": "pnpm@11.21.0" } diff --git a/packages/create-rstack/README.md b/packages/create-rstack/README.md index c0b7c4f0..fcac4ec4 100644 --- a/packages/create-rstack/README.md +++ b/packages/create-rstack/README.md @@ -17,20 +17,39 @@ npx create-rstack --dir my-project --template app-vanilla-ts # Using abbreviations npx create-rstack -d my-project -t app-vanilla-ts + +# Skip Git initialization +npx create-rstack --dir my-project --template app-vanilla-ts --no-git ``` ## Templates -- `app-vanilla-js` - JavaScript Vanilla application +- `app-vanilla` - JavaScript Vanilla application - `app-vanilla-ts` - TypeScript Vanilla application -- `app-react-js` - JavaScript React application +- `app-react` - JavaScript React application - `app-react-ts` - TypeScript React application -- `app-vue-js` - JavaScript Vue application +- `app-preact` - JavaScript Preact application +- `app-preact-ts` - TypeScript Preact application +- `app-vue` - JavaScript Vue application - `app-vue-ts` - TypeScript Vue application -- `lib-node-js` - JavaScript Node.js library +- `app-lit` - JavaScript Lit application +- `app-lit-ts` - TypeScript Lit application +- `app-svelte` - JavaScript Svelte application +- `app-svelte-ts` - TypeScript Svelte application +- `app-solid` - JavaScript Solid application +- `app-solid-ts` - TypeScript Solid application +- `lib-node` - JavaScript Node.js library - `lib-node-ts` - TypeScript Node.js library -- `lib-react-js` - JavaScript React library +- `lib-react` - JavaScript React library - `lib-react-ts` - TypeScript React library +- `lib-vue` - JavaScript Vue library +- `lib-vue-ts` - TypeScript Vue library +- `lib-svelte` - JavaScript Svelte library +- `lib-svelte-ts` - TypeScript Svelte library +- `lib-solid` - JavaScript Solid library +- `lib-solid-ts` - TypeScript Solid library +- `doc` - Basic documentation site +- `doc-i18n` - Multilingual documentation site ## Documentation diff --git a/packages/create-rstack/package.json b/packages/create-rstack/package.json index 7fbea590..0f9c3d1f 100644 --- a/packages/create-rstack/package.json +++ b/packages/create-rstack/package.json @@ -1,6 +1,6 @@ { "name": "create-rstack", - "version": "3.0.0", + "version": "3.1.0", "description": "Create a new Rstack project", "homepage": "https://rstack.rs", "bugs": { diff --git a/packages/create-rstack/src/index.ts b/packages/create-rstack/src/index.ts index e895cecd..f6f32f78 100644 --- a/packages/create-rstack/src/index.ts +++ b/packages/create-rstack/src/index.ts @@ -1,30 +1,59 @@ -import { type Argv, checkCancel, create, select } from '@rstackjs/create-toolkit'; +import { + type Argv, + type GitResolvedContext, + checkCancel, + create, + select, +} from '@rstackjs/create-toolkit'; +import { access, appendFile, mkdir, readFile, writeFile } from 'node:fs/promises'; import path from 'node:path'; -const getTemplateName = async ({ template }: Argv): Promise => { - if (typeof template === 'string') { - if (template === 'app' || template.startsWith('app-')) { - const [, framework = 'vanilla', language = 'js'] = template.split('-'); - - if (framework === 'js' || framework === 'ts') { - return `app-vanilla-${framework}`; - } +const packageRoot = path.join(import.meta.dirname, '..'); - return `app-${framework}-${language}`; - } +const templateNames = [ + 'app-vanilla', + 'app-vanilla-ts', + 'app-react', + 'app-react-ts', + 'app-preact', + 'app-preact-ts', + 'app-vue', + 'app-vue-ts', + 'app-lit', + 'app-lit-ts', + 'app-svelte', + 'app-svelte-ts', + 'app-solid', + 'app-solid-ts', + 'lib-node', + 'lib-node-ts', + 'lib-react', + 'lib-react-ts', + 'lib-vue', + 'lib-vue-ts', + 'lib-svelte', + 'lib-svelte-ts', + 'lib-solid', + 'lib-solid-ts', + 'doc', + 'doc-i18n', +]; - if (template === 'lib' || template.startsWith('lib-')) { - const [, libraryType = 'node', language = 'js'] = template.split('-'); +const resolveTemplateName = (template: string): string => { + if (!templateNames.includes(template)) { + throw new Error(`Invalid input: template "${template}" not found.`); + } - if (libraryType === 'js' || libraryType === 'ts') { - return `lib-node-${libraryType}`; - } + return template; +}; - return `lib-${libraryType}-${language}`; +const getTemplateName = async ({ template }: Argv): Promise => { + if (typeof template === 'string') { + if (/^(?:app|lib|doc)(?:-|$)/u.test(template)) { + return resolveTemplateName(template); } - const [type, language = 'js'] = template.split('-'); - return `${type}-${language}`; + return template; } const projectType = checkCancel( @@ -33,10 +62,34 @@ const getTemplateName = async ({ template }: Argv): Promise => { options: [ { value: 'app', label: 'Web Application' }, { value: 'lib', label: 'Library' }, + { value: 'doc', label: 'Documentation' }, ], }), ); + if (projectType === 'doc') { + const documentationType = checkCancel( + await select({ + message: 'Choose documentation language setup', + initialValue: 'basic', + options: [ + { + value: 'basic', + label: 'Single language', + hint: 'docs', + }, + { + value: 'i18n', + label: 'Multilingual', + hint: 'docs/en, docs/zh', + }, + ], + }), + ); + + return resolveTemplateName(documentationType === 'basic' ? 'doc' : 'doc-i18n'); + } + const templateType = checkCancel( await select({ message: projectType === 'app' ? 'Select framework' : 'Select library type', @@ -45,11 +98,18 @@ const getTemplateName = async ({ template }: Argv): Promise => { ? [ { value: 'vanilla', label: 'Vanilla' }, { value: 'react', label: 'React' }, + { value: 'preact', label: 'Preact' }, { value: 'vue', label: 'Vue' }, + { value: 'lit', label: 'Lit' }, + { value: 'svelte', label: 'Svelte' }, + { value: 'solid', label: 'Solid' }, ] : [ { value: 'node', label: 'Node.js' }, { value: 'react', label: 'React' }, + { value: 'vue', label: 'Vue' }, + { value: 'svelte', label: 'Svelte' }, + { value: 'solid', label: 'Solid' }, ], }), ); @@ -64,24 +124,71 @@ const getTemplateName = async ({ template }: Argv): Promise => { }), ); - return `${projectType}-${templateType}-${language}`; + const templateName = `${projectType}-${templateType}${language === 'ts' ? '-ts' : ''}`; + return resolveTemplateName(templateName); +}; + +const getStagedConfig = (templateName: string): string => { + const scriptExtensions = ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs', 'mts', 'cts']; + const formatExtensions = ['json', 'jsonc', 'md', 'mdx', 'css', 'html', 'yml', 'yaml']; + const componentExtensions = ['svelte', 'vue']; + const templateFormatExtensions = [ + ...formatExtensions, + ...componentExtensions.filter((extension) => templateName.includes(extension)), + ]; + + return [ + '', + 'define.staged({', + ` '*.{${scriptExtensions.join(',')}}': ['rs lint --fix', 'rs fmt'],`, + ` '*.{${templateFormatExtensions.join(',')}}': 'rs fmt',`, + '});', + '', + ].join('\n'); +}; + +const injectStagedSetup = async ({ + templateName, + distFolder, + gitEnabled, + isGitRoot, +}: GitResolvedContext): Promise => { + if (!gitEnabled || !isGitRoot) { + return; + } + + const configExtension = await access(path.join(distFolder, 'rstack.config.ts')).then( + () => 'ts', + () => 'js', + ); + const packageJsonPath = path.join(distFolder, 'package.json'); + const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')) as { + scripts: Record; + }; + + packageJson.scripts = Object.fromEntries( + Object.entries({ ...packageJson.scripts, prepare: 'rs setup' }).sort(([left], [right]) => + left.localeCompare(right), + ), + ); + + const hooksDirectory = path.join(distFolder, '.rstack', 'hooks'); + await mkdir(hooksDirectory, { recursive: true }); + await Promise.all([ + writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`), + appendFile( + path.join(distFolder, `rstack.config.${configExtension}`), + getStagedConfig(templateName), + ), + writeFile(path.join(hooksDirectory, 'pre-commit'), 'rs staged\n'), + ]); }; await create({ - root: path.join(import.meta.dirname, '..'), + root: packageRoot, name: 'rstack', - templates: [ - 'app-vanilla-js', - 'app-vanilla-ts', - 'app-react-js', - 'app-react-ts', - 'app-vue-js', - 'app-vue-ts', - 'lib-node-js', - 'lib-node-ts', - 'lib-react-js', - 'lib-react-ts', - ], + templates: templateNames, builtinTools: [], getTemplateName, + onGitResolved: injectStagedSetup, }); diff --git a/packages/create-rstack/template-app-lit-ts/package.json b/packages/create-rstack/template-app-lit-ts/package.json new file mode 100644 index 00000000..b1f4e6ba --- /dev/null +++ b/packages/create-rstack/template-app-lit-ts/package.json @@ -0,0 +1,25 @@ +{ + "name": "rstack-app-lit-ts", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "rs build", + "check": "rs check --type-check", + "dev": "rs dev", + "format": "rs fmt", + "lint": "rs lint", + "preview": "rs preview", + "test": "rs test", + "test:watch": "rs test --watch" + }, + "dependencies": { + "lit": "^3.3.3" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "happy-dom": "^20.11.2", + "rstack": "^0.5.0", + "typescript": "^7.0.2" + } +} diff --git a/packages/create-rstack/template-app-lit-ts/rstack.config.ts b/packages/create-rstack/template-app-lit-ts/rstack.config.ts new file mode 100644 index 00000000..cdfe0ac0 --- /dev/null +++ b/packages/create-rstack/template-app-lit-ts/rstack.config.ts @@ -0,0 +1,27 @@ +// Rstack configuration guide: https://rstack.rs/config +import { define } from 'rstack'; + +define.app({ + html: { + template: './src/index.html', + }, + source: { + decorators: { + version: 'legacy', + }, + }, +}); + +define.test({ + testEnvironment: 'happy-dom', +}); + +define.lint(async () => { + const { js, ts } = await import('rstack/lint'); + + return [js.configs.recommended, ts.configs.recommended]; +}); + +define.fmt({ + singleQuote: true, +}); diff --git a/packages/create-rstack/template-app-vue-js/src/index.css b/packages/create-rstack/template-app-lit-ts/src/index.css similarity index 100% rename from packages/create-rstack/template-app-vue-js/src/index.css rename to packages/create-rstack/template-app-lit-ts/src/index.css diff --git a/packages/create-rstack/template-app-lit-ts/src/index.html b/packages/create-rstack/template-app-lit-ts/src/index.html new file mode 100644 index 00000000..b3a6a02e --- /dev/null +++ b/packages/create-rstack/template-app-lit-ts/src/index.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/create-rstack/template-app-lit-ts/src/index.ts b/packages/create-rstack/template-app-lit-ts/src/index.ts new file mode 100644 index 00000000..ec31c9eb --- /dev/null +++ b/packages/create-rstack/template-app-lit-ts/src/index.ts @@ -0,0 +1,4 @@ +import './index.css'; +import { MyElement } from './my-element'; + +customElements.define('my-element', MyElement); diff --git a/packages/create-rstack/template-app-lit-ts/src/my-element.ts b/packages/create-rstack/template-app-lit-ts/src/my-element.ts new file mode 100644 index 00000000..9a370ce8 --- /dev/null +++ b/packages/create-rstack/template-app-lit-ts/src/my-element.ts @@ -0,0 +1,34 @@ +import { html, css, LitElement } from 'lit'; + +export class MyElement extends LitElement { + static styles = css` + .content { + display: flex; + min-height: 100vh; + line-height: 1.1; + text-align: center; + flex-direction: column; + justify-content: center; + } + + .content h1 { + font-size: 3.6rem; + font-weight: 700; + } + + .content p { + font-size: 1.2rem; + font-weight: 400; + opacity: 0.5; + } + `; + + render() { + return html` +
+

Rstack with Lit

+

Start building amazing things with Rstack.

+
+ `; + } +} diff --git a/packages/create-rstack/template-app-lit-ts/tests/index.test.ts b/packages/create-rstack/template-app-lit-ts/tests/index.test.ts new file mode 100644 index 00000000..352a5a74 --- /dev/null +++ b/packages/create-rstack/template-app-lit-ts/tests/index.test.ts @@ -0,0 +1,12 @@ +import { expect, test } from 'rstack/test'; +import { MyElement } from '../src/my-element'; + +test('renders the main page', async () => { + customElements.define('my-element', MyElement); + const element = document.createElement('my-element') as MyElement; + document.body.append(element); + + await element.updateComplete; + + expect(element.shadowRoot?.textContent).toContain('Rstack with Lit'); +}); diff --git a/packages/create-rstack/template-app-lit-ts/tsconfig.json b/packages/create-rstack/template-app-lit-ts/tsconfig.json new file mode 100644 index 00000000..a6f5c445 --- /dev/null +++ b/packages/create-rstack/template-app-lit-ts/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "lib": ["DOM", "ES2020"], + "target": "ES2020", + "noEmit": true, + "skipLibCheck": true, + "types": ["rstack/types", "node"], + "experimentalDecorators": true, + "useDefineForClassFields": false, + + /* modules */ + "moduleDetection": "force", + "moduleResolution": "bundler", + "verbatimModuleSyntax": true, + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + + /* type checking */ + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["src"] +} diff --git a/packages/create-rstack/template-app-react-js/package.json b/packages/create-rstack/template-app-lit/package.json similarity index 58% rename from packages/create-rstack/template-app-react-js/package.json rename to packages/create-rstack/template-app-lit/package.json index 09dde494..ceac56cf 100644 --- a/packages/create-rstack/template-app-react-js/package.json +++ b/packages/create-rstack/template-app-lit/package.json @@ -1,22 +1,23 @@ { - "name": "rstack-app-react-js", + "name": "rstack-app-lit", "version": "1.0.0", "private": true, "type": "module", "scripts": { "build": "rs build", + "check": "rs check", "dev": "rs dev", "format": "rs fmt", "lint": "rs lint", "preview": "rs preview", - "test": "rs test" + "test": "rs test", + "test:watch": "rs test --watch" }, "dependencies": { - "react": "^19.2.8", - "react-dom": "^19.2.8" + "lit": "^3.3.3" }, "devDependencies": { - "@rsbuild/plugin-react": "^2.1.0", - "rstack": "^0.3.5" + "happy-dom": "^20.11.2", + "rstack": "^0.5.0" } } diff --git a/packages/create-rstack/template-app-lit/rstack.config.js b/packages/create-rstack/template-app-lit/rstack.config.js new file mode 100644 index 00000000..d09e1329 --- /dev/null +++ b/packages/create-rstack/template-app-lit/rstack.config.js @@ -0,0 +1,28 @@ +// @ts-check +// Rstack configuration guide: https://rstack.rs/config +import { define } from 'rstack'; + +define.app({ + html: { + template: './src/index.html', + }, + source: { + decorators: { + version: 'legacy', + }, + }, +}); + +define.test({ + testEnvironment: 'happy-dom', +}); + +define.lint(async () => { + const { js } = await import('rstack/lint'); + + return [js.configs.recommended]; +}); + +define.fmt({ + singleQuote: true, +}); diff --git a/packages/create-rstack/template-app-lit/src/index.css b/packages/create-rstack/template-app-lit/src/index.css new file mode 100644 index 00000000..85e7e2b4 --- /dev/null +++ b/packages/create-rstack/template-app-lit/src/index.css @@ -0,0 +1,6 @@ +body { + margin: 0; + color: #fff; + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + background-image: linear-gradient(to bottom, #020917, #101725); +} diff --git a/packages/create-rstack/template-app-lit/src/index.html b/packages/create-rstack/template-app-lit/src/index.html new file mode 100644 index 00000000..b3a6a02e --- /dev/null +++ b/packages/create-rstack/template-app-lit/src/index.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/create-rstack/template-app-lit/src/index.js b/packages/create-rstack/template-app-lit/src/index.js new file mode 100644 index 00000000..ec31c9eb --- /dev/null +++ b/packages/create-rstack/template-app-lit/src/index.js @@ -0,0 +1,4 @@ +import './index.css'; +import { MyElement } from './my-element'; + +customElements.define('my-element', MyElement); diff --git a/packages/create-rstack/template-app-lit/src/my-element.js b/packages/create-rstack/template-app-lit/src/my-element.js new file mode 100644 index 00000000..9a370ce8 --- /dev/null +++ b/packages/create-rstack/template-app-lit/src/my-element.js @@ -0,0 +1,34 @@ +import { html, css, LitElement } from 'lit'; + +export class MyElement extends LitElement { + static styles = css` + .content { + display: flex; + min-height: 100vh; + line-height: 1.1; + text-align: center; + flex-direction: column; + justify-content: center; + } + + .content h1 { + font-size: 3.6rem; + font-weight: 700; + } + + .content p { + font-size: 1.2rem; + font-weight: 400; + opacity: 0.5; + } + `; + + render() { + return html` +
+

Rstack with Lit

+

Start building amazing things with Rstack.

+
+ `; + } +} diff --git a/packages/create-rstack/template-app-lit/tests/index.test.js b/packages/create-rstack/template-app-lit/tests/index.test.js new file mode 100644 index 00000000..abb38d26 --- /dev/null +++ b/packages/create-rstack/template-app-lit/tests/index.test.js @@ -0,0 +1,12 @@ +import { expect, test } from 'rstack/test'; +import { MyElement } from '../src/my-element'; + +test('renders the main page', async () => { + customElements.define('my-element', MyElement); + const element = document.createElement('my-element'); + document.body.append(element); + + await element.updateComplete; + + expect(element.shadowRoot?.textContent).toContain('Rstack with Lit'); +}); diff --git a/packages/create-rstack/template-app-preact-ts/package.json b/packages/create-rstack/template-app-preact-ts/package.json new file mode 100644 index 00000000..37a87670 --- /dev/null +++ b/packages/create-rstack/template-app-preact-ts/package.json @@ -0,0 +1,28 @@ +{ + "name": "rstack-app-preact-ts", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "rs build", + "check": "rs check --type-check", + "dev": "rs dev", + "format": "rs fmt", + "lint": "rs lint", + "preview": "rs preview", + "test": "rs test", + "test:watch": "rs test --watch" + }, + "dependencies": { + "preact": "^10.29.8" + }, + "devDependencies": { + "@rsbuild/plugin-preact": "^2.0.0", + "@testing-library/jest-dom": "^7.0.0", + "@testing-library/preact": "^3.2.4", + "@types/node": "^24.13.3", + "happy-dom": "^20.11.2", + "rstack": "^0.5.0", + "typescript": "^7.0.2" + } +} diff --git a/packages/create-rstack/template-app-preact-ts/rstack.config.ts b/packages/create-rstack/template-app-preact-ts/rstack.config.ts new file mode 100644 index 00000000..9ee763c8 --- /dev/null +++ b/packages/create-rstack/template-app-preact-ts/rstack.config.ts @@ -0,0 +1,29 @@ +// Rstack configuration guide: https://rstack.rs/config +import { define } from 'rstack'; + +define.app(async () => { + const { pluginPreact } = await import('@rsbuild/plugin-preact'); + + return { + plugins: [pluginPreact()], + }; +}); + +define.test({ + setupFiles: ['./tests/rstest.setup.ts'], +}); + +define.lint(async () => { + const { js, ts, reactHooksPlugin, reactPlugin } = await import('rstack/lint'); + + return [ + js.configs.recommended, + ts.configs.recommended, + reactPlugin.configs.recommended, + reactHooksPlugin.configs.recommended, + ]; +}); + +define.fmt({ + singleQuote: true, +}); diff --git a/packages/create-rstack/template-app-react-js/src/App.css b/packages/create-rstack/template-app-preact-ts/src/App.css similarity index 100% rename from packages/create-rstack/template-app-react-js/src/App.css rename to packages/create-rstack/template-app-preact-ts/src/App.css diff --git a/packages/create-rstack/template-app-preact-ts/src/App.tsx b/packages/create-rstack/template-app-preact-ts/src/App.tsx new file mode 100644 index 00000000..6509f4fa --- /dev/null +++ b/packages/create-rstack/template-app-preact-ts/src/App.tsx @@ -0,0 +1,12 @@ +import './App.css'; + +const App = () => { + return ( +
+

Rstack with Preact

+

Start building amazing things with Rstack.

+
+ ); +}; + +export default App; diff --git a/packages/create-rstack/template-app-preact-ts/src/index.tsx b/packages/create-rstack/template-app-preact-ts/src/index.tsx new file mode 100644 index 00000000..c2c9df7c --- /dev/null +++ b/packages/create-rstack/template-app-preact-ts/src/index.tsx @@ -0,0 +1,7 @@ +import { render } from 'preact'; +import App from './App'; + +const root = document.getElementById('root'); +if (root) { + render(, root); +} diff --git a/packages/create-rstack/template-app-preact-ts/tests/index.test.tsx b/packages/create-rstack/template-app-preact-ts/tests/index.test.tsx new file mode 100644 index 00000000..77cd1213 --- /dev/null +++ b/packages/create-rstack/template-app-preact-ts/tests/index.test.tsx @@ -0,0 +1,8 @@ +import { render, screen } from '@testing-library/preact'; +import { expect, test } from 'rstack/test'; +import App from '../src/App'; + +test('renders the main page', () => { + render(); + expect(screen.getByText('Rstack with Preact')).toBeInTheDocument(); +}); diff --git a/packages/create-rstack/template-lib-react-js/tests/rstest.setup.js b/packages/create-rstack/template-app-preact-ts/tests/rstest.setup.ts similarity index 100% rename from packages/create-rstack/template-lib-react-js/tests/rstest.setup.js rename to packages/create-rstack/template-app-preact-ts/tests/rstest.setup.ts diff --git a/packages/create-rstack/template-app-preact-ts/tests/tsconfig.json b/packages/create-rstack/template-app-preact-ts/tests/tsconfig.json new file mode 100644 index 00000000..3eb5dc12 --- /dev/null +++ b/packages/create-rstack/template-app-preact-ts/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["rstack/types", "node", "@testing-library/jest-dom"] + }, + "include": ["./"] +} diff --git a/packages/create-rstack/template-app-preact-ts/tsconfig.json b/packages/create-rstack/template-app-preact-ts/tsconfig.json new file mode 100644 index 00000000..54f737ff --- /dev/null +++ b/packages/create-rstack/template-app-preact-ts/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "lib": ["DOM", "ES2020"], + "jsx": "react-jsx", + "target": "ES2020", + "noEmit": true, + "skipLibCheck": true, + "types": ["rstack/types", "node"], + "jsxImportSource": "preact", + "useDefineForClassFields": true, + + /* modules */ + "moduleDetection": "force", + "moduleResolution": "bundler", + "verbatimModuleSyntax": true, + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + "paths": { + "react": ["./node_modules/preact/compat/"], + "react-dom": ["./node_modules/preact/compat/"] + }, + + /* type checking */ + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["src"] +} diff --git a/packages/create-rstack/template-app-preact/package.json b/packages/create-rstack/template-app-preact/package.json new file mode 100644 index 00000000..156c61d6 --- /dev/null +++ b/packages/create-rstack/template-app-preact/package.json @@ -0,0 +1,26 @@ +{ + "name": "rstack-app-preact", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "rs build", + "check": "rs check", + "dev": "rs dev", + "format": "rs fmt", + "lint": "rs lint", + "preview": "rs preview", + "test": "rs test", + "test:watch": "rs test --watch" + }, + "dependencies": { + "preact": "^10.29.8" + }, + "devDependencies": { + "@rsbuild/plugin-preact": "^2.0.0", + "@testing-library/jest-dom": "^7.0.0", + "@testing-library/preact": "^3.2.4", + "happy-dom": "^20.11.2", + "rstack": "^0.5.0" + } +} diff --git a/packages/create-rstack/template-app-preact/rstack.config.js b/packages/create-rstack/template-app-preact/rstack.config.js new file mode 100644 index 00000000..e2b64e47 --- /dev/null +++ b/packages/create-rstack/template-app-preact/rstack.config.js @@ -0,0 +1,29 @@ +// @ts-check +// Rstack configuration guide: https://rstack.rs/config +import { define } from 'rstack'; + +define.app(async () => { + const { pluginPreact } = await import('@rsbuild/plugin-preact'); + + return { + plugins: [pluginPreact()], + }; +}); + +define.test({ + setupFiles: ['./tests/rstest.setup.js'], +}); + +define.lint(async () => { + const { js, reactHooksPlugin, reactPlugin } = await import('rstack/lint'); + + return [ + js.configs.recommended, + reactPlugin.configs.recommended, + reactHooksPlugin.configs.recommended, + ]; +}); + +define.fmt({ + singleQuote: true, +}); diff --git a/packages/create-rstack/template-app-vanilla-js/src/index.css b/packages/create-rstack/template-app-preact/src/App.css similarity index 100% rename from packages/create-rstack/template-app-vanilla-js/src/index.css rename to packages/create-rstack/template-app-preact/src/App.css diff --git a/packages/create-rstack/template-app-preact/src/App.jsx b/packages/create-rstack/template-app-preact/src/App.jsx new file mode 100644 index 00000000..6509f4fa --- /dev/null +++ b/packages/create-rstack/template-app-preact/src/App.jsx @@ -0,0 +1,12 @@ +import './App.css'; + +const App = () => { + return ( +
+

Rstack with Preact

+

Start building amazing things with Rstack.

+
+ ); +}; + +export default App; diff --git a/packages/create-rstack/template-app-preact/src/index.jsx b/packages/create-rstack/template-app-preact/src/index.jsx new file mode 100644 index 00000000..0fe15550 --- /dev/null +++ b/packages/create-rstack/template-app-preact/src/index.jsx @@ -0,0 +1,4 @@ +import { render } from 'preact'; +import App from './App'; + +render(, document.getElementById('root')); diff --git a/packages/create-rstack/template-app-preact/tests/index.test.jsx b/packages/create-rstack/template-app-preact/tests/index.test.jsx new file mode 100644 index 00000000..77cd1213 --- /dev/null +++ b/packages/create-rstack/template-app-preact/tests/index.test.jsx @@ -0,0 +1,8 @@ +import { render, screen } from '@testing-library/preact'; +import { expect, test } from 'rstack/test'; +import App from '../src/App'; + +test('renders the main page', () => { + render(); + expect(screen.getByText('Rstack with Preact')).toBeInTheDocument(); +}); diff --git a/packages/create-rstack/template-app-preact/tests/rstest.setup.js b/packages/create-rstack/template-app-preact/tests/rstest.setup.js new file mode 100644 index 00000000..93951b15 --- /dev/null +++ b/packages/create-rstack/template-app-preact/tests/rstest.setup.js @@ -0,0 +1,4 @@ +import { expect } from 'rstack/test'; +import * as jestDomMatchers from '@testing-library/jest-dom/matchers'; + +expect.extend(jestDomMatchers); diff --git a/packages/create-rstack/template-app-react-js/AGENTS.md b/packages/create-rstack/template-app-react-js/AGENTS.md deleted file mode 100644 index fb378511..00000000 --- a/packages/create-rstack/template-app-react-js/AGENTS.md +++ /dev/null @@ -1,12 +0,0 @@ -# AGENTS.md - -## Commands - -- `{{ packageManager }} run dev` - Start the development server -- `{{ packageManager }} run build` - Build the app for production -- `{{ packageManager }} run preview` - Preview the production build locally - -## Docs - -- Rsbuild: https://rsbuild.rs/llms.txt -- Rspack: https://rspack.rs/llms.txt diff --git a/packages/create-rstack/template-app-react-ts/AGENTS.md b/packages/create-rstack/template-app-react-ts/AGENTS.md deleted file mode 100644 index fb378511..00000000 --- a/packages/create-rstack/template-app-react-ts/AGENTS.md +++ /dev/null @@ -1,12 +0,0 @@ -# AGENTS.md - -## Commands - -- `{{ packageManager }} run dev` - Start the development server -- `{{ packageManager }} run build` - Build the app for production -- `{{ packageManager }} run preview` - Preview the production build locally - -## Docs - -- Rsbuild: https://rsbuild.rs/llms.txt -- Rspack: https://rspack.rs/llms.txt diff --git a/packages/create-rstack/template-app-react-ts/package.json b/packages/create-rstack/template-app-react-ts/package.json index a8b73f0a..755c6b1c 100644 --- a/packages/create-rstack/template-app-react-ts/package.json +++ b/packages/create-rstack/template-app-react-ts/package.json @@ -5,11 +5,13 @@ "type": "module", "scripts": { "build": "rs build", + "check": "rs check --type-check", "dev": "rs dev", "format": "rs fmt", "lint": "rs lint", "preview": "rs preview", - "test": "rs test" + "test": "rs test", + "test:watch": "rs test --watch" }, "dependencies": { "react": "^19.2.8", @@ -17,10 +19,14 @@ }, "devDependencies": { "@rsbuild/plugin-react": "^2.1.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^7.0.0", + "@testing-library/react": "^16.3.2", "@types/node": "^24.13.3", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", - "rstack": "^0.3.5", + "happy-dom": "^20.11.2", + "rstack": "^0.5.0", "typescript": "^7.0.2" } } diff --git a/packages/create-rstack/template-app-react-ts/rstack.config.ts b/packages/create-rstack/template-app-react-ts/rstack.config.ts index 29ab420e..356c98e1 100644 --- a/packages/create-rstack/template-app-react-ts/rstack.config.ts +++ b/packages/create-rstack/template-app-react-ts/rstack.config.ts @@ -10,7 +10,7 @@ define.app(async () => { }); define.test({ - // Configure Rstest + setupFiles: ['./tests/rstest.setup.ts'], }); define.lint(async () => { @@ -23,3 +23,7 @@ define.lint(async () => { reactHooksPlugin.configs.recommended, ]; }); + +define.fmt({ + singleQuote: true, +}); diff --git a/packages/create-rstack/template-app-react-ts/tests/index.test.tsx b/packages/create-rstack/template-app-react-ts/tests/index.test.tsx new file mode 100644 index 00000000..2fe4174b --- /dev/null +++ b/packages/create-rstack/template-app-react-ts/tests/index.test.tsx @@ -0,0 +1,8 @@ +import { expect, test } from 'rstack/test'; +import { render, screen } from '@testing-library/react'; +import App from '../src/App'; + +test('renders the main page', () => { + render(); + expect(screen.getByText('Rstack with React')).toBeInTheDocument(); +}); diff --git a/packages/create-rstack/template-app-react-ts/tests/rstest.setup.ts b/packages/create-rstack/template-app-react-ts/tests/rstest.setup.ts new file mode 100644 index 00000000..93951b15 --- /dev/null +++ b/packages/create-rstack/template-app-react-ts/tests/rstest.setup.ts @@ -0,0 +1,4 @@ +import { expect } from 'rstack/test'; +import * as jestDomMatchers from '@testing-library/jest-dom/matchers'; + +expect.extend(jestDomMatchers); diff --git a/packages/create-rstack/template-app-react-ts/tests/tsconfig.json b/packages/create-rstack/template-app-react-ts/tests/tsconfig.json new file mode 100644 index 00000000..3eb5dc12 --- /dev/null +++ b/packages/create-rstack/template-app-react-ts/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["rstack/types", "node", "@testing-library/jest-dom"] + }, + "include": ["./"] +} diff --git a/packages/create-rstack/template-app-react/package.json b/packages/create-rstack/template-app-react/package.json new file mode 100644 index 00000000..83f717ba --- /dev/null +++ b/packages/create-rstack/template-app-react/package.json @@ -0,0 +1,28 @@ +{ + "name": "rstack-app-react", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "rs build", + "check": "rs check", + "dev": "rs dev", + "format": "rs fmt", + "lint": "rs lint", + "preview": "rs preview", + "test": "rs test", + "test:watch": "rs test --watch" + }, + "dependencies": { + "react": "^19.2.8", + "react-dom": "^19.2.8" + }, + "devDependencies": { + "@rsbuild/plugin-react": "^2.1.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^7.0.0", + "@testing-library/react": "^16.3.2", + "happy-dom": "^20.11.2", + "rstack": "^0.5.0" + } +} diff --git a/packages/create-rstack/template-app-react-js/rstack.config.js b/packages/create-rstack/template-app-react/rstack.config.js similarity index 85% rename from packages/create-rstack/template-app-react-js/rstack.config.js rename to packages/create-rstack/template-app-react/rstack.config.js index a89f2994..ddd9f056 100644 --- a/packages/create-rstack/template-app-react-js/rstack.config.js +++ b/packages/create-rstack/template-app-react/rstack.config.js @@ -11,7 +11,7 @@ define.app(async () => { }); define.test({ - // Configure Rstest + setupFiles: ['./tests/rstest.setup.js'], }); define.lint(async () => { @@ -23,3 +23,7 @@ define.lint(async () => { reactHooksPlugin.configs.recommended, ]; }); + +define.fmt({ + singleQuote: true, +}); diff --git a/packages/create-rstack/template-app-react/src/App.css b/packages/create-rstack/template-app-react/src/App.css new file mode 100644 index 00000000..164c0a6a --- /dev/null +++ b/packages/create-rstack/template-app-react/src/App.css @@ -0,0 +1,26 @@ +body { + margin: 0; + color: #fff; + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + background-image: linear-gradient(to bottom, #020917, #101725); +} + +.content { + display: flex; + min-height: 100vh; + line-height: 1.1; + text-align: center; + flex-direction: column; + justify-content: center; +} + +.content h1 { + font-size: 3.6rem; + font-weight: 700; +} + +.content p { + font-size: 1.2rem; + font-weight: 400; + opacity: 0.5; +} diff --git a/packages/create-rstack/template-app-react-js/src/App.jsx b/packages/create-rstack/template-app-react/src/App.jsx similarity index 100% rename from packages/create-rstack/template-app-react-js/src/App.jsx rename to packages/create-rstack/template-app-react/src/App.jsx diff --git a/packages/create-rstack/template-app-react-js/src/index.jsx b/packages/create-rstack/template-app-react/src/index.jsx similarity index 100% rename from packages/create-rstack/template-app-react-js/src/index.jsx rename to packages/create-rstack/template-app-react/src/index.jsx diff --git a/packages/create-rstack/template-app-react/tests/index.test.jsx b/packages/create-rstack/template-app-react/tests/index.test.jsx new file mode 100644 index 00000000..2fe4174b --- /dev/null +++ b/packages/create-rstack/template-app-react/tests/index.test.jsx @@ -0,0 +1,8 @@ +import { expect, test } from 'rstack/test'; +import { render, screen } from '@testing-library/react'; +import App from '../src/App'; + +test('renders the main page', () => { + render(); + expect(screen.getByText('Rstack with React')).toBeInTheDocument(); +}); diff --git a/packages/create-rstack/template-app-react/tests/rstest.setup.js b/packages/create-rstack/template-app-react/tests/rstest.setup.js new file mode 100644 index 00000000..93951b15 --- /dev/null +++ b/packages/create-rstack/template-app-react/tests/rstest.setup.js @@ -0,0 +1,4 @@ +import { expect } from 'rstack/test'; +import * as jestDomMatchers from '@testing-library/jest-dom/matchers'; + +expect.extend(jestDomMatchers); diff --git a/packages/create-rstack/template-app-solid-ts/package.json b/packages/create-rstack/template-app-solid-ts/package.json new file mode 100644 index 00000000..3164eddb --- /dev/null +++ b/packages/create-rstack/template-app-solid-ts/package.json @@ -0,0 +1,29 @@ +{ + "name": "rstack-app-solid-ts", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "rs build", + "check": "rs check --type-check", + "dev": "rs dev", + "format": "rs fmt", + "lint": "rs lint", + "preview": "rs preview", + "test": "rs test", + "test:watch": "rs test --watch" + }, + "dependencies": { + "solid-js": "^1.9.14" + }, + "devDependencies": { + "@rsbuild/plugin-babel": "^2.0.1", + "@rsbuild/plugin-solid": "^1.2.2", + "@solidjs/testing-library": "^0.8.10", + "@testing-library/jest-dom": "^7.0.0", + "@types/node": "^24.13.3", + "happy-dom": "^20.11.2", + "rstack": "^0.5.0", + "typescript": "^7.0.2" + } +} diff --git a/packages/create-rstack/template-app-solid-ts/rstack.config.ts b/packages/create-rstack/template-app-solid-ts/rstack.config.ts new file mode 100644 index 00000000..355d1b46 --- /dev/null +++ b/packages/create-rstack/template-app-solid-ts/rstack.config.ts @@ -0,0 +1,30 @@ +// Rstack configuration guide: https://rstack.rs/config +import { define } from 'rstack'; + +define.app(async () => { + const { pluginBabel } = await import('@rsbuild/plugin-babel'); + const { pluginSolid } = await import('@rsbuild/plugin-solid'); + + return { + plugins: [ + pluginBabel({ + include: /\.(?:jsx|tsx)$/, + }), + pluginSolid(), + ], + }; +}); + +define.test({ + setupFiles: ['./tests/rstest.setup.ts'], +}); + +define.lint(async () => { + const { js, ts } = await import('rstack/lint'); + + return [js.configs.recommended, ts.configs.recommended]; +}); + +define.fmt({ + singleQuote: true, +}); diff --git a/packages/create-rstack/template-app-solid-ts/src/App.css b/packages/create-rstack/template-app-solid-ts/src/App.css new file mode 100644 index 00000000..164c0a6a --- /dev/null +++ b/packages/create-rstack/template-app-solid-ts/src/App.css @@ -0,0 +1,26 @@ +body { + margin: 0; + color: #fff; + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + background-image: linear-gradient(to bottom, #020917, #101725); +} + +.content { + display: flex; + min-height: 100vh; + line-height: 1.1; + text-align: center; + flex-direction: column; + justify-content: center; +} + +.content h1 { + font-size: 3.6rem; + font-weight: 700; +} + +.content p { + font-size: 1.2rem; + font-weight: 400; + opacity: 0.5; +} diff --git a/packages/create-rstack/template-app-solid-ts/src/App.tsx b/packages/create-rstack/template-app-solid-ts/src/App.tsx new file mode 100644 index 00000000..732c63f9 --- /dev/null +++ b/packages/create-rstack/template-app-solid-ts/src/App.tsx @@ -0,0 +1,12 @@ +import './App.css'; + +const App = () => { + return ( +
+

Rstack with Solid

+

Start building amazing things with Rstack.

+
+ ); +}; + +export default App; diff --git a/packages/create-rstack/template-app-solid-ts/src/index.tsx b/packages/create-rstack/template-app-solid-ts/src/index.tsx new file mode 100644 index 00000000..56ad2157 --- /dev/null +++ b/packages/create-rstack/template-app-solid-ts/src/index.tsx @@ -0,0 +1,7 @@ +import { render } from 'solid-js/web'; +import App from './App'; + +const root = document.getElementById('root'); +if (root) { + render(() => , root); +} diff --git a/packages/create-rstack/template-app-solid-ts/tests/index.test.tsx b/packages/create-rstack/template-app-solid-ts/tests/index.test.tsx new file mode 100644 index 00000000..629411ff --- /dev/null +++ b/packages/create-rstack/template-app-solid-ts/tests/index.test.tsx @@ -0,0 +1,8 @@ +import { render, screen } from '@solidjs/testing-library'; +import { expect, test } from 'rstack/test'; +import App from '../src/App'; + +test('renders the main page', () => { + render(() => ); + expect(screen.getByText('Rstack with Solid')).toBeInTheDocument(); +}); diff --git a/packages/create-rstack/template-app-solid-ts/tests/rstest.setup.ts b/packages/create-rstack/template-app-solid-ts/tests/rstest.setup.ts new file mode 100644 index 00000000..93951b15 --- /dev/null +++ b/packages/create-rstack/template-app-solid-ts/tests/rstest.setup.ts @@ -0,0 +1,4 @@ +import { expect } from 'rstack/test'; +import * as jestDomMatchers from '@testing-library/jest-dom/matchers'; + +expect.extend(jestDomMatchers); diff --git a/packages/create-rstack/template-app-solid-ts/tests/tsconfig.json b/packages/create-rstack/template-app-solid-ts/tests/tsconfig.json new file mode 100644 index 00000000..3eb5dc12 --- /dev/null +++ b/packages/create-rstack/template-app-solid-ts/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["rstack/types", "node", "@testing-library/jest-dom"] + }, + "include": ["./"] +} diff --git a/packages/create-rstack/template-app-solid-ts/tsconfig.json b/packages/create-rstack/template-app-solid-ts/tsconfig.json new file mode 100644 index 00000000..0437f985 --- /dev/null +++ b/packages/create-rstack/template-app-solid-ts/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "lib": ["DOM", "ES2020"], + "jsx": "preserve", + "target": "ES2020", + "noEmit": true, + "skipLibCheck": true, + "types": ["rstack/types", "node"], + "jsxImportSource": "solid-js", + "useDefineForClassFields": true, + + /* modules */ + "moduleDetection": "force", + "moduleResolution": "bundler", + "verbatimModuleSyntax": true, + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + + /* type checking */ + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["src"] +} diff --git a/packages/create-rstack/template-app-solid/package.json b/packages/create-rstack/template-app-solid/package.json new file mode 100644 index 00000000..05325231 --- /dev/null +++ b/packages/create-rstack/template-app-solid/package.json @@ -0,0 +1,27 @@ +{ + "name": "rstack-app-solid", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "rs build", + "check": "rs check", + "dev": "rs dev", + "format": "rs fmt", + "lint": "rs lint", + "preview": "rs preview", + "test": "rs test", + "test:watch": "rs test --watch" + }, + "dependencies": { + "solid-js": "^1.9.14" + }, + "devDependencies": { + "@rsbuild/plugin-babel": "^2.0.1", + "@rsbuild/plugin-solid": "^1.2.2", + "@solidjs/testing-library": "^0.8.10", + "@testing-library/jest-dom": "^7.0.0", + "happy-dom": "^20.11.2", + "rstack": "^0.5.0" + } +} diff --git a/packages/create-rstack/template-app-solid/rstack.config.js b/packages/create-rstack/template-app-solid/rstack.config.js new file mode 100644 index 00000000..80b7b6f7 --- /dev/null +++ b/packages/create-rstack/template-app-solid/rstack.config.js @@ -0,0 +1,31 @@ +// @ts-check +// Rstack configuration guide: https://rstack.rs/config +import { define } from 'rstack'; + +define.app(async () => { + const { pluginBabel } = await import('@rsbuild/plugin-babel'); + const { pluginSolid } = await import('@rsbuild/plugin-solid'); + + return { + plugins: [ + pluginBabel({ + include: /\.(?:jsx|tsx)$/, + }), + pluginSolid(), + ], + }; +}); + +define.test({ + setupFiles: ['./tests/rstest.setup.js'], +}); + +define.lint(async () => { + const { js } = await import('rstack/lint'); + + return [js.configs.recommended]; +}); + +define.fmt({ + singleQuote: true, +}); diff --git a/packages/create-rstack/template-app-solid/src/App.css b/packages/create-rstack/template-app-solid/src/App.css new file mode 100644 index 00000000..164c0a6a --- /dev/null +++ b/packages/create-rstack/template-app-solid/src/App.css @@ -0,0 +1,26 @@ +body { + margin: 0; + color: #fff; + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + background-image: linear-gradient(to bottom, #020917, #101725); +} + +.content { + display: flex; + min-height: 100vh; + line-height: 1.1; + text-align: center; + flex-direction: column; + justify-content: center; +} + +.content h1 { + font-size: 3.6rem; + font-weight: 700; +} + +.content p { + font-size: 1.2rem; + font-weight: 400; + opacity: 0.5; +} diff --git a/packages/create-rstack/template-app-solid/src/App.jsx b/packages/create-rstack/template-app-solid/src/App.jsx new file mode 100644 index 00000000..732c63f9 --- /dev/null +++ b/packages/create-rstack/template-app-solid/src/App.jsx @@ -0,0 +1,12 @@ +import './App.css'; + +const App = () => { + return ( +
+

Rstack with Solid

+

Start building amazing things with Rstack.

+
+ ); +}; + +export default App; diff --git a/packages/create-rstack/template-app-solid/src/index.jsx b/packages/create-rstack/template-app-solid/src/index.jsx new file mode 100644 index 00000000..c6c556ad --- /dev/null +++ b/packages/create-rstack/template-app-solid/src/index.jsx @@ -0,0 +1,4 @@ +import { render } from 'solid-js/web'; +import App from './App'; + +render(() => , document.getElementById('root')); diff --git a/packages/create-rstack/template-app-solid/tests/index.test.jsx b/packages/create-rstack/template-app-solid/tests/index.test.jsx new file mode 100644 index 00000000..629411ff --- /dev/null +++ b/packages/create-rstack/template-app-solid/tests/index.test.jsx @@ -0,0 +1,8 @@ +import { render, screen } from '@solidjs/testing-library'; +import { expect, test } from 'rstack/test'; +import App from '../src/App'; + +test('renders the main page', () => { + render(() => ); + expect(screen.getByText('Rstack with Solid')).toBeInTheDocument(); +}); diff --git a/packages/create-rstack/template-app-solid/tests/rstest.setup.js b/packages/create-rstack/template-app-solid/tests/rstest.setup.js new file mode 100644 index 00000000..93951b15 --- /dev/null +++ b/packages/create-rstack/template-app-solid/tests/rstest.setup.js @@ -0,0 +1,4 @@ +import { expect } from 'rstack/test'; +import * as jestDomMatchers from '@testing-library/jest-dom/matchers'; + +expect.extend(jestDomMatchers); diff --git a/packages/create-rstack/template-app-svelte-ts/package.json b/packages/create-rstack/template-app-svelte-ts/package.json new file mode 100644 index 00000000..f55af1ff --- /dev/null +++ b/packages/create-rstack/template-app-svelte-ts/package.json @@ -0,0 +1,30 @@ +{ + "name": "rstack-app-svelte-ts", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "svelte-check && rs build", + "check": "rs check", + "dev": "rs dev", + "format": "rs fmt", + "lint": "rs lint", + "preview": "rs preview", + "test": "rs test", + "test:watch": "rs test --watch" + }, + "dependencies": { + "svelte": "^5.56.8" + }, + "devDependencies": { + "@rsbuild/plugin-svelte": "^2.0.1", + "@testing-library/jest-dom": "^7.0.0", + "@testing-library/svelte": "^5.4.2", + "@types/node": "^24.13.3", + "happy-dom": "^20.11.2", + "prettier-plugin-svelte": "^4.1.1", + "rstack": "^0.5.0", + "svelte-check": "^4.7.5", + "typescript": "^6.0.3" + } +} diff --git a/packages/create-rstack/template-app-svelte-ts/pnpm-workspace.yaml b/packages/create-rstack/template-app-svelte-ts/pnpm-workspace.yaml new file mode 100644 index 00000000..3c064313 --- /dev/null +++ b/packages/create-rstack/template-app-svelte-ts/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + svelte-preprocess: false diff --git a/packages/create-rstack/template-app-svelte-ts/rstack.config.ts b/packages/create-rstack/template-app-svelte-ts/rstack.config.ts new file mode 100644 index 00000000..66320b37 --- /dev/null +++ b/packages/create-rstack/template-app-svelte-ts/rstack.config.ts @@ -0,0 +1,25 @@ +// Rstack configuration guide: https://rstack.rs/config +import { define } from 'rstack'; + +define.app(async () => { + const { pluginSvelte } = await import('@rsbuild/plugin-svelte'); + + return { + plugins: [pluginSvelte()], + }; +}); + +define.test({ + setupFiles: ['./tests/rstest.setup.ts'], +}); + +define.lint(async () => { + const { js, ts } = await import('rstack/lint'); + + return [js.configs.recommended, ts.configs.recommended]; +}); + +define.fmt({ + plugins: ['prettier-plugin-svelte'], + singleQuote: true, +}); diff --git a/packages/create-rstack/template-app-svelte-ts/src/App.svelte b/packages/create-rstack/template-app-svelte-ts/src/App.svelte new file mode 100644 index 00000000..35f6758e --- /dev/null +++ b/packages/create-rstack/template-app-svelte-ts/src/App.svelte @@ -0,0 +1,37 @@ + + +
+
+

{tool} with {framework}

+

Start building amazing things with Rstack.

+
+
+ + diff --git a/packages/create-rstack/template-app-svelte-ts/src/index.css b/packages/create-rstack/template-app-svelte-ts/src/index.css new file mode 100644 index 00000000..85e7e2b4 --- /dev/null +++ b/packages/create-rstack/template-app-svelte-ts/src/index.css @@ -0,0 +1,6 @@ +body { + margin: 0; + color: #fff; + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + background-image: linear-gradient(to bottom, #020917, #101725); +} diff --git a/packages/create-rstack/template-app-svelte-ts/src/index.ts b/packages/create-rstack/template-app-svelte-ts/src/index.ts new file mode 100644 index 00000000..0dc0b8b9 --- /dev/null +++ b/packages/create-rstack/template-app-svelte-ts/src/index.ts @@ -0,0 +1,14 @@ +import { mount } from 'svelte'; +import App from './App.svelte'; +import './index.css'; + +const root = document.getElementById('root'); +if (root) { + mount(App, { + target: root, + props: { + tool: 'Rstack', + framework: 'Svelte', + }, + }); +} diff --git a/packages/create-rstack/template-app-svelte-ts/tests/index.test.ts b/packages/create-rstack/template-app-svelte-ts/tests/index.test.ts new file mode 100644 index 00000000..def49543 --- /dev/null +++ b/packages/create-rstack/template-app-svelte-ts/tests/index.test.ts @@ -0,0 +1,11 @@ +import { render, screen } from '@testing-library/svelte'; +import { expect, test } from 'rstack/test'; +import App from '../src/App.svelte'; + +test('renders the main page', () => { + render(App, { + tool: 'Rstack', + framework: 'Svelte', + }); + expect(screen.getByText('Rstack with Svelte')).toBeInTheDocument(); +}); diff --git a/packages/create-rstack/template-app-svelte-ts/tests/rstest.setup.ts b/packages/create-rstack/template-app-svelte-ts/tests/rstest.setup.ts new file mode 100644 index 00000000..93951b15 --- /dev/null +++ b/packages/create-rstack/template-app-svelte-ts/tests/rstest.setup.ts @@ -0,0 +1,4 @@ +import { expect } from 'rstack/test'; +import * as jestDomMatchers from '@testing-library/jest-dom/matchers'; + +expect.extend(jestDomMatchers); diff --git a/packages/create-rstack/template-app-svelte-ts/tests/tsconfig.json b/packages/create-rstack/template-app-svelte-ts/tests/tsconfig.json new file mode 100644 index 00000000..3eb5dc12 --- /dev/null +++ b/packages/create-rstack/template-app-svelte-ts/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["rstack/types", "node", "@testing-library/jest-dom"] + }, + "include": ["./"] +} diff --git a/packages/create-rstack/template-app-svelte-ts/tsconfig.json b/packages/create-rstack/template-app-svelte-ts/tsconfig.json new file mode 100644 index 00000000..333158f0 --- /dev/null +++ b/packages/create-rstack/template-app-svelte-ts/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "lib": ["DOM", "ES2020"], + "target": "ES2020", + "noEmit": true, + "skipLibCheck": true, + "types": ["rstack/types", "node", "svelte"], + "useDefineForClassFields": true, + + /* modules */ + "moduleDetection": "force", + "moduleResolution": "bundler", + "verbatimModuleSyntax": true, + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + + /* type checking */ + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["src/**/*.ts", "src/**/*.svelte"] +} diff --git a/packages/create-rstack/template-app-svelte/package.json b/packages/create-rstack/template-app-svelte/package.json new file mode 100644 index 00000000..2da1fd4f --- /dev/null +++ b/packages/create-rstack/template-app-svelte/package.json @@ -0,0 +1,27 @@ +{ + "name": "rstack-app-svelte", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "rs build", + "check": "rs check", + "dev": "rs dev", + "format": "rs fmt", + "lint": "rs lint", + "preview": "rs preview", + "test": "rs test", + "test:watch": "rs test --watch" + }, + "dependencies": { + "svelte": "^5.56.8" + }, + "devDependencies": { + "@rsbuild/plugin-svelte": "^2.0.1", + "@testing-library/jest-dom": "^7.0.0", + "@testing-library/svelte": "^5.4.2", + "happy-dom": "^20.11.2", + "prettier-plugin-svelte": "^4.1.1", + "rstack": "^0.5.0" + } +} diff --git a/packages/create-rstack/template-app-svelte/pnpm-workspace.yaml b/packages/create-rstack/template-app-svelte/pnpm-workspace.yaml new file mode 100644 index 00000000..3c064313 --- /dev/null +++ b/packages/create-rstack/template-app-svelte/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + svelte-preprocess: false diff --git a/packages/create-rstack/template-app-svelte/rstack.config.js b/packages/create-rstack/template-app-svelte/rstack.config.js new file mode 100644 index 00000000..742edde8 --- /dev/null +++ b/packages/create-rstack/template-app-svelte/rstack.config.js @@ -0,0 +1,26 @@ +// @ts-check +// Rstack configuration guide: https://rstack.rs/config +import { define } from 'rstack'; + +define.app(async () => { + const { pluginSvelte } = await import('@rsbuild/plugin-svelte'); + + return { + plugins: [pluginSvelte()], + }; +}); + +define.test({ + setupFiles: ['./tests/rstest.setup.js'], +}); + +define.lint(async () => { + const { js } = await import('rstack/lint'); + + return [js.configs.recommended]; +}); + +define.fmt({ + plugins: ['prettier-plugin-svelte'], + singleQuote: true, +}); diff --git a/packages/create-rstack/template-app-svelte/src/App.svelte b/packages/create-rstack/template-app-svelte/src/App.svelte new file mode 100644 index 00000000..5a680d73 --- /dev/null +++ b/packages/create-rstack/template-app-svelte/src/App.svelte @@ -0,0 +1,28 @@ +
+
+

Rstack with Svelte

+

Start building amazing things with Rstack.

+
+
+ + diff --git a/packages/create-rstack/template-app-svelte/src/index.css b/packages/create-rstack/template-app-svelte/src/index.css new file mode 100644 index 00000000..85e7e2b4 --- /dev/null +++ b/packages/create-rstack/template-app-svelte/src/index.css @@ -0,0 +1,6 @@ +body { + margin: 0; + color: #fff; + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + background-image: linear-gradient(to bottom, #020917, #101725); +} diff --git a/packages/create-rstack/template-app-svelte/src/index.js b/packages/create-rstack/template-app-svelte/src/index.js new file mode 100644 index 00000000..b8cf0263 --- /dev/null +++ b/packages/create-rstack/template-app-svelte/src/index.js @@ -0,0 +1,8 @@ +import { mount } from 'svelte'; +import App from './App.svelte'; +import './index.css'; + +const root = document.getElementById('root'); +if (root) { + mount(App, { target: root }); +} diff --git a/packages/create-rstack/template-app-svelte/tests/index.test.js b/packages/create-rstack/template-app-svelte/tests/index.test.js new file mode 100644 index 00000000..067c6161 --- /dev/null +++ b/packages/create-rstack/template-app-svelte/tests/index.test.js @@ -0,0 +1,8 @@ +import { render, screen } from '@testing-library/svelte'; +import { expect, test } from 'rstack/test'; +import App from '../src/App.svelte'; + +test('renders the main page', () => { + render(App); + expect(screen.getByText('Rstack with Svelte')).toBeInTheDocument(); +}); diff --git a/packages/create-rstack/template-app-svelte/tests/rstest.setup.js b/packages/create-rstack/template-app-svelte/tests/rstest.setup.js new file mode 100644 index 00000000..93951b15 --- /dev/null +++ b/packages/create-rstack/template-app-svelte/tests/rstest.setup.js @@ -0,0 +1,4 @@ +import { expect } from 'rstack/test'; +import * as jestDomMatchers from '@testing-library/jest-dom/matchers'; + +expect.extend(jestDomMatchers); diff --git a/packages/create-rstack/template-app-vanilla-js/AGENTS.md b/packages/create-rstack/template-app-vanilla-js/AGENTS.md deleted file mode 100644 index fb378511..00000000 --- a/packages/create-rstack/template-app-vanilla-js/AGENTS.md +++ /dev/null @@ -1,12 +0,0 @@ -# AGENTS.md - -## Commands - -- `{{ packageManager }} run dev` - Start the development server -- `{{ packageManager }} run build` - Build the app for production -- `{{ packageManager }} run preview` - Preview the production build locally - -## Docs - -- Rsbuild: https://rsbuild.rs/llms.txt -- Rspack: https://rspack.rs/llms.txt diff --git a/packages/create-rstack/template-app-vanilla-js/package.json b/packages/create-rstack/template-app-vanilla-js/package.json deleted file mode 100644 index 7dd220ef..00000000 --- a/packages/create-rstack/template-app-vanilla-js/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "rstack-app-vanilla-js", - "version": "1.0.0", - "private": true, - "type": "module", - "scripts": { - "build": "rs build", - "dev": "rs dev", - "format": "rs fmt", - "lint": "rs lint", - "preview": "rs preview", - "test": "rs test" - }, - "devDependencies": { - "rstack": "^0.3.5" - } -} diff --git a/packages/create-rstack/template-app-vanilla-ts/AGENTS.md b/packages/create-rstack/template-app-vanilla-ts/AGENTS.md deleted file mode 100644 index fb378511..00000000 --- a/packages/create-rstack/template-app-vanilla-ts/AGENTS.md +++ /dev/null @@ -1,12 +0,0 @@ -# AGENTS.md - -## Commands - -- `{{ packageManager }} run dev` - Start the development server -- `{{ packageManager }} run build` - Build the app for production -- `{{ packageManager }} run preview` - Preview the production build locally - -## Docs - -- Rsbuild: https://rsbuild.rs/llms.txt -- Rspack: https://rspack.rs/llms.txt diff --git a/packages/create-rstack/template-app-vanilla-ts/package.json b/packages/create-rstack/template-app-vanilla-ts/package.json index 65121cbf..d8806d43 100644 --- a/packages/create-rstack/template-app-vanilla-ts/package.json +++ b/packages/create-rstack/template-app-vanilla-ts/package.json @@ -5,15 +5,20 @@ "type": "module", "scripts": { "build": "rs build", + "check": "rs check --type-check", "dev": "rs dev", "format": "rs fmt", "lint": "rs lint", "preview": "rs preview", - "test": "rs test" + "test": "rs test", + "test:watch": "rs test --watch" }, "devDependencies": { + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^7.0.0", "@types/node": "^24.13.3", - "rstack": "^0.3.5", + "happy-dom": "^20.11.2", + "rstack": "^0.5.0", "typescript": "^7.0.2" } } diff --git a/packages/create-rstack/template-app-vanilla-ts/rstack.config.ts b/packages/create-rstack/template-app-vanilla-ts/rstack.config.ts index 3563b865..349fcfae 100644 --- a/packages/create-rstack/template-app-vanilla-ts/rstack.config.ts +++ b/packages/create-rstack/template-app-vanilla-ts/rstack.config.ts @@ -6,7 +6,7 @@ define.app({ }); define.test({ - // Configure Rstest + setupFiles: ['./tests/rstest.setup.ts'], }); define.lint(async () => { @@ -14,3 +14,7 @@ define.lint(async () => { return [js.configs.recommended, ts.configs.recommended]; }); + +define.fmt({ + singleQuote: true, +}); diff --git a/packages/create-rstack/template-app-vanilla-ts/tests/dom.test.ts b/packages/create-rstack/template-app-vanilla-ts/tests/dom.test.ts new file mode 100644 index 00000000..45c267a8 --- /dev/null +++ b/packages/create-rstack/template-app-vanilla-ts/tests/dom.test.ts @@ -0,0 +1,12 @@ +import { expect, test } from 'rstack/test'; +import { screen } from '@testing-library/dom'; + +test('test dom', () => { + document.body.innerHTML = ` + +
Visible Example
+ `; + + expect(screen.queryByTestId('not-empty')).toBeInTheDocument(); + expect(screen.getByText('Visible Example')).toBeVisible(); +}); diff --git a/packages/create-rstack/template-app-vanilla-ts/tests/rstest.setup.ts b/packages/create-rstack/template-app-vanilla-ts/tests/rstest.setup.ts new file mode 100644 index 00000000..93951b15 --- /dev/null +++ b/packages/create-rstack/template-app-vanilla-ts/tests/rstest.setup.ts @@ -0,0 +1,4 @@ +import { expect } from 'rstack/test'; +import * as jestDomMatchers from '@testing-library/jest-dom/matchers'; + +expect.extend(jestDomMatchers); diff --git a/packages/create-rstack/template-app-vanilla-ts/tests/tsconfig.json b/packages/create-rstack/template-app-vanilla-ts/tests/tsconfig.json new file mode 100644 index 00000000..3eb5dc12 --- /dev/null +++ b/packages/create-rstack/template-app-vanilla-ts/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["rstack/types", "node", "@testing-library/jest-dom"] + }, + "include": ["./"] +} diff --git a/packages/create-rstack/template-app-vanilla/package.json b/packages/create-rstack/template-app-vanilla/package.json new file mode 100644 index 00000000..82684f23 --- /dev/null +++ b/packages/create-rstack/template-app-vanilla/package.json @@ -0,0 +1,22 @@ +{ + "name": "rstack-app-vanilla", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "rs build", + "check": "rs check", + "dev": "rs dev", + "format": "rs fmt", + "lint": "rs lint", + "preview": "rs preview", + "test": "rs test", + "test:watch": "rs test --watch" + }, + "devDependencies": { + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^7.0.0", + "happy-dom": "^20.11.2", + "rstack": "^0.5.0" + } +} diff --git a/packages/create-rstack/template-app-vanilla-js/rstack.config.js b/packages/create-rstack/template-app-vanilla/rstack.config.js similarity index 77% rename from packages/create-rstack/template-app-vanilla-js/rstack.config.js rename to packages/create-rstack/template-app-vanilla/rstack.config.js index fd6c52e0..23e75fdf 100644 --- a/packages/create-rstack/template-app-vanilla-js/rstack.config.js +++ b/packages/create-rstack/template-app-vanilla/rstack.config.js @@ -7,7 +7,7 @@ define.app({ }); define.test({ - // Configure Rstest + setupFiles: ['./tests/rstest.setup.js'], }); define.lint(async () => { @@ -15,3 +15,7 @@ define.lint(async () => { return [js.configs.recommended]; }); + +define.fmt({ + singleQuote: true, +}); diff --git a/packages/create-rstack/template-app-vanilla/src/index.css b/packages/create-rstack/template-app-vanilla/src/index.css new file mode 100644 index 00000000..164c0a6a --- /dev/null +++ b/packages/create-rstack/template-app-vanilla/src/index.css @@ -0,0 +1,26 @@ +body { + margin: 0; + color: #fff; + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + background-image: linear-gradient(to bottom, #020917, #101725); +} + +.content { + display: flex; + min-height: 100vh; + line-height: 1.1; + text-align: center; + flex-direction: column; + justify-content: center; +} + +.content h1 { + font-size: 3.6rem; + font-weight: 700; +} + +.content p { + font-size: 1.2rem; + font-weight: 400; + opacity: 0.5; +} diff --git a/packages/create-rstack/template-app-vanilla-js/src/index.js b/packages/create-rstack/template-app-vanilla/src/index.js similarity index 100% rename from packages/create-rstack/template-app-vanilla-js/src/index.js rename to packages/create-rstack/template-app-vanilla/src/index.js diff --git a/packages/create-rstack/template-app-vanilla/tests/dom.test.js b/packages/create-rstack/template-app-vanilla/tests/dom.test.js new file mode 100644 index 00000000..45c267a8 --- /dev/null +++ b/packages/create-rstack/template-app-vanilla/tests/dom.test.js @@ -0,0 +1,12 @@ +import { expect, test } from 'rstack/test'; +import { screen } from '@testing-library/dom'; + +test('test dom', () => { + document.body.innerHTML = ` + +
Visible Example
+ `; + + expect(screen.queryByTestId('not-empty')).toBeInTheDocument(); + expect(screen.getByText('Visible Example')).toBeVisible(); +}); diff --git a/packages/create-rstack/template-app-vanilla/tests/rstest.setup.js b/packages/create-rstack/template-app-vanilla/tests/rstest.setup.js new file mode 100644 index 00000000..93951b15 --- /dev/null +++ b/packages/create-rstack/template-app-vanilla/tests/rstest.setup.js @@ -0,0 +1,4 @@ +import { expect } from 'rstack/test'; +import * as jestDomMatchers from '@testing-library/jest-dom/matchers'; + +expect.extend(jestDomMatchers); diff --git a/packages/create-rstack/template-app-vue-js/AGENTS.md b/packages/create-rstack/template-app-vue-js/AGENTS.md deleted file mode 100644 index fb378511..00000000 --- a/packages/create-rstack/template-app-vue-js/AGENTS.md +++ /dev/null @@ -1,12 +0,0 @@ -# AGENTS.md - -## Commands - -- `{{ packageManager }} run dev` - Start the development server -- `{{ packageManager }} run build` - Build the app for production -- `{{ packageManager }} run preview` - Preview the production build locally - -## Docs - -- Rsbuild: https://rsbuild.rs/llms.txt -- Rspack: https://rspack.rs/llms.txt diff --git a/packages/create-rstack/template-app-vue-ts/AGENTS.md b/packages/create-rstack/template-app-vue-ts/AGENTS.md deleted file mode 100644 index fb378511..00000000 --- a/packages/create-rstack/template-app-vue-ts/AGENTS.md +++ /dev/null @@ -1,12 +0,0 @@ -# AGENTS.md - -## Commands - -- `{{ packageManager }} run dev` - Start the development server -- `{{ packageManager }} run build` - Build the app for production -- `{{ packageManager }} run preview` - Preview the production build locally - -## Docs - -- Rsbuild: https://rsbuild.rs/llms.txt -- Rspack: https://rspack.rs/llms.txt diff --git a/packages/create-rstack/template-app-vue-ts/package.json b/packages/create-rstack/template-app-vue-ts/package.json index 77e0ab0b..8f6fde9c 100644 --- a/packages/create-rstack/template-app-vue-ts/package.json +++ b/packages/create-rstack/template-app-vue-ts/package.json @@ -5,19 +5,24 @@ "type": "module", "scripts": { "build": "vue-tsc && rs build", + "check": "rs check", "dev": "rs dev", "format": "rs fmt", "lint": "rs lint", "preview": "rs preview", - "test": "rs test" + "test": "rs test", + "test:watch": "rs test --watch" }, "dependencies": { - "vue": "^3.5.40" + "vue": "^3.5.41" }, "devDependencies": { "@rsbuild/plugin-vue": "^2.0.1", + "@testing-library/jest-dom": "^7.0.0", "@types/node": "^24.13.3", - "rstack": "^0.3.5", + "@vue/test-utils": "^2.4.11", + "happy-dom": "^20.11.2", + "rstack": "^0.5.0", "typescript": "^6.0.3", "vue-tsc": "^3.3.9" } diff --git a/packages/create-rstack/template-app-vue-ts/rstack.config.ts b/packages/create-rstack/template-app-vue-ts/rstack.config.ts index 0399271d..f2224449 100644 --- a/packages/create-rstack/template-app-vue-ts/rstack.config.ts +++ b/packages/create-rstack/template-app-vue-ts/rstack.config.ts @@ -10,7 +10,7 @@ define.app(async () => { }); define.test({ - // Configure Rstest + setupFiles: ['./tests/rstest.setup.ts'], }); define.lint(async () => { @@ -18,3 +18,7 @@ define.lint(async () => { return [js.configs.recommended, ts.configs.recommended]; }); + +define.fmt({ + singleQuote: true, +}); diff --git a/packages/create-rstack/template-app-vue-ts/tests/index.test.ts b/packages/create-rstack/template-app-vue-ts/tests/index.test.ts new file mode 100644 index 00000000..27ece847 --- /dev/null +++ b/packages/create-rstack/template-app-vue-ts/tests/index.test.ts @@ -0,0 +1,8 @@ +import { expect, test } from 'rstack/test'; +import { mount } from '@vue/test-utils'; +import App from '../src/App.vue'; + +test('renders the main page', () => { + const wrapper = mount(App); + expect(wrapper.element).toHaveTextContent('Rstack with Vue'); +}); diff --git a/packages/create-rstack/template-app-vue-ts/tests/rstest.setup.ts b/packages/create-rstack/template-app-vue-ts/tests/rstest.setup.ts new file mode 100644 index 00000000..5b719f5b --- /dev/null +++ b/packages/create-rstack/template-app-vue-ts/tests/rstest.setup.ts @@ -0,0 +1,6 @@ +import { afterEach, expect } from 'rstack/test'; +import * as jestDomMatchers from '@testing-library/jest-dom/matchers'; +import { enableAutoUnmount } from '@vue/test-utils'; + +expect.extend(jestDomMatchers); +enableAutoUnmount(afterEach); diff --git a/packages/create-rstack/template-app-vue-ts/tests/tsconfig.json b/packages/create-rstack/template-app-vue-ts/tests/tsconfig.json new file mode 100644 index 00000000..3eb5dc12 --- /dev/null +++ b/packages/create-rstack/template-app-vue-ts/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["rstack/types", "node", "@testing-library/jest-dom"] + }, + "include": ["./"] +} diff --git a/packages/create-rstack/template-app-vue-js/package.json b/packages/create-rstack/template-app-vue/package.json similarity index 53% rename from packages/create-rstack/template-app-vue-js/package.json rename to packages/create-rstack/template-app-vue/package.json index c3466a77..76b2f617 100644 --- a/packages/create-rstack/template-app-vue-js/package.json +++ b/packages/create-rstack/template-app-vue/package.json @@ -1,21 +1,26 @@ { - "name": "rstack-app-vue-js", + "name": "rstack-app-vue", "version": "1.0.0", "private": true, "type": "module", "scripts": { "build": "rs build", + "check": "rs check", "dev": "rs dev", "format": "rs fmt", "lint": "rs lint", "preview": "rs preview", - "test": "rs test" + "test": "rs test", + "test:watch": "rs test --watch" }, "dependencies": { - "vue": "^3.5.40" + "vue": "^3.5.41" }, "devDependencies": { "@rsbuild/plugin-vue": "^2.0.1", - "rstack": "^0.3.5" + "@testing-library/jest-dom": "^7.0.0", + "@vue/test-utils": "^2.4.11", + "happy-dom": "^20.11.2", + "rstack": "^0.5.0" } } diff --git a/packages/create-rstack/template-app-vue-js/rstack.config.js b/packages/create-rstack/template-app-vue/rstack.config.js similarity index 81% rename from packages/create-rstack/template-app-vue-js/rstack.config.js rename to packages/create-rstack/template-app-vue/rstack.config.js index 376d57d9..b67b85e1 100644 --- a/packages/create-rstack/template-app-vue-js/rstack.config.js +++ b/packages/create-rstack/template-app-vue/rstack.config.js @@ -11,7 +11,7 @@ define.app(async () => { }); define.test({ - // Configure Rstest + setupFiles: ['./tests/rstest.setup.js'], }); define.lint(async () => { @@ -19,3 +19,7 @@ define.lint(async () => { return [js.configs.recommended]; }); + +define.fmt({ + singleQuote: true, +}); diff --git a/packages/create-rstack/template-app-vue-js/src/App.vue b/packages/create-rstack/template-app-vue/src/App.vue similarity index 100% rename from packages/create-rstack/template-app-vue-js/src/App.vue rename to packages/create-rstack/template-app-vue/src/App.vue diff --git a/packages/create-rstack/template-app-vue/src/index.css b/packages/create-rstack/template-app-vue/src/index.css new file mode 100644 index 00000000..85e7e2b4 --- /dev/null +++ b/packages/create-rstack/template-app-vue/src/index.css @@ -0,0 +1,6 @@ +body { + margin: 0; + color: #fff; + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + background-image: linear-gradient(to bottom, #020917, #101725); +} diff --git a/packages/create-rstack/template-app-vue-js/src/index.js b/packages/create-rstack/template-app-vue/src/index.js similarity index 100% rename from packages/create-rstack/template-app-vue-js/src/index.js rename to packages/create-rstack/template-app-vue/src/index.js diff --git a/packages/create-rstack/template-app-vue/tests/index.test.js b/packages/create-rstack/template-app-vue/tests/index.test.js new file mode 100644 index 00000000..27ece847 --- /dev/null +++ b/packages/create-rstack/template-app-vue/tests/index.test.js @@ -0,0 +1,8 @@ +import { expect, test } from 'rstack/test'; +import { mount } from '@vue/test-utils'; +import App from '../src/App.vue'; + +test('renders the main page', () => { + const wrapper = mount(App); + expect(wrapper.element).toHaveTextContent('Rstack with Vue'); +}); diff --git a/packages/create-rstack/template-app-vue/tests/rstest.setup.js b/packages/create-rstack/template-app-vue/tests/rstest.setup.js new file mode 100644 index 00000000..5b719f5b --- /dev/null +++ b/packages/create-rstack/template-app-vue/tests/rstest.setup.js @@ -0,0 +1,6 @@ +import { afterEach, expect } from 'rstack/test'; +import * as jestDomMatchers from '@testing-library/jest-dom/matchers'; +import { enableAutoUnmount } from '@vue/test-utils'; + +expect.extend(jestDomMatchers); +enableAutoUnmount(afterEach); diff --git a/packages/create-rstack/template-common/AGENTS.md b/packages/create-rstack/template-common/AGENTS.md index 9f8558e0..6f0dd873 100644 --- a/packages/create-rstack/template-common/AGENTS.md +++ b/packages/create-rstack/template-common/AGENTS.md @@ -1,7 +1,7 @@ # AGENTS.md -## Commands +This project uses Rstack CLI as its JavaScript toolchain. -## Docs - -- Rstack: https://rstack.rs/llms.txt +- Before working with `rs` commands, `rstack.config.*` files, or imports from `rstack`, start with `node_modules/rstack/docs/llms.txt`, then read only the linked pages relevant to the task. +- For command details, use `rs -h` or `rs -h`. +- If the local documentation is unavailable, use https://rstack.rs/llms.txt and `rs -h`. diff --git a/packages/create-rstack/template-common/README.md b/packages/create-rstack/template-common/README.md index d62a167e..abd777a3 100644 --- a/packages/create-rstack/template-common/README.md +++ b/packages/create-rstack/template-common/README.md @@ -8,25 +8,16 @@ Install the dependencies: {{ packageManager }} install ``` -## Get started - -Start the development server: - -```bash -{{ packageManager }} run dev -``` - -Build the app for production: - -```bash -{{ packageManager }} run build -``` - -Preview the production build locally: - -```bash -{{ packageManager }} run preview -``` +## Scripts + +- `{{ packageManager }} run build`: Build the app for production. +- `{{ packageManager }} run check`: Run static checks, including lint and format. +- `{{ packageManager }} run dev`: Run the app dev server. +- `{{ packageManager }} run format`: Format code. +- `{{ packageManager }} run lint`: Lint code. +- `{{ packageManager }} run preview`: Preview the app production build. +- `{{ packageManager }} run test`: Run tests. +- `{{ packageManager }} run test:watch`: Run tests in watch mode. ## Learn more diff --git a/packages/create-rstack/template-doc-i18n/README.md b/packages/create-rstack/template-doc-i18n/README.md new file mode 100644 index 00000000..ba4906ab --- /dev/null +++ b/packages/create-rstack/template-doc-i18n/README.md @@ -0,0 +1,23 @@ +# Rstack multilingual documentation site + +## Setup + +Install the dependencies: + +```bash +{{ packageManager }} install +``` + +## Scripts + +- `{{ packageManager }} run build`: Build the documentation site for production. +- `{{ packageManager }} run check`: Run static checks, including lint and format. +- `{{ packageManager }} run dev`: Start the documentation dev server. +- `{{ packageManager }} run format`: Format code. +- `{{ packageManager }} run lint`: Lint code. +- `{{ packageManager }} run preview`: Preview the production build locally. + +## Learn more + +- [Rstack documentation](https://rstack.rs) +- [Rspress documentation](https://rspress.rs) diff --git a/packages/create-rstack/template-doc-i18n/docs/en/_nav.json b/packages/create-rstack/template-doc-i18n/docs/en/_nav.json new file mode 100644 index 00000000..1c47924a --- /dev/null +++ b/packages/create-rstack/template-doc-i18n/docs/en/_nav.json @@ -0,0 +1,16 @@ +[ + { + "text": "Guide", + "link": "/guide/start/introduction", + "activeMatch": "/guide/" + }, + { + "text": "API", + "link": "/api/", + "activeMatch": "/api/" + }, + { + "text": "Rspress", + "link": "https://rspress.rs/" + } +] diff --git a/packages/create-rstack/template-doc-i18n/docs/en/api/_meta.json b/packages/create-rstack/template-doc-i18n/docs/en/api/_meta.json new file mode 100644 index 00000000..f0ff0de6 --- /dev/null +++ b/packages/create-rstack/template-doc-i18n/docs/en/api/_meta.json @@ -0,0 +1 @@ +["index", "commands"] diff --git a/packages/create-rstack/template-doc-i18n/docs/en/api/commands.mdx b/packages/create-rstack/template-doc-i18n/docs/en/api/commands.mdx new file mode 100644 index 00000000..6e7494c5 --- /dev/null +++ b/packages/create-rstack/template-doc-i18n/docs/en/api/commands.mdx @@ -0,0 +1,25 @@ +# Commands + +## dev + +Start the local development server: + +```bash +rs doc +``` + +## build + +Build the documentation site for production: + +```bash +rs doc build +``` + +## preview + +Preview the production build locally: + +```bash +rs doc preview +``` diff --git a/packages/create-rstack/template-doc-i18n/docs/en/api/index.mdx b/packages/create-rstack/template-doc-i18n/docs/en/api/index.mdx new file mode 100644 index 00000000..939b2956 --- /dev/null +++ b/packages/create-rstack/template-doc-i18n/docs/en/api/index.mdx @@ -0,0 +1,6 @@ +--- +title: API Overview +overview: true +--- + +This is an API Overview page which outlines all the available APIs. diff --git a/packages/create-rstack/template-doc-i18n/docs/en/guide/_meta.json b/packages/create-rstack/template-doc-i18n/docs/en/guide/_meta.json new file mode 100644 index 00000000..2dc352c6 --- /dev/null +++ b/packages/create-rstack/template-doc-i18n/docs/en/guide/_meta.json @@ -0,0 +1,7 @@ +[ + { + "type": "dir-section-header", + "name": "start", + "label": "Getting Started" + } +] diff --git a/packages/create-rstack/template-doc-i18n/docs/en/guide/start/_meta.json b/packages/create-rstack/template-doc-i18n/docs/en/guide/start/_meta.json new file mode 100644 index 00000000..f818265c --- /dev/null +++ b/packages/create-rstack/template-doc-i18n/docs/en/guide/start/_meta.json @@ -0,0 +1 @@ +["introduction"] diff --git a/packages/create-rstack/template-doc-i18n/docs/en/guide/start/introduction.md b/packages/create-rstack/template-doc-i18n/docs/en/guide/start/introduction.md new file mode 100644 index 00000000..0bf0ab47 --- /dev/null +++ b/packages/create-rstack/template-doc-i18n/docs/en/guide/start/introduction.md @@ -0,0 +1,5 @@ +# Introduction + +Rspress is a fast static site generator based on [Rsbuild](https://rsbuild.rs/). It supports Markdown and MDX and includes a default documentation theme. + +Learn more in the [Rspress documentation](https://rspress.rs/). diff --git a/packages/create-rstack/template-doc-i18n/docs/en/index.md b/packages/create-rstack/template-doc-i18n/docs/en/index.md new file mode 100644 index 00000000..d47c03f4 --- /dev/null +++ b/packages/create-rstack/template-doc-i18n/docs/en/index.md @@ -0,0 +1,39 @@ +--- +pageType: home + +hero: + name: My Site + text: A cool website! + tagline: This is the tagline + actions: + - theme: brand + text: Quick Start + link: /guide/start/introduction + - theme: alt + text: GitHub + link: https://github.com/rstackjs/rstack-cli + image: + src: https://assets.rspack.rs/rspress/rspress-logo.svg + alt: Logo +features: + - title: Blazing fast build speed + details: The core compilation module is based on the Rust front-end toolchain, providing a more ultimate development experience. + icon: 🏃🏻‍♀️ + link: /guide/start/introduction + - title: Built-in full-text search + details: Automatically generates a full-text search index for you during construction, providing out-of-the-box full-text search capabilities. + icon: 🎨 + link: https://rspress.rs/guide/advanced/custom-search + - title: AI-friendly + details: Generate llms.txt and Markdown files compliant with the llms.txt specification through SSG-MD, making it easier for large language models to understand and use your documentation. + icon: 🤖 + link: https://rspress.rs/guide/basic/ssg-md + - title: Static site generation + details: In production, it automatically builds into static HTML files, which can be easily deployed anywhere. + icon: 🌈 + link: https://rspress.rs/guide/basic/ssg + - title: Providing multiple custom capabilities + details: Through its extension mechanism, you can easily extend theme UI and build process. + icon: 🔥 + link: https://rspress.rs/guide/basic/custom-theme +--- diff --git a/packages/create-rstack/template-doc-i18n/docs/zh/_nav.json b/packages/create-rstack/template-doc-i18n/docs/zh/_nav.json new file mode 100644 index 00000000..fd08ce7c --- /dev/null +++ b/packages/create-rstack/template-doc-i18n/docs/zh/_nav.json @@ -0,0 +1,16 @@ +[ + { + "text": "指南", + "link": "/guide/start/introduction", + "activeMatch": "/guide/" + }, + { + "text": "API", + "link": "/api/", + "activeMatch": "/api/" + }, + { + "text": "Rspress", + "link": "https://rspress.rs/zh/" + } +] diff --git a/packages/create-rstack/template-doc-i18n/docs/zh/api/_meta.json b/packages/create-rstack/template-doc-i18n/docs/zh/api/_meta.json new file mode 100644 index 00000000..ceb13efc --- /dev/null +++ b/packages/create-rstack/template-doc-i18n/docs/zh/api/_meta.json @@ -0,0 +1,12 @@ +[ + { + "type": "file", + "name": "index", + "label": "API 概览" + }, + { + "type": "file", + "name": "commands", + "label": "命令" + } +] diff --git a/packages/create-rstack/template-doc-i18n/docs/zh/api/commands.mdx b/packages/create-rstack/template-doc-i18n/docs/zh/api/commands.mdx new file mode 100644 index 00000000..142a6ec4 --- /dev/null +++ b/packages/create-rstack/template-doc-i18n/docs/zh/api/commands.mdx @@ -0,0 +1,25 @@ +# 命令 + +## dev + +启动本地开发服务器: + +```bash +rs doc +``` + +## build + +构建用于生产环境的文档站点: + +```bash +rs doc build +``` + +## preview + +在本地预览生产构建: + +```bash +rs doc preview +``` diff --git a/packages/create-rstack/template-doc-i18n/docs/zh/api/index.mdx b/packages/create-rstack/template-doc-i18n/docs/zh/api/index.mdx new file mode 100644 index 00000000..0aa87d8c --- /dev/null +++ b/packages/create-rstack/template-doc-i18n/docs/zh/api/index.mdx @@ -0,0 +1,6 @@ +--- +title: API 概览 +overview: true +--- + +这是一个 API 概览页面,用于列出所有可用 API。 diff --git a/packages/create-rstack/template-doc-i18n/docs/zh/guide/_meta.json b/packages/create-rstack/template-doc-i18n/docs/zh/guide/_meta.json new file mode 100644 index 00000000..d200fa82 --- /dev/null +++ b/packages/create-rstack/template-doc-i18n/docs/zh/guide/_meta.json @@ -0,0 +1,7 @@ +[ + { + "type": "dir-section-header", + "name": "start", + "label": "快速开始" + } +] diff --git a/packages/create-rstack/template-doc-i18n/docs/zh/guide/start/_meta.json b/packages/create-rstack/template-doc-i18n/docs/zh/guide/start/_meta.json new file mode 100644 index 00000000..9993e602 --- /dev/null +++ b/packages/create-rstack/template-doc-i18n/docs/zh/guide/start/_meta.json @@ -0,0 +1,7 @@ +[ + { + "type": "file", + "name": "introduction", + "label": "介绍" + } +] diff --git a/packages/create-rstack/template-doc-i18n/docs/zh/guide/start/introduction.md b/packages/create-rstack/template-doc-i18n/docs/zh/guide/start/introduction.md new file mode 100644 index 00000000..16f39008 --- /dev/null +++ b/packages/create-rstack/template-doc-i18n/docs/zh/guide/start/introduction.md @@ -0,0 +1,5 @@ +# 介绍 + +Rspress 是一个基于 [Rsbuild](https://rsbuild.rs/) 的快速静态站点生成器。它支持 Markdown 和 MDX,并内置默认文档主题。 + +前往 [Rspress 文档](https://rspress.rs/zh/)了解更多。 diff --git a/packages/create-rstack/template-doc-i18n/docs/zh/index.md b/packages/create-rstack/template-doc-i18n/docs/zh/index.md new file mode 100644 index 00000000..b7066f95 --- /dev/null +++ b/packages/create-rstack/template-doc-i18n/docs/zh/index.md @@ -0,0 +1,39 @@ +--- +pageType: home + +hero: + name: 我的站点 + text: 一个很酷的网站! + tagline: 这是网站的副标题 + actions: + - theme: brand + text: 快速开始 + link: /zh/guide/start/introduction + - theme: alt + text: GitHub + link: https://github.com/rstackjs/rstack-cli + image: + src: https://assets.rspack.rs/rspress/rspress-logo.svg + alt: Logo +features: + - title: 极速构建 + details: 核心编译模块基于 Rust 前端工具链,提供更极致的开发体验。 + icon: 🏃🏻‍♀️ + link: /zh/guide/start/introduction + - title: 内置全文搜索 + details: 构建时自动生成全文搜索索引,提供开箱即用的全文搜索能力。 + icon: 🎨 + link: https://rspress.rs/zh/guide/advanced/custom-search + - title: AI 友好 + details: 通过 SSG-MD 生成符合 llms.txt 规范的 llms.txt 和 Markdown 文件,让大语言模型更容易理解和使用你的文档。 + icon: 🤖 + link: https://rspress.rs/zh/guide/basic/ssg-md + - title: 静态站点生成 + details: 在生产环境中自动构建为静态 HTML 文件,可以轻松部署到任意位置。 + icon: 🌈 + link: https://rspress.rs/zh/guide/basic/ssg + - title: 提供多种自定义能力 + details: 通过扩展机制,你可以轻松扩展主题 UI 和构建流程。 + icon: 🔥 + link: https://rspress.rs/zh/guide/basic/custom-theme +--- diff --git a/packages/create-rstack/template-doc-i18n/gitignore b/packages/create-rstack/template-doc-i18n/gitignore new file mode 100644 index 00000000..23a4bc92 --- /dev/null +++ b/packages/create-rstack/template-doc-i18n/gitignore @@ -0,0 +1,14 @@ +# Local +.DS_Store +*.local +*.log* + +# Dist +node_modules +dist/ +doc_build/ + +# IDE +.vscode/* +!.vscode/extensions.json +.idea diff --git a/packages/create-rstack/template-doc-i18n/package.json b/packages/create-rstack/template-doc-i18n/package.json new file mode 100644 index 00000000..147ab50c --- /dev/null +++ b/packages/create-rstack/template-doc-i18n/package.json @@ -0,0 +1,24 @@ +{ + "name": "rstack-doc-i18n", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "rs doc build", + "check": "rs check --type-check", + "dev": "rs doc", + "format": "rs fmt", + "lint": "rs lint", + "preview": "rs doc preview" + }, + "devDependencies": { + "@rspress/core": "^2.0.19", + "@types/node": "^24.13.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "rstack": "^0.5.0", + "typescript": "^7.0.2" + } +} diff --git a/packages/create-rstack/template-doc-i18n/rstack.config.ts b/packages/create-rstack/template-doc-i18n/rstack.config.ts new file mode 100644 index 00000000..2ab17eff --- /dev/null +++ b/packages/create-rstack/template-doc-i18n/rstack.config.ts @@ -0,0 +1,39 @@ +// Rstack configuration guide: https://rstack.rs/config +import path from 'node:path'; +import { define } from 'rstack'; + +define.doc({ + root: path.join(import.meta.dirname, 'docs'), + title: 'My Site', + description: 'A multilingual Rspress documentation site.', + lang: 'en', + locales: [ + { + lang: 'en', + label: 'English', + title: 'My Site', + description: 'A multilingual Rspress documentation site.', + }, + { + lang: 'zh', + label: '简体中文', + title: '我的站点', + description: '一个多语言 Rspress 文档站点。', + }, + ], +}); + +define.lint(async () => { + const { js, ts, reactPlugin, reactHooksPlugin } = await import('rstack/lint'); + + return [ + js.configs.recommended, + ts.configs.recommended, + reactPlugin.configs.recommended, + reactHooksPlugin.configs.recommended, + ]; +}); + +define.fmt({ + singleQuote: true, +}); diff --git a/packages/create-rstack/template-doc-i18n/tsconfig.json b/packages/create-rstack/template-doc-i18n/tsconfig.json new file mode 100644 index 00000000..559f2a04 --- /dev/null +++ b/packages/create-rstack/template-doc-i18n/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["DOM", "ES2020"], + "jsx": "react-jsx", + "target": "ES2020", + "noEmit": true, + "skipLibCheck": true, + "types": ["rstack/types", "node"], + "useDefineForClassFields": true, + + /* modules */ + "moduleDetection": "force", + "moduleResolution": "bundler", + "verbatimModuleSyntax": true, + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + + /* type checking */ + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["docs", "rstack.config.ts"], + "mdx": { + "checkMdx": true + } +} diff --git a/packages/create-rstack/template-doc/README.md b/packages/create-rstack/template-doc/README.md new file mode 100644 index 00000000..cd9ac929 --- /dev/null +++ b/packages/create-rstack/template-doc/README.md @@ -0,0 +1,23 @@ +# Rstack documentation site + +## Setup + +Install the dependencies: + +```bash +{{ packageManager }} install +``` + +## Scripts + +- `{{ packageManager }} run build`: Build the documentation site for production. +- `{{ packageManager }} run check`: Run static checks, including lint and format. +- `{{ packageManager }} run dev`: Start the documentation dev server. +- `{{ packageManager }} run format`: Format code. +- `{{ packageManager }} run lint`: Lint code. +- `{{ packageManager }} run preview`: Preview the production build locally. + +## Learn more + +- [Rstack documentation](https://rstack.rs) +- [Rspress documentation](https://rspress.rs) diff --git a/packages/create-rstack/template-doc/docs/_nav.json b/packages/create-rstack/template-doc/docs/_nav.json new file mode 100644 index 00000000..1c47924a --- /dev/null +++ b/packages/create-rstack/template-doc/docs/_nav.json @@ -0,0 +1,16 @@ +[ + { + "text": "Guide", + "link": "/guide/start/introduction", + "activeMatch": "/guide/" + }, + { + "text": "API", + "link": "/api/", + "activeMatch": "/api/" + }, + { + "text": "Rspress", + "link": "https://rspress.rs/" + } +] diff --git a/packages/create-rstack/template-doc/docs/api/_meta.json b/packages/create-rstack/template-doc/docs/api/_meta.json new file mode 100644 index 00000000..f0ff0de6 --- /dev/null +++ b/packages/create-rstack/template-doc/docs/api/_meta.json @@ -0,0 +1 @@ +["index", "commands"] diff --git a/packages/create-rstack/template-doc/docs/api/commands.mdx b/packages/create-rstack/template-doc/docs/api/commands.mdx new file mode 100644 index 00000000..6e7494c5 --- /dev/null +++ b/packages/create-rstack/template-doc/docs/api/commands.mdx @@ -0,0 +1,25 @@ +# Commands + +## dev + +Start the local development server: + +```bash +rs doc +``` + +## build + +Build the documentation site for production: + +```bash +rs doc build +``` + +## preview + +Preview the production build locally: + +```bash +rs doc preview +``` diff --git a/packages/create-rstack/template-doc/docs/api/index.mdx b/packages/create-rstack/template-doc/docs/api/index.mdx new file mode 100644 index 00000000..939b2956 --- /dev/null +++ b/packages/create-rstack/template-doc/docs/api/index.mdx @@ -0,0 +1,6 @@ +--- +title: API Overview +overview: true +--- + +This is an API Overview page which outlines all the available APIs. diff --git a/packages/create-rstack/template-doc/docs/guide/_meta.json b/packages/create-rstack/template-doc/docs/guide/_meta.json new file mode 100644 index 00000000..2dc352c6 --- /dev/null +++ b/packages/create-rstack/template-doc/docs/guide/_meta.json @@ -0,0 +1,7 @@ +[ + { + "type": "dir-section-header", + "name": "start", + "label": "Getting Started" + } +] diff --git a/packages/create-rstack/template-doc/docs/guide/start/_meta.json b/packages/create-rstack/template-doc/docs/guide/start/_meta.json new file mode 100644 index 00000000..f818265c --- /dev/null +++ b/packages/create-rstack/template-doc/docs/guide/start/_meta.json @@ -0,0 +1 @@ +["introduction"] diff --git a/packages/create-rstack/template-doc/docs/guide/start/introduction.md b/packages/create-rstack/template-doc/docs/guide/start/introduction.md new file mode 100644 index 00000000..0bf0ab47 --- /dev/null +++ b/packages/create-rstack/template-doc/docs/guide/start/introduction.md @@ -0,0 +1,5 @@ +# Introduction + +Rspress is a fast static site generator based on [Rsbuild](https://rsbuild.rs/). It supports Markdown and MDX and includes a default documentation theme. + +Learn more in the [Rspress documentation](https://rspress.rs/). diff --git a/packages/create-rstack/template-doc/docs/index.md b/packages/create-rstack/template-doc/docs/index.md new file mode 100644 index 00000000..d47c03f4 --- /dev/null +++ b/packages/create-rstack/template-doc/docs/index.md @@ -0,0 +1,39 @@ +--- +pageType: home + +hero: + name: My Site + text: A cool website! + tagline: This is the tagline + actions: + - theme: brand + text: Quick Start + link: /guide/start/introduction + - theme: alt + text: GitHub + link: https://github.com/rstackjs/rstack-cli + image: + src: https://assets.rspack.rs/rspress/rspress-logo.svg + alt: Logo +features: + - title: Blazing fast build speed + details: The core compilation module is based on the Rust front-end toolchain, providing a more ultimate development experience. + icon: 🏃🏻‍♀️ + link: /guide/start/introduction + - title: Built-in full-text search + details: Automatically generates a full-text search index for you during construction, providing out-of-the-box full-text search capabilities. + icon: 🎨 + link: https://rspress.rs/guide/advanced/custom-search + - title: AI-friendly + details: Generate llms.txt and Markdown files compliant with the llms.txt specification through SSG-MD, making it easier for large language models to understand and use your documentation. + icon: 🤖 + link: https://rspress.rs/guide/basic/ssg-md + - title: Static site generation + details: In production, it automatically builds into static HTML files, which can be easily deployed anywhere. + icon: 🌈 + link: https://rspress.rs/guide/basic/ssg + - title: Providing multiple custom capabilities + details: Through its extension mechanism, you can easily extend theme UI and build process. + icon: 🔥 + link: https://rspress.rs/guide/basic/custom-theme +--- diff --git a/packages/create-rstack/template-doc/gitignore b/packages/create-rstack/template-doc/gitignore new file mode 100644 index 00000000..23a4bc92 --- /dev/null +++ b/packages/create-rstack/template-doc/gitignore @@ -0,0 +1,14 @@ +# Local +.DS_Store +*.local +*.log* + +# Dist +node_modules +dist/ +doc_build/ + +# IDE +.vscode/* +!.vscode/extensions.json +.idea diff --git a/packages/create-rstack/template-doc/package.json b/packages/create-rstack/template-doc/package.json new file mode 100644 index 00000000..fa500b08 --- /dev/null +++ b/packages/create-rstack/template-doc/package.json @@ -0,0 +1,24 @@ +{ + "name": "rstack-doc", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "rs doc build", + "check": "rs check --type-check", + "dev": "rs doc", + "format": "rs fmt", + "lint": "rs lint", + "preview": "rs doc preview" + }, + "devDependencies": { + "@rspress/core": "^2.0.19", + "@types/node": "^24.13.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "rstack": "^0.5.0", + "typescript": "^7.0.2" + } +} diff --git a/packages/create-rstack/template-doc/rstack.config.ts b/packages/create-rstack/template-doc/rstack.config.ts new file mode 100644 index 00000000..466b9d5c --- /dev/null +++ b/packages/create-rstack/template-doc/rstack.config.ts @@ -0,0 +1,23 @@ +// Rstack configuration guide: https://rstack.rs/config +import path from 'node:path'; +import { define } from 'rstack'; + +define.doc({ + root: path.join(import.meta.dirname, 'docs'), + title: 'My Site', +}); + +define.lint(async () => { + const { js, ts, reactPlugin, reactHooksPlugin } = await import('rstack/lint'); + + return [ + js.configs.recommended, + ts.configs.recommended, + reactPlugin.configs.recommended, + reactHooksPlugin.configs.recommended, + ]; +}); + +define.fmt({ + singleQuote: true, +}); diff --git a/packages/create-rstack/template-doc/tsconfig.json b/packages/create-rstack/template-doc/tsconfig.json new file mode 100644 index 00000000..559f2a04 --- /dev/null +++ b/packages/create-rstack/template-doc/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["DOM", "ES2020"], + "jsx": "react-jsx", + "target": "ES2020", + "noEmit": true, + "skipLibCheck": true, + "types": ["rstack/types", "node"], + "useDefineForClassFields": true, + + /* modules */ + "moduleDetection": "force", + "moduleResolution": "bundler", + "verbatimModuleSyntax": true, + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + + /* type checking */ + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["docs", "rstack.config.ts"], + "mdx": { + "checkMdx": true + } +} diff --git a/packages/create-rstack/template-lib-node-js/AGENTS.md b/packages/create-rstack/template-lib-node-js/AGENTS.md deleted file mode 100644 index 234cc720..00000000 --- a/packages/create-rstack/template-lib-node-js/AGENTS.md +++ /dev/null @@ -1,14 +0,0 @@ -# AGENTS.md - -## Commands - -- `{{ packageManager }} run build` - Build the library for production -- `{{ packageManager }} run dev` - Rebuild the library when source files change -- `{{ packageManager }} run test` - Run tests -- `{{ packageManager }} run test:watch` - Run tests in watch mode - -## Docs - -- Rslib: https://rslib.rs/llms.txt -- Rspack: https://rspack.rs/llms.txt -- Rstest: https://rstest.rs/llms.txt diff --git a/packages/create-rstack/template-lib-node-js/README.md b/packages/create-rstack/template-lib-node-js/README.md deleted file mode 100644 index ab4b6927..00000000 --- a/packages/create-rstack/template-lib-node-js/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# Rstack library - -## Setup - -Install the dependencies: - -```bash -{{ packageManager }} install -``` - -## Get started - -Build the library: - -```bash -{{ packageManager }} run build -``` - -Build the library in watch mode: - -```bash -{{ packageManager }} run dev -``` - -Run tests: - -```bash -{{ packageManager }} run test -``` - -Run tests in watch mode: - -```bash -{{ packageManager }} run test:watch -``` - -## Learn more - -- [Rstack documentation](https://rstack.rs) -- [Rslib documentation](https://rslib.rs) diff --git a/packages/create-rstack/template-lib-node-ts/AGENTS.md b/packages/create-rstack/template-lib-node-ts/AGENTS.md deleted file mode 100644 index 234cc720..00000000 --- a/packages/create-rstack/template-lib-node-ts/AGENTS.md +++ /dev/null @@ -1,14 +0,0 @@ -# AGENTS.md - -## Commands - -- `{{ packageManager }} run build` - Build the library for production -- `{{ packageManager }} run dev` - Rebuild the library when source files change -- `{{ packageManager }} run test` - Run tests -- `{{ packageManager }} run test:watch` - Run tests in watch mode - -## Docs - -- Rslib: https://rslib.rs/llms.txt -- Rspack: https://rspack.rs/llms.txt -- Rstest: https://rstest.rs/llms.txt diff --git a/packages/create-rstack/template-lib-node-ts/README.md b/packages/create-rstack/template-lib-node-ts/README.md index ab4b6927..93e8a06e 100644 --- a/packages/create-rstack/template-lib-node-ts/README.md +++ b/packages/create-rstack/template-lib-node-ts/README.md @@ -8,31 +8,15 @@ Install the dependencies: {{ packageManager }} install ``` -## Get started - -Build the library: - -```bash -{{ packageManager }} run build -``` - -Build the library in watch mode: - -```bash -{{ packageManager }} run dev -``` - -Run tests: - -```bash -{{ packageManager }} run test -``` - -Run tests in watch mode: - -```bash -{{ packageManager }} run test:watch -``` +## Scripts + +- `{{ packageManager }} run build`: Build the library. +- `{{ packageManager }} run check`: Run static checks, including lint and format. +- `{{ packageManager }} run dev`: Build the library in watch mode. +- `{{ packageManager }} run format`: Format code. +- `{{ packageManager }} run lint`: Lint code. +- `{{ packageManager }} run test`: Run tests. +- `{{ packageManager }} run test:watch`: Run tests in watch mode. ## Learn more diff --git a/packages/create-rstack/template-lib-node-ts/package.json b/packages/create-rstack/template-lib-node-ts/package.json index 4805daf9..830a72c1 100644 --- a/packages/create-rstack/template-lib-node-ts/package.json +++ b/packages/create-rstack/template-lib-node-ts/package.json @@ -16,6 +16,7 @@ ], "scripts": { "build": "rs lib", + "check": "rs check --type-check", "dev": "rs lib --watch", "format": "rs fmt", "lint": "rs lint", @@ -24,7 +25,7 @@ }, "devDependencies": { "@types/node": "^24.13.3", - "rstack": "^0.3.5", + "rstack": "^0.5.0", "typescript": "^7.0.2" }, "engines": { diff --git a/packages/create-rstack/template-lib-node-ts/rstack.config.ts b/packages/create-rstack/template-lib-node-ts/rstack.config.ts index bee703a8..d0f94060 100644 --- a/packages/create-rstack/template-lib-node-ts/rstack.config.ts +++ b/packages/create-rstack/template-lib-node-ts/rstack.config.ts @@ -15,3 +15,7 @@ define.lint(async () => { return [js.configs.recommended, ts.configs.recommended]; }); + +define.fmt({ + singleQuote: true, +}); diff --git a/packages/create-rstack/template-lib-node/README.md b/packages/create-rstack/template-lib-node/README.md new file mode 100644 index 00000000..93e8a06e --- /dev/null +++ b/packages/create-rstack/template-lib-node/README.md @@ -0,0 +1,24 @@ +# Rstack library + +## Setup + +Install the dependencies: + +```bash +{{ packageManager }} install +``` + +## Scripts + +- `{{ packageManager }} run build`: Build the library. +- `{{ packageManager }} run check`: Run static checks, including lint and format. +- `{{ packageManager }} run dev`: Build the library in watch mode. +- `{{ packageManager }} run format`: Format code. +- `{{ packageManager }} run lint`: Lint code. +- `{{ packageManager }} run test`: Run tests. +- `{{ packageManager }} run test:watch`: Run tests in watch mode. + +## Learn more + +- [Rstack documentation](https://rstack.rs) +- [Rslib documentation](https://rslib.rs) diff --git a/packages/create-rstack/template-lib-node-js/package.json b/packages/create-rstack/template-lib-node/package.json similarity index 86% rename from packages/create-rstack/template-lib-node-js/package.json rename to packages/create-rstack/template-lib-node/package.json index d586a11b..5da6e4a7 100644 --- a/packages/create-rstack/template-lib-node-js/package.json +++ b/packages/create-rstack/template-lib-node/package.json @@ -1,5 +1,5 @@ { - "name": "rstack-lib-node-js", + "name": "rstack-lib-node", "version": "0.0.0", "sideEffects": false, "type": "module", @@ -14,6 +14,7 @@ ], "scripts": { "build": "rs lib", + "check": "rs check", "dev": "rs lib --watch", "format": "rs fmt", "lint": "rs lint", @@ -21,7 +22,7 @@ "test:watch": "rs test --watch" }, "devDependencies": { - "rstack": "^0.3.5" + "rstack": "^0.5.0" }, "engines": { "node": ">=22.12.0" diff --git a/packages/create-rstack/template-lib-node-js/rstack.config.js b/packages/create-rstack/template-lib-node/rstack.config.js similarity index 88% rename from packages/create-rstack/template-lib-node-js/rstack.config.js rename to packages/create-rstack/template-lib-node/rstack.config.js index 4d3e4b05..3f62051d 100644 --- a/packages/create-rstack/template-lib-node-js/rstack.config.js +++ b/packages/create-rstack/template-lib-node/rstack.config.js @@ -15,3 +15,7 @@ define.lint(async () => { return [js.configs.recommended]; }); + +define.fmt({ + singleQuote: true, +}); diff --git a/packages/create-rstack/template-lib-node-js/src/index.js b/packages/create-rstack/template-lib-node/src/index.js similarity index 100% rename from packages/create-rstack/template-lib-node-js/src/index.js rename to packages/create-rstack/template-lib-node/src/index.js diff --git a/packages/create-rstack/template-lib-node-js/tests/index.test.js b/packages/create-rstack/template-lib-node/tests/index.test.js similarity index 100% rename from packages/create-rstack/template-lib-node-js/tests/index.test.js rename to packages/create-rstack/template-lib-node/tests/index.test.js diff --git a/packages/create-rstack/template-lib-react-js/AGENTS.md b/packages/create-rstack/template-lib-react-js/AGENTS.md deleted file mode 100644 index 234cc720..00000000 --- a/packages/create-rstack/template-lib-react-js/AGENTS.md +++ /dev/null @@ -1,14 +0,0 @@ -# AGENTS.md - -## Commands - -- `{{ packageManager }} run build` - Build the library for production -- `{{ packageManager }} run dev` - Rebuild the library when source files change -- `{{ packageManager }} run test` - Run tests -- `{{ packageManager }} run test:watch` - Run tests in watch mode - -## Docs - -- Rslib: https://rslib.rs/llms.txt -- Rspack: https://rspack.rs/llms.txt -- Rstest: https://rstest.rs/llms.txt diff --git a/packages/create-rstack/template-lib-react-js/README.md b/packages/create-rstack/template-lib-react-js/README.md deleted file mode 100644 index ab4b6927..00000000 --- a/packages/create-rstack/template-lib-react-js/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# Rstack library - -## Setup - -Install the dependencies: - -```bash -{{ packageManager }} install -``` - -## Get started - -Build the library: - -```bash -{{ packageManager }} run build -``` - -Build the library in watch mode: - -```bash -{{ packageManager }} run dev -``` - -Run tests: - -```bash -{{ packageManager }} run test -``` - -Run tests in watch mode: - -```bash -{{ packageManager }} run test:watch -``` - -## Learn more - -- [Rstack documentation](https://rstack.rs) -- [Rslib documentation](https://rslib.rs) diff --git a/packages/create-rstack/template-lib-react-ts/AGENTS.md b/packages/create-rstack/template-lib-react-ts/AGENTS.md deleted file mode 100644 index 234cc720..00000000 --- a/packages/create-rstack/template-lib-react-ts/AGENTS.md +++ /dev/null @@ -1,14 +0,0 @@ -# AGENTS.md - -## Commands - -- `{{ packageManager }} run build` - Build the library for production -- `{{ packageManager }} run dev` - Rebuild the library when source files change -- `{{ packageManager }} run test` - Run tests -- `{{ packageManager }} run test:watch` - Run tests in watch mode - -## Docs - -- Rslib: https://rslib.rs/llms.txt -- Rspack: https://rspack.rs/llms.txt -- Rstest: https://rstest.rs/llms.txt diff --git a/packages/create-rstack/template-lib-react-ts/README.md b/packages/create-rstack/template-lib-react-ts/README.md index ab4b6927..93e8a06e 100644 --- a/packages/create-rstack/template-lib-react-ts/README.md +++ b/packages/create-rstack/template-lib-react-ts/README.md @@ -8,31 +8,15 @@ Install the dependencies: {{ packageManager }} install ``` -## Get started - -Build the library: - -```bash -{{ packageManager }} run build -``` - -Build the library in watch mode: - -```bash -{{ packageManager }} run dev -``` - -Run tests: - -```bash -{{ packageManager }} run test -``` - -Run tests in watch mode: - -```bash -{{ packageManager }} run test:watch -``` +## Scripts + +- `{{ packageManager }} run build`: Build the library. +- `{{ packageManager }} run check`: Run static checks, including lint and format. +- `{{ packageManager }} run dev`: Build the library in watch mode. +- `{{ packageManager }} run format`: Format code. +- `{{ packageManager }} run lint`: Lint code. +- `{{ packageManager }} run test`: Run tests. +- `{{ packageManager }} run test:watch`: Run tests in watch mode. ## Learn more diff --git a/packages/create-rstack/template-lib-react-ts/package.json b/packages/create-rstack/template-lib-react-ts/package.json index 5e02a5d0..6ace72bf 100644 --- a/packages/create-rstack/template-lib-react-ts/package.json +++ b/packages/create-rstack/template-lib-react-ts/package.json @@ -15,6 +15,7 @@ ], "scripts": { "build": "rs lib", + "check": "rs check --type-check", "dev": "rs lib --watch", "format": "rs fmt", "lint": "rs lint", @@ -29,10 +30,10 @@ "@types/node": "^24.13.3", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", - "happy-dom": "^20.11.1", + "happy-dom": "^20.11.2", "react": "^19.2.8", "react-dom": "^19.2.8", - "rstack": "^0.3.5", + "rstack": "^0.5.0", "typescript": "^7.0.2" }, "peerDependencies": { diff --git a/packages/create-rstack/template-lib-react-ts/rstack.config.ts b/packages/create-rstack/template-lib-react-ts/rstack.config.ts index 93cd4782..89cb5d03 100644 --- a/packages/create-rstack/template-lib-react-ts/rstack.config.ts +++ b/packages/create-rstack/template-lib-react-ts/rstack.config.ts @@ -33,3 +33,7 @@ define.lint(async () => { reactHooksPlugin.configs.recommended, ]; }); + +define.fmt({ + singleQuote: true, +}); diff --git a/packages/create-rstack/template-lib-react/README.md b/packages/create-rstack/template-lib-react/README.md new file mode 100644 index 00000000..93e8a06e --- /dev/null +++ b/packages/create-rstack/template-lib-react/README.md @@ -0,0 +1,24 @@ +# Rstack library + +## Setup + +Install the dependencies: + +```bash +{{ packageManager }} install +``` + +## Scripts + +- `{{ packageManager }} run build`: Build the library. +- `{{ packageManager }} run check`: Run static checks, including lint and format. +- `{{ packageManager }} run dev`: Build the library in watch mode. +- `{{ packageManager }} run format`: Format code. +- `{{ packageManager }} run lint`: Lint code. +- `{{ packageManager }} run test`: Run tests. +- `{{ packageManager }} run test:watch`: Run tests in watch mode. + +## Learn more + +- [Rstack documentation](https://rstack.rs) +- [Rslib documentation](https://rslib.rs) diff --git a/packages/create-rstack/template-lib-react-js/package.json b/packages/create-rstack/template-lib-react/package.json similarity index 87% rename from packages/create-rstack/template-lib-react-js/package.json rename to packages/create-rstack/template-lib-react/package.json index 7580d09f..976ab8ae 100644 --- a/packages/create-rstack/template-lib-react-js/package.json +++ b/packages/create-rstack/template-lib-react/package.json @@ -1,5 +1,5 @@ { - "name": "rstack-lib-react-js", + "name": "rstack-lib-react", "version": "0.0.0", "type": "module", "exports": { @@ -13,6 +13,7 @@ ], "scripts": { "build": "rs lib", + "check": "rs check", "dev": "rs lib --watch", "format": "rs fmt", "lint": "rs lint", @@ -25,10 +26,10 @@ "@testing-library/jest-dom": "^7.0.0", "@testing-library/react": "^16.3.2", "@types/react-dom": "^19.2.4", - "happy-dom": "^20.11.1", + "happy-dom": "^20.11.2", "react": "^19.2.8", "react-dom": "^19.2.8", - "rstack": "^0.3.5" + "rstack": "^0.5.0" }, "peerDependencies": { "react": ">=18.0.0", diff --git a/packages/create-rstack/template-lib-react-js/rstack.config.js b/packages/create-rstack/template-lib-react/rstack.config.js similarity index 94% rename from packages/create-rstack/template-lib-react-js/rstack.config.js rename to packages/create-rstack/template-lib-react/rstack.config.js index 30b3b512..3ef9c799 100644 --- a/packages/create-rstack/template-lib-react-js/rstack.config.js +++ b/packages/create-rstack/template-lib-react/rstack.config.js @@ -32,3 +32,7 @@ define.lint(async () => { reactHooksPlugin.configs.recommended, ]; }); + +define.fmt({ + singleQuote: true, +}); diff --git a/packages/create-rstack/template-lib-react-js/src/Button.jsx b/packages/create-rstack/template-lib-react/src/Button.jsx similarity index 73% rename from packages/create-rstack/template-lib-react-js/src/Button.jsx rename to packages/create-rstack/template-lib-react/src/Button.jsx index 91960089..1e0570fb 100644 --- a/packages/create-rstack/template-lib-react-js/src/Button.jsx +++ b/packages/create-rstack/template-lib-react/src/Button.jsx @@ -1,6 +1,12 @@ import './button.css'; -export const Button = ({ primary = false, size = 'medium', backgroundColor, label, ...props }) => { +export const Button = ({ + primary = false, + size = 'medium', + backgroundColor, + label, + ...props +}) => { const mode = primary ? 'demo-button--primary' : 'demo-button--secondary'; return ( + ); +} diff --git a/packages/create-rstack/template-lib-solid-ts/src/button.css b/packages/create-rstack/template-lib-solid-ts/src/button.css new file mode 100644 index 00000000..257ef46f --- /dev/null +++ b/packages/create-rstack/template-lib-solid-ts/src/button.css @@ -0,0 +1,34 @@ +.demo-button { + font-weight: 700; + border: 0; + border-radius: 3em; + cursor: pointer; + display: inline-block; + line-height: 1; +} + +.demo-button--primary { + color: white; + background-color: #1ea7fd; +} + +.demo-button--secondary { + color: #333; + background-color: transparent; + box-shadow: rgba(0, 0, 0, 0.15) 0 0 0 1px inset; +} + +.demo-button--small { + font-size: 12px; + padding: 10px 16px; +} + +.demo-button--medium { + font-size: 14px; + padding: 11px 20px; +} + +.demo-button--large { + font-size: 16px; + padding: 12px 24px; +} diff --git a/packages/create-rstack/template-lib-solid-ts/src/index.tsx b/packages/create-rstack/template-lib-solid-ts/src/index.tsx new file mode 100644 index 00000000..fa3c8a51 --- /dev/null +++ b/packages/create-rstack/template-lib-solid-ts/src/index.tsx @@ -0,0 +1,2 @@ +export { Button } from './Button'; +export type { ButtonProps } from './Button'; diff --git a/packages/create-rstack/template-lib-solid-ts/tests/index.test.tsx b/packages/create-rstack/template-lib-solid-ts/tests/index.test.tsx new file mode 100644 index 00000000..b2a49c13 --- /dev/null +++ b/packages/create-rstack/template-lib-solid-ts/tests/index.test.tsx @@ -0,0 +1,11 @@ +import { render, screen } from '@solidjs/testing-library'; +import { expect, test } from 'rstack/test'; +import { Button } from '../src/Button'; + +test('The button should have correct background color', async () => { + render(() => + ); +} diff --git a/packages/create-rstack/template-lib-solid/src/button.css b/packages/create-rstack/template-lib-solid/src/button.css new file mode 100644 index 00000000..257ef46f --- /dev/null +++ b/packages/create-rstack/template-lib-solid/src/button.css @@ -0,0 +1,34 @@ +.demo-button { + font-weight: 700; + border: 0; + border-radius: 3em; + cursor: pointer; + display: inline-block; + line-height: 1; +} + +.demo-button--primary { + color: white; + background-color: #1ea7fd; +} + +.demo-button--secondary { + color: #333; + background-color: transparent; + box-shadow: rgba(0, 0, 0, 0.15) 0 0 0 1px inset; +} + +.demo-button--small { + font-size: 12px; + padding: 10px 16px; +} + +.demo-button--medium { + font-size: 14px; + padding: 11px 20px; +} + +.demo-button--large { + font-size: 16px; + padding: 12px 24px; +} diff --git a/packages/create-rstack/template-lib-solid/src/index.jsx b/packages/create-rstack/template-lib-solid/src/index.jsx new file mode 100644 index 00000000..fe9c53c5 --- /dev/null +++ b/packages/create-rstack/template-lib-solid/src/index.jsx @@ -0,0 +1 @@ +export { Button } from './Button'; diff --git a/packages/create-rstack/template-lib-solid/tests/index.test.jsx b/packages/create-rstack/template-lib-solid/tests/index.test.jsx new file mode 100644 index 00000000..b2a49c13 --- /dev/null +++ b/packages/create-rstack/template-lib-solid/tests/index.test.jsx @@ -0,0 +1,11 @@ +import { render, screen } from '@solidjs/testing-library'; +import { expect, test } from 'rstack/test'; +import { Button } from '../src/Button'; + +test('The button should have correct background color', async () => { + render(() => diff --git a/packages/create-rstack/template-lib-svelte-ts/src/button.css b/packages/create-rstack/template-lib-svelte-ts/src/button.css new file mode 100644 index 00000000..257ef46f --- /dev/null +++ b/packages/create-rstack/template-lib-svelte-ts/src/button.css @@ -0,0 +1,34 @@ +.demo-button { + font-weight: 700; + border: 0; + border-radius: 3em; + cursor: pointer; + display: inline-block; + line-height: 1; +} + +.demo-button--primary { + color: white; + background-color: #1ea7fd; +} + +.demo-button--secondary { + color: #333; + background-color: transparent; + box-shadow: rgba(0, 0, 0, 0.15) 0 0 0 1px inset; +} + +.demo-button--small { + font-size: 12px; + padding: 10px 16px; +} + +.demo-button--medium { + font-size: 14px; + padding: 11px 20px; +} + +.demo-button--large { + font-size: 16px; + padding: 12px 24px; +} diff --git a/packages/create-rstack/template-lib-svelte-ts/src/index.ts b/packages/create-rstack/template-lib-svelte-ts/src/index.ts new file mode 100644 index 00000000..ae34e39c --- /dev/null +++ b/packages/create-rstack/template-lib-svelte-ts/src/index.ts @@ -0,0 +1 @@ +export { default as Button } from './Button.svelte'; diff --git a/packages/create-rstack/template-lib-svelte-ts/tests/index.test.ts b/packages/create-rstack/template-lib-svelte-ts/tests/index.test.ts new file mode 100644 index 00000000..cfcfa131 --- /dev/null +++ b/packages/create-rstack/template-lib-svelte-ts/tests/index.test.ts @@ -0,0 +1,19 @@ +import { expect, test } from 'rstack/test'; +import { mount, unmount } from 'svelte'; +import Button from '../src/Button.svelte'; + +test('The button should render the default label', () => { + const target = document.createElement('div'); + document.body.append(target); + const component = mount(Button, { target }); + const button = target.querySelector('button'); + + if (!button) { + throw new Error('Expected button to be rendered'); + } + + expect(button.textContent).toBe('Demo Button'); + + unmount(component); + target.remove(); +}); diff --git a/packages/create-rstack/template-lib-svelte-ts/tests/tsconfig.json b/packages/create-rstack/template-lib-svelte-ts/tests/tsconfig.json new file mode 100644 index 00000000..91484919 --- /dev/null +++ b/packages/create-rstack/template-lib-svelte-ts/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": ".." + }, + "include": ["./"] +} diff --git a/packages/create-rstack/template-lib-svelte-ts/tsconfig.json b/packages/create-rstack/template-lib-svelte-ts/tsconfig.json new file mode 100644 index 00000000..e69b0204 --- /dev/null +++ b/packages/create-rstack/template-lib-svelte-ts/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "lib": ["DOM", "ES2022"], + "target": "ES2022", + "declaration": true, + "emitDeclarationOnly": true, + "isolatedModules": true, + "skipLibCheck": true, + "types": ["rstack/types", "node", "svelte"], + "useDefineForClassFields": true, + + /* modules */ + "module": "preserve", + "moduleDetection": "force", + "moduleResolution": "bundler", + "verbatimModuleSyntax": true, + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + + /* type checking */ + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["src", "scripts", "*.config.ts"] +} diff --git a/packages/create-rstack/template-lib-svelte/README.md b/packages/create-rstack/template-lib-svelte/README.md new file mode 100644 index 00000000..93e8a06e --- /dev/null +++ b/packages/create-rstack/template-lib-svelte/README.md @@ -0,0 +1,24 @@ +# Rstack library + +## Setup + +Install the dependencies: + +```bash +{{ packageManager }} install +``` + +## Scripts + +- `{{ packageManager }} run build`: Build the library. +- `{{ packageManager }} run check`: Run static checks, including lint and format. +- `{{ packageManager }} run dev`: Build the library in watch mode. +- `{{ packageManager }} run format`: Format code. +- `{{ packageManager }} run lint`: Lint code. +- `{{ packageManager }} run test`: Run tests. +- `{{ packageManager }} run test:watch`: Run tests in watch mode. + +## Learn more + +- [Rstack documentation](https://rstack.rs) +- [Rslib documentation](https://rslib.rs) diff --git a/packages/create-rstack/template-lib-svelte/package.json b/packages/create-rstack/template-lib-svelte/package.json new file mode 100644 index 00000000..72861ffd --- /dev/null +++ b/packages/create-rstack/template-lib-svelte/package.json @@ -0,0 +1,36 @@ +{ + "name": "rstack-lib-svelte", + "version": "0.0.0", + "type": "module", + "exports": { + ".": { + "default": "./dist/index.js" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "rs lib", + "check": "rs check", + "dev": "rs lib --watch", + "format": "rs fmt", + "lint": "rs lint", + "test": "rs test", + "test:watch": "rs test --watch" + }, + "devDependencies": { + "@rsbuild/plugin-svelte": "^2.0.1", + "happy-dom": "^20.11.2", + "prettier-plugin-svelte": "^4.1.1", + "rstack": "^0.5.0", + "svelte": "^5.56.8" + }, + "peerDependencies": { + "svelte": "^5.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/create-rstack/template-lib-svelte/pnpm-workspace.yaml b/packages/create-rstack/template-lib-svelte/pnpm-workspace.yaml new file mode 100644 index 00000000..3c064313 --- /dev/null +++ b/packages/create-rstack/template-lib-svelte/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + svelte-preprocess: false diff --git a/packages/create-rstack/template-lib-svelte/rstack.config.js b/packages/create-rstack/template-lib-svelte/rstack.config.js new file mode 100644 index 00000000..b7d7ba0f --- /dev/null +++ b/packages/create-rstack/template-lib-svelte/rstack.config.js @@ -0,0 +1,35 @@ +// @ts-check +// Rstack configuration guide: https://rstack.rs/config +import { define } from 'rstack'; + +define.lib(async () => { + const { pluginSvelte } = await import('@rsbuild/plugin-svelte'); + + return { + bundle: false, + source: { + entry: { + index: ['./src/**'], + }, + }, + output: { + target: 'web', + }, + plugins: [pluginSvelte()], + }; +}); + +define.test({ + testEnvironment: 'happy-dom', +}); + +define.lint(async () => { + const { js } = await import('rstack/lint'); + + return [js.configs.recommended]; +}); + +define.fmt({ + plugins: ['prettier-plugin-svelte'], + singleQuote: true, +}); diff --git a/packages/create-rstack/template-lib-svelte/src/Button.svelte b/packages/create-rstack/template-lib-svelte/src/Button.svelte new file mode 100644 index 00000000..6a7eb755 --- /dev/null +++ b/packages/create-rstack/template-lib-svelte/src/Button.svelte @@ -0,0 +1,24 @@ + + + diff --git a/packages/create-rstack/template-lib-svelte/src/button.css b/packages/create-rstack/template-lib-svelte/src/button.css new file mode 100644 index 00000000..257ef46f --- /dev/null +++ b/packages/create-rstack/template-lib-svelte/src/button.css @@ -0,0 +1,34 @@ +.demo-button { + font-weight: 700; + border: 0; + border-radius: 3em; + cursor: pointer; + display: inline-block; + line-height: 1; +} + +.demo-button--primary { + color: white; + background-color: #1ea7fd; +} + +.demo-button--secondary { + color: #333; + background-color: transparent; + box-shadow: rgba(0, 0, 0, 0.15) 0 0 0 1px inset; +} + +.demo-button--small { + font-size: 12px; + padding: 10px 16px; +} + +.demo-button--medium { + font-size: 14px; + padding: 11px 20px; +} + +.demo-button--large { + font-size: 16px; + padding: 12px 24px; +} diff --git a/packages/create-rstack/template-lib-svelte/src/index.js b/packages/create-rstack/template-lib-svelte/src/index.js new file mode 100644 index 00000000..ae34e39c --- /dev/null +++ b/packages/create-rstack/template-lib-svelte/src/index.js @@ -0,0 +1 @@ +export { default as Button } from './Button.svelte'; diff --git a/packages/create-rstack/template-lib-svelte/tests/index.test.js b/packages/create-rstack/template-lib-svelte/tests/index.test.js new file mode 100644 index 00000000..cfcfa131 --- /dev/null +++ b/packages/create-rstack/template-lib-svelte/tests/index.test.js @@ -0,0 +1,19 @@ +import { expect, test } from 'rstack/test'; +import { mount, unmount } from 'svelte'; +import Button from '../src/Button.svelte'; + +test('The button should render the default label', () => { + const target = document.createElement('div'); + document.body.append(target); + const component = mount(Button, { target }); + const button = target.querySelector('button'); + + if (!button) { + throw new Error('Expected button to be rendered'); + } + + expect(button.textContent).toBe('Demo Button'); + + unmount(component); + target.remove(); +}); diff --git a/packages/create-rstack/template-lib-vue-ts/README.md b/packages/create-rstack/template-lib-vue-ts/README.md new file mode 100644 index 00000000..93e8a06e --- /dev/null +++ b/packages/create-rstack/template-lib-vue-ts/README.md @@ -0,0 +1,24 @@ +# Rstack library + +## Setup + +Install the dependencies: + +```bash +{{ packageManager }} install +``` + +## Scripts + +- `{{ packageManager }} run build`: Build the library. +- `{{ packageManager }} run check`: Run static checks, including lint and format. +- `{{ packageManager }} run dev`: Build the library in watch mode. +- `{{ packageManager }} run format`: Format code. +- `{{ packageManager }} run lint`: Lint code. +- `{{ packageManager }} run test`: Run tests. +- `{{ packageManager }} run test:watch`: Run tests in watch mode. + +## Learn more + +- [Rstack documentation](https://rstack.rs) +- [Rslib documentation](https://rslib.rs) diff --git a/packages/create-rstack/template-lib-vue-ts/package.json b/packages/create-rstack/template-lib-vue-ts/package.json new file mode 100644 index 00000000..ee6e5524 --- /dev/null +++ b/packages/create-rstack/template-lib-vue-ts/package.json @@ -0,0 +1,42 @@ +{ + "name": "rstack-lib-vue-ts", + "version": "0.0.0", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "types": "./dist/index.d.ts", + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "rs lib && vue-tsc", + "check": "rs check", + "dev": "rs lib --watch", + "format": "rs fmt", + "lint": "rs lint", + "test": "rs test", + "test:watch": "rs test --watch" + }, + "devDependencies": { + "@rsbuild/plugin-vue": "^2.0.1", + "@testing-library/jest-dom": "^7.0.0", + "@types/node": "^24.13.3", + "@vue/test-utils": "^2.4.11", + "happy-dom": "^20.11.2", + "rstack": "^0.5.0", + "typescript": "^6.0.3", + "vue": "^3.5.41", + "vue-tsc": "^3.3.9" + }, + "peerDependencies": { + "vue": ">=3.2.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/create-rstack/template-lib-vue-ts/rstack.config.ts b/packages/create-rstack/template-lib-vue-ts/rstack.config.ts new file mode 100644 index 00000000..e9c37a60 --- /dev/null +++ b/packages/create-rstack/template-lib-vue-ts/rstack.config.ts @@ -0,0 +1,33 @@ +// Rstack configuration guide: https://rstack.rs/config +import { define } from 'rstack'; + +define.lib(async () => { + const { pluginVue } = await import('@rsbuild/plugin-vue'); + + return { + bundle: false, + source: { + entry: { + index: ['./src/**'], + }, + }, + output: { + target: 'web', + }, + plugins: [pluginVue()], + }; +}); + +define.test({ + setupFiles: ['./tests/rstest.setup.ts'], +}); + +define.lint(async () => { + const { js, ts } = await import('rstack/lint'); + + return [js.configs.recommended, ts.configs.recommended]; +}); + +define.fmt({ + singleQuote: true, +}); diff --git a/packages/create-rstack/template-lib-vue-ts/src/Button.vue b/packages/create-rstack/template-lib-vue-ts/src/Button.vue new file mode 100644 index 00000000..c6fe5ac5 --- /dev/null +++ b/packages/create-rstack/template-lib-vue-ts/src/Button.vue @@ -0,0 +1,35 @@ + + + diff --git a/packages/create-rstack/template-lib-vue-ts/src/button.css b/packages/create-rstack/template-lib-vue-ts/src/button.css new file mode 100644 index 00000000..257ef46f --- /dev/null +++ b/packages/create-rstack/template-lib-vue-ts/src/button.css @@ -0,0 +1,34 @@ +.demo-button { + font-weight: 700; + border: 0; + border-radius: 3em; + cursor: pointer; + display: inline-block; + line-height: 1; +} + +.demo-button--primary { + color: white; + background-color: #1ea7fd; +} + +.demo-button--secondary { + color: #333; + background-color: transparent; + box-shadow: rgba(0, 0, 0, 0.15) 0 0 0 1px inset; +} + +.demo-button--small { + font-size: 12px; + padding: 10px 16px; +} + +.demo-button--medium { + font-size: 14px; + padding: 11px 20px; +} + +.demo-button--large { + font-size: 16px; + padding: 12px 24px; +} diff --git a/packages/create-rstack/template-lib-vue-ts/src/index.ts b/packages/create-rstack/template-lib-vue-ts/src/index.ts new file mode 100644 index 00000000..652e7950 --- /dev/null +++ b/packages/create-rstack/template-lib-vue-ts/src/index.ts @@ -0,0 +1 @@ +export { default as Button } from './Button.vue'; diff --git a/packages/create-rstack/template-lib-vue-ts/tests/index.test.ts b/packages/create-rstack/template-lib-vue-ts/tests/index.test.ts new file mode 100644 index 00000000..991849c2 --- /dev/null +++ b/packages/create-rstack/template-lib-vue-ts/tests/index.test.ts @@ -0,0 +1,17 @@ +import { expect, test } from 'rstack/test'; +import { mount } from '@vue/test-utils'; +import Button from '../src/Button.vue'; + +test('The button should have correct background color', () => { + const wrapper = mount(Button, { + attachTo: document.body, + props: { + backgroundColor: '#ccc', + label: 'Demo Button', + }, + }); + expect(wrapper.get('button').element).toHaveStyle({ + backgroundColor: '#ccc', + }); + wrapper.unmount(); +}); diff --git a/packages/create-rstack/template-lib-vue-ts/tests/rstest.setup.ts b/packages/create-rstack/template-lib-vue-ts/tests/rstest.setup.ts new file mode 100644 index 00000000..5b719f5b --- /dev/null +++ b/packages/create-rstack/template-lib-vue-ts/tests/rstest.setup.ts @@ -0,0 +1,6 @@ +import { afterEach, expect } from 'rstack/test'; +import * as jestDomMatchers from '@testing-library/jest-dom/matchers'; +import { enableAutoUnmount } from '@vue/test-utils'; + +expect.extend(jestDomMatchers); +enableAutoUnmount(afterEach); diff --git a/packages/create-rstack/template-lib-vue-ts/tests/tsconfig.json b/packages/create-rstack/template-lib-vue-ts/tests/tsconfig.json new file mode 100644 index 00000000..b13d0ac4 --- /dev/null +++ b/packages/create-rstack/template-lib-vue-ts/tests/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "..", + "types": ["@testing-library/jest-dom"] + }, + "include": ["./"] +} diff --git a/packages/create-rstack/template-lib-vue-ts/tsconfig.json b/packages/create-rstack/template-lib-vue-ts/tsconfig.json new file mode 100644 index 00000000..9d608297 --- /dev/null +++ b/packages/create-rstack/template-lib-vue-ts/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "lib": ["DOM", "ES2022"], + "jsx": "preserve", + "target": "ES2022", + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "dist", + "skipLibCheck": true, + "types": ["rstack/types", "node"], + "jsxImportSource": "vue", + "useDefineForClassFields": true, + "rootDir": "src", + + /* modules */ + "moduleDetection": "force", + "moduleResolution": "bundler", + "verbatimModuleSyntax": true, + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + + /* type checking */ + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["src"] +} diff --git a/packages/create-rstack/template-lib-vue/README.md b/packages/create-rstack/template-lib-vue/README.md new file mode 100644 index 00000000..93e8a06e --- /dev/null +++ b/packages/create-rstack/template-lib-vue/README.md @@ -0,0 +1,24 @@ +# Rstack library + +## Setup + +Install the dependencies: + +```bash +{{ packageManager }} install +``` + +## Scripts + +- `{{ packageManager }} run build`: Build the library. +- `{{ packageManager }} run check`: Run static checks, including lint and format. +- `{{ packageManager }} run dev`: Build the library in watch mode. +- `{{ packageManager }} run format`: Format code. +- `{{ packageManager }} run lint`: Lint code. +- `{{ packageManager }} run test`: Run tests. +- `{{ packageManager }} run test:watch`: Run tests in watch mode. + +## Learn more + +- [Rstack documentation](https://rstack.rs) +- [Rslib documentation](https://rslib.rs) diff --git a/packages/create-rstack/template-lib-vue/package.json b/packages/create-rstack/template-lib-vue/package.json new file mode 100644 index 00000000..7a281102 --- /dev/null +++ b/packages/create-rstack/template-lib-vue/package.json @@ -0,0 +1,37 @@ +{ + "name": "rstack-lib-vue", + "version": "0.0.0", + "type": "module", + "exports": { + ".": { + "default": "./dist/index.js" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "rs lib", + "check": "rs check", + "dev": "rs lib --watch", + "format": "rs fmt", + "lint": "rs lint", + "test": "rs test", + "test:watch": "rs test --watch" + }, + "devDependencies": { + "@rsbuild/plugin-vue": "^2.0.1", + "@testing-library/jest-dom": "^7.0.0", + "@vue/test-utils": "^2.4.11", + "happy-dom": "^20.11.2", + "rstack": "^0.5.0", + "vue": "^3.5.41" + }, + "peerDependencies": { + "vue": ">=3.2.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/create-rstack/template-lib-vue/rstack.config.js b/packages/create-rstack/template-lib-vue/rstack.config.js new file mode 100644 index 00000000..09a3de03 --- /dev/null +++ b/packages/create-rstack/template-lib-vue/rstack.config.js @@ -0,0 +1,34 @@ +// @ts-check +// Rstack configuration guide: https://rstack.rs/config +import { define } from 'rstack'; + +define.lib(async () => { + const { pluginVue } = await import('@rsbuild/plugin-vue'); + + return { + bundle: false, + source: { + entry: { + index: ['./src/**'], + }, + }, + output: { + target: 'web', + }, + plugins: [pluginVue()], + }; +}); + +define.test({ + setupFiles: ['./tests/rstest.setup.js'], +}); + +define.lint(async () => { + const { js } = await import('rstack/lint'); + + return [js.configs.recommended]; +}); + +define.fmt({ + singleQuote: true, +}); diff --git a/packages/create-rstack/template-lib-vue/src/Button.vue b/packages/create-rstack/template-lib-vue/src/Button.vue new file mode 100644 index 00000000..6867449f --- /dev/null +++ b/packages/create-rstack/template-lib-vue/src/Button.vue @@ -0,0 +1,43 @@ + + + diff --git a/packages/create-rstack/template-lib-vue/src/button.css b/packages/create-rstack/template-lib-vue/src/button.css new file mode 100644 index 00000000..257ef46f --- /dev/null +++ b/packages/create-rstack/template-lib-vue/src/button.css @@ -0,0 +1,34 @@ +.demo-button { + font-weight: 700; + border: 0; + border-radius: 3em; + cursor: pointer; + display: inline-block; + line-height: 1; +} + +.demo-button--primary { + color: white; + background-color: #1ea7fd; +} + +.demo-button--secondary { + color: #333; + background-color: transparent; + box-shadow: rgba(0, 0, 0, 0.15) 0 0 0 1px inset; +} + +.demo-button--small { + font-size: 12px; + padding: 10px 16px; +} + +.demo-button--medium { + font-size: 14px; + padding: 11px 20px; +} + +.demo-button--large { + font-size: 16px; + padding: 12px 24px; +} diff --git a/packages/create-rstack/template-lib-vue/src/index.js b/packages/create-rstack/template-lib-vue/src/index.js new file mode 100644 index 00000000..652e7950 --- /dev/null +++ b/packages/create-rstack/template-lib-vue/src/index.js @@ -0,0 +1 @@ +export { default as Button } from './Button.vue'; diff --git a/packages/create-rstack/template-lib-vue/tests/index.test.js b/packages/create-rstack/template-lib-vue/tests/index.test.js new file mode 100644 index 00000000..991849c2 --- /dev/null +++ b/packages/create-rstack/template-lib-vue/tests/index.test.js @@ -0,0 +1,17 @@ +import { expect, test } from 'rstack/test'; +import { mount } from '@vue/test-utils'; +import Button from '../src/Button.vue'; + +test('The button should have correct background color', () => { + const wrapper = mount(Button, { + attachTo: document.body, + props: { + backgroundColor: '#ccc', + label: 'Demo Button', + }, + }); + expect(wrapper.get('button').element).toHaveStyle({ + backgroundColor: '#ccc', + }); + wrapper.unmount(); +}); diff --git a/packages/create-rstack/template-lib-vue/tests/rstest.setup.js b/packages/create-rstack/template-lib-vue/tests/rstest.setup.js new file mode 100644 index 00000000..5b719f5b --- /dev/null +++ b/packages/create-rstack/template-lib-vue/tests/rstest.setup.js @@ -0,0 +1,6 @@ +import { afterEach, expect } from 'rstack/test'; +import * as jestDomMatchers from '@testing-library/jest-dom/matchers'; +import { enableAutoUnmount } from '@vue/test-utils'; + +expect.extend(jestDomMatchers); +enableAutoUnmount(afterEach); diff --git a/packages/create-rstack/tests/create.test.ts b/packages/create-rstack/tests/create.test.ts index 74306421..32576411 100644 --- a/packages/create-rstack/tests/create.test.ts +++ b/packages/create-rstack/tests/create.test.ts @@ -1,5 +1,5 @@ import { execFile } from 'node:child_process'; -import { access, mkdtemp, readFile, rm } from 'node:fs/promises'; +import { access, mkdir, mkdtemp, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { promisify } from 'node:util'; @@ -9,6 +9,129 @@ const execFileAsync = promisify(execFile); const packageRoot = path.resolve(import.meta.dirname, '..'); const binPath = path.join(packageRoot, 'bin.js'); const tempDirectories: string[] = []; +const checkScript = 'rs check'; +const typeCheckScript = 'rs check --type-check'; +const templatesWithoutTypeCheck = new Set([ + 'app-svelte-ts', + 'app-vue-ts', + 'lib-svelte-ts', + 'lib-vue-ts', +]); + +type ProjectPackage = { + name: string; + scripts: Record; +}; + +type SourceTemplate = { + template: string; + sourceExtension: string; + testFile: string; +}; + +const sourceTemplates: SourceTemplate[] = [ + { template: 'app-vanilla', sourceExtension: 'js', testFile: 'dom.test.js' }, + { template: 'app-vanilla-ts', sourceExtension: 'ts', testFile: 'dom.test.ts' }, + { template: 'app-react', sourceExtension: 'jsx', testFile: 'index.test.jsx' }, + { template: 'app-react-ts', sourceExtension: 'tsx', testFile: 'index.test.tsx' }, + { template: 'app-preact', sourceExtension: 'jsx', testFile: 'index.test.jsx' }, + { template: 'app-preact-ts', sourceExtension: 'tsx', testFile: 'index.test.tsx' }, + { template: 'app-vue', sourceExtension: 'js', testFile: 'index.test.js' }, + { template: 'app-vue-ts', sourceExtension: 'ts', testFile: 'index.test.ts' }, + { template: 'app-lit', sourceExtension: 'js', testFile: 'index.test.js' }, + { template: 'app-lit-ts', sourceExtension: 'ts', testFile: 'index.test.ts' }, + { template: 'app-svelte', sourceExtension: 'js', testFile: 'index.test.js' }, + { template: 'app-svelte-ts', sourceExtension: 'ts', testFile: 'index.test.ts' }, + { template: 'app-solid', sourceExtension: 'jsx', testFile: 'index.test.jsx' }, + { template: 'app-solid-ts', sourceExtension: 'tsx', testFile: 'index.test.tsx' }, + { template: 'lib-node', sourceExtension: 'js', testFile: 'index.test.js' }, + { template: 'lib-node-ts', sourceExtension: 'ts', testFile: 'index.test.ts' }, + { template: 'lib-react', sourceExtension: 'jsx', testFile: 'index.test.jsx' }, + { template: 'lib-react-ts', sourceExtension: 'tsx', testFile: 'index.test.tsx' }, + { template: 'lib-vue', sourceExtension: 'js', testFile: 'index.test.js' }, + { template: 'lib-vue-ts', sourceExtension: 'ts', testFile: 'index.test.ts' }, + { template: 'lib-svelte', sourceExtension: 'js', testFile: 'index.test.js' }, + { template: 'lib-svelte-ts', sourceExtension: 'ts', testFile: 'index.test.ts' }, + { template: 'lib-solid', sourceExtension: 'jsx', testFile: 'index.test.jsx' }, + { template: 'lib-solid-ts', sourceExtension: 'tsx', testFile: 'index.test.tsx' }, +]; + +const docTemplates = [ + { + template: 'doc', + files: [ + 'README.md', + '.gitignore', + 'rstack.config.ts', + 'docs/index.md', + 'docs/guide/start/introduction.md', + ], + }, + { + template: 'doc-i18n', + files: ['rstack.config.ts', 'docs/en/index.md', 'docs/zh/index.md'], + }, +]; + +const getCheckScript = (template: string, hasTypeScript: boolean): string => + hasTypeScript && !templatesWithoutTypeCheck.has(template) ? typeCheckScript : checkScript; + +const readProjectPackage = async (projectDirectory: string): Promise => + JSON.parse(await readFile(path.join(projectDirectory, 'package.json'), 'utf8')) as ProjectPackage; + +const expectFiles = async (projectDirectory: string, files: string[]): Promise => { + for (const file of files) { + await expect(access(path.join(projectDirectory, file))).resolves.toBeUndefined(); + } +}; + +const expectStagedSetup = async ( + projectDirectory: string, + configExtension: string, + scripts: Record, +): Promise => { + expect(scripts.prepare).toBe('rs setup'); + expect( + await readFile(path.join(projectDirectory, '.rstack', 'hooks', 'pre-commit'), 'utf8'), + ).toBe('rs staged\n'); + expect( + await readFile(path.join(projectDirectory, `rstack.config.${configExtension}`), 'utf8'), + ).toContain('define.staged({'); +}; + +const expectNoStagedSetup = async ( + projectDirectory: string, + configExtension: string, + scripts: Record, +): Promise => { + expect(scripts.prepare).toBeUndefined(); + await expect( + access(path.join(projectDirectory, '.rstack', 'hooks', 'pre-commit')), + ).rejects.toThrow(); + expect( + await readFile(path.join(projectDirectory, `rstack.config.${configExtension}`), 'utf8'), + ).not.toContain('define.staged({'); +}; + +const expectProjectSetup = async ( + projectDirectory: string, + template: string, + configExtension: string, + hasTypeScript: boolean, +): Promise => { + const packageJson = await readProjectPackage(projectDirectory); + + expect(packageJson.name).toBe('my-app'); + expect(packageJson.scripts.check).toBe(getCheckScript(template, hasTypeScript)); + await expectStagedSetup(projectDirectory, configExtension, packageJson.scripts); + + const tsconfig = access(path.join(projectDirectory, 'tsconfig.json')); + if (hasTypeScript) { + await expect(tsconfig).resolves.toBeUndefined(); + } else { + await expect(tsconfig).rejects.toThrow(); + } +}; afterEach(async () => { await Promise.all( @@ -16,137 +139,81 @@ afterEach(async () => { ); }); -const createProject = async (template: string) => { +const createProject = async ( + template: string, + { + args = [], + initializeGitIn, + }: { + args?: string[]; + initializeGitIn?: 'parent' | 'project'; + } = {}, +) => { const tempDirectory = await mkdtemp(path.join(tmpdir(), 'create-rstack-')); const projectDirectory = path.join(tempDirectory, 'my-app'); tempDirectories.push(tempDirectory); - await execFileAsync(process.execPath, [binPath, projectDirectory, '--template', template], { - cwd: tempDirectory, - env: { - ...process.env, - npm_config_user_agent: 'pnpm/11.20.0', + if (initializeGitIn) { + const gitDirectory = initializeGitIn === 'project' ? projectDirectory : tempDirectory; + await mkdir(gitDirectory, { recursive: true }); + await execFileAsync('git', ['init', '--quiet'], { cwd: gitDirectory }); + } + + await execFileAsync( + process.execPath, + [binPath, projectDirectory, '--template', template, ...args], + { + cwd: tempDirectory, + env: { + ...process.env, + npm_config_user_agent: 'pnpm/11.20.0', + }, }, - }); + ); return projectDirectory; }; test.each([ { - template: 'app-vanilla-js', - configExtension: 'js', - sourceExtension: 'js', - hasTypeScript: false, - }, - { - template: 'app-vanilla-ts', - configExtension: 'ts', - sourceExtension: 'ts', - hasTypeScript: true, + scenario: 'Git initialization is disabled', + options: { args: ['--no-git'], initializeGitIn: 'project' as const }, }, { - template: 'app-react-js', - configExtension: 'js', - sourceExtension: 'jsx', - hasTypeScript: false, + scenario: 'the project is inside an existing Git repository', + options: { initializeGitIn: 'parent' as const }, }, - { - template: 'app-react-ts', - configExtension: 'ts', - sourceExtension: 'tsx', - hasTypeScript: true, - }, - { - template: 'app-vue-js', - configExtension: 'js', - sourceExtension: 'js', - hasTypeScript: false, - }, - { - template: 'app-vue-ts', - configExtension: 'ts', - sourceExtension: 'ts', - hasTypeScript: true, - }, -])( - 'creates the $template template', - async ({ template, configExtension, sourceExtension, hasTypeScript }) => { - const projectDirectory = await createProject(template); - const packageJson = JSON.parse( - await readFile(path.join(projectDirectory, 'package.json'), 'utf8'), - ); - - expect(packageJson.name).toBe('my-app'); - - await expect(access(path.join(projectDirectory, 'README.md'))).resolves.toBeUndefined(); - await expect(access(path.join(projectDirectory, '.gitignore'))).resolves.toBeUndefined(); - await expect( - access(path.join(projectDirectory, `rstack.config.${configExtension}`)), - ).resolves.toBeUndefined(); - await expect( - access(path.join(projectDirectory, `src/index.${sourceExtension}`)), - ).resolves.toBeUndefined(); - - const tsconfigPath = path.join(projectDirectory, 'tsconfig.json'); - if (hasTypeScript) { - await expect(access(tsconfigPath)).resolves.toBeUndefined(); - } else { - await expect(access(tsconfigPath)).rejects.toThrow(); - } - }, -); +])('omits staged setup when $scenario', async ({ options }) => { + const projectDirectory = await createProject('app-vanilla-ts', options); + const packageJson = await readProjectPackage(projectDirectory); -test.each([ - { - template: 'lib-node-js', - configExtension: 'js', - sourceExtension: 'js', - hasTypeScript: false, - }, - { - template: 'lib-node-ts', - configExtension: 'ts', - sourceExtension: 'ts', - hasTypeScript: true, - }, - { - template: 'lib-react-js', - configExtension: 'js', - sourceExtension: 'jsx', - hasTypeScript: false, - }, - { - template: 'lib-react-ts', - configExtension: 'ts', - sourceExtension: 'tsx', - hasTypeScript: true, - }, -])( + await expectNoStagedSetup(projectDirectory, 'ts', packageJson.scripts); +}); + +test.each(sourceTemplates)( 'creates the $template template', - async ({ template, configExtension, sourceExtension, hasTypeScript }) => { + async ({ template, sourceExtension, testFile }) => { + const hasTypeScript = template.endsWith('-ts'); + const configExtension = hasTypeScript ? 'ts' : 'js'; const projectDirectory = await createProject(template); - const packageJson = JSON.parse( - await readFile(path.join(projectDirectory, 'package.json'), 'utf8'), - ); - - expect(packageJson.name).toBe('my-app'); - - await expect( - access(path.join(projectDirectory, `rstack.config.${configExtension}`)), - ).resolves.toBeUndefined(); - await expect( - access(path.join(projectDirectory, `src/index.${sourceExtension}`)), - ).resolves.toBeUndefined(); - await expect( - access(path.join(projectDirectory, `tests/index.test.${sourceExtension}`)), - ).resolves.toBeUndefined(); - - const tsconfigPath = path.join(projectDirectory, 'tsconfig.json'); - if (hasTypeScript) { - await expect(access(tsconfigPath)).resolves.toBeUndefined(); - } else { - await expect(access(tsconfigPath)).rejects.toThrow(); + const files = [ + `rstack.config.${configExtension}`, + `src/index.${sourceExtension}`, + `tests/${testFile}`, + ]; + + if (template.startsWith('app-')) { + files.push('README.md', '.gitignore'); } + + await expectProjectSetup(projectDirectory, template, configExtension, hasTypeScript); + await expectFiles(projectDirectory, files); }, ); + +test.each(docTemplates)('creates the $template template', async ({ template, files }) => { + const projectDirectory = await createProject(template); + + await expectProjectSetup(projectDirectory, template, 'ts', true); + await expectFiles(projectDirectory, files); +}); diff --git a/packages/rstack/THIRD_PARTY_NOTICES.md b/packages/rstack/THIRD_PARTY_NOTICES.md index 7348005e..dbd76f6b 100644 --- a/packages/rstack/THIRD_PARTY_NOTICES.md +++ b/packages/rstack/THIRD_PARTY_NOTICES.md @@ -87,34 +87,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -## ignore - -This package includes bundled code from [ignore](https://github.com/kaelzhang/node-ignore). - -License: MIT - -Copyright (c) 2013 Kael Zhang , contributors -http://kael.me/ - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ## import-meta-resolve This package includes bundled code from @@ -300,6 +272,33 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +## NAPI-RS + +The native binding and its generated loader include software from +[NAPI-RS](https://github.com/napi-rs/napi-rs). + +License: MIT + +Copyright (c) 2020 LongYinan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + ## picomatch This package includes bundled code from [picomatch](https://github.com/micromatch/picomatch). diff --git a/packages/rstack/binding.cjs b/packages/rstack/binding.cjs new file mode 100644 index 00000000..93a9a722 --- /dev/null +++ b/packages/rstack/binding.cjs @@ -0,0 +1,704 @@ +// prettier-ignore +/* eslint-disable */ +// @ts-nocheck +/* auto-generated by NAPI-RS */ + +const { readFileSync } = require('fs') +let nativeBinding = null +const loadErrors = [] + +const isMusl = () => { + let musl = false + if (process.platform === 'linux') { + musl = isMuslFromFilesystem() + if (musl === null) { + musl = isMuslFromReport() + } + if (musl === null) { + musl = isMuslFromChildProcess() + } + } + return musl +} + +const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-') + +const isMuslFromFilesystem = () => { + try { + return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl') + } catch { + return null + } +} + +const isMuslFromReport = () => { + let report = null + if (process.report && typeof process.report.getReport === 'function') { + process.report.excludeNetwork = true + report = process.report.getReport() + } + if (!report) { + return null + } + if (report.header && report.header.glibcVersionRuntime) { + return false + } + if (Array.isArray(report.sharedObjects)) { + if (report.sharedObjects.some(isFileMusl)) { + return true + } + } + return false +} + +const isMuslFromChildProcess = () => { + try { + return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl') + } catch (e) { + // If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false + return false + } +} + +function requireNative() { + if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) { + try { + return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH); + } catch (err) { + loadErrors.push(err) + } + } else if (process.platform === 'android') { + if (process.arch === 'arm64') { + try { + return require('./rstack.android-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@rstackjs/cli-android-arm64') + const bindingPackageVersion = require('@rstackjs/cli-android-arm64/package.json').version + if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm') { + try { + return require('./rstack.android-arm-eabi.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@rstackjs/cli-android-arm-eabi') + const bindingPackageVersion = require('@rstackjs/cli-android-arm-eabi/package.json').version + if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`)) + } + } else if (process.platform === 'win32') { + if (process.arch === 'x64') { + if ((process.config && process.config.variables && process.config.variables.shlib_suffix === 'dll.a') || (process.config && process.config.variables && process.config.variables.node_target_type === 'shared_library')) { + try { + return require('./rstack.win32-x64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@rstackjs/cli-win32-x64-gnu') + const bindingPackageVersion = require('@rstackjs/cli-win32-x64-gnu/package.json').version + if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./rstack.win32-x64-msvc.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@rstackjs/cli-win32-x64-msvc') + const bindingPackageVersion = require('@rstackjs/cli-win32-x64-msvc/package.json').version + if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'ia32') { + try { + return require('./rstack.win32-ia32-msvc.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@rstackjs/cli-win32-ia32-msvc') + const bindingPackageVersion = require('@rstackjs/cli-win32-ia32-msvc/package.json').version + if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm64') { + try { + return require('./rstack.win32-arm64-msvc.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@rstackjs/cli-win32-arm64-msvc') + const bindingPackageVersion = require('@rstackjs/cli-win32-arm64-msvc/package.json').version + if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`)) + } + } else if (process.platform === 'darwin') { + try { + return require('./rstack.darwin-universal.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@rstackjs/cli-darwin-universal') + const bindingPackageVersion = require('@rstackjs/cli-darwin-universal/package.json').version + if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + if (process.arch === 'x64') { + try { + return require('./rstack.darwin-x64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@rstackjs/cli-darwin-x64') + const bindingPackageVersion = require('@rstackjs/cli-darwin-x64/package.json').version + if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm64') { + try { + return require('./rstack.darwin-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@rstackjs/cli-darwin-arm64') + const bindingPackageVersion = require('@rstackjs/cli-darwin-arm64/package.json').version + if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`)) + } + } else if (process.platform === 'freebsd') { + if (process.arch === 'x64') { + try { + return require('./rstack.freebsd-x64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@rstackjs/cli-freebsd-x64') + const bindingPackageVersion = require('@rstackjs/cli-freebsd-x64/package.json').version + if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm64') { + try { + return require('./rstack.freebsd-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@rstackjs/cli-freebsd-arm64') + const bindingPackageVersion = require('@rstackjs/cli-freebsd-arm64/package.json').version + if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`)) + } + } else if (process.platform === 'linux') { + if (process.arch === 'x64') { + if (isMusl()) { + try { + return require('./rstack.linux-x64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@rstackjs/cli-linux-x64-musl') + const bindingPackageVersion = require('@rstackjs/cli-linux-x64-musl/package.json').version + if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./rstack.linux-x64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@rstackjs/cli-linux-x64-gnu') + const bindingPackageVersion = require('@rstackjs/cli-linux-x64-gnu/package.json').version + if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'arm64') { + if (isMusl()) { + try { + return require('./rstack.linux-arm64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@rstackjs/cli-linux-arm64-musl') + const bindingPackageVersion = require('@rstackjs/cli-linux-arm64-musl/package.json').version + if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./rstack.linux-arm64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@rstackjs/cli-linux-arm64-gnu') + const bindingPackageVersion = require('@rstackjs/cli-linux-arm64-gnu/package.json').version + if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'arm') { + if (isMusl()) { + try { + return require('./rstack.linux-arm-musleabihf.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@rstackjs/cli-linux-arm-musleabihf') + const bindingPackageVersion = require('@rstackjs/cli-linux-arm-musleabihf/package.json').version + if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./rstack.linux-arm-gnueabihf.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@rstackjs/cli-linux-arm-gnueabihf') + const bindingPackageVersion = require('@rstackjs/cli-linux-arm-gnueabihf/package.json').version + if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'loong64') { + if (isMusl()) { + try { + return require('./rstack.linux-loong64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@rstackjs/cli-linux-loong64-musl') + const bindingPackageVersion = require('@rstackjs/cli-linux-loong64-musl/package.json').version + if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./rstack.linux-loong64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@rstackjs/cli-linux-loong64-gnu') + const bindingPackageVersion = require('@rstackjs/cli-linux-loong64-gnu/package.json').version + if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'riscv64') { + if (isMusl()) { + try { + return require('./rstack.linux-riscv64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@rstackjs/cli-linux-riscv64-musl') + const bindingPackageVersion = require('@rstackjs/cli-linux-riscv64-musl/package.json').version + if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./rstack.linux-riscv64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@rstackjs/cli-linux-riscv64-gnu') + const bindingPackageVersion = require('@rstackjs/cli-linux-riscv64-gnu/package.json').version + if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'ppc64') { + try { + return require('./rstack.linux-ppc64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@rstackjs/cli-linux-ppc64-gnu') + const bindingPackageVersion = require('@rstackjs/cli-linux-ppc64-gnu/package.json').version + if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 's390x') { + try { + return require('./rstack.linux-s390x-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@rstackjs/cli-linux-s390x-gnu') + const bindingPackageVersion = require('@rstackjs/cli-linux-s390x-gnu/package.json').version + if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`)) + } + } else if (process.platform === 'openharmony') { + if (process.arch === 'arm64') { + try { + return require('./rstack.openharmony-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@rstackjs/cli-openharmony-arm64') + const bindingPackageVersion = require('@rstackjs/cli-openharmony-arm64/package.json').version + if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'x64') { + try { + return require('./rstack.openharmony-x64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@rstackjs/cli-openharmony-x64') + const bindingPackageVersion = require('@rstackjs/cli-openharmony-x64/package.json').version + if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm') { + try { + return require('./rstack.openharmony-arm.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@rstackjs/cli-openharmony-arm') + const bindingPackageVersion = require('@rstackjs/cli-openharmony-arm/package.json').version + if (bindingPackageVersion !== '0.5.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on OpenHarmony: ${process.arch}`)) + } + } else { + loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`)) + } +} + +function createLoadErrorChain(errors) { + return errors.reduce((previous, current) => { + let message + try { + message = + current && typeof current.message === 'string' + ? current.message + : String(current) + } catch { + message = 'Unknown error' + } + const error = new Error(message) + error.cause = previous + return error + }, null) +} + +// NAPI_RS_FORCE_WASI is a tri-state flag: +// unset / any other value → native binding preferred, WASI is only a fallback +// 'true' → prefer WASI, but retain native as a lazy fallback +// 'error' → require WASI without initializing a native fallback +// Treating any non-empty string as truthy (the historical behavior) meant +// NAPI_RS_FORCE_WASI=false, NAPI_RS_FORCE_WASI=0, etc. inadvertently triggered +// the WASI path, causing ENOENT for packages shipped without a .wasi.cjs file. +// +// NAPI_RS_WASI_FLAVOR selects one exact generated flavor and implies strict +// WASI loading. It never crosses into another flavor or falls back to native. +const __napiWasiFlavors = ["wasm32-wasi"] +const __napiWasiFlavor = process.env.NAPI_RS_WASI_FLAVOR +const __napiWasiFlavorRequested = + typeof __napiWasiFlavor === 'string' && __napiWasiFlavor.length > 0 +if ( + __napiWasiFlavorRequested && + __napiWasiFlavors.indexOf(__napiWasiFlavor) === -1 +) { + throw new Error( + 'Unsupported WASI flavor "' + + __napiWasiFlavor + + '". Available flavors: ' + + __napiWasiFlavors.join(', '), + ) +} +const forceWasiError = process.env.NAPI_RS_FORCE_WASI === 'error' +const forceWasi = + process.env.NAPI_RS_FORCE_WASI === 'true' || + forceWasiError || + __napiWasiFlavorRequested + +if (!forceWasi) { + nativeBinding = requireNative() +} + +if (!nativeBinding || forceWasi) { + let wasiBinding = null + let wasiBindingLoaded = false + const wasiBindingErrors = [] + const __napiWasiResolveCandidate = (specifier, isPackage, localArtifacts) => { + try { + require.resolve(specifier) + } catch (resolveError) { + if (!resolveError || resolveError.code !== 'MODULE_NOT_FOUND') { + throw resolveError + } + if (isPackage) { + try { + require.resolve(specifier + '/package.json') + } catch (packageError) { + if (packageError && packageError.code === 'MODULE_NOT_FOUND') { + return resolveError + } + // An exports restriction proves the package exists even when its + // package.json is not public. Preserve the root resolution failure. + throw resolveError + } + // The package exists but its main/export target is broken. + throw resolveError + } + return resolveError + } + if (localArtifacts) { + let artifactError = null + for (let i = 0; i < localArtifacts.length; i++) { + try { + require.resolve(localArtifacts[i]) + return null + } catch (resolveError) { + if (!resolveError || resolveError.code !== 'MODULE_NOT_FOUND') { + throw resolveError + } + artifactError = resolveError + } + } + return artifactError + } + return null + } + if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === "wasm32-wasi")) { + let candidateError = null + let candidateFailed = false + try { + candidateError = __napiWasiResolveCandidate('./rstack.wasi.cjs', false, ["./rstack.wasm32-wasi.debug.wasm","./rstack.wasm32-wasi.wasm"]) + candidateFailed = candidateError !== null + if (!candidateFailed) { + wasiBinding = require('./rstack.wasi.cjs') + nativeBinding = wasiBinding + wasiBindingLoaded = true + } + } catch (err) { + candidateError = err + candidateFailed = true + } + if (candidateFailed) { + wasiBindingErrors.push(candidateError) + loadErrors.push(candidateError) + } + } + if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === "wasm32-wasi")) { + let candidateError = null + let candidateFailed = false + try { + candidateError = __napiWasiResolveCandidate('@rstackjs/cli-wasm32-wasi', true, undefined) + candidateFailed = candidateError !== null + if (!candidateFailed) { + if (process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + const bindingPackageVersion = require('@rstackjs/cli-wasm32-wasi/package.json').version + if (bindingPackageVersion !== '0.5.0') { + throw new Error(`WASI binding package version mismatch, expected 0.5.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + } + wasiBinding = require('@rstackjs/cli-wasm32-wasi') + nativeBinding = wasiBinding + wasiBindingLoaded = true + } + } catch (err) { + candidateError = err + candidateFailed = true + } + if (candidateFailed) { + wasiBindingErrors.push(candidateError) + loadErrors.push(candidateError) + } + } + if ( + !wasiBindingLoaded && + forceWasi && + !forceWasiError && + !__napiWasiFlavorRequested + ) { + nativeBinding = requireNative() + } + if ((forceWasiError || __napiWasiFlavorRequested) && !wasiBindingLoaded) { + const error = new Error( + __napiWasiFlavorRequested + ? 'WASI binding for flavor "' + __napiWasiFlavor + '" not found' + : 'WASI binding not found and NAPI_RS_FORCE_WASI is set to error', + ) + error.cause = createLoadErrorChain(wasiBindingErrors) + throw error + } +} + +if (!nativeBinding) { + if (loadErrors.length > 0) { + const error = new Error( + `Cannot find native binding. ` + + `npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). ` + + 'Please try `npm i` again after removing both package-lock.json and node_modules directory.', + ) + // assign instead of the `new Error(message, { cause })` options form, + // which Node < 16.9 silently ignores + error.cause = createLoadErrorChain(loadErrors) + throw error + } + throw new Error(`Failed to load native binding`) +} + +module.exports = nativeBinding +module.exports.GitIgnoreMatcher = nativeBinding.GitIgnoreMatcher +module.exports.IgnoreMatcher = nativeBinding.IgnoreMatcher diff --git a/packages/rstack/binding.d.cts b/packages/rstack/binding.d.cts new file mode 100644 index 00000000..fdbfacfc --- /dev/null +++ b/packages/rstack/binding.d.cts @@ -0,0 +1,33 @@ +/* auto-generated by NAPI-RS */ +/* eslint-disable */ +/** JavaScript-facing hierarchy for repository `.gitignore` files. */ +export declare class GitIgnoreMatcher { + /** Creates an empty matcher whose sources can be added during directory traversal. */ + constructor() + /** Compiles or replaces rules rooted at a repository-relative POSIX directory. */ + addSource(relativeRoot: string, patterns: string): boolean + /** Returns whether one repository-relative POSIX path is ignored. */ + isIgnored(relativePath: string, isDirectory: boolean): boolean + /** Matches one directory's entries in a native call and returns one byte per name. */ + isIgnoredBatch(relativeParent: string, names: Array, directoryFlags: Uint8Array): Uint8Array + /** Matches up to 32 entries while avoiding per-directory typed-array allocation. */ + isIgnoredBatchMask(relativeParent: string, names: Array, directoryMask: number): number + /** Matches a single directory entry without constructing a JavaScript array. */ + isIgnoredChild(relativeParent: string, name: string, isDirectory: boolean): boolean +} + +/** JavaScript-facing wrapper around the compiled Rust matcher. */ +export declare class IgnoreMatcher { + /** Compiles all pattern sources once and keeps the result for repeated path checks. */ + constructor(sources: Array) + /** Returns whether a file or directory is ignored by any source. */ + isIgnored(filePath: string, isDirectory: boolean): boolean +} + +/** A Gitignore-compatible pattern source received from JavaScript. */ +export interface IgnoreSource { + /** Directory that patterns are resolved from. */ + rootPath: string + /** Newline-delimited Gitignore patterns. */ + patterns: string +} diff --git a/packages/rstack/napi.json b/packages/rstack/napi.json new file mode 100644 index 00000000..0041345c --- /dev/null +++ b/packages/rstack/napi.json @@ -0,0 +1,19 @@ +{ + "binaryName": "rstack", + "packageName": "@rstackjs/cli", + "targets": [ + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", + "x86_64-pc-windows-msvc", + "aarch64-apple-darwin", + "powerpc64le-unknown-linux-gnu", + "s390x-unknown-linux-gnu", + "aarch64-pc-windows-msvc", + "x86_64-apple-darwin", + "x86_64-unknown-linux-musl", + "riscv64gc-unknown-linux-gnu", + "aarch64-unknown-linux-musl", + "riscv64gc-unknown-linux-musl", + "i686-pc-windows-msvc" + ] +} diff --git a/packages/rstack/npm/pnpm-workspace.yaml b/packages/rstack/npm/pnpm-workspace.yaml new file mode 100644 index 00000000..e34eedde --- /dev/null +++ b/packages/rstack/npm/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +# Isolate generated native packages from the root workspace while enabling recursive publish. +packages: + - '*' diff --git a/packages/rstack/package.json b/packages/rstack/package.json index c2ca2c9a..72a6f027 100644 --- a/packages/rstack/package.json +++ b/packages/rstack/package.json @@ -1,6 +1,6 @@ { "name": "rstack", - "version": "0.4.0", + "version": "0.5.0", "description": "One CLI for JavaScript development, powered by Rstack.", "homepage": "https://rstack.rs", "bugs": { @@ -56,13 +56,20 @@ }, "files": [ "bin", + "binding.cjs", + "binding.d.cts", "dist", + "docs", "types", "THIRD_PARTY_NOTICES.md" ], "scripts": { "build": "rslib", + "build:native": "napi build --config-path napi.json --platform --manifest-path ../../Cargo.toml --package rstack-binding --package-json-path package.json --output-dir . --js binding.cjs --dts binding.d.cts", + "build:native:ci": "napi build --config-path napi.json --platform --profile ci --manifest-path ../../Cargo.toml --package rstack-binding --package-json-path package.json --output-dir . --js binding.cjs --dts binding.d.cts", + "build:native:release": "napi build --config-path napi.json --platform --release --manifest-path ../../Cargo.toml --package rstack-binding --package-json-path package.json --output-dir . --js binding.cjs --dts binding.d.cts -- --config ../../.cargo/release.toml", "dev": "rslib -w", + "package:native": "napi create-npm-dirs --config-path napi.json", "test": "rs test" }, "dependencies": { @@ -75,6 +82,7 @@ "yuku-parser": "catalog:" }, "devDependencies": { + "@napi-rs/cli": "catalog:", "@rspress/core": "catalog:", "@rstackjs/load-config": "catalog:", "@rstackjs/test-utils": "catalog:", @@ -83,7 +91,6 @@ "@types/micromatch": "catalog:", "@types/node": "catalog:", "fast-json-stable-stringify": "catalog:", - "ignore": "catalog:", "import-meta-resolve": "catalog:", "is-binary-path": "catalog:", "lint-staged": "catalog:", diff --git a/packages/rstack/rslib.config.ts b/packages/rstack/rslib.config.ts index bf5cf0ec..80c4fe54 100644 --- a/packages/rstack/rslib.config.ts +++ b/packages/rstack/rslib.config.ts @@ -20,6 +20,7 @@ export default defineConfig({ lib: './src/lib.ts', lint: './src/lint.ts', test: './src/test.ts', + 'native/index': './src/native/index.ts', fmtWorker: './src/fmt/worker.ts', }, define: { diff --git a/packages/rstack/src/cli/commands.ts b/packages/rstack/src/cli/commands.ts index cbf2eae0..3b19d6cd 100644 --- a/packages/rstack/src/cli/commands.ts +++ b/packages/rstack/src/cli/commands.ts @@ -1,38 +1,444 @@ import { join } from 'node:path'; -import { color } from 'rslog'; import { getConfigState } from '../config.ts'; -import { insertConfigArg, parseCliArgs } from './args.ts'; +import { insertConfigArg, parseArgs, parseCliArgs } from './args.ts'; +import { hasHelpFlag, renderHelp } from './help.ts'; -declare global { - const RSTACK_VERSION: string; -} +const renderRootHelp = (): string => + renderHelp({ + usage: 'rs [command] [options]', + sections: [ + { + title: 'Commands', + items: [ + ['dev', 'Run the app dev server'], + ['build', 'Build the app for production'], + ['preview', 'Preview the app production build'], + ['lib', 'Build library'], + ['doc', 'Serve or build docs'], + ['fmt, format', 'Format code'], + ['lint', 'Lint code'], + ['check', 'Run static checks, including lint and format'], + ['test', 'Run tests'], + ['staged', 'Run tasks on staged Git files'], + ['setup', 'Install Git hooks'], + ], + }, + { + content: `For command-specific options, run: + $ rs -h`, + dim: true, + }, + { + title: 'Options', + items: [ + ['-c, --config ', 'Specify Rstack config file path'], + ['-h, --help', 'Display this help message'], + ['-v, --version', 'Display version number'], + ], + }, + ], + }); + +const renderCheckHelp = (): string => + renderHelp({ + usage: 'rs check [options]', + description: 'Run static checks, including lint and format', + sections: [ + { + title: 'Options', + items: [ + ['--type-check', 'Enable TypeScript type checking'], + ['-c, --config ', 'Specify Rstack config file path'], + ['-h, --help', 'Display this help message'], + ], + }, + ], + }); + +const renderDevHelp = (): string => + renderHelp({ + usage: 'rs dev [options]', + description: 'Run the app dev server', + sections: [ + { + title: 'Options', + items: [ + ['-o, --open [url]', 'Open the page in browser on startup'], + ['--port ', 'Set the port number for the server'], + ['--strict-port', 'Exit if the specified port is already in use'], + ['--host [host]', 'Set the host that the server listens to'], + ['-c, --config ', 'Specify Rstack config file path'], + ['-h, --help', 'Display this help message'], + ], + }, + ], + }); + +const renderBuildHelp = (): string => + renderHelp({ + usage: 'rs build [options]', + description: 'Build the app for production', + sections: [ + { + title: 'Options', + items: [ + ['-w, --watch', 'Enable watch mode to automatically rebuild on file changes'], + ['--dist-path ', 'Set the root directory of output files'], + ['--source-map', 'Enable source map'], + ['-c, --config ', 'Specify Rstack config file path'], + ['-h, --help', 'Display this help message'], + ], + }, + ], + }); + +const renderPreviewHelp = (): string => + renderHelp({ + usage: 'rs preview [options]', + description: 'Preview the app production build', + sections: [ + { + title: 'Options', + items: [ + ['-o, --open [url]', 'Open the page in browser on startup'], + ['--port ', 'Set the port number for the server'], + ['--strict-port', 'Exit if the specified port is already in use'], + ['--host [host]', 'Set the host that the server listens to'], + ['-c, --config ', 'Specify Rstack config file path'], + ['-h, --help', 'Display this help message'], + ], + }, + ], + }); + +const renderDocHelp = (): string => + renderHelp({ + usage: 'rs doc [command] [root] [options]', + sections: [ + { + title: 'Commands', + items: [ + ['[root]', 'Run the docs dev server (default)'], + ['build [root]', 'Build docs for production'], + ['preview [root]', 'Preview the docs production build'], + ['eject [component]', 'Eject a theme component'], + ], + }, + { + content: `For command-specific options, run: + $ rs doc -h`, + dim: true, + }, + { + title: 'Options', + items: [ + ['--port ', 'Set the port number for the server'], + ['--host [host]', 'Set the host that the server listens to'], + ['--base ', 'Set the base path and override config.base'], + ['-c, --config ', 'Specify Rstack config file path'], + ['-h, --help', 'Display this help message'], + ], + }, + ], + }); + +const renderDocBuildHelp = (): string => + renderHelp({ + usage: 'rs doc build [root] [options]', + description: 'Build docs for production', + sections: [ + { + title: 'Options', + items: [ + ['--base ', 'Set the base path and override config.base'], + ['-c, --config ', 'Specify Rstack config file path'], + ['-h, --help', 'Display this help message'], + ], + }, + ], + }); + +const renderDocPreviewHelp = (): string => + renderHelp({ + usage: 'rs doc preview [root] [options]', + description: 'Preview the docs production build', + sections: [ + { + title: 'Options', + items: [ + ['--port ', 'Set the port number for the server'], + ['--host [host]', 'Set the host that the server listens to'], + ['--base ', 'Set the base path and override config.base'], + ['-c, --config ', 'Specify Rstack config file path'], + ['-h, --help', 'Display this help message'], + ], + }, + ], + }); + +const renderDocEjectHelp = (): string => + renderHelp({ + usage: 'rs doc eject [component] [options]', + description: 'Eject a theme component', + sections: [ + { + title: 'Options', + items: [['-h, --help', 'Display this help message']], + }, + ], + }); + +const renderTestHelp = (): string => + renderHelp({ + usage: 'rs test [command] [...filters] [options]', + sections: [ + { + title: 'Commands', + items: [ + ['[...filters]', 'Run tests (default)'], + ['run [...filters]', 'Run tests once'], + ['watch [...filters]', 'Run tests in watch mode'], + ['list [...filters]', 'List matching tests'], + ['merge-reports [path]', 'Merge blob reports'], + ['init [project]', 'Initialize Rstest configuration'], + ], + }, + { + content: `For command-specific options, run: + $ rs test -h`, + dim: true, + }, + { + title: 'Options', + items: [ + ['-w, --watch', 'Enable watch mode'], + ['-u, --update', 'Update snapshot files'], + ['--coverage', 'Enable code coverage'], + ['--project ', 'Filter test projects by name'], + ['-t, --test-name-pattern ', 'Run tests with names matching the pattern'], + ['-c, --config ', 'Specify Rstack config file path'], + ['-h, --help', 'Display this help message'], + ], + }, + ], + }); + +const renderTestRunHelp = (): string => + renderHelp({ + usage: 'rs test run [...filters] [options]', + description: 'Run tests once', + sections: [ + { + title: 'Options', + items: [ + ['--related', 'Run tests related to source files'], + ['--changed [commit]', 'Run tests related to changed files'], + ['--shard ', 'Split tests into shards'], + ['-u, --update', 'Update snapshot files'], + ['--coverage', 'Enable code coverage'], + ['--project ', 'Filter test projects by name'], + ['-t, --test-name-pattern ', 'Run tests with names matching the pattern'], + ['-c, --config ', 'Specify Rstack config file path'], + ['-h, --help', 'Display this help message'], + ], + }, + ], + }); -const helpMessage = `Rstack v${RSTACK_VERSION} +const renderTestWatchHelp = (): string => + renderHelp({ + usage: 'rs test watch [...filters] [options]', + description: 'Run tests in watch mode', + sections: [ + { + title: 'Options', + items: [ + ['-u, --update', 'Update snapshot files'], + ['--coverage', 'Enable code coverage'], + ['--project ', 'Filter test projects by name'], + ['-t, --test-name-pattern ', 'Run tests with names matching the pattern'], + ['-c, --config ', 'Specify Rstack config file path'], + ['-h, --help', 'Display this help message'], + ], + }, + ], + }); -${color.cyan('Usage')}: -${color.yellow(' $ rs [command] [...options]')} +const renderTestListHelp = (): string => + renderHelp({ + usage: 'rs test list [...filters] [options]', + description: 'List matching tests', + sections: [ + { + title: 'Options', + items: [ + ['--related', 'List tests related to source files'], + ['--changed [commit]', 'List tests related to changed files'], + ['--files-only', 'List matching test files only'], + ['--json [path]', 'Print JSON or write it to a file'], + ['--include-suites', 'Include test suites'], + ['--print-location', 'Print test locations'], + ['--summary', 'Print a summary'], + ['--project ', 'Filter test projects by name'], + ['-t, --test-name-pattern ', 'List tests with names matching the pattern'], + ['-c, --config ', 'Specify Rstack config file path'], + ['-h, --help', 'Display this help message'], + ], + }, + ], + }); -${color.cyan('Commands')}: - dev Run the app dev server - build Build the app for production - preview Preview the app production build - lib Build library - doc Serve or build docs - fmt, format Format code - lint Lint code - test Run tests - staged Run tasks on staged Git files - setup Install Git hooks +const renderTestMergeReportsHelp = (): string => + renderHelp({ + usage: 'rs test merge-reports [path] [options]', + description: 'Merge blob reports', + sections: [ + { + title: 'Options', + items: [ + ['--coverage', 'Generate coverage reports'], + ['--reporters, --reporter ', 'Specify test reporters'], + ['--cleanup', 'Remove blob reports after merging'], + ['-c, --config ', 'Specify Rstack config file path'], + ['-h, --help', 'Display this help message'], + ], + }, + ], + }); -${color.dim(`For command-specific options, run: - $ rs -h`)} +const renderTestInitHelp = (): string => + renderHelp({ + usage: 'rs test init [project] [options]', + description: 'Initialize Rstest configuration', + sections: [ + { + title: 'Options', + items: [ + ['--yes', 'Use default options without prompts'], + ['-h, --help', 'Display this help message'], + ], + }, + ], + }); -${color.cyan('Options')}: - -c, --config Specify Rstack config file path - -h, --help Display this help message - -v, --version Display version number`; +const renderLibHelp = (): string => + renderHelp({ + usage: 'rs lib [command] [options]', + sections: [ + { + title: 'Commands', + items: [ + ['build', 'Build the library for production (default)'], + ['inspect', 'Inspect Rslib, Rsbuild, and Rspack configs'], + ['mf-dev', 'Start Rsbuild dev server for Module Federation'], + ], + }, + { + content: `For command-specific options, run: + $ rs lib -h`, + dim: true, + }, + { + title: 'Options', + items: [ + ['-w, --watch', 'Enable watch mode and rebuild on changes'], + ['--dts', 'Emit declaration files (use --no-dts to disable)'], + ['-c, --config ', 'Specify Rstack config file path'], + ['-h, --help', 'Display this help message'], + ], + }, + ], + }); + +const renderLibBuildHelp = (): string => + renderHelp({ + usage: 'rs lib build [options]', + description: 'Build the library for production', + sections: [ + { + title: 'Options', + items: [ + ['-w, --watch', 'Enable watch mode and rebuild on changes'], + ['--dts', 'Emit declaration files (use --no-dts to disable)'], + ['-c, --config ', 'Specify Rstack config file path'], + ['-h, --help', 'Display this help message'], + ], + }, + ], + }); + +const renderLibInspectHelp = (): string => + renderHelp({ + usage: 'rs lib inspect [options]', + description: 'Inspect Rslib, Rsbuild, and Rspack configs', + sections: [ + { + title: 'Options', + items: [ + ['--output ', 'Set the output path for inspection results (default: .rsbuild)'], + ['--verbose', 'Show complete function definitions in output'], + ['-c, --config ', 'Specify Rstack config file path'], + ['-h, --help', 'Display this help message'], + ], + }, + ], + }); + +const renderLibMfDevHelp = (): string => + renderHelp({ + usage: 'rs lib mf-dev [options]', + description: 'Start Rsbuild dev server for Module Federation', + sections: [ + { + title: 'Options', + items: [ + ['-c, --config ', 'Specify Rstack config file path'], + ['-h, --help', 'Display this help message'], + ], + }, + ], + }); + +const renderLintHelp = (): string => + renderHelp({ + usage: 'rs lint [options] [files...]', + description: 'Lint code', + sections: [ + { + title: 'Options', + items: [ + ['--fix', 'Automatically fix problems'], + ['--type-check', 'Enable TypeScript type checking'], + ['--type-check-only', 'Run only TypeScript type checking'], + ['--format ', 'Set output format (default | jsonline | github | gitlab)'], + ['--quiet', 'Report errors only'], + ['--timing [all|N]', 'Print a per-rule timing table (all rules or top N)'], + ['--max-warnings ', 'Set the maximum number of warnings'], + ['--rule ', 'Override a rule (repeatable)'], + ['--no-color', 'Disable colored output'], + ['--force-color', 'Force colored output'], + ['-c, --config ', 'Specify Rstack config file path'], + ['-h, --help', 'Display this help message'], + ], + }, + ], + }); async function runRsbuildCLI(args: string[]): Promise { + if (hasHelpFlag(args)) { + switch (args[0]) { + case 'dev': + console.log(renderDevHelp()); + return; + case 'build': + console.log(renderBuildHelp()); + return; + case 'preview': + console.log(renderPreviewHelp()); + return; + } + } + const argv = [ process.execPath, 'rsbuild', @@ -44,6 +450,29 @@ async function runRsbuildCLI(args: string[]): Promise { } async function runRstestCLI(args: string[]): Promise { + if (hasHelpFlag(args)) { + switch (args[0]) { + case 'run': + console.log(renderTestRunHelp()); + return; + case 'watch': + console.log(renderTestWatchHelp()); + return; + case 'list': + console.log(renderTestListHelp()); + return; + case 'merge-reports': + console.log(renderTestMergeReportsHelp()); + return; + case 'init': + console.log(renderTestInitHelp()); + return; + default: + console.log(renderTestHelp()); + return; + } + } + const argv = [ process.execPath, 'rstest', @@ -55,6 +484,23 @@ async function runRstestCLI(args: string[]): Promise { } async function runRslibCLI(args: string[]): Promise { + if (hasHelpFlag(args)) { + switch (args[0]) { + case 'build': + console.log(renderLibBuildHelp()); + return; + case 'inspect': + console.log(renderLibInspectHelp()); + return; + case 'mf-dev': + console.log(renderLibMfDevHelp()); + return; + default: + console.log(renderLibHelp()); + return; + } + } + const argv = [ process.execPath, 'rslib', @@ -75,6 +521,23 @@ const isMissingRspressCoreError = (error: unknown): boolean => { }; async function runRspressCLI(args: string[]): Promise { + if (hasHelpFlag(args)) { + switch (args[0]) { + case 'build': + console.log(renderDocBuildHelp()); + return; + case 'preview': + console.log(renderDocPreviewHelp()); + return; + case 'eject': + console.log(renderDocEjectHelp()); + return; + default: + console.log(renderDocHelp()); + return; + } + } + const argv = [ process.execPath, 'rspress', @@ -96,6 +559,11 @@ async function runRspressCLI(args: string[]): Promise { } async function runRslintCLI(args: string[]): Promise { + if (hasHelpFlag(args)) { + console.log(renderLintHelp()); + return; + } + const argv = [ process.execPath, 'rslint', @@ -106,6 +574,34 @@ async function runRslintCLI(args: string[]): Promise { await runCLI({ argv }); } +async function runCheckCLI(args: string[]): Promise { + const { values } = parseArgs({ + args, + options: { + 'type-check': { type: 'boolean' }, + help: { type: 'boolean', short: 'h' }, + }, + allowPositionals: false, + strict: true, + }); + + if (values.help) { + console.log(renderCheckHelp()); + return; + } + + await runRslintCLI(values.typeCheck ? ['--type-check'] : []); + if (process.exitCode) { + return; + } + + const { runFmtCLI } = await import( + /* rspackChunkName: 'fmt' */ + '../fmt/cli.ts' + ); + await runFmtCLI(['--check']); +} + export async function setupCommands(): Promise { const { args, configPath } = parseCliArgs(process.argv.slice(2)); const command = args[0]; @@ -113,7 +609,7 @@ export async function setupCommands(): Promise { getConfigState().configPath = configPath; if (!command || command === '-h' || command === '--help') { - console.log(helpMessage); + console.log(renderRootHelp()); return; } @@ -142,6 +638,11 @@ export async function setupCommands(): Promise { return; } + if (command === 'check') { + await runCheckCLI(args.slice(1)); + return; + } + if (command === 'fmt' || command === 'format') { const { runFmtCLI } = await import( /* rspackChunkName: 'fmt' */ diff --git a/packages/rstack/src/cli/help.ts b/packages/rstack/src/cli/help.ts new file mode 100644 index 00000000..1936ecfa --- /dev/null +++ b/packages/rstack/src/cli/help.ts @@ -0,0 +1,61 @@ +import { color } from 'rslog'; + +declare global { + const RSTACK_VERSION: string; +} + +export type HelpItem = readonly [label: string, description: string]; + +export type HelpSection = + | { + title: string; + items: readonly HelpItem[]; + } + | { + content: string; + dim?: boolean; + }; + +export type HelpDefinition = { + usage: string; + description?: string; + sections?: readonly HelpSection[]; +}; + +export const hasHelpFlag = (args: readonly string[]): boolean => { + const end = args.indexOf('--'); + const flags = end === -1 ? args : args.slice(0, end); + + return flags.some((flag) => flag === '-h' || flag === '--help'); +}; + +const renderItems = (items: readonly HelpItem[]): string => { + const labelWidth = items.reduce((width, [label]) => Math.max(width, label.length), 0); + + return items + .map(([label, description]) => ` ${label.padEnd(labelWidth)} ${description}`) + .join('\n'); +}; + +const renderSection = (section: HelpSection): string => { + if ('items' in section) { + return `${color.cyan(section.title)}:\n${renderItems(section.items)}`; + } + + return section.dim ? color.dim(section.content) : section.content; +}; + +export const renderHelp = ({ usage, description, sections = [] }: HelpDefinition): string => { + const blocks = [ + color.bold(`Rstack v${RSTACK_VERSION}`), + `${color.cyan('Usage')}:\n${color.yellow(` $ ${usage}`)}`, + ]; + + if (description) { + blocks.push(description); + } + + blocks.push(...sections.map(renderSection)); + + return blocks.join('\n\n'); +}; diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index 55af2173..9344a2a0 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -2,6 +2,7 @@ import path from 'node:path'; import { performance } from 'node:perf_hooks'; import { color, logger } from 'rslog'; import { parseArgs } from '../cli/args.ts'; +import { renderHelp } from '../cli/help.ts'; import { loadRstackConfig } from '../config.ts'; import { ensureProjectCacheDir } from '../projectCache.ts'; import { fmtCacheFileName } from './cacheStore.ts'; @@ -26,26 +27,31 @@ interface ParsedFmtCLIArgs { stdinFilepath?: string; } -const fmtHelpMessage: string = `Rstack v${RSTACK_VERSION} - -${color.cyan('Usage')}: -${color.yellow(' $ rs fmt [options] [files/globs...]')} - -Format files with Prettier. - -${color.cyan('Options')}: - -w, --write Write formatted files in place (default) - --check Check whether files are formatted - -l, --list-different Print paths of unformatted files - --ignore-path Path to an additional ignore file (repeatable) - -u, --ignore-unknown Ignore unknown files - --no-cache Disable the formatting cache - --cache-location Path to the formatting cache directory - --no-error-on-unmatched-pattern Do not error when no files match - --with-node-modules Process files inside node_modules - --parallel-workers Number of parallel workers - --stdin-filepath Format stdin as if it were saved at - -h, --help Display this help message`; +const renderFmtHelp = (): string => + renderHelp({ + usage: 'rs fmt [options] [files/globs...]', + description: 'Format code', + sections: [ + { + title: 'Options', + items: [ + ['-w, --write', 'Write formatted files in place (default)'], + ['--check', 'Check whether files are formatted'], + ['-l, --list-different', 'Print paths of unformatted files'], + ['--ignore-path ', 'Path to an additional ignore file (repeatable)'], + ['-u, --ignore-unknown', 'Ignore unknown files'], + ['--no-cache', 'Disable the formatting cache'], + ['--cache-location ', 'Path to the formatting cache directory'], + ['--no-error-on-unmatched-pattern', 'Do not error when no files match'], + ['--with-node-modules', 'Process files inside node_modules'], + ['--parallel-workers ', 'Number of parallel workers'], + ['--stdin-filepath ', 'Format stdin as if it were saved at '], + ['-c, --config ', 'Specify Rstack config file path'], + ['-h, --help', 'Display this help message'], + ], + }, + ], + }); const parseMaxWorkers = (value: string | undefined): number | undefined => { if (value === undefined) { @@ -268,7 +274,7 @@ const runFmtCLI = async (args: string[]): Promise => { withNodeModules, } = parseFmtCLIArgs(args); if (help) { - logger.log(fmtHelpMessage); + logger.log(renderFmtHelp()); return; } @@ -335,7 +341,9 @@ const runFmtCLI = async (args: string[]): Promise => { } } - if (mode === 'check') { + if (mode === 'write') { + logger.start('Formatting...'); + } else if (mode === 'check') { logger.start('Checking formatting...'); } @@ -350,6 +358,8 @@ const runFmtCLI = async (args: string[]): Promise => { if (ignoreUnknown) { if (mode === 'check') { logger.success('No supported files to check.'); + } else if (mode === 'write') { + logger.success('No supported files to format.'); } return; } @@ -366,5 +376,4 @@ const runFmtCLI = async (args: string[]): Promise => { } }; -export { fmtHelpMessage, parseFmtCLIArgs, prettyTime, runFmtCLI }; -export type { ParsedFmtCLIArgs }; +export { runFmtCLI }; diff --git a/packages/rstack/src/fmt/discoverPaths.ts b/packages/rstack/src/fmt/discoverPaths.ts index fba8da05..0a213400 100644 --- a/packages/rstack/src/fmt/discoverPaths.ts +++ b/packages/rstack/src/fmt/discoverPaths.ts @@ -1,9 +1,10 @@ import { lstat, readFile } from 'node:fs/promises'; import path from 'node:path'; -import ignore from 'ignore'; import isBinaryPath from 'is-binary-path'; 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 { createRelativePathResolver, toPosixPath, @@ -20,6 +21,9 @@ const defaultIgnoredDirNames = new Set([ 'node_modules', ]); +const gitIgnored = Symbol('gitIgnored'); +type GitIgnoreDirent = Dirent & { [gitIgnored]?: true }; + interface DiscoverFmtPathsOptions { /** Absolute directory used to resolve input paths. */ cwd: string; @@ -81,20 +85,21 @@ const findGitRoot = async (cwd: string): Promise => { } }; -class GitIgnoreMatcher { +/** Loads repository ignore files while Rust owns their compiled matching state. */ +class GitIgnoreFiles { readonly #rootPath: string; readonly #resolveRelativePath: RelativePathResolver; - readonly #matchers = new Map>(); readonly #loads = new Map>(); - readonly #ignoredDirectories = new Map(); + #matcher: NativeGitIgnoreMatcher | undefined; + #hasRules = false; private constructor(rootPath: string) { this.#rootPath = rootPath; this.#resolveRelativePath = createRelativePathResolver(rootPath); } - static async create(cwd: string): Promise { - const matcher = new GitIgnoreMatcher(await findGitRoot(cwd)); + static async create(cwd: string): Promise { + const matcher = new GitIgnoreFiles(await findGitRoot(cwd)); await matcher.loadThrough(cwd); return matcher; } @@ -124,7 +129,7 @@ class GitIgnoreMatcher { } isIgnored(filePath: string, isDirectory: boolean): boolean { - if (this.#matchers.size === 0) { + if (!this.#hasRules) { return false; } @@ -133,96 +138,81 @@ class GitIgnoreMatcher { return false; } - if (isDirectory) { - return this.#isDirectoryIgnored(filePath, relativePath); - } - - const parentPath = path.dirname(filePath); - return ( - (parentPath !== this.#rootPath && this.#isDirectoryIgnored(parentPath)) || - this.#matches(relativePath, false) - ); + return this.#matcher!.isIgnored(toPosixPath(relativePath), isDirectory); } - #load(directoryPath: string): Promise { - const cached = this.#loads.get(directoryPath); - if (cached) { - return cached; + /** 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) { + return; } - // Ignore files may disappear or become unreadable during traversal. - const loading = readFile(path.join(directoryPath, '.gitignore'), 'utf8') - .then((content) => { - const relativePath = toPosixPath(this.#resolveRelativePath(directoryPath)); - this.#matchers.set(relativePath, ignore().add(content)); - }) - .catch(() => undefined); - - this.#loads.set(directoryPath, loading); - return loading; - } - - #isDirectoryIgnored(directoryPath: string, relativePath?: string): boolean { - const cached = this.#ignoredDirectories.get(directoryPath); - if (cached !== undefined) { - return cached; + const relativeParentPath = this.#resolveRelativePath(parentPath); + if (!isRelativePathInside(relativeParentPath)) { + return; } - relativePath ??= this.#resolveRelativePath(directoryPath); + const relativeParent = toPosixPath(relativeParentPath); - // Git cannot re-include a path below an ignored directory. - const parentPath = path.dirname(directoryPath); - const ignored = - (parentPath !== this.#rootPath && this.#isDirectoryIgnored(parentPath)) || - this.#matches(relativePath, true); - this.#ignoredDirectories.set(directoryPath, ignored); - return ignored; - } + if (dirents.length === 1) { + const dirent = dirents[0]; + return this.#matcher!.isIgnoredChild(relativeParent, dirent.name, dirent.isDirectory()); + } - #matches(relativePath: string, isDirectory: boolean): boolean { - const pathFromRoot = toPosixPath(relativePath); + const names = new Array(dirents.length); - // Most repositories only use a root `.gitignore`. Avoid checking every path - // segment when no nested matcher can override its result. - const rootMatcher = this.#matchers.size === 1 ? this.#matchers.get('') : undefined; - if (rootMatcher) { - // `ignore` expects POSIX separators and uses a trailing slash to distinguish directories. - return rootMatcher.test(isDirectory ? `${pathFromRoot}/` : pathFromRoot).ignored; + 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 segments = pathFromRoot.split('/'); - let directoryPath = ''; - let pathFromMatcher = pathFromRoot; - let ignored = false; + 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()); + } - for (const segment of segments) { - const matcher = this.#matchers.get(directoryPath); - if (matcher) { - const result = matcher.test(isDirectory ? `${pathFromMatcher}/` : pathFromMatcher); - - if (result.ignored) { - ignored = true; - } else if (result.unignored) { - ignored = false; - } - } + return this.#matcher!.isIgnoredBatch(relativeParent, names, directoryFlags); + } - directoryPath = directoryPath ? `${directoryPath}/${segment}` : segment; - pathFromMatcher = pathFromMatcher.slice(segment.length + 1); + #load(directoryPath: string): Promise { + const cached = this.#loads.get(directoryPath); + if (cached) { + return cached; } - return ignored; + // Ignore files may disappear or become unreadable during traversal. + const loading = readFile(path.join(directoryPath, '.gitignore'), 'utf8').then( + (content) => { + const relativePath = toPosixPath(this.#resolveRelativePath(directoryPath)); + this.#matcher ??= new (loadNativeBinding().GitIgnoreMatcher)(); + this.#hasRules = this.#matcher.addSource(relativePath, content); + }, + () => undefined, + ); + + this.#loads.set(directoryPath, loading); + return loading; } } const createTraversalOptions = ( - gitIgnore: GitIgnoreMatcher, + gitIgnore: GitIgnoreFiles, ignoredDirNames: ReadonlySet, + signal: { aborted: boolean }, + onError: (error: unknown) => void, isIncluded?: (filePath: string) => boolean, isIgnored?: (filePath: string, isDirectory: boolean) => boolean, ) => { 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; @@ -231,7 +221,9 @@ const createTraversalOptions = ( } if (dirent.isDirectory()) { - return gitIgnore.isIgnored(targetPath, true) || isIgnored?.(targetPath, true) === true; + return ( + (dirent as GitIgnoreDirent)[gitIgnored] === true || isIgnored?.(targetPath, true) === true + ); } if (isIncluded !== undefined && !isIncluded(targetPath)) { @@ -241,21 +233,44 @@ const createTraversalOptions = ( return ( isIgnored?.(targetPath, false) === true || isBinaryPath(targetPath) || - gitIgnore.isIgnored(targetPath, false) + (dirent as GitIgnoreDirent)[gitIgnored] === true ); }, onDirents: async (dirents: Dirent[]) => { - const parentPath = getDirentParentPath(dirents[0]); - let hasGitIgnore = false; + try { + const parentPath = getDirentParentPath(dirents[0]); + let hasGitIgnore = false; + + for (const dirent of dirents) { + if (dirent.name === '.gitignore') { + hasGitIgnore = true; + } + } - for (const dirent of dirents) { - if (dirent.name === '.gitignore') { - hasGitIgnore = true; + if (hasGitIgnore) { + await gitIgnore.load(parentPath); } - } - if (hasGitIgnore) { - 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; + } + } + } + } catch (error) { + onError(error); } return undefined; @@ -263,6 +278,37 @@ const createTraversalOptions = ( }; }; +const discoverDirectoryFiles = async ( + rootPath: string, + gitIgnore: GitIgnoreFiles, + ignoredDirNames: ReadonlySet, + isIncluded?: (filePath: string) => boolean, + isIgnored?: (filePath: string, isDirectory: boolean) => boolean, +): Promise => { + let failed = false; + let failure: unknown; + const signal = { aborted: false }; + const onError = (error: unknown): void => { + if (!failed) { + failed = true; + failure = error; + } + signal.aborted = true; + }; + + const result = await readdir( + rootPath, + createTraversalOptions(gitIgnore, ignoredDirNames, signal, onError, isIncluded, isIgnored), + ); + + // tiny-readdir only handles fulfilled onDirents promises, so rethrow after its counter settles. + if (failed) { + throw failure; + } + + return result.files; +}; + const normalizeGlob = (cwd: string, pattern: string): string => { const relativePattern = path.isAbsolute(pattern) ? path.relative(cwd, pattern) : pattern; return toPosixPath(relativePattern); @@ -392,7 +438,7 @@ const discoverFmtPaths = async ({ const traversalRoots = getTraversalRoots(cwd, directoryRoots, globs); if (traversalRoots.length) { - const gitIgnore = await GitIgnoreMatcher.create(cwd); + const gitIgnore = await GitIgnoreFiles.create(cwd); const results = await Promise.all( traversalRoots.map(async (rootPath) => { const stats = await lstatSafe(rootPath); @@ -419,12 +465,7 @@ const discoverFmtPaths = async ({ return globMatchers.some((matches) => matches(relativePath)); }; - return ( - await readdir( - rootPath, - createTraversalOptions(gitIgnore, ignoredDirNames, isIncluded, isIgnored), - ) - ).files; + return discoverDirectoryFiles(rootPath, gitIgnore, ignoredDirNames, isIncluded, isIgnored); }), ); diff --git a/packages/rstack/src/fmt/ignore.ts b/packages/rstack/src/fmt/ignore.ts index b449318f..61ccfd1d 100644 --- a/packages/rstack/src/fmt/ignore.ts +++ b/packages/rstack/src/fmt/ignore.ts @@ -1,7 +1,7 @@ import { readFile } from 'node:fs/promises'; import path from 'node:path'; -import createIgnore from 'ignore'; -import { createRelativePathResolver } from './pathHelpers.ts'; +import type { IgnoreSource } from '../../binding.cjs'; +import { loadNativeBinding } from '../native/index.ts'; import type { ResolvedFmtConfig } from './types.ts'; /** @@ -12,7 +12,7 @@ import type { ResolvedFmtConfig } from './types.ts'; */ const defaultIgnoreNames = ['package-lock.json', 'pnpm-lock.yaml']; -type IgnoreMatcher = (filePath: string, isDirectory?: boolean) => boolean; +type IgnorePredicate = (filePath: string, isDirectory?: boolean) => boolean; interface CreateIgnoreMatcherOptions { config: ResolvedFmtConfig; @@ -21,28 +21,18 @@ interface CreateIgnoreMatcherOptions { ignorePaths?: string[]; } -const createDefaultIgnoreMatcher = (): IgnoreMatcher => { +const createDefaultIgnoreMatcher = (): IgnorePredicate => { const suffixes = defaultIgnoreNames.map((name) => `${path.sep}${name}`); return (filePath) => suffixes.some((suffix) => filePath.endsWith(suffix)); }; -const createPatternMatcher = (rootPath: string, patterns: string): IgnoreMatcher => { - const matcher = createIgnore({ allowRelativePaths: true }).add(patterns); - const resolveRelativePath = createRelativePathResolver(rootPath); - - return (filePath, isDirectory = false) => { - const relativePath = resolveRelativePath(filePath); - if (relativePath === '') { - return false; - } - - const posixPath = path.sep === '\\' ? relativePath.replaceAll('\\', '/') : relativePath; - return matcher.ignores(isDirectory ? `${posixPath}/` : posixPath); - }; +const createPatternMatcherSet = (sources: IgnoreSource[]): IgnorePredicate => { + const matcher = new (loadNativeBinding().IgnoreMatcher)(sources); + return (filePath, isDirectory = false) => matcher.isIgnored(filePath, isDirectory); }; -const loadIgnoreMatcher = async (cwd: string, ignorePath: string): Promise => { +const loadIgnoreSource = async (cwd: string, ignorePath: string): Promise => { const filePath = path.resolve(cwd, ignorePath); let patterns: string; @@ -54,7 +44,10 @@ const loadIgnoreMatcher = async (cwd: string, ignorePath: string): Promise => { - const configMatcher = config.ignorePatterns.length - ? createPatternMatcher( - config.rootPath, - [...defaultIgnoreNames, ...config.ignorePatterns].join('\n'), - ) - : createDefaultIgnoreMatcher(); - if (ignorePaths.length === 0) { - return configMatcher; +}: CreateIgnoreMatcherOptions): Promise => { + const ignoreFileSources = await Promise.all( + ignorePaths.map((ignorePath) => loadIgnoreSource(cwd, ignorePath)), + ); + if (config.ignorePatterns.length) { + return createPatternMatcherSet([ + { + rootPath: config.rootPath, + patterns: [...defaultIgnoreNames, ...config.ignorePatterns].join('\n'), + }, + ...ignoreFileSources, + ]); } - const ignoreMatchers = await Promise.all( - ignorePaths.map((ignorePath) => loadIgnoreMatcher(cwd, ignorePath)), - ); + const defaultMatcher = createDefaultIgnoreMatcher(); + if (ignoreFileSources.length === 0) { + return defaultMatcher; + } + const cliMatcher = createPatternMatcherSet(ignoreFileSources); return (filePath, isDirectory = false) => - configMatcher(filePath, isDirectory) || - ignoreMatchers.some((matches) => matches(filePath, isDirectory)); + defaultMatcher(filePath, isDirectory) || cliMatcher(filePath, isDirectory); }; export { createIgnoreMatcher }; diff --git a/packages/rstack/src/fmt/workerPool.ts b/packages/rstack/src/fmt/workerPool.ts index e350a149..7a569636 100644 --- a/packages/rstack/src/fmt/workerPool.ts +++ b/packages/rstack/src/fmt/workerPool.ts @@ -64,5 +64,5 @@ const createFmtWorkerPool = async ( }; }; -export { createFmtWorkerPool, getFmtWorkerCount }; +export { createFmtWorkerPool }; export type { FmtWorkerPool }; diff --git a/packages/rstack/src/fmt/yukuPlugin.ts b/packages/rstack/src/fmt/yukuPlugin.ts index 20a5635f..cf517ad2 100644 --- a/packages/rstack/src/fmt/yukuPlugin.ts +++ b/packages/rstack/src/fmt/yukuPlugin.ts @@ -1,17 +1,16 @@ import * as prettierEstreePlugin from 'prettier/plugins/estree'; import type { Parser, ParserOptions, Plugin, SupportLanguage } from 'prettier'; import { + langFromPath, parse as parseWithYuku, type Comment, type Diagnostic, type ParseOptions, type ParseResult, - type SourceLang, type SourceType, } from 'yuku-parser'; const AST_FORMAT = 'estree-yuku'; -const JSX_REGEXP = /^[^"'`]*<\/|^[^/]{2}.*\/>/m; const SOURCE_TYPE_COMBINATIONS: SourceType[] = ['module', 'commonjs']; type Range = [start: number, end: number]; @@ -200,15 +199,24 @@ const mergeNestedJsdocComments = (comments: PrettierComment[]): void => { }; const stripComments = (originalText: string, comments: PrettierComment[]): string => { - let text = originalText; + if (comments.length === 0) { + return originalText; + } + const chunks: string[] = []; + let cursor = 0; + + // Yuku returns comments in source order, so mask each range while copying the source only once. for (const comment of comments) { const start = locStart(comment); const end = locEnd(comment); - text = text.slice(0, start) + text.slice(start, end).replace(/[^\n]/g, ' ') + text.slice(end); + chunks.push(originalText.slice(cursor, start)); + chunks.push(originalText.slice(start, end).replace(/[^\n]/g, ' ')); + cursor = end; } - return text; + chunks.push(originalText.slice(cursor)); + return chunks.join(''); }; const setContentEnd = ( @@ -441,11 +449,7 @@ const parseWithOptions = (text: string, options: ParseOptions): ParseResult => { return result; }; -const getSourceType = (filepath: unknown): SourceType | undefined => { - if (typeof filepath !== 'string') { - return undefined; - } - +const getSourceType = (filepath: string): SourceType | undefined => { if (/\.(?:mjs|mts)$/i.test(filepath)) { return 'module'; } @@ -457,20 +461,6 @@ const getSourceType = (filepath: unknown): SourceType | undefined => { return undefined; }; -const getLanguageCombinations = (text: string, filepath: unknown): SourceLang[] => { - if (typeof filepath === 'string') { - if (/\.(?:jsx|tsx)$/i.test(filepath)) { - return ['tsx']; - } - - if (filepath.toLowerCase().endsWith('.d.ts')) { - return ['dts']; - } - } - - return JSX_REGEXP.test(text) ? ['tsx', 'ts', 'dts'] : ['ts', 'tsx', 'dts']; -}; - const tryCombinations = (combinations: (() => ParseResult)[]): ParseResult => { let firstError: unknown; let hasError = false; @@ -505,9 +495,9 @@ const parseJavaScript = (text: string, options: ParserOptions): AstNode const parseTypeScript = (text: string, options: ParserOptions): AstNode => { const sourceType = getSourceType(options.filepath); - const languages = getLanguageCombinations(text, options.filepath); - const combinations = (sourceType ? [sourceType] : SOURCE_TYPE_COMBINATIONS).flatMap((candidate) => - languages.map((lang) => () => parseWithOptions(text, { sourceType: candidate, lang })), + const lang = langFromPath(options.filepath.toLowerCase()); + const combinations = (sourceType ? [sourceType] : SOURCE_TYPE_COMBINATIONS).map( + (candidate) => () => parseWithOptions(text, { sourceType: candidate, lang }), ); const { program, comments } = tryCombinations(combinations); diff --git a/packages/rstack/src/native/index.ts b/packages/rstack/src/native/index.ts new file mode 100644 index 00000000..bf682324 --- /dev/null +++ b/packages/rstack/src/native/index.ts @@ -0,0 +1,10 @@ +import { createRequire } from 'node:module'; +import path from 'node:path'; + +export type NativeBinding = typeof import('../../binding.cjs'); + +const require = createRequire(import.meta.url); +export const loadNativeBinding = (): NativeBinding => { + const packageJsonPath = require.resolve('rstack/package.json'); + return require(path.join(path.dirname(packageJsonPath), 'binding.cjs')) as NativeBinding; +}; diff --git a/packages/rstack/src/projectCache.ts b/packages/rstack/src/projectCache.ts index 5959b1ce..86d0ffcb 100644 --- a/packages/rstack/src/projectCache.ts +++ b/packages/rstack/src/projectCache.ts @@ -31,5 +31,5 @@ const ensureProjectCacheDir = async (rootPath: string): Promise { return `'${shellPath.replaceAll("'", `'"'"'`)}'`; }; -// Generated shims live in `/_`. When a shim sources this -// dispatcher, `$0` still points to the shim, so the user hook is one level up. -const createDispatcher = (nodeExecutable: string): string => `#!/usr/bin/env sh +const createRunner = (nodeExecutable: string): string => `#!/usr/bin/env sh +# Generated by Rstack. Do not edit. -name=$(basename "$0") -dir=$(dirname "$(dirname "$0")") -hook="$dir/$name" +rs_name=\${0##*/} +rs_root=$PWD +rs_hook="\${rs_dir%/*}/$rs_name" +[ -f "$rs_hook" ] || exit 0 -[ -f "$hook" ] || exit 0 - -init="\${XDG_CONFIG_HOME:-$HOME/.config}/rstack/hooks-init.sh" -[ -f "$init" ] && . "$init" +rs_init="\${XDG_CONFIG_HOME:-$HOME/.config}/rstack/hooks-init.sh" +[ -f "$rs_init" ] && . "$rs_init" [ "\${RSTACK_HOOKS-}" = "0" ] && exit 0 [ "\${RSTACK_HOOKS-}" = "2" ] && set -x +IFS= read -r rs_project_path < "$rs_dir/.owner" || exit 1 +[ -n "$rs_project_path" ] || exit 1 + # Fall back to the Node.js executable that ran rs setup when GUI clients omit # it from PATH. Keep an existing Node.js environment ahead of this fallback. -node_fallback=${quoteShellPath(nodeExecutable)} -if ! command -v node >/dev/null 2>&1 && [ -x "$node_fallback" ]; then - PATH="\${PATH:+$PATH:}\${node_fallback%/*}" +rs_node_fallback=${quoteShellPath(nodeExecutable)} +if ! command -v node >/dev/null 2>&1 && [ -x "$rs_node_fallback" ]; then + PATH="\${PATH:+$PATH:}\${rs_node_fallback%/*}" fi -export PATH="node_modules/.bin\${PATH:+:$PATH}" +rs_run() { + cd "$rs_root/$rs_project_path" || return 1 + export PATH="node_modules/.bin\${PATH:+:$PATH}" -code=0 -sh -e "$hook" "$@" || code=$? + rs_code=0 + sh -e "$rs_hook" "$@" || rs_code=$? -[ "$code" = "0" ] || echo "Rstack - $name hook failed (code $code)" -[ "$code" = "127" ] && echo "Rstack - command not found in PATH=$PATH" -exit "$code" + [ "$rs_code" = "0" ] || echo "Rstack - $rs_name hook failed (code $rs_code)" + [ "$rs_code" = "127" ] && echo "Rstack - command not found in PATH=$PATH" + return "$rs_code" +} `; -// Every generated Git hook sources the same dispatcher to keep runtime behavior -// consistent and make future initialization changes local to one file. -const shim = `#!/usr/bin/env sh -. "$(dirname "$0")/runner" +const createShim = (prepareArguments = ''): string => `#!/usr/bin/env sh +rs_dir=$(CDPATH= cd "$(dirname "$0")" && pwd) || exit 1 +. "$rs_dir/runner" + +${prepareArguments} +rs_run "$@" `; export const createHookFiles = ( nodeExecutable: string = process.execPath, ): Record => { - const files: Record = { runner: createDispatcher(nodeExecutable) }; + const messageShim = createShim(`# Keep the message file valid after changing directories. +[ -n "\${1-}" ] || exit 1 +case "$1" in + /*|[A-Za-z]:/*) ;; + *) + rs_file=$1 + shift + set -- "$rs_root/$rs_file" "$@" + ;; +esac +`); + + const prePushShim = createShim(`# Keep a local remote path valid after changing directories. +rs_remote_name=\${1-} +rs_remote_location=\${2-} +[ -n "$rs_remote_name" ] && [ -n "$rs_remote_location" ] || exit 1 +case "$rs_remote_location" in + /*|[A-Za-z]:/*) ;; + *:*) + # Git treats a colon before any slash as a URL or SCP-style remote. + case "\${rs_remote_location%%:*}" in + */*) rs_remote_location="$rs_root/$rs_remote_location" ;; + esac + ;; + *) rs_remote_location="$rs_root/$rs_remote_location" ;; +esac +shift 2 +set -- "$rs_remote_name" "$rs_remote_location" "$@" +`); + + const defaultShim = createShim(); + const files: Record = { runner: createRunner(nodeExecutable) }; for (const name of hookNames) { - files[name] = shim; + files[name] = name.endsWith('-msg') + ? messageShim + : name === 'pre-push' + ? prePushShim + : defaultShim; } return files; diff --git a/packages/rstack/src/setup/index.ts b/packages/rstack/src/setup/index.ts index a8a039ac..5272ab98 100644 --- a/packages/rstack/src/setup/index.ts +++ b/packages/rstack/src/setup/index.ts @@ -1,17 +1,22 @@ import { color, logger } from 'rslog'; import { parseArgs } from '../cli/args.ts'; +import { renderHelp } from '../cli/help.ts'; import { installHooks } from './install.ts'; -const helpMessage = `Rstack v${RSTACK_VERSION} - -${color.cyan('Usage')}: -${color.yellow(' $ rs setup [options]')} - -Install Git hooks in the current repository. - -${color.cyan('Options')}: - --hooks-dir Specify hooks directory relative to the current directory - -h, --help Display this help message`; +const renderSetupHelp = (): string => + renderHelp({ + usage: 'rs setup [options]', + description: 'Install Git hooks in the current repository', + sections: [ + { + title: 'Options', + items: [ + ['--hooks-dir ', 'Specify hooks directory relative to the Git repository root'], + ['-h, --help', 'Display this help message'], + ], + }, + ], + }); export const runSetupCLI = (args: string[]): void => { const { values } = parseArgs({ @@ -32,7 +37,7 @@ export const runSetupCLI = (args: string[]): void => { const hooksDir = hooksDirs?.[0]; if (values.help) { - console.log(helpMessage); + console.log(renderSetupHelp()); return; } @@ -43,6 +48,11 @@ export const runSetupCLI = (args: string[]): void => { } if (result.status === 'skipped') { + if (result.message) { + logger.warn(`Git hooks setup skipped: ${color.yellow(result.message)}.`); + return; + } + const reason = result.reason === 'disabled' ? 'disabled by RSTACK_HOOKS' : 'not a Git repository'; logger.info(`Git hooks setup skipped: ${color.yellow(reason)}.`); diff --git a/packages/rstack/src/setup/install.ts b/packages/rstack/src/setup/install.ts index e73fdef9..81d4198f 100644 --- a/packages/rstack/src/setup/install.ts +++ b/packages/rstack/src/setup/install.ts @@ -1,9 +1,11 @@ import { spawnSync } from 'node:child_process'; -import { chmodSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'; +import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'; import path from 'node:path'; -import { createHookFiles } from './hooks.ts'; +import { createHookFiles, hookNames } from './hooks.ts'; const defaultHooksDir = '.rstack/hooks'; +const generatedDirectoryName = '_'; +const ownerFileName = '.owner'; const gitignore = '*\n'; type InstallHooksOptions = { @@ -17,18 +19,37 @@ type FailedInstallResult = { message: string; }; +type SkippedInstallResult = { + status: 'skipped'; + reason: string; + message?: string; +}; + type InstallResult = | { status: 'installed'; hooksPath: string } | { status: 'unchanged'; hooksPath: string } - | { status: 'skipped'; reason: string } + | SkippedInstallResult | FailedInstallResult; +type GitContext = { + defaultHooksDirectory: string; + effectiveHooksDirectory: string; + gitRoot: string; + projectPath: string; +}; + const fail = (reason: string, message: string): FailedInstallResult => ({ status: 'failed', reason, message, }); +const skip = (reason: string, message?: string): SkippedInstallResult => ({ + status: 'skipped', + reason, + ...(message ? { message } : {}), +}); + const resolveHooksDir = (hooksDir: string): string | FailedInstallResult => { const resolvedDir = hooksDir.replaceAll('\\', '/'); @@ -39,7 +60,7 @@ const resolveHooksDir = (hooksDir: string): string | FailedInstallResult => { if (path.isAbsolute(resolvedDir)) { return fail( 'invalid-hooks-directory', - 'Git hooks directory must be relative to the current directory.', + 'Git hooks directory must be relative to the Git repository root.', ); } @@ -54,7 +75,10 @@ const runGit = (cwd: string, args: string[]) => spawnSync('git', args, { cwd, en const removeLineEnding = (value: string): string => value.replace(/\r?\n$/u, ''); -const gitFailure = (error: NodeJS.ErrnoException | undefined, stderr: string): InstallResult => { +const gitFailure = ( + error: NodeJS.ErrnoException | undefined, + stderr: string, +): FailedInstallResult => { if (error?.code === 'ENOENT') { return fail('git-not-found', 'Git command not found.'); } @@ -62,6 +86,54 @@ const gitFailure = (error: NodeJS.ErrnoException | undefined, stderr: string): I return fail('git-command-failed', `Failed to run Git: ${error?.message || stderr.trim()}`); }; +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. + const repository = runGit(cwd, [ + 'rev-parse', + '--is-inside-work-tree', + '--path-format=absolute', + '--show-toplevel', + '--show-prefix', + '--git-common-dir', + '--git-path', + 'hooks', + ]); + if (repository.error || repository.status === null) { + return gitFailure(repository.error, repository.stderr); + } + + const [ + insideWorkTree = '', + gitRoot = '', + repositoryPrefix = '', + gitCommonDirectory = '', + effectiveHooksDirectory = '', + ] = removeLineEnding(repository.stdout).split(/\r?\n/u); + + if (insideWorkTree !== 'true') { + return skip('not-git-repository'); + } + + if (repository.status !== 0) { + return fail( + 'git-command-failed', + `Failed to resolve the Git repository paths: ${repository.stderr.trim()}`, + ); + } + + if (!gitRoot || !gitCommonDirectory || !effectiveHooksDirectory) { + return fail('git-command-failed', 'Failed to resolve the Git repository paths.'); + } + + return { + defaultHooksDirectory: path.join(gitCommonDirectory, 'hooks'), + effectiveHooksDirectory, + gitRoot, + projectPath: repositoryPrefix.replaceAll('\\', '/').replace(/\/$/u, '') || '.', + }; +}; + const isCurrentFile = (filePath: string, content: string, executable = false): boolean => { try { // Windows does not expose POSIX executable bits, but Git for Windows still runs hook shims. @@ -74,12 +146,75 @@ const isCurrentFile = (filePath: string, content: string, executable = false): b } }; +const isSamePath = (first: string, second: string): boolean => + path.resolve(first) === path.resolve(second); + +const readOwner = (directory: string): string | undefined => { + try { + const content = readFileSync(path.join(directory, ownerFileName), 'utf8'); + const owner = removeLineEnding(content); + return content === `${owner}\n` && owner.length > 0 && !/[\r\n]/u.test(owner) + ? owner + : undefined; + } catch { + return undefined; + } +}; + +const displayPath = (gitRoot: string, filePath: string): string => { + const relativePath = path.relative(gitRoot, filePath).replaceAll('\\', '/'); + return relativePath.length > 0 && !relativePath.startsWith('../') ? relativePath : filePath; +}; + +const ownerConflict = (project: string): SkippedInstallResult => + skip('owned-by-another-project', `Git hooks are already managed by Rstack project "${project}"`); + +const directoryConflict = (gitRoot: string, directory: string): SkippedInstallResult => + skip( + 'hooks-directory-conflict', + `the hooks directory "${displayPath(gitRoot, directory)}" is not managed by Rstack`, + ); + +const claimOwner = ( + directory: string, + gitRoot: string, + project: string, +): SkippedInstallResult | undefined => { + const ownerPath = path.join(directory, ownerFileName); + const owner = readOwner(directory); + + if (owner) { + return owner === project ? undefined : ownerConflict(owner); + } + + try { + // Exclusive creation makes concurrent prepare scripts agree on one owner. + writeFileSync(ownerPath, `${project}\n`, { flag: 'wx' }); + } catch (error) { + const code = error instanceof Error && 'code' in error ? error.code : undefined; + if (code !== 'EEXIST') { + throw error; + } + + const concurrentOwner = readOwner(directory); + if (!concurrentOwner) { + return directoryConflict(gitRoot, directory); + } + return concurrentOwner === project ? undefined : ownerConflict(concurrentOwner); + } + + return undefined; +}; + +const findExistingHooks = (directory: string): string[] => + hookNames.filter((name) => existsSync(path.join(directory, name))); + export const installHooks = ({ cwd = process.cwd(), hooksDir = defaultHooksDir, }: InstallHooksOptions = {}): InstallResult => { if (process.env.RSTACK_HOOKS === '0') { - return { status: 'skipped', reason: 'disabled' }; + return skip('disabled'); } const resolvedDir = resolveHooksDir(hooksDir); @@ -88,57 +223,57 @@ export const installHooks = ({ } // Check Git before touching the filesystem so non-repositories have no side effects. - const repository = runGit(cwd, [ - 'rev-parse', - '--is-inside-work-tree', - '--show-prefix', - '--git-path', - 'hooks', - ]); - if (repository.error || repository.status === null) { - return gitFailure(repository.error, repository.stderr); + const context = resolveGitContext(cwd); + if ('status' in context) { + return context; } - const [insideWorkTree = '', repositoryPrefix, configuredHooksPath] = removeLineEnding( - repository.stdout, - ).split(/\r?\n/u); + const { defaultHooksDirectory, effectiveHooksDirectory, gitRoot, projectPath } = context; + const hooksPath = `${resolvedDir}/${generatedDirectoryName}`; + const directory = path.join(gitRoot, resolvedDir, generatedDirectoryName); + const hooksPathMatches = isSamePath(effectiveHooksDirectory, directory); + const usesDefaultHooks = isSamePath(effectiveHooksDirectory, defaultHooksDirectory); - if (repository.status !== 0) { - if (insideWorkTree.trim() === 'true') { - return fail( - 'git-command-failed', - `Failed to resolve the Git repository paths: ${repository.stderr.trim()}`, + if (!hooksPathMatches && !usesDefaultHooks) { + const activeOwner = readOwner(effectiveHooksDirectory); + if (!activeOwner) { + return skip( + 'hooks-path-conflict', + `Git hooks are already configured at "${displayPath(gitRoot, effectiveHooksDirectory)}"`, ); } - return { status: 'skipped', reason: 'not-git-repository' }; - } - - if (insideWorkTree.trim() !== 'true') { - return { status: 'skipped', reason: 'not-git-repository' }; + if (activeOwner !== projectPath) { + return ownerConflict(activeOwner); + } } - if (repositoryPrefix === undefined || configuredHooksPath === undefined) { - return fail('git-command-failed', 'Failed to resolve the Git repository paths.'); + if (usesDefaultHooks) { + const existingHooks = findExistingHooks(defaultHooksDirectory); + if (existingHooks.length > 0) { + return skip( + 'existing-git-hooks', + `existing Git hooks were found: ${existingHooks.join(', ')}`, + ); + } } - const prefix = repositoryPrefix.replaceAll('\\', '/'); - const hooksPath = `${prefix}${resolvedDir}/_`; - - const directory = path.join(cwd, resolvedDir, '_'); const files = Object.entries(createHookFiles()); - const hooksPathMatches = path.resolve(cwd, configuredHooksPath) === directory; - // Skip all writes only when the config, generated content, and executable modes match. - const unchanged = - hooksPathMatches && - isCurrentFile(path.join(directory, '.gitignore'), gitignore) && - files.every(([name, content]) => isCurrentFile(path.join(directory, name), content, true)); - - if (unchanged) { - return { status: 'unchanged', hooksPath }; - } - try { mkdirSync(directory, { recursive: true }); + const ownerResult = claimOwner(directory, gitRoot, projectPath); + if (ownerResult) { + return ownerResult; + } + + // Skip generated file writes when their content and executable modes match. + const unchanged = + hooksPathMatches && + isCurrentFile(path.join(directory, '.gitignore'), gitignore) && + files.every(([name, content]) => isCurrentFile(path.join(directory, name), content, true)); + if (unchanged) { + return { status: 'unchanged', hooksPath }; + } + writeFileSync(path.join(directory, '.gitignore'), gitignore); for (const [name, content] of files) { diff --git a/packages/rstack/src/staged.ts b/packages/rstack/src/staged.ts index 7aee8839..339ddccd 100644 --- a/packages/rstack/src/staged.ts +++ b/packages/rstack/src/staged.ts @@ -1,6 +1,6 @@ import lintStaged from 'lint-staged'; -import { color } from 'rslog'; import { parseArgs } from './cli/args.ts'; +import { renderHelp } from './cli/help.ts'; import { loadRstackConfig } from './config.ts'; export type StagedSyncTaskGenerator = (stagedFileNames: readonly string[]) => string | string[]; @@ -21,23 +21,34 @@ export type StagedTask = export type StagedConfig = Record | StagedTaskGenerator; -const stagedHelpMessage = `Rstack v${RSTACK_VERSION} - -${color.cyan('Usage')}: -${color.yellow(' $ rs staged [options]')} - -Runs lint-staged with tasks from define.staged in rstack.config. - -${color.cyan('Options')}: - --allow-empty Allow empty commits when tasks revert all staged changes - -p, --concurrent The number of tasks to run concurrently, or false for serial - --cwd Working directory to run all tasks in - -d, --debug Print additional debug information - --no-stash Disable the backup stash. Implies "--no-revert". - -q, --quiet Disable lint-staged's own console output - -r, --relative Pass relative filepaths to tasks - -v, --verbose Show task output even when tasks succeed; by default only failed output is shown - -h, --help Display this help message`; +const renderStagedHelp = (): string => + renderHelp({ + usage: 'rs staged [options]', + description: 'Run tasks on staged Git files', + sections: [ + { + title: 'Options', + items: [ + ['--allow-empty', 'Allow empty commits when tasks revert all staged changes'], + [ + '-p, --concurrent ', + 'The number of tasks to run concurrently, or false for serial', + ], + ['--cwd ', 'Working directory to run all tasks in'], + ['-d, --debug', 'Print additional debug information'], + ['--no-stash', 'Disable backup stash and automatic revert'], + ['-q, --quiet', "Disable lint-staged's own console output"], + ['-r, --relative', 'Pass relative filepaths to tasks'], + [ + '-v, --verbose', + 'Show task output even when tasks succeed; by default only failed output is shown', + ], + ['-c, --config ', 'Specify Rstack config file path'], + ['-h, --help', 'Display this help message'], + ], + }, + ], + }); export async function runStagedCLI(args: string[]): Promise { const { values } = parseArgs({ @@ -58,7 +69,7 @@ export async function runStagedCLI(args: string[]): Promise { }); if (values.help) { - console.log(stagedHelpMessage); + console.log(renderStagedHelp()); return; } diff --git a/packages/rstack/tests/cli/__snapshots__/check.test.ts.snap b/packages/rstack/tests/cli/__snapshots__/check.test.ts.snap new file mode 100644 index 00000000..c24f532b --- /dev/null +++ b/packages/rstack/tests/cli/__snapshots__/check.test.ts.snap @@ -0,0 +1,16 @@ +// Rstest Snapshot v1 + +exports[`displays check help without loading config 1`] = ` +"Rstack v + +Usage: + $ rs check [options] + +Run static checks, including lint and format + +Options: + --type-check Enable TypeScript type checking + -c, --config Specify Rstack config file path + -h, --help Display this help message +" +`; diff --git a/packages/rstack/tests/cli/__snapshots__/help.test.ts.snap b/packages/rstack/tests/cli/__snapshots__/help.test.ts.snap new file mode 100644 index 00000000..6198d4dd --- /dev/null +++ b/packages/rstack/tests/cli/__snapshots__/help.test.ts.snap @@ -0,0 +1,365 @@ +// Rstest Snapshot v1 + +exports[`displays build help 1`] = ` +"Rstack v + +Usage: + $ rs build [options] + +Build the app for production + +Options: + -w, --watch Enable watch mode to automatically rebuild on file changes + --dist-path Set the root directory of output files + --source-map Enable source map + -c, --config Specify Rstack config file path + -h, --help Display this help message +" +`; + +exports[`displays dev help 1`] = ` +"Rstack v + +Usage: + $ rs dev [options] + +Run the app dev server + +Options: + -o, --open [url] Open the page in browser on startup + --port Set the port number for the server + --strict-port Exit if the specified port is already in use + --host [host] Set the host that the server listens to + -c, --config Specify Rstack config file path + -h, --help Display this help message +" +`; + +exports[`displays doc build help 1`] = ` +"Rstack v + +Usage: + $ rs doc build [root] [options] + +Build docs for production + +Options: + --base Set the base path and override config.base + -c, --config Specify Rstack config file path + -h, --help Display this help message +" +`; + +exports[`displays doc eject help 1`] = ` +"Rstack v + +Usage: + $ rs doc eject [component] [options] + +Eject a theme component + +Options: + -h, --help Display this help message +" +`; + +exports[`displays doc help 1`] = ` +"Rstack v + +Usage: + $ rs doc [command] [root] [options] + +Commands: + [root] Run the docs dev server (default) + build [root] Build docs for production + preview [root] Preview the docs production build + eject [component] Eject a theme component + +For command-specific options, run: + $ rs doc -h + +Options: + --port Set the port number for the server + --host [host] Set the host that the server listens to + --base Set the base path and override config.base + -c, --config Specify Rstack config file path + -h, --help Display this help message +" +`; + +exports[`displays doc preview help 1`] = ` +"Rstack v + +Usage: + $ rs doc preview [root] [options] + +Preview the docs production build + +Options: + --port Set the port number for the server + --host [host] Set the host that the server listens to + --base Set the base path and override config.base + -c, --config Specify Rstack config file path + -h, --help Display this help message +" +`; + +exports[`displays lib build help 1`] = ` +"Rstack v + +Usage: + $ rs lib build [options] + +Build the library for production + +Options: + -w, --watch Enable watch mode and rebuild on changes + --dts Emit declaration files (use --no-dts to disable) + -c, --config Specify Rstack config file path + -h, --help Display this help message +" +`; + +exports[`displays lib help 1`] = ` +"Rstack v + +Usage: + $ rs lib [command] [options] + +Commands: + build Build the library for production (default) + inspect Inspect Rslib, Rsbuild, and Rspack configs + mf-dev Start Rsbuild dev server for Module Federation + +For command-specific options, run: + $ rs lib -h + +Options: + -w, --watch Enable watch mode and rebuild on changes + --dts Emit declaration files (use --no-dts to disable) + -c, --config Specify Rstack config file path + -h, --help Display this help message +" +`; + +exports[`displays lib inspect help 1`] = ` +"Rstack v + +Usage: + $ rs lib inspect [options] + +Inspect Rslib, Rsbuild, and Rspack configs + +Options: + --output Set the output path for inspection results (default: .rsbuild) + --verbose Show complete function definitions in output + -c, --config Specify Rstack config file path + -h, --help Display this help message +" +`; + +exports[`displays lib mf-dev help 1`] = ` +"Rstack v + +Usage: + $ rs lib mf-dev [options] + +Start Rsbuild dev server for Module Federation + +Options: + -c, --config Specify Rstack config file path + -h, --help Display this help message +" +`; + +exports[`displays lint help 1`] = ` +"Rstack v + +Usage: + $ rs lint [options] [files...] + +Lint code + +Options: + --fix Automatically fix problems + --type-check Enable TypeScript type checking + --type-check-only Run only TypeScript type checking + --format Set output format (default | jsonline | github | gitlab) + --quiet Report errors only + --timing [all|N] Print a per-rule timing table (all rules or top N) + --max-warnings Set the maximum number of warnings + --rule Override a rule (repeatable) + --no-color Disable colored output + --force-color Force colored output + -c, --config Specify Rstack config file path + -h, --help Display this help message +" +`; + +exports[`displays preview help 1`] = ` +"Rstack v + +Usage: + $ rs preview [options] + +Preview the app production build + +Options: + -o, --open [url] Open the page in browser on startup + --port Set the port number for the server + --strict-port Exit if the specified port is already in use + --host [host] Set the host that the server listens to + -c, --config Specify Rstack config file path + -h, --help Display this help message +" +`; + +exports[`displays test help 1`] = ` +"Rstack v + +Usage: + $ rs test [command] [...filters] [options] + +Commands: + [...filters] Run tests (default) + run [...filters] Run tests once + watch [...filters] Run tests in watch mode + list [...filters] List matching tests + merge-reports [path] Merge blob reports + init [project] Initialize Rstest configuration + +For command-specific options, run: + $ rs test -h + +Options: + -w, --watch Enable watch mode + -u, --update Update snapshot files + --coverage Enable code coverage + --project Filter test projects by name + -t, --test-name-pattern Run tests with names matching the pattern + -c, --config Specify Rstack config file path + -h, --help Display this help message +" +`; + +exports[`displays test init help 1`] = ` +"Rstack v + +Usage: + $ rs test init [project] [options] + +Initialize Rstest configuration + +Options: + --yes Use default options without prompts + -h, --help Display this help message +" +`; + +exports[`displays test list help 1`] = ` +"Rstack v + +Usage: + $ rs test list [...filters] [options] + +List matching tests + +Options: + --related List tests related to source files + --changed [commit] List tests related to changed files + --files-only List matching test files only + --json [path] Print JSON or write it to a file + --include-suites Include test suites + --print-location Print test locations + --summary Print a summary + --project Filter test projects by name + -t, --test-name-pattern List tests with names matching the pattern + -c, --config Specify Rstack config file path + -h, --help Display this help message +" +`; + +exports[`displays test merge-reports help 1`] = ` +"Rstack v + +Usage: + $ rs test merge-reports [path] [options] + +Merge blob reports + +Options: + --coverage Generate coverage reports + --reporters, --reporter Specify test reporters + --cleanup Remove blob reports after merging + -c, --config Specify Rstack config file path + -h, --help Display this help message +" +`; + +exports[`displays test run help 1`] = ` +"Rstack v + +Usage: + $ rs test run [...filters] [options] + +Run tests once + +Options: + --related Run tests related to source files + --changed [commit] Run tests related to changed files + --shard Split tests into shards + -u, --update Update snapshot files + --coverage Enable code coverage + --project Filter test projects by name + -t, --test-name-pattern Run tests with names matching the pattern + -c, --config Specify Rstack config file path + -h, --help Display this help message +" +`; + +exports[`displays test watch help 1`] = ` +"Rstack v + +Usage: + $ rs test watch [...filters] [options] + +Run tests in watch mode + +Options: + -u, --update Update snapshot files + --coverage Enable code coverage + --project Filter test projects by name + -t, --test-name-pattern Run tests with names matching the pattern + -c, --config Specify Rstack config file path + -h, --help Display this help message +" +`; + +exports[`displays top-level help 1`] = ` +"Rstack v + +Usage: + $ rs [command] [options] + +Commands: + dev Run the app dev server + build Build the app for production + preview Preview the app production build + lib Build library + doc Serve or build docs + fmt, format Format code + lint Lint code + check Run static checks, including lint and format + test Run tests + staged Run tasks on staged Git files + setup Install Git hooks + +For command-specific options, run: + $ rs -h + +Options: + -c, --config Specify Rstack config file path + -h, --help Display this help message + -v, --version Display version number +" +`; diff --git a/packages/rstack/tests/cli/check.test.ts b/packages/rstack/tests/cli/check.test.ts new file mode 100644 index 00000000..87b92845 --- /dev/null +++ b/packages/rstack/tests/cli/check.test.ts @@ -0,0 +1,80 @@ +import { expect, test } from 'rstack/test'; +import { normalizeHelpOutput } from '#test-helpers'; +import { setupFmtTest } from './fmt/helpers.ts'; + +const { runCLI, writeProjectFile } = setupFmtTest(); +const runCheck = (args: string[] = []) => runCLI(['check', ...args]); + +const writeLintConfig = (): void => { + writeProjectFile( + 'rstack.config.ts', + `import { define } from "rstack"; + +define.lint([ + { + files: ["**/*.{js,ts}"], + rules: { "no-debugger": "error" }, + }, +]); +`, + ); +}; + +test('displays check help without loading config', () => { + writeProjectFile('rstack.config.ts', 'throw new Error("must not load");\n'); + + const result = runCheck(['--help']); + + expect(normalizeHelpOutput(result.stdout)).toMatchSnapshot(); +}); + +test('runs lint followed by a formatting check', () => { + writeLintConfig(); + writeProjectFile('src/index.ts', 'const value=true'); + + const unformatted = runCheck(); + + expect(unformatted.status).toBe(1); + expect(unformatted.stdout).toContain('Checking formatting...'); + expect(unformatted.stderr).toContain('Formatting issues found in 1 file.'); + + writeProjectFile('src/index.ts', 'const value = true;\n'); + const formatted = runCheck(); + + expect(formatted.status).toBe(0); + expect(formatted.stdout).toContain('No issues found.'); + expect(formatted.stderr).toBe(''); +}); + +test('enables type checking only with --type-check', () => { + writeLintConfig(); + writeProjectFile( + 'tsconfig.json', + `{ + "compilerOptions": { + "strict": true + }, + "include": ["src"] +} +`, + ); + writeProjectFile('src/index.ts', 'const value: string = 1;\n'); + + const withoutTypeCheck = runCheck(); + const withTypeCheck = runCheck(['--type-check']); + + expect(withoutTypeCheck.status).toBe(0); + expect(withTypeCheck.status).toBe(1); + expect(`${withTypeCheck.stdout}\n${withTypeCheck.stderr}`).toContain('TS2322'); +}); + +test('does not run the formatting check when lint fails', () => { + writeLintConfig(); + writeProjectFile('src/index.js', 'debugger;\n'); + + const result = runCheck(); + + expect(result.status).toBe(1); + expect(`${result.stdout}\n${result.stderr}`).toContain("Unexpected 'debugger' statement"); + expect(result.stdout).not.toContain('Checking formatting...'); +}); diff --git a/packages/rstack/tests/fmt/__snapshots__/cli.test.ts.snap b/packages/rstack/tests/cli/fmt/__snapshots__/files.test.ts.snap similarity index 78% rename from packages/rstack/tests/fmt/__snapshots__/cli.test.ts.snap rename to packages/rstack/tests/cli/fmt/__snapshots__/files.test.ts.snap index 75708741..7a669027 100644 --- a/packages/rstack/tests/fmt/__snapshots__/cli.test.ts.snap +++ b/packages/rstack/tests/cli/fmt/__snapshots__/files.test.ts.snap @@ -1,10 +1,12 @@ // Rstest Snapshot v1 -exports[`provides command help 1`] = ` -"Usage: +exports[`displays fmt help without loading config 1`] = ` +"Rstack v + +Usage: $ rs fmt [options] [files/globs...] -Format files with Prettier. +Format code Options: -w, --write Write formatted files in place (default) @@ -18,5 +20,7 @@ Options: --with-node-modules Process files inside node_modules --parallel-workers Number of parallel workers --stdin-filepath Format stdin as if it were saved at - -h, --help Display this help message" + -c, --config Specify Rstack config file path + -h, --help Display this help message +" `; diff --git a/packages/rstack/tests/cli/fmt/cache.test.ts b/packages/rstack/tests/cli/fmt/cache.test.ts new file mode 100644 index 00000000..15e51dea --- /dev/null +++ b/packages/rstack/tests/cli/fmt/cache.test.ts @@ -0,0 +1,132 @@ +import { expect, test } from 'rstack/test'; +import { expectWriteSummary, normalizeDuration, setupFmtTest } from './helpers.ts'; + +const { projectFileExists, readProjectFile, resolveProjectPath, runFmt, writeProjectFile } = + setupFmtTest(); + +test.each([ + ['write', []], + ['check', ['--check']], + ['list-different', ['--list-different']], +] as const)('uses the default cache in %s mode', (_, args) => { + writeProjectFile('index.ts', 'const value = 1;\n'); + writeProjectFile('.rstack/cache/fmt-v1.json', 'legacy'); + + const result = runFmt([...args, 'index.ts']); + + expect(result.status).toBe(0); + expect(readProjectFile('.rstack/cache/.gitignore')).toBe('*\n'); + expect(JSON.parse(readProjectFile('.rstack/cache/fmt/v1.json'))).toMatchObject({ + version: 1, + files: { + 'index.ts': [expect.any(String), expect.any(String), 'clean'], + }, + }); + expect(readProjectFile('.rstack/cache/fmt-v1.json')).toBe('legacy'); +}); + +test('--no-cache bypasses cache reads and writes', () => { + writeProjectFile('index.ts', 'const value=1'); + writeProjectFile('custom-cache/v1.json', '{"value":true}'); + + const first = runFmt([ + '--no-cache', + '--cache-location', + 'custom-cache', + 'index.ts', + 'custom-cache/v1.json', + ]); + + expect(first.status).toBe(0); + expect(readProjectFile('custom-cache/v1.json')).toBe('{ "value": true }\n'); + expect(projectFileExists('.rstack')).toBe(false); + + writeProjectFile('.rstack/cache/fmt-v1.json', 'stale'); + writeProjectFile('index.ts', 'const value=2'); + const second = runFmt(['--no-cache', 'index.ts']); + + expect(second.status).toBe(0); + expect(readProjectFile('index.ts')).toBe('const value = 2;\n'); + expect(readProjectFile('.rstack/cache/fmt-v1.json')).toBe('stale'); + expect(projectFileExists('.rstack/cache/.gitignore')).toBe(false); +}); + +test.each(['relative', 'absolute'] as const)('uses a %s custom cache location', (kind) => { + const cacheLocation = kind === 'relative' ? 'custom-cache' : resolveProjectPath('custom-cache'); + writeProjectFile('index.ts', 'const value = 1;\n'); + + const result = runFmt(['--cache-location', cacheLocation, 'index.ts']); + + expect(result.status).toBe(0); + expect(JSON.parse(readProjectFile('custom-cache/v1.json'))).toMatchObject({ + version: 1, + files: { + 'index.ts': [expect.any(String), expect.any(String), 'clean'], + }, + }); + expect(projectFileExists('custom-cache/.gitignore')).toBe(false); + expect(projectFileExists('.rstack')).toBe(false); +}); + +test.each(['.', '..'])('rejects a custom cache location at %s', (cacheLocation) => { + const result = runFmt(['--cache-location', cacheLocation, '.']); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain( + 'The --cache-location directory cannot be the current working directory or an ancestor.', + ); +}); + +test('excludes the custom cache directory from formatting', () => { + const cacheLocation = 'custom-cache'; + writeProjectFile('index.ts', 'const value = 1;\n'); + writeProjectFile('custom-cache/nested/ignored.ts', 'const value=2'); + expect(runFmt(['--cache-location', cacheLocation, 'index.ts']).status).toBe(0); + + const result = runFmt(['--cache-location', cacheLocation, '.']); + + expect(result.status).toBe(0); + expectWriteSummary(result.stdout, 2, 0); + expect(readProjectFile('custom-cache/nested/ignored.ts')).toBe('const value=2'); +}); + +test('uses an explicit config root cache from a subdirectory', () => { + const appPath = resolveProjectPath('packages/app'); + writeProjectFile('packages/app/index.ts', 'const value=1'); + + const result = runFmt(['index.ts', '--config', '../../rstack.config.ts'], appPath); + + expect(result.status).toBe(0); + expect(readProjectFile('packages/app/index.ts')).toBe('const value = 1;\n'); + expect(projectFileExists('.rstack/cache/fmt/v1.json')).toBe(true); + expect(projectFileExists('packages/app/.rstack')).toBe(false); + expect(JSON.parse(readProjectFile('.rstack/cache/fmt/v1.json'))).toMatchObject({ + files: { + 'packages/app/index.ts': [expect.any(String), expect.any(String), 'clean'], + }, + }); +}); + +test('recovers from a corrupted cache', () => { + writeProjectFile('index.ts', 'const value = 1;\n'); + const first = runFmt(['--check', 'index.ts']); + writeProjectFile('.rstack/cache/fmt/v1.json', '{'); + + const second = runFmt(['--check', 'index.ts']); + + expect(second.status).toBe(0); + expect(normalizeDuration(second.stdout)).toBe(normalizeDuration(first.stdout)); + expect(second.stderr).toBe(first.stderr); + expect(JSON.parse(readProjectFile('.rstack/cache/fmt/v1.json'))).toMatchObject({ version: 1 }); +}); + +test('formats without a writable cache directory', () => { + writeProjectFile('.rstack', 'not a directory'); + writeProjectFile('index.ts', 'const value=1'); + + const result = runFmt(['index.ts']); + + expect(result.status).toBe(0); + expect(readProjectFile('index.ts')).toBe('const value = 1;\n'); +}); diff --git a/packages/rstack/tests/cli/fmt/config.test.ts b/packages/rstack/tests/cli/fmt/config.test.ts new file mode 100644 index 00000000..d5947aae --- /dev/null +++ b/packages/rstack/tests/cli/fmt/config.test.ts @@ -0,0 +1,238 @@ +import { expect, test } from 'rstack/test'; +import { + expectWriteSummary, + packageJsonSource, + setupFmtTest, + sortedPackageJson, +} from './helpers.ts'; + +const { readProjectFile, runFmt, writeFixturePlugin, writeProjectFile } = setupFmtTest(); + +test('does not sort package.json by default', () => { + writeProjectFile('package.json', packageJsonSource); + + const result = runFmt(['package.json']); + + expect(result.status).toBe(0); + expect(readProjectFile('package.json')).toContain( + '"dependencies": {\n "z": "1.0.0",\n "a": "1.0.0"', + ); +}); + +test('sorts package.json with workers', () => { + writeProjectFile( + 'rstack.config.ts', + `import { define } from 'rstack'; + +define.fmt({ sortPackageJson: true }); +`, + ); + writeProjectFile('package.json', packageJsonSource); + writeProjectFile('packages/example/package.json', packageJsonSource); + + const result = runFmt(['package.json', 'packages/example/package.json']); + + expect(result.status).toBe(0); + expect(result.stderr).toBe(''); + expect(readProjectFile('package.json')).toBe(sortedPackageJson); + expect(readProjectFile('packages/example/package.json')).toBe(sortedPackageJson); +}); + +test('supports configuring the worker count', () => { + writeProjectFile('first.ts', 'const first="first"'); + writeProjectFile('second.ts', 'const second="second"'); + + const result = runFmt(['--parallel-workers', '1', 'first.ts', 'second.ts']); + + expect(result.status).toBe(0); + expectWriteSummary(result.stdout, 2, 2); + expect(result.stderr).toBe(''); + expect(readProjectFile('first.ts')).toBe('const first = "first";\n'); + expect(readProjectFile('second.ts')).toBe('const second = "second";\n'); +}); + +test('does not load Prettier config or ignore files', () => { + writeProjectFile('.prettierrc.json', '{ "singleQuote": true, "semi": false }\n'); + writeProjectFile('.prettierignore', 'index.ts\n'); + writeProjectFile('.editorconfig', 'root = true\n\n[*]\nindent_style = space\nindent_size = 8\n'); + writeProjectFile('index.ts', "function getMessage(){\n return 'hello'\n}"); + + const result = runFmt(['index.ts']); + + expect(result.status).toBe(0); + expectWriteSummary(result.stdout, 1, 1); + expect(result.stderr).toBe(''); + expect(readProjectFile('index.ts')).toBe('function getMessage() {\n return "hello";\n}\n'); +}); + +test('applies repeated ignore paths', () => { + writeProjectFile('.prettierignore', 'src/ignored-by-root.ts\n'); + writeProjectFile('config/extra.ignore', '../src/ignored-by-extra.ts\n'); + writeProjectFile('src/ignored-by-root.ts', 'const root="ignored"'); + writeProjectFile('src/ignored-by-extra.ts', 'const extra="ignored"'); + writeProjectFile('src/index.ts', 'const index="formatted"'); + + const result = runFmt([ + '--ignore-path', + '.prettierignore', + '--ignore-path=config/extra.ignore', + 'src/ignored-by-root.ts', + 'src/ignored-by-extra.ts', + 'src/index.ts', + ]); + + expect(result.status).toBe(0); + expectWriteSummary(result.stdout, 1, 1); + expect(result.stderr).toBe(''); + expect(readProjectFile('src/ignored-by-root.ts')).toBe('const root="ignored"'); + expect(readProjectFile('src/ignored-by-extra.ts')).toBe('const extra="ignored"'); + expect(readProjectFile('src/index.ts')).toBe('const index = "formatted";\n'); +}); + +test('returns exit code 2 for an unreadable ignore path', () => { + writeProjectFile('index.ts', 'const value=true'); + + const result = runFmt(['--ignore-path', 'missing.ignore', 'index.ts']); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain('Failed to read ignore file "missing.ignore".'); + expect(readProjectFile('index.ts')).toBe('const value=true'); +}); + +test('applies define.fmt options, overrides, ignore patterns, and globs', () => { + writeProjectFile( + 'rstack.config.ts', + `import { define } from 'rstack'; + +define.fmt({ + singleQuote: true, + ignorePatterns: ['src/ignored.ts'], + overrides: [ + { + files: '*.test.ts', + options: { + semi: false, + }, + }, + ], +}); +`, + ); + writeProjectFile('src/index.ts', 'const message="hello"'); + writeProjectFile('src/index.test.ts', 'const test="test"'); + writeProjectFile('src/ignored.ts', 'const ignored="ignored"'); + writeProjectFile('src/index.js', 'const javascript="untouched"'); + + const result = runFmt(['--write', 'src/**/*.ts']); + + expect(result.status).toBe(0); + expectWriteSummary(result.stdout, 2, 2); + expect(result.stderr).toBe(''); + expect(readProjectFile('src/index.ts')).toBe("const message = 'hello';\n"); + expect(readProjectFile('src/index.test.ts')).toBe("const test = 'test'\n"); + expect(readProjectFile('src/ignored.ts')).toBe('const ignored="ignored"'); + expect(readProjectFile('src/index.js')).toBe('const javascript="untouched"'); +}); + +test('uses an explicit Rstack config', () => { + writeProjectFile( + 'custom.config.ts', + `import { define } from 'rstack'; + +define.fmt({ + singleQuote: true, +}); +`, + ); + writeProjectFile('index.ts', 'const message="hello"'); + + const result = runFmt(['index.ts', '--config', 'custom.config.ts']); + + expect(result.status).toBe(0); + expectWriteSummary(result.stdout, 1, 1); + expect(result.stderr).toBe(''); + expect(readProjectFile('index.ts')).toBe("const message = 'hello';\n"); +}); + +test('returns exit code 2 for config errors', () => { + writeProjectFile('rstack.config.ts', 'throw new Error("invalid fmt config");\n'); + + const result = runFmt(['index.ts']); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain('invalid fmt config'); +}); + +test('formats with a project-local plugin in workers', () => { + writeProjectFile( + 'rstack.config.ts', + `import { define } from 'rstack'; + +define.fmt({ + plugins: ['prettier-plugin-fixture'], +}); +`, + ); + writeFixturePlugin(); + writeProjectFile('first.fixture', '{"first":true}'); + writeProjectFile('second.fixture', '{"second":true}'); + + const result = runFmt(['*.fixture']); + + expect(result.status).toBe(0); + expectWriteSummary(result.stdout, 2, 2); + expect(result.stderr).toBe(''); + expect(readProjectFile('first.fixture')).toBe('{ "first": true }\n'); + expect(readProjectFile('second.fixture')).toBe('{ "second": true }\n'); +}); + +test('formats mixed plugin overrides in workers', () => { + writeProjectFile( + 'rstack.config.ts', + `import { define } from 'rstack'; + +define.fmt({ + overrides: [ + { + files: '*.fixture', + options: { plugins: ['prettier-plugin-fixture'] }, + }, + ], +}); +`, + ); + writeFixturePlugin(); + writeProjectFile('data.fixture', '{"value":true}'); + writeProjectFile('index.ts', 'const value=true'); + + const result = runFmt(['data.fixture', 'index.ts']); + + expect(result.status).toBe(0); + expectWriteSummary(result.stdout, 2, 2); + expect(result.stderr).toBe(''); + expect(readProjectFile('data.fixture')).toBe('{ "value": true }\n'); + expect(readProjectFile('index.ts')).toBe('const value = true;\n'); +}); + +test('returns exit code 2 for imported plugin objects', () => { + writeProjectFile( + 'rstack.config.ts', + `import { define } from 'rstack'; + +define.fmt({ + plugins: [{ languages: [] }], +}); +`, + ); + writeProjectFile('index.ts', 'const value=true'); + + const result = runFmt(['index.ts']); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain( + 'Prettier plugin objects are not supported. Use a package name, path, or URL instead.', + ); +}); diff --git a/packages/rstack/tests/cli/fmt/files.test.ts b/packages/rstack/tests/cli/fmt/files.test.ts new file mode 100644 index 00000000..d650eac1 --- /dev/null +++ b/packages/rstack/tests/cli/fmt/files.test.ts @@ -0,0 +1,155 @@ +import { expect, test } from 'rstack/test'; +import { normalizeHelpOutput } from '#test-helpers'; +import { expectWriteSummary, normalizeDuration, setupFmtTest } from './helpers.ts'; + +const { readProjectFile, runCLI, runFmt, writeProjectFile } = setupFmtTest(); + +test('displays fmt help without loading config', () => { + writeProjectFile('rstack.config.ts', 'throw new Error("must not load");\n'); + + const fmt = runFmt(['--help']); + + expect(fmt.status).toBe(0); + expect(normalizeHelpOutput(fmt.stdout)).toMatchSnapshot(); + expect(fmt.stderr).toBe(''); +}); + +test('supports format as an alias for fmt', () => { + writeProjectFile('index.ts', 'const message="hello"'); + + const result = runCLI(['format', 'index.ts']); + + expect(result.status).toBe(0); + expectWriteSummary(result.stdout, 1, 1); + expect(result.stderr).toBe(''); + expect(readProjectFile('index.ts')).toBe('const message = "hello";\n'); +}); + +test('returns exit code 2 for invalid arguments', () => { + const result = runFmt(['--write', '--check']); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain( + 'The --write, --check, and --list-different options cannot be used together.', + ); +}); + +test('returns exit code 2 for unknown options', () => { + const result = runFmt(['--bogus']); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain('--bogus'); +}); + +test('formats the current directory with Prettier defaults', () => { + writeProjectFile('index.ts', 'const message="hello"'); + + const result = runFmt(); + + expect(result.status).toBe(0); + expectWriteSummary(result.stdout, 2, 1); + expect(result.stderr).toBe(''); + expect(readProjectFile('index.ts')).toBe('const message = "hello";\n'); +}); + +test('accepts -w as an alias for --write', () => { + writeProjectFile('index.ts', 'const message="hello"'); + + const result = runFmt(['-w', 'index.ts']); + + expect(result.status).toBe(0); + expectWriteSummary(result.stdout, 1, 1); + expect(result.stderr).toBe(''); + expect(readProjectFile('index.ts')).toBe('const message = "hello";\n'); +}); + +test('formats files in node_modules with --with-node-modules', () => { + const source = 'const message="hello"'; + writeProjectFile('node_modules/example/index.ts', source); + + const skipped = runFmt(['node_modules/example']); + expect(skipped.status).toBe(2); + expect(readProjectFile('node_modules/example/index.ts')).toBe(source); + + const result = runFmt(['--with-node-modules', 'node_modules/example']); + expect(result.status).toBe(0); + expectWriteSummary(result.stdout, 1, 1); + expect(result.stderr).toBe(''); + expect(readProjectFile('node_modules/example/index.ts')).toBe('const message = "hello";\n'); +}); + +test('summarizes write mode when no files change', () => { + writeProjectFile('index.ts', 'const message = "hello";\n'); + + const result = runFmt(['index.ts']); + + expect(result.status).toBe(0); + expectWriteSummary(result.stdout, 1, 0); + expect(result.stderr).toBe(''); +}); + +test('checks formatting without writing files', () => { + const source = 'const message="hello"'; + writeProjectFile('index.ts', source); + + const result = runFmt(['--check', 'index.ts']); + + expect(result.status).toBe(1); + expect(normalizeDuration(result.stdout)).toBe( + 'start Checking formatting...\ninfo Checked 1 file in .\n', + ); + expect(result.stderr).toContain('error index.ts'); + expect(normalizeDuration(result.stderr)).toContain( + 'error Formatting issues found in 1 file. Run without --check to fix.', + ); + expect(readProjectFile('index.ts')).toBe(source); + + writeProjectFile('index.ts', 'const message = "hello";\n'); + const formattedResult = runFmt(['--check', 'index.ts']); + + expect(formattedResult.status).toBe(0); + expect(normalizeDuration(formattedResult.stdout)).toBe( + 'start Checking formatting...\nsuccess Checked 1 file in . No issues found.\n', + ); + expect(formattedResult.stderr).toBe(''); +}); + +test.each(['-l', '--list-different'])('lists only paths that differ with %s', (option) => { + const source = 'const message="hello"'; + writeProjectFile('src/index.ts', source); + writeProjectFile('src/formatted.ts', 'const formatted = true;\n'); + + const result = runFmt([option, 'src/*.ts']); + + expect(result.status).toBe(1); + expect(result.stdout).toBe('src/index.ts\n'); + expect(result.stderr).toBe(''); + expect(readProjectFile('src/index.ts')).toBe(source); +}); + +test('returns exit code 2 for formatting errors', () => { + writeProjectFile('index.ts', 'const value = ;'); + + const result = runFmt(['index.ts']); + + expect(result.status).toBe(2); + expect(result.stdout).toBe('start Formatting...\n'); + expect(result.stderr).toContain('error index.ts: SyntaxError:'); +}); + +test('reports partial writes when formatting fails', () => { + writeProjectFile('valid.ts', 'const value=true'); + writeProjectFile('invalid.ts', 'const invalid = ;'); + + const result = runFmt(['valid.ts', 'invalid.ts']); + + expect(result.status).toBe(2); + expect(normalizeDuration(result.stdout)).toBe( + 'start Formatting...\ninfo Formatted 1 of 2 files in .\n', + ); + expect(result.stderr).toContain('error invalid.ts: SyntaxError:'); + expect(readProjectFile('valid.ts')).toBe('const value = true;\n'); + expect(readProjectFile('invalid.ts')).toBe('const invalid = ;'); +}); diff --git a/packages/rstack/tests/cli/fmt/helpers.ts b/packages/rstack/tests/cli/fmt/helpers.ts new file mode 100644 index 00000000..aece7a8a --- /dev/null +++ b/packages/rstack/tests/cli/fmt/helpers.ts @@ -0,0 +1,108 @@ +import { type SpawnSyncReturns, spawnSync } from 'node:child_process'; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { afterEach, beforeEach, expect } from 'rstack/test'; +import { RSTACK_BIN_PATH } from '#test-helpers'; + +export const packageJsonSource = + '{"dependencies":{"z":"1.0.0","a":"1.0.0"},"type":"module","version":"1.0.0","name":"fixture"}'; +export const sortedPackageJson = + '{\n "name": "fixture",\n "version": "1.0.0",\n "type": "module",\n "dependencies": {\n "a": "1.0.0",\n "z": "1.0.0"\n }\n}\n'; + +type RunCLI = (args: string[], input?: string, cwd?: string) => SpawnSyncReturns; + +type FmtTestHarness = { + projectFileExists: (filePath: string) => boolean; + readProjectFile: (filePath: string) => string; + resolveProjectPath: (filePath: string) => string; + runCLI: RunCLI; + runFmt: (args?: string[], cwd?: string) => SpawnSyncReturns; + runFmtStdin: (args: string[], input: string) => SpawnSyncReturns; + writeFixturePlugin: () => void; + writeProjectFile: (filePath: string, content: string) => void; +}; + +export const normalizeDuration = (output: string): string => + output.replace(/\d+m(?: \d+(?:\.\d+)?s)?|\d+(?:\.\d+)?s/g, ''); + +export const expectWriteSummary = ( + output: string, + matchedFileCount: number, + writtenCount: number, +): void => { + const files = matchedFileCount === 1 ? 'file' : 'files'; + const message = writtenCount + ? `Formatted ${writtenCount} of ${matchedFileCount} ${files} in .` + : `Checked ${matchedFileCount} ${files} in . No changes needed.`; + expect(normalizeDuration(output)).toBe(`start Formatting...\nsuccess ${message}\n`); +}; + +export const setupFmtTest = (): FmtTestHarness => { + let projectPath: string; + + const resolveProjectPath = (filePath: string): string => path.join(projectPath, filePath); + + const projectFileExists = (filePath: string): boolean => existsSync(resolveProjectPath(filePath)); + + const writeProjectFile = (filePath: string, content: string): void => { + const absolutePath = resolveProjectPath(filePath); + mkdirSync(path.dirname(absolutePath), { recursive: true }); + writeFileSync(absolutePath, content); + }; + + const readProjectFile = (filePath: string): string => + readFileSync(resolveProjectPath(filePath), 'utf8'); + + const writeFixturePlugin = (): void => { + writeProjectFile( + 'node_modules/prettier-plugin-fixture/package.json', + JSON.stringify({ name: 'prettier-plugin-fixture', exports: './index.mjs' }), + ); + writeProjectFile( + 'node_modules/prettier-plugin-fixture/index.mjs', + `export default { + languages: [{ name: 'Fixture JSON', parsers: ['json'], extensions: ['.fixture'] }], +}; +`, + ); + }; + + const runCLI: RunCLI = (args, input, cwd = projectPath) => { + const env: NodeJS.ProcessEnv = { ...process.env, NO_COLOR: '1' }; + delete env.FORCE_COLOR; + + return spawnSync(process.execPath, [RSTACK_BIN_PATH, ...args], { + cwd, + encoding: 'utf8', + env, + input, + }); + }; + + const runFmt = (args: string[] = [], cwd = projectPath) => + runCLI(['fmt', ...args], undefined, cwd); + + const runFmtStdin = (args: string[], input: string) => runCLI(['fmt', ...args], input); + + beforeEach(() => { + projectPath = mkdtempSync(path.join(import.meta.dirname, 'test-temp-fmt-')); + // Prevent repository-level ignore rules from affecting the fixture. + mkdirSync(path.join(projectPath, '.git')); + writeProjectFile('rstack.config.ts', 'export {};\n'); + }); + + afterEach(() => { + rmSync(projectPath, { force: true, recursive: true }); + }); + + return { + projectFileExists, + readProjectFile, + resolveProjectPath, + runCLI, + runFmt, + runFmtStdin, + writeFixturePlugin, + writeProjectFile, + }; +}; diff --git a/packages/rstack/tests/cli/fmt/index.test.ts b/packages/rstack/tests/cli/fmt/index.test.ts deleted file mode 100644 index 289789a7..00000000 --- a/packages/rstack/tests/cli/fmt/index.test.ts +++ /dev/null @@ -1,838 +0,0 @@ -import { spawnSync } from 'node:child_process'; -import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; -import path from 'node:path'; -import { afterEach, beforeEach, expect, test } from 'rstack/test'; -import { RSTACK_BIN_PATH } from '#test-helpers'; - -let projectPath: string; -const packageJsonSource = - '{"dependencies":{"z":"1.0.0","a":"1.0.0"},"type":"module","version":"1.0.0","name":"fixture"}'; -const sortedPackageJson = - '{\n "name": "fixture",\n "version": "1.0.0",\n "type": "module",\n "dependencies": {\n "a": "1.0.0",\n "z": "1.0.0"\n }\n}\n'; - -const writeProjectFile = (filePath: string, content: string): void => { - const absolutePath = path.join(projectPath, filePath); - mkdirSync(path.dirname(absolutePath), { recursive: true }); - writeFileSync(absolutePath, content); -}; - -const readProjectFile = (filePath: string): string => - readFileSync(path.join(projectPath, filePath), 'utf8'); - -const writeFixturePlugin = (): void => { - writeProjectFile( - 'node_modules/prettier-plugin-fixture/package.json', - JSON.stringify({ name: 'prettier-plugin-fixture', exports: './index.mjs' }), - ); - writeProjectFile( - 'node_modules/prettier-plugin-fixture/index.mjs', - `export default { - languages: [{ name: 'Fixture JSON', parsers: ['json'], extensions: ['.fixture'] }], -}; -`, - ); -}; - -const runCLI = (args: string[], input?: string, cwd = projectPath) => { - const env: NodeJS.ProcessEnv = { ...process.env, NO_COLOR: '1' }; - delete env.FORCE_COLOR; - - return spawnSync(process.execPath, [RSTACK_BIN_PATH, ...args], { - cwd, - encoding: 'utf8', - env, - input, - }); -}; - -const runFmt = (args: string[] = [], cwd = projectPath) => runCLI(['fmt', ...args], undefined, cwd); - -const runFmtStdin = (args: string[], input: string) => runCLI(['fmt', ...args], input); - -const normalizeDuration = (output: string): string => - output.replace(/\d+m(?: \d+(?:\.\d+)?s)?|\d+(?:\.\d+)?s/g, ''); - -const expectWriteSummary = ( - output: string, - matchedFileCount: number, - writtenCount: number, -): void => { - const files = matchedFileCount === 1 ? 'file' : 'files'; - const message = writtenCount - ? `Formatted ${writtenCount} of ${matchedFileCount} ${files} in .` - : `Checked ${matchedFileCount} ${files} in . No changes needed.`; - expect(normalizeDuration(output)).toBe(`success ${message}\n`); -}; - -beforeEach(() => { - projectPath = mkdtempSync(path.join(import.meta.dirname, 'test-temp-fmt-')); - // Prevent repository-level ignore rules from affecting the fixture. - mkdirSync(path.join(projectPath, '.git')); - writeProjectFile('rstack.config.ts', 'export {};\n'); -}); - -afterEach(() => { - rmSync(projectPath, { force: true, recursive: true }); -}); - -test('displays fmt help without loading config', () => { - writeProjectFile('rstack.config.ts', 'throw new Error("must not load");\n'); - - const topLevel = runCLI(['--help']); - const fmt = runFmt(['--help']); - - expect(topLevel.status).toBe(0); - expect(topLevel.stdout).toContain('fmt, format Format code'); - expect(fmt.status).toBe(0); - expect(fmt.stdout).toContain('Usage:\n $ rs fmt [options] [files/globs...]'); - expect(fmt.stderr).toBe(''); -}); - -test('supports format as an alias for fmt', () => { - writeProjectFile('index.ts', 'const message="hello"'); - - const result = runCLI(['format', 'index.ts']); - - expect(result.status).toBe(0); - expectWriteSummary(result.stdout, 1, 1); - expect(result.stderr).toBe(''); - expect(readProjectFile('index.ts')).toBe('const message = "hello";\n'); -}); - -test('returns exit code 2 for invalid arguments', () => { - const result = runFmt(['--write', '--check']); - - expect(result.status).toBe(2); - expect(result.stdout).toBe(''); - expect(result.stderr).toContain( - 'The --write, --check, and --list-different options cannot be used together.', - ); -}); - -test('returns exit code 2 for unknown options', () => { - const result = runFmt(['--bogus']); - - expect(result.status).toBe(2); - expect(result.stdout).toBe(''); - expect(result.stderr).toContain('--bogus'); -}); - -test('formats the current directory with Prettier defaults', () => { - writeProjectFile('index.ts', 'const message="hello"'); - - const result = runFmt(); - - expect(result.status).toBe(0); - expectWriteSummary(result.stdout, 2, 1); - expect(result.stderr).toBe(''); - expect(readProjectFile('index.ts')).toBe('const message = "hello";\n'); -}); - -test('accepts -w as an alias for --write', () => { - writeProjectFile('index.ts', 'const message="hello"'); - - const result = runFmt(['-w', 'index.ts']); - - expect(result.status).toBe(0); - expectWriteSummary(result.stdout, 1, 1); - expect(result.stderr).toBe(''); - expect(readProjectFile('index.ts')).toBe('const message = "hello";\n'); -}); - -test('formats files in node_modules with --with-node-modules', () => { - const source = 'const message="hello"'; - writeProjectFile('node_modules/example/index.ts', source); - - const skipped = runFmt(['node_modules/example']); - expect(skipped.status).toBe(2); - expect(readProjectFile('node_modules/example/index.ts')).toBe(source); - - const result = runFmt(['--with-node-modules', 'node_modules/example']); - expect(result.status).toBe(0); - expectWriteSummary(result.stdout, 1, 1); - expect(result.stderr).toBe(''); - expect(readProjectFile('node_modules/example/index.ts')).toBe('const message = "hello";\n'); -}); - -test('summarizes write mode when no files change', () => { - writeProjectFile('index.ts', 'const message = "hello";\n'); - - const result = runFmt(['index.ts']); - - expect(result.status).toBe(0); - expectWriteSummary(result.stdout, 1, 0); - expect(result.stderr).toBe(''); -}); - -test.each([ - ['write', []], - ['check', ['--check']], - ['list-different', ['--list-different']], -] as const)('uses the default cache in %s mode', (_, args) => { - writeProjectFile('index.ts', 'const value = 1;\n'); - writeProjectFile('.rstack/cache/fmt-v1.json', 'legacy'); - - const result = runFmt([...args, 'index.ts']); - - expect(result.status).toBe(0); - expect(readProjectFile('.rstack/cache/.gitignore')).toBe('*\n'); - expect(JSON.parse(readProjectFile('.rstack/cache/fmt/v1.json'))).toMatchObject({ - version: 1, - files: { - 'index.ts': [expect.any(String), expect.any(String), 'clean'], - }, - }); - expect(readProjectFile('.rstack/cache/fmt-v1.json')).toBe('legacy'); -}); - -test('--no-cache bypasses cache reads and writes', () => { - writeProjectFile('index.ts', 'const value=1'); - writeProjectFile('custom-cache/v1.json', '{"value":true}'); - - const first = runFmt([ - '--no-cache', - '--cache-location', - 'custom-cache', - 'index.ts', - 'custom-cache/v1.json', - ]); - - expect(first.status).toBe(0); - expect(readProjectFile('custom-cache/v1.json')).toBe('{ "value": true }\n'); - expect(existsSync(path.join(projectPath, '.rstack'))).toBe(false); - - writeProjectFile('.rstack/cache/fmt-v1.json', 'stale'); - writeProjectFile('index.ts', 'const value=2'); - const second = runFmt(['--no-cache', 'index.ts']); - - expect(second.status).toBe(0); - expect(readProjectFile('index.ts')).toBe('const value = 2;\n'); - expect(readProjectFile('.rstack/cache/fmt-v1.json')).toBe('stale'); - expect(existsSync(path.join(projectPath, '.rstack/cache/.gitignore'))).toBe(false); -}); - -test.each(['relative', 'absolute'] as const)('uses a %s custom cache location', (kind) => { - const cacheDir = path.join(projectPath, 'custom-cache'); - const cacheLocation = kind === 'relative' ? path.relative(projectPath, cacheDir) : cacheDir; - const cachePath = path.join(cacheDir, 'v1.json'); - writeProjectFile('index.ts', 'const value = 1;\n'); - - const result = runFmt(['--cache-location', cacheLocation, 'index.ts']); - - expect(result.status).toBe(0); - expect(JSON.parse(readFileSync(cachePath, 'utf8'))).toMatchObject({ - version: 1, - files: { - 'index.ts': [expect.any(String), expect.any(String), 'clean'], - }, - }); - expect(existsSync(path.join(projectPath, 'custom-cache/.gitignore'))).toBe(false); - expect(existsSync(path.join(projectPath, '.rstack'))).toBe(false); -}); - -test.each(['.', '..'])('rejects a custom cache location at %s', (cacheLocation) => { - const result = runFmt(['--cache-location', cacheLocation, '.']); - - expect(result.status).toBe(2); - expect(result.stdout).toBe(''); - expect(result.stderr).toContain( - 'The --cache-location directory cannot be the current working directory or an ancestor.', - ); -}); - -test('excludes the custom cache directory from formatting', () => { - const cacheLocation = 'custom-cache'; - writeProjectFile('index.ts', 'const value = 1;\n'); - writeProjectFile('custom-cache/nested/ignored.ts', 'const value=2'); - expect(runFmt(['--cache-location', cacheLocation, 'index.ts']).status).toBe(0); - - const result = runFmt(['--cache-location', cacheLocation, '.']); - - expect(result.status).toBe(0); - expectWriteSummary(result.stdout, 2, 0); - expect(readProjectFile('custom-cache/nested/ignored.ts')).toBe('const value=2'); -}); - -test('uses an explicit config root cache from a subdirectory', () => { - const appPath = path.join(projectPath, 'packages/app'); - writeProjectFile('packages/app/index.ts', 'const value=1'); - - const result = runFmt(['index.ts', '--config', '../../rstack.config.ts'], appPath); - - expect(result.status).toBe(0); - expect(readProjectFile('packages/app/index.ts')).toBe('const value = 1;\n'); - expect(existsSync(path.join(projectPath, '.rstack/cache/fmt/v1.json'))).toBe(true); - expect(existsSync(path.join(appPath, '.rstack'))).toBe(false); - expect(JSON.parse(readProjectFile('.rstack/cache/fmt/v1.json'))).toMatchObject({ - files: { - 'packages/app/index.ts': [expect.any(String), expect.any(String), 'clean'], - }, - }); -}); - -test('recovers from a corrupted cache', () => { - writeProjectFile('index.ts', 'const value = 1;\n'); - const first = runFmt(['--check', 'index.ts']); - writeProjectFile('.rstack/cache/fmt/v1.json', '{'); - - const second = runFmt(['--check', 'index.ts']); - - expect(second.status).toBe(0); - expect(normalizeDuration(second.stdout)).toBe(normalizeDuration(first.stdout)); - expect(second.stderr).toBe(first.stderr); - expect(JSON.parse(readProjectFile('.rstack/cache/fmt/v1.json'))).toMatchObject({ version: 1 }); -}); - -test('formats without a writable cache directory', () => { - writeProjectFile('.rstack', 'not a directory'); - writeProjectFile('index.ts', 'const value=1'); - - const result = runFmt(['index.ts']); - - expect(result.status).toBe(0); - expect(readProjectFile('index.ts')).toBe('const value = 1;\n'); -}); - -test('does not sort package.json by default', () => { - writeProjectFile('package.json', packageJsonSource); - - const result = runFmt(['package.json']); - - expect(result.status).toBe(0); - expect(readProjectFile('package.json')).toContain( - '"dependencies": {\n "z": "1.0.0",\n "a": "1.0.0"', - ); -}); - -test('sorts package.json with workers', () => { - writeProjectFile( - 'rstack.config.ts', - `import { define } from 'rstack'; - -define.fmt({ sortPackageJson: true }); -`, - ); - writeProjectFile('package.json', packageJsonSource); - writeProjectFile('packages/example/package.json', packageJsonSource); - - const result = runFmt(['package.json', 'packages/example/package.json']); - - expect(result.status).toBe(0); - expect(result.stderr).toBe(''); - expect(readProjectFile('package.json')).toBe(sortedPackageJson); - expect(readProjectFile('packages/example/package.json')).toBe(sortedPackageJson); -}); - -test('supports configuring the worker count', () => { - writeProjectFile('first.ts', 'const first="first"'); - writeProjectFile('second.ts', 'const second="second"'); - - const result = runFmt(['--parallel-workers', '1', 'first.ts', 'second.ts']); - - expect(result.status).toBe(0); - expectWriteSummary(result.stdout, 2, 2); - expect(result.stderr).toBe(''); - expect(readProjectFile('first.ts')).toBe('const first = "first";\n'); - expect(readProjectFile('second.ts')).toBe('const second = "second";\n'); -}); - -test('does not load Prettier config or ignore files', () => { - writeProjectFile('.prettierrc.json', '{ "singleQuote": true, "semi": false }\n'); - writeProjectFile('.prettierignore', 'index.ts\n'); - writeProjectFile('.editorconfig', 'root = true\n\n[*]\nindent_style = space\nindent_size = 8\n'); - writeProjectFile('index.ts', "function getMessage(){\n return 'hello'\n}"); - - const result = runFmt(['index.ts']); - - expect(result.status).toBe(0); - expectWriteSummary(result.stdout, 1, 1); - expect(result.stderr).toBe(''); - expect(readProjectFile('index.ts')).toBe('function getMessage() {\n return "hello";\n}\n'); -}); - -test('applies repeated ignore paths', () => { - writeProjectFile('.prettierignore', 'src/ignored-by-root.ts\n'); - writeProjectFile('config/extra.ignore', '../src/ignored-by-extra.ts\n'); - writeProjectFile('src/ignored-by-root.ts', 'const root="ignored"'); - writeProjectFile('src/ignored-by-extra.ts', 'const extra="ignored"'); - writeProjectFile('src/index.ts', 'const index="formatted"'); - - const result = runFmt([ - '--ignore-path', - '.prettierignore', - '--ignore-path=config/extra.ignore', - 'src/ignored-by-root.ts', - 'src/ignored-by-extra.ts', - 'src/index.ts', - ]); - - expect(result.status).toBe(0); - expectWriteSummary(result.stdout, 1, 1); - expect(result.stderr).toBe(''); - expect(readProjectFile('src/ignored-by-root.ts')).toBe('const root="ignored"'); - expect(readProjectFile('src/ignored-by-extra.ts')).toBe('const extra="ignored"'); - expect(readProjectFile('src/index.ts')).toBe('const index = "formatted";\n'); -}); - -test('returns exit code 2 for an unreadable ignore path', () => { - writeProjectFile('index.ts', 'const value=true'); - - const result = runFmt(['--ignore-path', 'missing.ignore', 'index.ts']); - - expect(result.status).toBe(2); - expect(result.stdout).toBe(''); - expect(result.stderr).toContain('Failed to read ignore file "missing.ignore".'); - expect(readProjectFile('index.ts')).toBe('const value=true'); -}); - -test('applies define.fmt options, overrides, ignore patterns, and globs', () => { - writeProjectFile( - 'rstack.config.ts', - `import { define } from 'rstack'; - -define.fmt({ - singleQuote: true, - ignorePatterns: ['src/ignored.ts'], - overrides: [ - { - files: '*.test.ts', - options: { - semi: false, - }, - }, - ], -}); -`, - ); - writeProjectFile('src/index.ts', 'const message="hello"'); - writeProjectFile('src/index.test.ts', 'const test="test"'); - writeProjectFile('src/ignored.ts', 'const ignored="ignored"'); - writeProjectFile('src/index.js', 'const javascript="untouched"'); - - const result = runFmt(['--write', 'src/**/*.ts']); - - expect(result.status).toBe(0); - expectWriteSummary(result.stdout, 2, 2); - expect(result.stderr).toBe(''); - expect(readProjectFile('src/index.ts')).toBe("const message = 'hello';\n"); - expect(readProjectFile('src/index.test.ts')).toBe("const test = 'test'\n"); - expect(readProjectFile('src/ignored.ts')).toBe('const ignored="ignored"'); - expect(readProjectFile('src/index.js')).toBe('const javascript="untouched"'); -}); - -test('uses an explicit Rstack config', () => { - writeProjectFile( - 'custom.config.ts', - `import { define } from 'rstack'; - -define.fmt({ - singleQuote: true, -}); -`, - ); - writeProjectFile('index.ts', 'const message="hello"'); - - const result = runFmt(['index.ts', '--config', 'custom.config.ts']); - - expect(result.status).toBe(0); - expectWriteSummary(result.stdout, 1, 1); - expect(result.stderr).toBe(''); - expect(readProjectFile('index.ts')).toBe("const message = 'hello';\n"); -}); - -test('checks formatting without writing files', () => { - const source = 'const message="hello"'; - writeProjectFile('index.ts', source); - - const result = runFmt(['--check', 'index.ts']); - - expect(result.status).toBe(1); - expect(normalizeDuration(result.stdout)).toBe( - 'start Checking formatting...\ninfo Checked 1 file in .\n', - ); - expect(result.stderr).toContain('error index.ts'); - expect(normalizeDuration(result.stderr)).toContain( - 'error Formatting issues found in 1 file. Run without --check to fix.', - ); - expect(readProjectFile('index.ts')).toBe(source); - - writeProjectFile('index.ts', 'const message = "hello";\n'); - const formattedResult = runFmt(['--check', 'index.ts']); - - expect(formattedResult.status).toBe(0); - expect(normalizeDuration(formattedResult.stdout)).toBe( - 'start Checking formatting...\nsuccess Checked 1 file in . No issues found.\n', - ); - expect(formattedResult.stderr).toBe(''); -}); - -test.each(['-l', '--list-different'])('lists only paths that differ with %s', (option) => { - const source = 'const message="hello"'; - writeProjectFile('src/index.ts', source); - writeProjectFile('src/formatted.ts', 'const formatted = true;\n'); - - const result = runFmt([option, 'src/*.ts']); - - expect(result.status).toBe(1); - expect(result.stdout).toBe('src/index.ts\n'); - expect(result.stderr).toBe(''); - expect(readProjectFile('src/index.ts')).toBe(source); -}); - -test('returns exit code 2 for config errors', () => { - writeProjectFile('rstack.config.ts', 'throw new Error("invalid fmt config");\n'); - - const result = runFmt(['index.ts']); - - expect(result.status).toBe(2); - expect(result.stdout).toBe(''); - expect(result.stderr).toContain('invalid fmt config'); -}); - -test('formats with a project-local plugin in workers', () => { - writeProjectFile( - 'rstack.config.ts', - `import { define } from 'rstack'; - -define.fmt({ - plugins: ['prettier-plugin-fixture'], -}); -`, - ); - writeFixturePlugin(); - writeProjectFile('first.fixture', '{"first":true}'); - writeProjectFile('second.fixture', '{"second":true}'); - - const result = runFmt(['*.fixture']); - - expect(result.status).toBe(0); - expectWriteSummary(result.stdout, 2, 2); - expect(result.stderr).toBe(''); - expect(readProjectFile('first.fixture')).toBe('{ "first": true }\n'); - expect(readProjectFile('second.fixture')).toBe('{ "second": true }\n'); -}); - -test('formats mixed plugin overrides in workers', () => { - writeProjectFile( - 'rstack.config.ts', - `import { define } from 'rstack'; - -define.fmt({ - overrides: [ - { - files: '*.fixture', - options: { plugins: ['prettier-plugin-fixture'] }, - }, - ], -}); -`, - ); - writeFixturePlugin(); - writeProjectFile('data.fixture', '{"value":true}'); - writeProjectFile('index.ts', 'const value=true'); - - const result = runFmt(['data.fixture', 'index.ts']); - - expect(result.status).toBe(0); - expectWriteSummary(result.stdout, 2, 2); - expect(result.stderr).toBe(''); - expect(readProjectFile('data.fixture')).toBe('{ "value": true }\n'); - expect(readProjectFile('index.ts')).toBe('const value = true;\n'); -}); - -test('returns exit code 2 for imported plugin objects', () => { - writeProjectFile( - 'rstack.config.ts', - `import { define } from 'rstack'; - -define.fmt({ - plugins: [{ languages: [] }], -}); -`, - ); - writeProjectFile('index.ts', 'const value=true'); - - const result = runFmt(['index.ts']); - - expect(result.status).toBe(2); - expect(result.stdout).toBe(''); - expect(result.stderr).toContain( - 'Prettier plugin objects are not supported. Use a package name, path, or URL instead.', - ); -}); - -test('returns exit code 2 for formatting errors', () => { - writeProjectFile('index.ts', 'const value = ;'); - - const result = runFmt(['index.ts']); - - expect(result.status).toBe(2); - expect(result.stdout).toBe(''); - expect(result.stderr).toContain('error index.ts: SyntaxError:'); -}); - -test('reports partial writes when formatting fails', () => { - writeProjectFile('valid.ts', 'const value=true'); - writeProjectFile('invalid.ts', 'const invalid = ;'); - - const result = runFmt(['valid.ts', 'invalid.ts']); - - expect(result.status).toBe(2); - expect(normalizeDuration(result.stdout)).toBe('info Formatted 1 of 2 files in .\n'); - expect(result.stderr).toContain('error invalid.ts: SyntaxError:'); - expect(readProjectFile('valid.ts')).toBe('const value = true;\n'); - expect(readProjectFile('invalid.ts')).toBe('const invalid = ;'); -}); - -test('formats stdin for the given filepath', () => { - const result = runFmtStdin(['--stdin-filepath', 'src/index.ts'], 'const message="hello"'); - - expect(result.status).toBe(0); - expect(result.stdout).toBe('const message = "hello";\n'); - expect(result.stderr).toBe(''); - expect(existsSync(path.join(projectPath, '.rstack'))).toBe(false); -}); - -test('applies define.fmt options and overrides to stdin', () => { - writeProjectFile( - 'rstack.config.ts', - `import { define } from 'rstack'; - -define.fmt({ - singleQuote: true, - overrides: [ - { - files: '*.test.ts', - options: { - semi: false, - }, - }, - ], -}); -`, - ); - - const result = runFmtStdin(['--stdin-filepath', 'src/index.test.ts'], 'const test="test"'); - - expect(result.status).toBe(0); - expect(result.stdout).toBe("const test = 'test'\n"); - expect(result.stderr).toBe(''); -}); - -test('sorts package.json from stdin', () => { - writeProjectFile( - 'rstack.config.ts', - `import { define } from 'rstack'; - -define.fmt({ sortPackageJson: true }); -`, - ); - - const result = runFmtStdin(['--stdin-filepath', 'package.json'], packageJsonSource); - - expect(result.status).toBe(0); - expect(result.stdout).toBe(sortedPackageJson); - expect(result.stderr).toBe(''); -}); - -test('echoes ignored stdin paths verbatim', () => { - writeProjectFile( - 'rstack.config.ts', - `import { define } from 'rstack'; - -define.fmt({ ignorePatterns: ['src/ignored.ts'] }); -`, - ); - - const source = 'const ignored="ignored"'; - const result = runFmtStdin(['--stdin-filepath', 'src/ignored.ts'], source); - - expect(result.status).toBe(0); - expect(result.stdout).toBe(source); - expect(result.stderr).toBe(''); -}); - -test('echoes stdin paths ignored by --ignore-path', () => { - writeProjectFile('.prettierignore', 'src/ignored.ts\n'); - - const source = 'const ignored="ignored"'; - const result = runFmtStdin( - ['--ignore-path', '.prettierignore', '--stdin-filepath', 'src/ignored.ts'], - source, - ); - - expect(result.status).toBe(0); - expect(result.stdout).toBe(source); - expect(result.stderr).toBe(''); -}); - -test('echoes stdin for default ignored lock files', () => { - const source = 'lockfileVersion: "9.0"\n'; - const result = runFmtStdin(['--stdin-filepath', 'pnpm-lock.yaml'], source); - - expect(result.status).toBe(0); - expect(result.stdout).toBe(source); - expect(result.stderr).toBe(''); -}); - -test('returns exit code 2 when no parser can be inferred for stdin', () => { - const result = runFmtStdin(['--stdin-filepath', 'data.unknown'], 'value'); - - expect(result.status).toBe(2); - expect(result.stdout).toBe(''); - expect(result.stderr).toContain('No parser could be inferred for "data.unknown".'); -}); - -test('ignores stdin when no parser can be inferred with --ignore-unknown', () => { - const result = runFmtStdin(['--stdin-filepath', 'data.unknown', '--ignore-unknown'], 'value'); - - expect(result.status).toBe(0); - expect(result.stdout).toBe(''); - expect(result.stderr).toBe(''); -}); - -test('returns exit code 2 for stdin parse errors', () => { - const result = runFmtStdin(['--stdin-filepath', 'index.ts'], 'const value = ;'); - - expect(result.status).toBe(2); - expect(result.stdout).toBe(''); - expect(result.stderr).toContain("Unexpected token ';'"); -}); - -test.each(['--write', '--check', '--list-different'])( - 'returns exit code 2 for %s with --stdin-filepath', - (option) => { - const result = runFmtStdin(['--stdin-filepath', 'index.ts', option], 'const value=1'); - - expect(result.status).toBe(2); - expect(result.stdout).toBe(''); - expect(result.stderr).toContain( - 'The --stdin-filepath option cannot be used with --write, --check, or --list-different.', - ); - }, -); - -test('returns exit code 2 for file arguments with --stdin-filepath', () => { - const result = runFmtStdin(['--stdin-filepath', 'index.ts', 'src/other.ts'], 'const value=1'); - - expect(result.status).toBe(2); - expect(result.stdout).toBe(''); - expect(result.stderr).toContain( - 'The --stdin-filepath option cannot be used with file arguments.', - ); -}); - -test('accepts --parallel-workers with --stdin-filepath', () => { - const result = runFmtStdin( - ['--stdin-filepath', 'index.ts', '--parallel-workers', '2'], - 'const value=1', - ); - - expect(result.status).toBe(0); - expect(result.stdout).toBe('const value = 1;\n'); - expect(result.stderr).toBe(''); -}); - -test('writes nothing for empty stdin', () => { - const result = runFmtStdin(['--stdin-filepath', 'index.ts'], ''); - - expect(result.status).toBe(0); - expect(result.stdout).toBe(''); - expect(result.stderr).toBe(''); -}); - -test('returns exit code 2 when no files match', () => { - for (const modeArgs of [[], ['--check'], ['--list-different']]) { - const result = runFmt([...modeArgs, 'missing/**/*.ts']); - - expect(result.status).toBe(2); - expect(result.stdout).toBe(''); - expect(result.stderr).toContain( - 'No supported files matched "missing/**/*.ts", or all matching files were ignored.', - ); - expect(result.stderr).not.toContain('\n at '); - } - expect(existsSync(path.join(projectPath, '.rstack'))).toBe(false); -}); - -test('allows no files to match with --no-error-on-unmatched-pattern', () => { - for (const modeArgs of [[], ['--check'], ['--list-different']]) { - const result = runFmt([...modeArgs, '--no-error-on-unmatched-pattern', 'missing/**/*.ts']); - - expect(result.status).toBe(0); - expect(result.stdout).toBe(''); - expect(result.stderr).toBe(''); - } -}); - -test('counts only supported files', () => { - writeProjectFile('index.ts', 'const value = 1;\n'); - writeProjectFile('notes.unknown', 'plain text'); - - const result = runFmt(['--check', 'index.ts', 'notes.unknown']); - - expect(result.status).toBe(0); - expect(normalizeDuration(result.stdout)).toBe( - 'start Checking formatting...\nsuccess Checked 1 file in . No issues found.\n', - ); - expect(result.stderr).toBe(''); -}); - -test('returns exit code 2 when all matched files are unsupported', () => { - writeProjectFile('notes.unknown', 'plain text'); - - for (const modeArgs of [[], ['--check'], ['--list-different']]) { - const result = runFmt([...modeArgs, 'notes.unknown']); - - expect(result.status).toBe(2); - expect(result.stdout).not.toContain('success'); - expect(result.stderr).toContain( - 'No supported files matched "notes.unknown", or all matching files were ignored.', - ); - expect(result.stderr).not.toContain('\n at '); - } -}); - -test('ignores unsupported files with --ignore-unknown', () => { - writeProjectFile('notes.unknown', 'plain text'); - - for (const modeArgs of [[], ['--check'], ['--list-different']]) { - const result = runFmt([...modeArgs, '--ignore-unknown', 'notes.unknown']); - - expect(result.status).toBe(0); - expect(result.stdout).toBe( - modeArgs.includes('--check') - ? 'start Checking formatting...\nsuccess No supported files to check.\n' - : '', - ); - expect(result.stderr).toBe(''); - } -}); - -test('supports -u as an alias for --ignore-unknown', () => { - writeProjectFile('notes.unknown', 'plain text'); - - const result = runFmt(['-u', 'notes.unknown']); - - expect(result.status).toBe(0); - expect(result.stdout).toBe(''); - expect(result.stderr).toBe(''); -}); - -test('does not treat unmatched patterns as unknown files', () => { - const result = runFmt(['--ignore-unknown', 'missing/**/*.unknown']); - - expect(result.status).toBe(2); - expect(result.stdout).toBe(''); - expect(result.stderr).toContain('No supported files matched "missing/**/*.unknown"'); -}); - -test('does not treat unsupported files as unmatched patterns', () => { - writeProjectFile('notes.unknown', 'plain text'); - - const result = runFmt(['--no-error-on-unmatched-pattern', 'notes.unknown']); - - expect(result.status).toBe(2); - expect(result.stdout).toBe(''); - expect(result.stderr).toContain('No supported files matched "notes.unknown"'); -}); diff --git a/packages/rstack/tests/cli/fmt/patterns.test.ts b/packages/rstack/tests/cli/fmt/patterns.test.ts new file mode 100644 index 00000000..52ec4480 --- /dev/null +++ b/packages/rstack/tests/cli/fmt/patterns.test.ts @@ -0,0 +1,101 @@ +import { expect, test } from 'rstack/test'; +import { normalizeDuration, setupFmtTest } from './helpers.ts'; + +const { projectFileExists, runFmt, writeProjectFile } = setupFmtTest(); + +test('returns exit code 2 when no files match', () => { + for (const modeArgs of [[], ['--check'], ['--list-different']]) { + const result = runFmt([...modeArgs, 'missing/**/*.ts']); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain( + 'No supported files matched "missing/**/*.ts", or all matching files were ignored.', + ); + expect(result.stderr).not.toContain('\n at '); + } + expect(projectFileExists('.rstack')).toBe(false); +}); + +test('allows no files to match with --no-error-on-unmatched-pattern', () => { + for (const modeArgs of [[], ['--check'], ['--list-different']]) { + const result = runFmt([...modeArgs, '--no-error-on-unmatched-pattern', 'missing/**/*.ts']); + + expect(result.status).toBe(0); + expect(result.stdout).toBe(''); + expect(result.stderr).toBe(''); + } +}); + +test('counts only supported files', () => { + writeProjectFile('index.ts', 'const value = 1;\n'); + writeProjectFile('notes.unknown', 'plain text'); + + const result = runFmt(['--check', 'index.ts', 'notes.unknown']); + + expect(result.status).toBe(0); + expect(normalizeDuration(result.stdout)).toBe( + 'start Checking formatting...\nsuccess Checked 1 file in . No issues found.\n', + ); + expect(result.stderr).toBe(''); +}); + +test('returns exit code 2 when all matched files are unsupported', () => { + writeProjectFile('notes.unknown', 'plain text'); + + for (const modeArgs of [[], ['--check'], ['--list-different']]) { + const result = runFmt([...modeArgs, 'notes.unknown']); + + expect(result.status).toBe(2); + expect(result.stdout).not.toContain('success'); + expect(result.stderr).toContain( + 'No supported files matched "notes.unknown", or all matching files were ignored.', + ); + expect(result.stderr).not.toContain('\n at '); + } +}); + +test('ignores unsupported files with --ignore-unknown', () => { + writeProjectFile('notes.unknown', 'plain text'); + + for (const modeArgs of [[], ['--check'], ['--list-different']]) { + const result = runFmt([...modeArgs, '--ignore-unknown', 'notes.unknown']); + + expect(result.status).toBe(0); + const expectedStdout = modeArgs.includes('--check') + ? 'start Checking formatting...\nsuccess No supported files to check.\n' + : modeArgs.includes('--list-different') + ? '' + : 'start Formatting...\nsuccess No supported files to format.\n'; + expect(result.stdout).toBe(expectedStdout); + expect(result.stderr).toBe(''); + } +}); + +test('supports -u as an alias for --ignore-unknown', () => { + writeProjectFile('notes.unknown', 'plain text'); + + const result = runFmt(['-u', 'notes.unknown']); + + expect(result.status).toBe(0); + expect(result.stdout).toBe('start Formatting...\nsuccess No supported files to format.\n'); + expect(result.stderr).toBe(''); +}); + +test('does not treat unmatched patterns as unknown files', () => { + const result = runFmt(['--ignore-unknown', 'missing/**/*.unknown']); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain('No supported files matched "missing/**/*.unknown"'); +}); + +test('does not treat unsupported files as unmatched patterns', () => { + writeProjectFile('notes.unknown', 'plain text'); + + const result = runFmt(['--no-error-on-unmatched-pattern', 'notes.unknown']); + + expect(result.status).toBe(2); + expect(result.stdout).toBe('start Formatting...\n'); + expect(result.stderr).toContain('No supported files matched "notes.unknown"'); +}); diff --git a/packages/rstack/tests/cli/fmt/stdin.test.ts b/packages/rstack/tests/cli/fmt/stdin.test.ts new file mode 100644 index 00000000..21e5a4c5 --- /dev/null +++ b/packages/rstack/tests/cli/fmt/stdin.test.ts @@ -0,0 +1,161 @@ +import { expect, test } from 'rstack/test'; +import { packageJsonSource, setupFmtTest, sortedPackageJson } from './helpers.ts'; + +const { projectFileExists, runFmtStdin, writeProjectFile } = setupFmtTest(); + +test('formats stdin for the given filepath', () => { + const result = runFmtStdin(['--stdin-filepath', 'src/index.ts'], 'const message="hello"'); + + expect(result.status).toBe(0); + expect(result.stdout).toBe('const message = "hello";\n'); + expect(result.stderr).toBe(''); + expect(projectFileExists('.rstack')).toBe(false); +}); + +test('applies define.fmt options and overrides to stdin', () => { + writeProjectFile( + 'rstack.config.ts', + `import { define } from 'rstack'; + +define.fmt({ + singleQuote: true, + overrides: [ + { + files: '*.test.ts', + options: { + semi: false, + }, + }, + ], +}); +`, + ); + + const result = runFmtStdin(['--stdin-filepath', 'src/index.test.ts'], 'const test="test"'); + + expect(result.status).toBe(0); + expect(result.stdout).toBe("const test = 'test'\n"); + expect(result.stderr).toBe(''); +}); + +test('sorts package.json from stdin', () => { + writeProjectFile( + 'rstack.config.ts', + `import { define } from 'rstack'; + +define.fmt({ sortPackageJson: true }); +`, + ); + + const result = runFmtStdin(['--stdin-filepath', 'package.json'], packageJsonSource); + + expect(result.status).toBe(0); + expect(result.stdout).toBe(sortedPackageJson); + expect(result.stderr).toBe(''); +}); + +test('echoes ignored stdin paths verbatim', () => { + writeProjectFile( + 'rstack.config.ts', + `import { define } from 'rstack'; + +define.fmt({ ignorePatterns: ['src/ignored.ts'] }); +`, + ); + + const source = 'const ignored="ignored"'; + const result = runFmtStdin(['--stdin-filepath', 'src/ignored.ts'], source); + + expect(result.status).toBe(0); + expect(result.stdout).toBe(source); + expect(result.stderr).toBe(''); +}); + +test('echoes stdin paths ignored by --ignore-path', () => { + writeProjectFile('.prettierignore', 'src/ignored.ts\n'); + + const source = 'const ignored="ignored"'; + const result = runFmtStdin( + ['--ignore-path', '.prettierignore', '--stdin-filepath', 'src/ignored.ts'], + source, + ); + + expect(result.status).toBe(0); + expect(result.stdout).toBe(source); + expect(result.stderr).toBe(''); +}); + +test('echoes stdin for default ignored lock files', () => { + const source = 'lockfileVersion: "9.0"\n'; + const result = runFmtStdin(['--stdin-filepath', 'pnpm-lock.yaml'], source); + + expect(result.status).toBe(0); + expect(result.stdout).toBe(source); + expect(result.stderr).toBe(''); +}); + +test('returns exit code 2 when no parser can be inferred for stdin', () => { + const result = runFmtStdin(['--stdin-filepath', 'data.unknown'], 'value'); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain('No parser could be inferred for "data.unknown".'); +}); + +test('ignores stdin when no parser can be inferred with --ignore-unknown', () => { + const result = runFmtStdin(['--stdin-filepath', 'data.unknown', '--ignore-unknown'], 'value'); + + expect(result.status).toBe(0); + expect(result.stdout).toBe(''); + expect(result.stderr).toBe(''); +}); + +test('returns exit code 2 for stdin parse errors', () => { + const result = runFmtStdin(['--stdin-filepath', 'index.ts'], 'const value = ;'); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain("Unexpected token ';'"); +}); + +test.each(['--write', '--check', '--list-different'])( + 'returns exit code 2 for %s with --stdin-filepath', + (option) => { + const result = runFmtStdin(['--stdin-filepath', 'index.ts', option], 'const value=1'); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain( + 'The --stdin-filepath option cannot be used with --write, --check, or --list-different.', + ); + }, +); + +test('returns exit code 2 for file arguments with --stdin-filepath', () => { + const result = runFmtStdin(['--stdin-filepath', 'index.ts', 'src/other.ts'], 'const value=1'); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain( + 'The --stdin-filepath option cannot be used with file arguments.', + ); +}); + +test('accepts --parallel-workers with --stdin-filepath', () => { + const result = runFmtStdin( + ['--stdin-filepath', 'index.ts', '--parallel-workers', '2'], + 'const value=1', + ); + + expect(result.status).toBe(0); + expect(result.stdout).toBe('const value = 1;\n'); + expect(result.stderr).toBe(''); +}); + +test('writes nothing for empty stdin', () => { + const result = runFmtStdin(['--stdin-filepath', 'index.ts'], ''); + + expect(result.status).toBe(0); + expect(result.stdout).toBe(''); + expect(result.stderr).toBe(''); +}); diff --git a/packages/rstack/tests/cli/help.test.ts b/packages/rstack/tests/cli/help.test.ts new file mode 100644 index 00000000..09717ba0 --- /dev/null +++ b/packages/rstack/tests/cli/help.test.ts @@ -0,0 +1,56 @@ +import { normalizeHelpOutput, test } from '#test-helpers'; +import { hasHelpFlag } from '../../src/cli/help.ts'; + +test('detects help flags before the option terminator', ({ expect }) => { + expect(hasHelpFlag(['dev', '-h'])).toBe(true); + expect(hasHelpFlag(['dev', '--help'])).toBe(true); + expect(hasHelpFlag(['dev', '--', '--help'])).toBe(false); + expect(hasHelpFlag(['dev'])).toBe(false); +}); + +test('displays top-level help', ({ execCli, expect }) => { + expect(normalizeHelpOutput(execCli('--help'))).toMatchSnapshot(); +}); + +for (const command of ['dev', 'build', 'preview', 'lint']) { + test(`displays ${command} help`, ({ execCli, expect }) => { + const output = execCli(`${command} --help`); + + expect(execCli(`${command} -h`)).toBe(output); + expect(normalizeHelpOutput(output)).toMatchSnapshot(); + }); +} + +for (const command of ['lib', 'lib build', 'lib inspect', 'lib mf-dev']) { + test(`displays ${command} help`, ({ execCli, expect }) => { + const output = execCli(`${command} --help`); + + expect(execCli(`${command} -h`)).toBe(output); + expect(normalizeHelpOutput(output)).toMatchSnapshot(); + }); +} + +for (const command of ['doc', 'doc build', 'doc preview', 'doc eject']) { + test(`displays ${command} help`, ({ execCli, expect }) => { + const output = execCli(`${command} --help`); + + expect(execCli(`${command} -h`)).toBe(output); + expect(normalizeHelpOutput(output)).toMatchSnapshot(); + }); +} + +for (const command of [ + 'test', + 'test run', + 'test watch', + 'test list', + 'test merge-reports', + 'test init', +]) { + test(`displays ${command} help`, ({ execCli, expect }) => { + const output = execCli(`${command} --help`); + + expect(execCli(`${command} -h`)).toBe(output); + expect(normalizeHelpOutput(output)).toMatchSnapshot(); + }); +} diff --git a/packages/rstack/tests/cli/setup/__snapshots__/index.test.ts.snap b/packages/rstack/tests/cli/setup/__snapshots__/index.test.ts.snap new file mode 100644 index 00000000..d238e172 --- /dev/null +++ b/packages/rstack/tests/cli/setup/__snapshots__/index.test.ts.snap @@ -0,0 +1,15 @@ +// Rstest Snapshot v1 + +exports[`displays setup help 1`] = ` +"Rstack v + +Usage: + $ rs setup [options] + +Install Git hooks in the current repository + +Options: + --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 8a3c3e89..3f29104f 100644 --- a/packages/rstack/tests/cli/setup/index.test.ts +++ b/packages/rstack/tests/cli/setup/index.test.ts @@ -2,7 +2,7 @@ import { spawnSync } from 'node:child_process'; import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import path from 'node:path'; import { afterEach, beforeEach } from 'rstack/test'; -import { RSTACK_BIN_PATH, test } from '#test-helpers'; +import { normalizeHelpOutput, RSTACK_BIN_PATH, test } from '#test-helpers'; const hooksPath = '.rstack/hooks/_'; @@ -44,14 +44,10 @@ afterEach(() => { }); test('displays setup help', ({ execCli, expect }) => { - expect(execCli('--help', { cwd })).toMatch(/setup\s+Install Git hooks/); - const output = execCli('setup --help', { cwd }); expect(execCli('setup -h', { cwd })).toBe(output); - expect(output).toContain('Usage:\n $ rs setup [options]'); - expect(output).toContain('--hooks-dir '); - expect(output).toContain('-h, --help'); + expect(normalizeHelpOutput(output)).toMatchSnapshot(); }); test('rejects unknown setup options', ({ execCli, expect }) => { @@ -76,7 +72,7 @@ test('rejects invalid hooks directory options', ({ expect }) => { const absolute = runSetup(['--hooks-dir', path.join(cwd, 'hooks')]); expect(absolute.status).toBe(1); expect(absolute.stderr).toContain( - 'Git hooks directory must be relative to the current directory.', + 'Git hooks directory must be relative to the Git repository root.', ); const parent = runSetup(['--hooks-dir', '../hooks']); @@ -96,14 +92,22 @@ test('installs hooks silently without loading Rstack config', ({ execCli, expect expect(execCli('setup', { cwd, env })).toBe(''); }); -test('installs a custom hooks directory from a nested project', ({ execCli, expect }) => { +test('installs root-relative hooks and reports owner conflicts', ({ execCli, expect }) => { initRepository(); - const projectDirectory = path.join(cwd, 'frontend'); - mkdirSync(projectDirectory); - - expect(execCli('setup --hooks-dir "custom hooks"', { cwd: projectDirectory, env })).toBe(''); - expect(git(['config', '--local', '--get', 'core.hooksPath'])).toBe('frontend/custom hooks/_'); - expect(existsSync(path.join(projectDirectory, 'custom hooks', '_', 'runner'))).toBe(true); + const frontend = path.join(cwd, 'frontend'); + const docs = path.join(cwd, 'docs'); + mkdirSync(frontend); + mkdirSync(docs); + + expect(execCli('setup --hooks-dir "custom hooks"', { cwd: frontend, env })).toBe(''); + expect(git(['config', '--local', '--get', 'core.hooksPath'])).toBe('custom hooks/_'); + expect(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( + 'Git hooks are already managed by Rstack project "frontend"', + ); }); test('skips non-Git directories without creating files', ({ execCli, expect }) => { diff --git a/packages/rstack/tests/cli/staged/__snapshots__/index.test.ts.snap b/packages/rstack/tests/cli/staged/__snapshots__/index.test.ts.snap new file mode 100644 index 00000000..0cd5e1a6 --- /dev/null +++ b/packages/rstack/tests/cli/staged/__snapshots__/index.test.ts.snap @@ -0,0 +1,23 @@ +// Rstest Snapshot v1 + +exports[`should display the staged help message 1`] = ` +"Rstack v + +Usage: + $ rs staged [options] + +Run tasks on staged Git files + +Options: + --allow-empty Allow empty commits when tasks revert all staged changes + -p, --concurrent The number of tasks to run concurrently, or false for serial + --cwd Working directory to run all tasks in + -d, --debug Print additional debug information + --no-stash Disable backup stash and automatic revert + -q, --quiet Disable lint-staged's own console output + -r, --relative Pass relative filepaths to tasks + -v, --verbose Show task output even when tasks succeed; by default only failed output is shown + -c, --config Specify Rstack config file path + -h, --help Display this help message +" +`; diff --git a/packages/rstack/tests/cli/staged/index.test.ts b/packages/rstack/tests/cli/staged/index.test.ts index d7c0146d..5fb293f1 100644 --- a/packages/rstack/tests/cli/staged/index.test.ts +++ b/packages/rstack/tests/cli/staged/index.test.ts @@ -1,6 +1,6 @@ import lintStaged from 'lint-staged'; import { afterEach, beforeEach, rs } from 'rstack/test'; -import { test } from '#test-helpers'; +import { normalizeHelpOutput, test } from '#test-helpers'; import { loadRstackConfig } from '../../../src/config.ts'; import { runStagedCLI, type StagedConfig } from '../../../src/staged.ts'; @@ -34,17 +34,7 @@ afterEach(() => { test('should display the staged help message', ({ execCli, expect }) => { const output = execCli('staged --help'); - expect(output).toContain('Rstack v'); - expect(output).toContain('Usage:\n $ rs staged [options]'); - expect(output).toContain('Runs lint-staged with tasks from define.staged in rstack.config.'); - expect(output).toContain('--allow-empty'); - expect(output).toContain('--cwd '); - expect(output).toContain('-d, --debug'); - expect(output).toContain('--no-stash'); - expect(output).toContain('-q, --quiet'); - expect(output).toContain('-r, --relative'); - expect(output).toContain('-v, --verbose'); - expect(output).toContain('-h, --help'); + expect(normalizeHelpOutput(output)).toMatchSnapshot(); }); test('should reject unknown staged options', ({ execCli, expect }) => { diff --git a/packages/rstack/tests/config/define-doc/index.test.ts b/packages/rstack/tests/config/define-doc/index.test.ts index 464e7580..58ce1fc4 100644 --- a/packages/rstack/tests/config/define-doc/index.test.ts +++ b/packages/rstack/tests/config/define-doc/index.test.ts @@ -12,4 +12,4 @@ test('should build docs with define.doc config', async ({ prepareDist, execCli, const output = getFileContent(files, 'index.html'); expect(output).toContain(expectedText); -}); +}, 30_000); diff --git a/packages/rstack/tests/fmt/cli.test.ts b/packages/rstack/tests/fmt/cli.test.ts deleted file mode 100644 index 5bd45d27..00000000 --- a/packages/rstack/tests/fmt/cli.test.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { stripVTControlCharacters } from 'node:util'; -import { expect, test } from 'rstack/test'; -import { fmtHelpMessage, parseFmtCLIArgs, prettyTime } from '../../src/fmt/cli.ts'; - -test.each([ - [0, '0.000s'], - [0.009, '0.009s'], - [0.01, '0.01s'], - [9.876, '9.88s'], - [10, '10.0s'], - [59.9, '59.9s'], - [60, '1m'], - [61, '1m 1s'], - [61.25, '1m 1.3s'], - [125.25, '2m 5.3s'], -] as const)('formats %s seconds as %s', (seconds, expected) => { - expect(stripVTControlCharacters(prettyTime(seconds))).toBe(expected); -}); - -test('uses write mode by default', () => { - expect(parseFmtCLIArgs([])).toEqual({ - cache: true, - mode: 'write', - patterns: [], - ignorePaths: [], - ignoreUnknown: false, - noErrorOnUnmatchedPattern: false, - withNodeModules: false, - maxWorkers: undefined, - help: false, - }); -}); - -test.each([ - ['-w', 'write'], - ['--write', 'write'], - ['--check', 'check'], - ['-l', 'list-different'], - ['--list-different', 'list-different'], -] as const)('parses %s mode', (option, mode) => { - expect(parseFmtCLIArgs([option])).toEqual({ - cache: true, - mode, - patterns: [], - ignorePaths: [], - ignoreUnknown: false, - noErrorOnUnmatchedPattern: false, - withNodeModules: false, - maxWorkers: undefined, - help: false, - }); -}); - -test('configures parallel worker count', () => { - expect(parseFmtCLIArgs(['--parallel-workers', '3'])).toEqual({ - cache: true, - mode: 'write', - patterns: [], - ignorePaths: [], - ignoreUnknown: false, - noErrorOnUnmatchedPattern: false, - withNodeModules: false, - maxWorkers: 3, - help: false, - }); -}); - -test.each(['0', '-1', '1.5', 'invalid', '9007199254740992'])( - 'rejects invalid parallel worker count %s', - (count) => { - expect(() => parseFmtCLIArgs([`--parallel-workers=${count}`])).toThrow( - 'The --parallel-workers option must be a positive integer.', - ); - }, -); - -test('preserves file paths and globs', () => { - const patterns = ['src/file with spaces.ts', 'src/**/*.{js,ts}', '!src/generated/**']; - - expect(parseFmtCLIArgs([patterns[0], '--check', ...patterns.slice(1)])).toEqual({ - cache: true, - mode: 'check', - patterns, - ignorePaths: [], - ignoreUnknown: false, - noErrorOnUnmatchedPattern: false, - withNodeModules: false, - maxWorkers: undefined, - help: false, - }); -}); - -test('treats arguments after the terminator as paths', () => { - expect(parseFmtCLIArgs(['--check', '--', '--write', '--help'])).toEqual({ - cache: true, - mode: 'check', - patterns: ['--write', '--help'], - ignorePaths: [], - ignoreUnknown: false, - noErrorOnUnmatchedPattern: false, - withNodeModules: false, - maxWorkers: undefined, - help: false, - }); -}); - -test.each(['--help', '-h'])('parses %s', (option) => { - expect(parseFmtCLIArgs([option]).help).toBe(true); -}); - -test('collects repeated ignore paths', () => { - expect( - parseFmtCLIArgs(['--ignore-path', '.prettierignore', '--ignore-path=config/format.ignore']) - .ignorePaths, - ).toEqual(['.prettierignore', 'config/format.ignore']); -}); - -test('parses --no-error-on-unmatched-pattern', () => { - expect(parseFmtCLIArgs(['--no-error-on-unmatched-pattern']).noErrorOnUnmatchedPattern).toBe(true); -}); - -test.each(['-u', '--ignore-unknown', '--ignoreUnknown'])('parses %s', (option) => { - expect(parseFmtCLIArgs([option]).ignoreUnknown).toBe(true); -}); - -test('parses --no-cache', () => { - expect(parseFmtCLIArgs(['--no-cache']).cache).toBe(false); -}); - -test('parses --cache-location', () => { - expect(parseFmtCLIArgs(['--cache-location', '.cache/fmt']).cacheLocation).toBe('.cache/fmt'); -}); - -test('--no-cache ignores --cache-location', () => { - expect(parseFmtCLIArgs(['--no-cache', '--cache-location='])).toMatchObject({ - cache: false, - cacheLocation: undefined, - }); -}); - -test('rejects an empty cache location', () => { - expect(() => parseFmtCLIArgs(['--cache-location='])).toThrow( - 'The --cache-location option requires a path.', - ); -}); - -test('parses --with-node-modules', () => { - expect(parseFmtCLIArgs(['--with-node-modules']).withNodeModules).toBe(true); -}); - -test('parses --stdin-filepath', () => { - expect(parseFmtCLIArgs(['--stdin-filepath', 'src/index.ts'])).toEqual({ - cache: true, - mode: 'write', - patterns: [], - ignorePaths: [], - ignoreUnknown: false, - noErrorOnUnmatchedPattern: false, - withNodeModules: false, - maxWorkers: undefined, - help: false, - stdinFilepath: 'src/index.ts', - }); -}); - -test('accepts a worker count with --stdin-filepath', () => { - expect(parseFmtCLIArgs(['--stdin-filepath', 'index.ts', '--parallel-workers', '2'])).toEqual({ - cache: true, - mode: 'write', - patterns: [], - ignorePaths: [], - ignoreUnknown: false, - noErrorOnUnmatchedPattern: false, - withNodeModules: false, - maxWorkers: 2, - help: false, - stdinFilepath: 'index.ts', - }); -}); - -test.each(['--write', '--check', '--list-different'])( - 'rejects %s with --stdin-filepath', - (option) => { - expect(() => parseFmtCLIArgs(['--stdin-filepath', 'index.ts', option])).toThrow( - 'The --stdin-filepath option cannot be used with --write, --check, or --list-different.', - ); - }, -); - -test('rejects file arguments with --stdin-filepath', () => { - expect(() => parseFmtCLIArgs(['--stdin-filepath', 'index.ts', 'src/other.ts'])).toThrow( - 'The --stdin-filepath option cannot be used with file arguments.', - ); -}); - -test('provides command help', () => { - const helpMessage = stripVTControlCharacters(fmtHelpMessage).replace(/^Rstack v.*\n\n/, ''); - - expect(helpMessage).toContain('Usage:\n $ rs fmt [options] [files/globs...]'); - expect(helpMessage).toMatchSnapshot(); -}); - -test.each([ - ['--write', '--check'], - ['--write', '--list-different'], - ['--check', '--list-different'], - ['--write', '--check', '--list-different'], -])('rejects conflicting modes: %s', (...args) => { - expect(() => parseFmtCLIArgs(args)).toThrow( - 'The --write, --check, and --list-different options cannot be used together.', - ); -}); diff --git a/packages/rstack/tests/fmt/discoverPaths.test.ts b/packages/rstack/tests/fmt/discoverPaths.test.ts index 22f88b99..514cdc92 100644 --- a/packages/rstack/tests/fmt/discoverPaths.test.ts +++ b/packages/rstack/tests/fmt/discoverPaths.test.ts @@ -1,7 +1,8 @@ import { symlinkSync } from 'node:fs'; import path from 'node:path'; -import { expect, test } from 'rstack/test'; +import { expect, rs, test } from 'rstack/test'; import { discoverFmtPaths } from '../../src/fmt/discoverPaths.ts'; +import * as nativeBinding from '../../src/native/index.ts'; import { withTempProject, writeProjectFile } from './helpers.ts'; const relativePaths = (rootPath: string, files: string[]): string[] => @@ -122,6 +123,18 @@ test('applies nested gitignore rules with child negation', async () => { }); }); +test('does not extend a nested directory negation to its files', async () => { + await withTempProject(async (rootPath) => { + writeProjectFile(rootPath, '.gitignore', 'debug/\n'); + writeProjectFile(rootPath, 'scripts/.gitignore', '!debug\n'); + writeProjectFile(rootPath, 'scripts/debug/launch.mjs'); + + const files = await discoverFmtPaths({ cwd: rootPath, patterns: ['**/*.mjs'] }); + + expect(files).toEqual([]); + }); +}); + test('applies a nested gitignore without a root matcher', async () => { await withTempProject(async (rootPath) => { writeProjectFile(rootPath, 'src/.gitignore', '*.js\n'); @@ -139,6 +152,39 @@ test('applies a nested gitignore without a root matcher', async () => { }); }); +test('keeps valid nested gitignore rules around normalized and malformed lines', async () => { + await withTempProject(async (rootPath) => { + writeProjectFile(rootPath, '.gitignore', '\uFEFF*.js\r\nmalformed\\\r\n'); + writeProjectFile(rootPath, 'src/.gitignore', '!keep.js\r\n'); + writeProjectFile(rootPath, 'src/keep.js'); + writeProjectFile(rootPath, 'src/drop.js'); + writeProjectFile(rootPath, 'visible.ts'); + + const files = await discoverFmtPaths({ cwd: rootPath, patterns: ['**/*.{js,ts}'] }); + + expect(relativePaths(rootPath, files)).toEqual([path.join('src', 'keep.js'), 'visible.ts']); + }); +}); + +test('propagates native binding errors while loading a nested gitignore', async () => { + await withTempProject(async (rootPath) => { + writeProjectFile(rootPath, 'src/.gitignore', '*.js\n'); + writeProjectFile(rootPath, 'src/index.js'); + const nativeError = new Error('Failed to load native binding'); + const loadNativeBinding = rs + .spyOn(nativeBinding, 'loadNativeBinding') + .mockImplementation(() => { + throw nativeError; + }); + + try { + await expect(discoverFmtPaths({ cwd: rootPath })).rejects.toBe(nativeError); + } finally { + loadNativeBinding.mockRestore(); + } + }); +}); + test('lets explicit files bypass gitignore', async () => { await withTempProject(async (rootPath) => { writeProjectFile(rootPath, '.gitignore', '/generated/\n'); diff --git a/packages/rstack/tests/fmt/format.test.ts b/packages/rstack/tests/fmt/format.test.ts deleted file mode 100644 index e8a7abe2..00000000 --- a/packages/rstack/tests/fmt/format.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import path from 'node:path'; -import { expect, test } from 'rstack/test'; -import { formatFmtSource } from '../../src/fmt/format.ts'; - -const rootPath = path.join(import.meta.dirname, 'fixture'); - -test('formats sources without touching the file system', async () => { - await expect( - formatFmtSource( - { path: path.join(rootPath, 'missing.ts'), options: {} }, - () => 'const value=1', - ), - ).resolves.toEqual({ - status: 'formatted', - source: 'const value=1', - formatted: 'const value = 1;\n', - }); -}); - -test('applies resolved options to the source', async () => { - const result = await formatFmtSource( - { path: path.join(rootPath, 'missing.ts'), options: { singleQuote: true, semi: false } }, - () => 'const message="hello"', - ); - - expect(result).toEqual({ - status: 'formatted', - source: 'const message="hello"', - formatted: "const message = 'hello'\n", - }); -}); - -test('sorts package.json when the option is enabled', async () => { - const result = await formatFmtSource( - { path: path.join(rootPath, 'package.json'), options: { sortPackageJson: true } }, - () => '{"version":"1.0.0","name":"fixture"}', - ); - - expect(result).toEqual({ - status: 'formatted', - source: '{"version":"1.0.0","name":"fixture"}', - formatted: '{\n "name": "fixture",\n "version": "1.0.0"\n}\n', - }); -}); - -test('reports unsupported files before reading the source', async () => { - let read = false; - - await expect( - formatFmtSource({ path: path.join(rootPath, 'missing.unknown'), options: {} }, () => { - read = true; - return ''; - }), - ).resolves.toEqual({ status: 'unsupported' }); - expect(read).toBe(false); -}); - -test('rejects sources that cannot be parsed', async () => { - await expect( - formatFmtSource({ path: path.join(rootPath, 'invalid.ts'), options: {} }, () => 'const x = ;'), - ).rejects.toThrow("Unexpected token ';'"); -}); diff --git a/packages/rstack/tests/fmt/helpers.ts b/packages/rstack/tests/fmt/helpers.ts index 89d280c5..2a6c8ac3 100644 --- a/packages/rstack/tests/fmt/helpers.ts +++ b/packages/rstack/tests/fmt/helpers.ts @@ -1,5 +1,20 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import path from 'node:path'; +import { fmtCacheFileName } from '../../src/fmt/cacheStore.ts'; +import type { FmtCacheContext, FmtFileRequest, ResolvedFmtOptions } from '../../src/fmt/types.ts'; + +export const createFmtRequest = ( + filePath: string, + options: ResolvedFmtOptions = { parser: 'typescript' }, +): FmtFileRequest => ({ + path: filePath, + options, +}); + +export const createFmtCacheContext = (rootPath: string): FmtCacheContext => ({ + filePath: path.join(rootPath, '.rstack', 'cache', 'fmt', fmtCacheFileName), + rootPath, +}); export const withTempProject = async ( callback: (rootPath: string) => Promise, diff --git a/packages/rstack/tests/fmt/ignore.test.ts b/packages/rstack/tests/fmt/ignore.test.ts index 1d033618..68220775 100644 --- a/packages/rstack/tests/fmt/ignore.test.ts +++ b/packages/rstack/tests/fmt/ignore.test.ts @@ -29,6 +29,13 @@ test('matches gitignore patterns relative to the config root', async () => { expect(isIgnored(path.join(rootPath, 'src/index.js'))).toBe(false); }); +test('skips malformed patterns without discarding valid patterns', async () => { + const isIgnored = await createMatcher(['ignored.js', 'malformed\\']); + + expect(isIgnored(path.join(rootPath, 'ignored.js'))).toBe(true); + expect(isIgnored(path.join(rootPath, 'other.js'))).toBe(false); +}); + test('distinguishes directory-only patterns from files', async () => { const isIgnored = await createMatcher(['dist/']); const directoryPath = path.join(rootPath, 'dist'); diff --git a/packages/rstack/tests/fmt/pathHelpers.test.ts b/packages/rstack/tests/fmt/pathHelpers.test.ts deleted file mode 100644 index e1b1a2c6..00000000 --- a/packages/rstack/tests/fmt/pathHelpers.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import path from 'node:path'; -import { expect, test } from 'rstack/test'; -import { createRelativePathResolver, toPosixPath } from '../../src/fmt/pathHelpers.ts'; - -const rootPath = path.join(import.meta.dirname, 'project'); - -test('converts platform paths to POSIX paths', () => { - expect(toPosixPath(path.join('src', 'index.ts'))).toBe('src/index.ts'); -}); - -test('resolves paths relative to a fixed root', () => { - const resolveRelativePath = createRelativePathResolver(rootPath); - - expect(resolveRelativePath(rootPath)).toBe(''); - expect(resolveRelativePath(path.join(rootPath, 'src/index.ts'))).toBe( - path.join('src', 'index.ts'), - ); -}); - -test('falls back for paths outside the fixed root', () => { - const resolveRelativePath = createRelativePathResolver(rootPath); - const siblingPath = path.join(`${rootPath}-other`, 'index.ts'); - - expect(resolveRelativePath(siblingPath)).toBe(path.relative(rootPath, siblingPath)); -}); diff --git a/packages/rstack/tests/fmt/runner.test.ts b/packages/rstack/tests/fmt/runner.test.ts index 7a31d387..dbcd59e6 100644 --- a/packages/rstack/tests/fmt/runner.test.ts +++ b/packages/rstack/tests/fmt/runner.test.ts @@ -3,14 +3,7 @@ import path from 'node:path'; import { expect, test } from 'rstack/test'; import { runFmtFiles } from '../../src/fmt/runner.ts'; import type { FmtFileRequest, FmtMode } from '../../src/fmt/types.ts'; -import { withTempProject } from './helpers.ts'; - -const createRequest = (filePath: string): FmtFileRequest => ({ - path: filePath, - options: { - parser: 'typescript', - }, -}); +import { createFmtRequest, withTempProject } from './helpers.ts'; const run = (files: FmtFileRequest[], mode: FmtMode = 'write') => runFmtFiles({ @@ -26,7 +19,7 @@ test('does not rewrite unchanged files', async () => { utimesSync(filePath, timestamp, timestamp); const mtimeMs = statSync(filePath).mtimeMs; - const result = await run([createRequest(filePath)]); + const result = await run([createFmtRequest(filePath)]); expect(result).toMatchObject({ exitCode: 0, @@ -42,7 +35,7 @@ test('writes changed files', async () => { const filePath = path.join(rootPath, 'changed.ts'); writeFileSync(filePath, 'const value=1'); - const result = await run([createRequest(filePath)]); + const result = await run([createFmtRequest(filePath)]); expect(result).toMatchObject({ exitCode: 0, @@ -59,7 +52,7 @@ test.runIf(process.platform !== 'win32')('preserves file mode when writing', asy writeFileSync(filePath, 'const value=1'); chmodSync(filePath, 0o744); - await run([createRequest(filePath)]); + await run([createFmtRequest(filePath)]); expect(statSync(filePath).mode & 0o777).toBe(0o744); }); @@ -72,7 +65,7 @@ for (const mode of ['check', 'list-different'] as const) { const source = 'const value=1'; writeFileSync(filePath, source); - const result = await run([createRequest(filePath)], mode); + const result = await run([createFmtRequest(filePath)], mode); expect(result).toMatchObject({ exitCode: 1, @@ -91,7 +84,7 @@ test('continues after a file fails and gives errors exit-code precedence', async writeFileSync(invalidPath, 'const value = ;'); writeFileSync(validPath, 'const value=1'); - const result = await run([createRequest(invalidPath), createRequest(validPath)], 'check'); + const result = await run([createFmtRequest(invalidPath), createFmtRequest(validPath)], 'check'); expect(result).toMatchObject({ exitCode: 2, diff --git a/packages/rstack/tests/fmt/runnerCache.test.ts b/packages/rstack/tests/fmt/runnerCache.test.ts index 943b225c..f9432a94 100644 --- a/packages/rstack/tests/fmt/runnerCache.test.ts +++ b/packages/rstack/tests/fmt/runnerCache.test.ts @@ -5,26 +5,13 @@ import { expect, test } from 'rstack/test'; import { cacheNamespace, createOptionsHasher, sha256 } from '../../src/fmt/cacheIdentity.ts'; import { loadFmtCacheStore } from '../../src/fmt/cacheStore.ts'; import { runFmtFiles } from '../../src/fmt/runner.ts'; -import type { - FmtCacheContext, - FmtFileRequest, - FmtMode, - ResolvedFmtOptions, -} from '../../src/fmt/types.ts'; -import { withTempProject, writeProjectFile } from './helpers.ts'; - -const createRequest = ( - filePath: string, - options: ResolvedFmtOptions = { parser: 'typescript' }, -): FmtFileRequest => ({ - path: filePath, - options, -}); - -const createCache = (rootPath: string): FmtCacheContext => ({ - filePath: path.join(rootPath, 'cache', 'fmt-v1.json'), - rootPath, -}); +import type { FmtCacheContext, FmtFileRequest, FmtMode } from '../../src/fmt/types.ts'; +import { + createFmtCacheContext, + createFmtRequest, + withTempProject, + writeProjectFile, +} from './helpers.ts'; const run = (files: FmtFileRequest[], mode: FmtMode, cache: FmtCacheContext) => runFmtFiles({ files, mode, cache }); @@ -34,11 +21,11 @@ for (const mode of ['check', 'list-different'] as const) { await withTempProject(async (rootPath) => { const cleanPath = path.join(rootPath, 'clean.ts'); const dirtyPath = path.join(rootPath, 'dirty.ts'); - const cache = createCache(rootPath); + const cache = createFmtCacheContext(rootPath); writeFileSync(cleanPath, 'const clean = 1;\n'); writeFileSync(dirtyPath, 'const dirty=1'); - const files = [createRequest(cleanPath), createRequest(dirtyPath)]; + const files = [createFmtRequest(cleanPath), createFmtRequest(dirtyPath)]; const first = await run(files, mode, cache); expect(first).toMatchObject({ @@ -67,14 +54,14 @@ for (const mode of ['check', 'list-different'] as const) { test('uses content hashes instead of file metadata', async () => { await withTempProject(async (rootPath) => { const filePath = path.join(rootPath, 'index.ts'); - const cache = createCache(rootPath); + const cache = createFmtCacheContext(rootPath); const timestamp = new Date('2020-01-01T00:00:00.000Z'); const clean = 'const value = 1;\n'; const dirty = 'const value= 1;\n'; writeFileSync(filePath, clean); utimesSync(filePath, timestamp, timestamp); - await run([createRequest(filePath)], 'check', cache); + await run([createFmtRequest(filePath)], 'check', cache); const firstStore = await loadFmtCacheStore(cache.filePath, cacheNamespace); const firstEntry = firstStore.get('index.ts'); @@ -85,7 +72,7 @@ test('uses content hashes instead of file metadata', async () => { size: Buffer.byteLength(clean), }); - await expect(run([createRequest(filePath)], 'check', cache)).resolves.toMatchObject({ + await expect(run([createFmtRequest(filePath)], 'check', cache)).resolves.toMatchObject({ exitCode: 1, files: [{ path: filePath, status: 'different' }], }); @@ -100,13 +87,13 @@ test('uses content hashes instead of file metadata', async () => { test('invalidates entries when final options change', async () => { await withTempProject(async (rootPath) => { const filePath = path.join(rootPath, 'index.ts'); - const cache = createCache(rootPath); + const cache = createFmtCacheContext(rootPath); writeFileSync(filePath, 'const value = "text";\n'); - const initial = createRequest(filePath, { parser: 'typescript', singleQuote: false }); + const initial = createFmtRequest(filePath, { parser: 'typescript', singleQuote: false }); await run([initial], 'check', cache); - const changed = createRequest(filePath, { parser: 'typescript', singleQuote: true }); + const changed = createFmtRequest(filePath, { parser: 'typescript', singleQuote: true }); await expect(run([changed], 'check', cache)).resolves.toMatchObject({ exitCode: 1, files: [{ path: filePath, status: 'different' }], @@ -124,8 +111,8 @@ test('invalidates entries when final options change', async () => { test('caches unsupported parser results until final options change', async () => { await withTempProject(async (rootPath) => { const filePath = writeProjectFile(rootPath, 'data.unknown', '{"value":true}'); - const cache = createCache(rootPath); - const unsupported = createRequest(filePath, {}); + const cache = createFmtCacheContext(rootPath); + const unsupported = createFmtRequest(filePath, {}); const first = await run([unsupported], 'check', cache); expect(first).toEqual({ @@ -141,7 +128,7 @@ test('caches unsupported parser results until final options change', async () => await expect(run([unsupported], 'check', cache)).resolves.toEqual(first); - const supported = createRequest(filePath, { parser: 'json' }); + const supported = createFmtRequest(filePath, { parser: 'json' }); await expect(run([supported], 'check', cache)).resolves.toMatchObject({ exitCode: 1, files: [{ path: filePath, status: 'different' }], @@ -158,8 +145,8 @@ test('caches unsupported parser results until final options change', async () => test('invalidates cached unsupported parser results when content changes without an extension', async () => { await withTempProject(async (rootPath) => { const filePath = writeProjectFile(rootPath, 'script', 'plain text\n'); - const cache = createCache(rootPath); - const file = createRequest(filePath, {}); + const cache = createFmtCacheContext(rootPath); + const file = createFmtRequest(filePath, {}); const first = await run([file], 'check', cache); expect(first).toEqual({ @@ -211,8 +198,8 @@ test('caches only plugins with stable fingerprints', async () => { ...(version ? { version } : {}), }), ); - const cache = createCache(rootPath); - const file = createRequest(filePath, { plugins: [pathToFileURL(pluginEntry).href] }); + const cache = createFmtCacheContext(rootPath); + const file = createFmtRequest(filePath, { plugins: [pathToFileURL(pluginEntry).href] }); writePackageJson(); await run([file], 'check', cache); @@ -241,16 +228,16 @@ test('preserves entries outside the formatted subset', async () => { await withTempProject(async (rootPath) => { const firstPath = path.join(rootPath, 'first.ts'); const secondPath = path.join(rootPath, 'second.ts'); - const cache = createCache(rootPath); + const cache = createFmtCacheContext(rootPath); writeFileSync(firstPath, 'const first = 1;\n'); writeFileSync(secondPath, 'const second = 2;\n'); - await run([createRequest(firstPath), createRequest(secondPath)], 'check', cache); + await run([createFmtRequest(firstPath), createFmtRequest(secondPath)], 'check', cache); const firstStore = await loadFmtCacheStore(cache.filePath, cacheNamespace); const secondEntry = firstStore.get('second.ts'); writeFileSync(firstPath, 'const first=1'); - await run([createRequest(firstPath)], 'check', cache); + await run([createFmtRequest(firstPath)], 'check', cache); const secondStore = await loadFmtCacheStore(cache.filePath, cacheNamespace); expect(secondStore.get('second.ts')).toEqual(secondEntry); @@ -261,12 +248,12 @@ test('does not cache formatting errors', async () => { await withTempProject(async (rootPath) => { const validPath = path.join(rootPath, 'valid.ts'); const invalidPath = path.join(rootPath, 'invalid.ts'); - const cache = createCache(rootPath); + const cache = createFmtCacheContext(rootPath); writeFileSync(validPath, 'const valid = 1;\n'); writeFileSync(invalidPath, 'const invalid = ;'); - await run([createRequest(validPath)], 'check', cache); - await expect(run([createRequest(invalidPath)], 'check', cache)).resolves.toMatchObject({ + await run([createFmtRequest(validPath)], 'check', cache); + await expect(run([createFmtRequest(invalidPath)], 'check', cache)).resolves.toMatchObject({ exitCode: 2, files: [{ path: invalidPath, status: 'error' }], }); @@ -281,11 +268,11 @@ test('write persists clean results for misses and hits', async () => { await withTempProject(async (rootPath) => { const cleanPath = path.join(rootPath, 'clean.ts'); const dirtyPath = path.join(rootPath, 'dirty.ts'); - const cache = createCache(rootPath); + const cache = createFmtCacheContext(rootPath); writeFileSync(cleanPath, 'const clean = 1;\n'); writeFileSync(dirtyPath, 'const dirty=1'); - const files = [createRequest(cleanPath), createRequest(dirtyPath)]; + const files = [createFmtRequest(cleanPath), createFmtRequest(dirtyPath)]; await expect(run(files, 'write', cache)).resolves.toMatchObject({ exitCode: 0, files: [{ path: dirtyPath, status: 'written' }], @@ -317,8 +304,8 @@ test('write persists clean results for misses and hits', async () => { test('write converts a dirty entry to clean', async () => { await withTempProject(async (rootPath) => { const filePath = path.join(rootPath, 'index.ts'); - const cache = createCache(rootPath); - const file = createRequest(filePath); + const cache = createFmtCacheContext(rootPath); + const file = createFmtRequest(filePath); writeFileSync(filePath, 'const value=1'); await run([file], 'check', cache); diff --git a/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts b/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts index 640fe832..ce433ec1 100644 --- a/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts +++ b/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts @@ -1,11 +1,13 @@ -import { readFileSync } from 'node:fs'; -import path from 'node:path'; import { beforeEach, expect, rs, test } from 'rstack/test'; import { cacheNamespace, createOptionsHasher } from '../../src/fmt/cacheIdentity.ts'; import { loadFmtCacheStore } from '../../src/fmt/cacheStore.ts'; import { runFmtFiles } from '../../src/fmt/runner.ts'; -import type { FmtFileRequest } from '../../src/fmt/types.ts'; -import { withTempProject, writeProjectFile } from './helpers.ts'; +import { + createFmtCacheContext, + createFmtRequest, + withTempProject, + writeProjectFile, +} from './helpers.ts'; const mocks = rs.hoisted(() => ({ createFmtWorkerPoolCalls: [] as [number, number | undefined][], @@ -22,58 +24,31 @@ beforeEach(() => { mocks.createFmtWorkerPoolCalls.length = 0; }); -const createRequest = (filePath: string): FmtFileRequest => ({ - path: filePath, - options: { - parser: 'typescript', - }, -}); +const createCachedUnsupportedFile = async (rootPath: string, fileName: string) => { + const filePath = writeProjectFile(rootPath, fileName, 'plain text'); + const cache = createFmtCacheContext(rootPath); + const file = createFmtRequest(filePath, {}); + const optionsHash = createOptionsHasher()(file.options); + if (optionsHash === undefined) { + throw new Error('Expected cacheable formatter options.'); + } -test('starts the worker pool before formatting a single file', async () => { - await withTempProject(async (rootPath) => { - const filePath = writeProjectFile(rootPath, 'index.ts', 'const value=1'); + const store = await loadFmtCacheStore(cache.filePath, cacheNamespace); + store.set(fileName, [null, optionsHash, 'unsupported']); + await expect(store.save()).resolves.toBe(true); - await expect( - runFmtFiles({ - files: [createRequest(filePath)], - mode: 'write', - maxWorkers: 1, - }), - ).rejects.toThrow('worker startup failed'); - - expect(mocks.createFmtWorkerPoolCalls).toEqual([[1, 1]]); - expect(readFileSync(filePath, 'utf8')).toBe('const value=1'); - }); -}); - -test('does not start the worker pool when there are no files', async () => { - await expect(runFmtFiles({ files: [], mode: 'write' })).resolves.toMatchObject({ - files: [], - exitCode: 0, - processedFileCount: 0, - }); - expect(mocks.createFmtWorkerPoolCalls).toEqual([]); -}); + return { cache, file }; +}; test('does not start the worker pool when every parser result is cached as unsupported', async () => { await withTempProject(async (rootPath) => { - const filePath = writeProjectFile(rootPath, 'example.unknown', 'plain text'); - const cachePath = path.join(rootPath, 'cache', 'fmt-v1.json'); - const file: FmtFileRequest = { path: filePath, options: {} }; - const optionsHash = createOptionsHasher()(file.options); - if (optionsHash === undefined) { - throw new Error('Expected cacheable formatter options.'); - } - - const store = await loadFmtCacheStore(cachePath, cacheNamespace); - store.set('example.unknown', [null, optionsHash, 'unsupported']); - await expect(store.save()).resolves.toBe(true); + const { cache, file } = await createCachedUnsupportedFile(rootPath, 'example.unknown'); await expect( runFmtFiles({ files: [file], mode: 'check', - cache: { filePath: cachePath, rootPath }, + cache, }), ).resolves.toEqual({ exitCode: 2, @@ -86,23 +61,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 () => { await withTempProject(async (rootPath) => { - const filePath = writeProjectFile(rootPath, 'script', 'plain text'); - const cachePath = path.join(rootPath, 'cache', 'fmt-v1.json'); - const file: FmtFileRequest = { path: filePath, options: {} }; - const optionsHash = createOptionsHasher()(file.options); - if (optionsHash === undefined) { - throw new Error('Expected cacheable formatter options.'); - } - - const store = await loadFmtCacheStore(cachePath, cacheNamespace); - store.set('script', [null, optionsHash, 'unsupported']); - await expect(store.save()).resolves.toBe(true); + const { cache, file } = await createCachedUnsupportedFile(rootPath, 'script'); await expect( runFmtFiles({ files: [file], mode: 'check', - cache: { filePath: cachePath, rootPath }, + cache, }), ).rejects.toThrow('worker startup failed'); expect(mocks.createFmtWorkerPoolCalls).toEqual([[1, undefined]]); diff --git a/packages/rstack/tests/fmt/runnerWriteFailure.test.ts b/packages/rstack/tests/fmt/runnerWriteFailure.test.ts deleted file mode 100644 index efb96dfd..00000000 --- a/packages/rstack/tests/fmt/runnerWriteFailure.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { expect, rs, test } from 'rstack/test'; -import { runFmtFiles } from '../../src/fmt/runner.ts'; - -const mocks = rs.hoisted(() => ({ - terminateCalls: 0, -})); - -rs.mock('../../src/fmt/workerPool.ts', () => ({ - createFmtWorkerPool: () => - Promise.resolve({ - workerCount: 1, - formatFile: () => Promise.reject(new Error('file write failed')), - terminate: () => { - mocks.terminateCalls++; - return Promise.resolve(); - }, - }), -})); - -test('returns an error when a file write fails', async () => { - const filePath = '/virtual/example.ts'; - - const result = await runFmtFiles({ - files: [ - { - path: filePath, - options: { - parser: 'typescript', - }, - }, - ], - mode: 'write', - }); - - expect(result).toMatchObject({ - exitCode: 2, - files: [ - { - path: filePath, - status: 'error', - error: { message: 'file write failed' }, - }, - ], - processedFileCount: 1, - }); - expect(mocks.terminateCalls).toBe(1); -}); diff --git a/packages/rstack/tests/fmt/worker.test.ts b/packages/rstack/tests/fmt/worker.test.ts index 29b0a901..6f3631d5 100644 --- a/packages/rstack/tests/fmt/worker.test.ts +++ b/packages/rstack/tests/fmt/worker.test.ts @@ -5,45 +5,6 @@ import { sha256 } from '../../src/fmt/cacheIdentity.ts'; import { formatFile } from '../../src/fmt/worker.ts'; import { withTempProject, writeProjectFile } from './helpers.ts'; -test('writes formatted files', async () => { - await withTempProject(async (rootPath) => { - const filePath = writeProjectFile(rootPath, 'example.ts', 'const value=1'); - - await expect( - formatFile({ - file: { - path: filePath, - options: { - parser: 'typescript', - }, - }, - shouldWrite: true, - }), - ).resolves.toEqual({ status: 'changed' }); - - expect(readFileSync(filePath, 'utf8')).toBe('const value = 1;\n'); - }); -}); - -test('infers the parser for an explicitly provided node_modules file', async () => { - await withTempProject(async (rootPath) => { - const source = 'const value=1'; - const filePath = writeProjectFile(rootPath, 'node_modules/example/index.ts', source); - - await expect( - formatFile({ - file: { - path: filePath, - options: {}, - }, - shouldWrite: false, - }), - ).resolves.toEqual({ status: 'changed' }); - - expect(readFileSync(filePath, 'utf8')).toBe(source); - }); -}); - test('returns cached states before resolving the parser', async () => { await withTempProject(async (rootPath) => { const source = 'const value=1'; diff --git a/packages/rstack/tests/fmt/workerPool.test.ts b/packages/rstack/tests/fmt/workerPool.test.ts deleted file mode 100644 index 39637c0b..00000000 --- a/packages/rstack/tests/fmt/workerPool.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { expect, test } from 'rstack/test'; -import { getFmtWorkerCount } from '../../src/fmt/workerPool.ts'; - -test.each([ - [4, 1, 1], - [4, 2, 2], - [2, 4, 2], - [12, 10, 10], -])('uses %s files and %s configured workers as %s workers', (files, workers, expected) => { - expect(getFmtWorkerCount(files, workers)).toBe(expected); -}); diff --git a/packages/rstack/tests/fmt/yukuPlugin.test.ts b/packages/rstack/tests/fmt/yukuPlugin.test.ts index 5d221446..b4841ce3 100644 --- a/packages/rstack/tests/fmt/yukuPlugin.test.ts +++ b/packages/rstack/tests/fmt/yukuPlugin.test.ts @@ -7,9 +7,9 @@ const formatWithYuku = ( options: Options & { parser: 'yuku' | 'yuku-ts' }, ): Promise => format(source, { - filepath: `example.${options.parser === 'yuku' ? 'js' : 'ts'}`, plugins: [yukuPlugin], ...options, + filepath: options.filepath ?? `example.${options.parser === 'yuku' ? 'js' : 'ts'}`, }); test('exposes the same JavaScript and TypeScript language mappings as the official plugin', async () => { @@ -34,6 +34,39 @@ test('exposes the same JavaScript and TypeScript language mappings as the offici ]); }); +test('parses JSX in JavaScript files', async () => { + await expect( + formatWithYuku('const view=', { + filepath: 'example.js', + parser: 'yuku', + }), + ).resolves.toBe('const view = ;\n'); +}); + +test.each(['example.ts', 'example.mts', 'example.cts'])( + 'rejects JSX syntax in %s', + async (filepath) => { + await expect( + formatWithYuku('const view=', { + filepath, + parser: 'yuku-ts', + }), + ).rejects.toThrow(); + }, +); + +test.each(['example.d.ts', 'example.d.mts', 'example.d.cts'])( + 'rejects function implementations in %s', + async (filepath) => { + await expect( + formatWithYuku('export function value() { return 1; }', { + filepath, + parser: 'yuku-ts', + }), + ).rejects.toThrow('An implementation cannot be declared in ambient contexts'); + }, +); + test.each([ { name: 'hashbangs and unicode locations', diff --git a/packages/rstack/tests/helpers/cli.ts b/packages/rstack/tests/helpers/cli.ts index 6046db43..d1d287d0 100644 --- a/packages/rstack/tests/helpers/cli.ts +++ b/packages/rstack/tests/helpers/cli.ts @@ -10,6 +10,9 @@ export type ExecCliOptions = ExecSyncOptions & { export type ExecCli = (command: string, options?: ExecCliOptions) => string; +export const normalizeHelpOutput = (output: string): string => + output.replace(/^Rstack v.+/u, 'Rstack v'); + type ExecCliError = Error & { stdout?: Buffer | string; stderr?: Buffer | string; @@ -25,7 +28,7 @@ export const execCli: ExecCli = (command, options = {}) => { const { logHelper, ...execOptions } = options; try { - const output = execSync(`${RSTACK_BIN_PATH} ${command}`, { + const output = execSync(`"${process.execPath}" "${RSTACK_BIN_PATH}" ${command}`, { stdio: 'pipe', ...execOptions, env: { diff --git a/packages/rstack/tests/projectCache.test.ts b/packages/rstack/tests/projectCache.test.ts deleted file mode 100644 index eff83131..00000000 --- a/packages/rstack/tests/projectCache.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { existsSync, readFileSync, writeFileSync } from 'node:fs'; -import path from 'node:path'; -import { expect, test } from 'rstack/test'; -import { ensureProjectCacheDir, getProjectCacheDir } from '../src/projectCache.ts'; -import { withTempProject, writeProjectFile } from './fmt/helpers.ts'; - -test('creates and repairs an ignored project cache only when requested', async () => { - await withTempProject(async (rootPath) => { - const cachePath = getProjectCacheDir(rootPath); - const ignorePath = path.join(cachePath, '.gitignore'); - - expect(cachePath).toBe(path.join(rootPath, '.rstack', 'cache')); - expect(existsSync(cachePath)).toBe(false); - - await expect(ensureProjectCacheDir(rootPath)).resolves.toEqual({ - status: 'available', - path: cachePath, - }); - expect(readFileSync(ignorePath, 'utf8')).toBe('*\n'); - - writeFileSync(ignorePath, 'stale\n'); - await ensureProjectCacheDir(rootPath); - expect(readFileSync(ignorePath, 'utf8')).toBe('*\n'); - }); -}); - -test('reports an unavailable project cache without throwing', async () => { - await withTempProject(async (rootPath) => { - writeProjectFile(rootPath, '.rstack', 'not a directory'); - - const result = await ensureProjectCacheDir(rootPath); - - expect(result).toMatchObject({ - status: 'unavailable', - path: getProjectCacheDir(rootPath), - }); - }); -}); diff --git a/packages/rstack/tests/setup/directories.test.ts b/packages/rstack/tests/setup/directories.test.ts index 59ce17f6..dfaf62da 100644 --- a/packages/rstack/tests/setup/directories.test.ts +++ b/packages/rstack/tests/setup/directories.test.ts @@ -22,52 +22,43 @@ test('installs a custom hooks directory from the Git root and runs its hook', () }); }); -test('installs the default hooks directory from a nested project', () => { +test('installs repository-level hooks from a nested project', () => { withRepository((cwd) => { const projectDirectory = path.join(cwd, 'frontend'); - const nestedHooksPath = `frontend/${hooksPath}`; mkdirSync(projectDirectory); - writeHook( - projectDirectory, - `printf 'root\\n' > nested-hook-cwd -cd frontend -printf 'nested\\n' > nested-hook-ran -`, - ); + writeHook(cwd, "printf 'ran\\n' > nested-hook-ran\n"); expect(installHooks({ cwd: projectDirectory })).toEqual({ status: 'installed', - hooksPath: nestedHooksPath, + hooksPath, }); expect(installHooks({ cwd: projectDirectory })).toEqual({ status: 'unchanged', - hooksPath: nestedHooksPath, + hooksPath, }); - expect(runGit(cwd, ['config', '--local', '--get', 'core.hooksPath'])).toBe(nestedHooksPath); - expect(existsSync(path.join(projectDirectory, hooksPath, 'runner'))).toBe(true); + expect(runGit(cwd, ['config', '--local', '--get', 'core.hooksPath'])).toBe(hooksPath); + expect(existsSync(path.join(cwd, hooksPath, 'runner'))).toBe(true); + expect(readFileSync(path.join(cwd, hooksPath, '.owner'), 'utf8')).toBe('frontend\n'); expect(runHook(cwd).status).toBe(0); - expect(readFileSync(path.join(cwd, 'nested-hook-cwd'), 'utf8')).toBe('root\n'); - expect(readFileSync(path.join(projectDirectory, 'nested-hook-ran'), 'utf8')).toBe('nested\n'); + expect(readFileSync(path.join(projectDirectory, 'nested-hook-ran'), 'utf8')).toBe('ran\n'); }); }); -test('installs a custom hooks directory from a nested project', () => { +test('installs a root-relative custom hooks directory from a nested project', () => { withRepository((cwd) => { const projectDirectory = path.join(cwd, 'frontend app'); mkdirSync(projectDirectory); expect(installHooks({ cwd: projectDirectory, hooksDir: 'config\\hooks' })).toEqual({ status: 'installed', - hooksPath: 'frontend app/config/hooks/_', + hooksPath: 'config/hooks/_', }); expect(installHooks({ cwd: projectDirectory, hooksDir: 'config\\hooks' })).toEqual({ status: 'unchanged', - hooksPath: 'frontend app/config/hooks/_', + hooksPath: 'config/hooks/_', }); - expect(runGit(cwd, ['config', '--local', '--get', 'core.hooksPath'])).toBe( - 'frontend app/config/hooks/_', - ); - expect(existsSync(path.join(projectDirectory, 'config', 'hooks', '_', 'runner'))).toBe(true); + expect(runGit(cwd, ['config', '--local', '--get', 'core.hooksPath'])).toBe('config/hooks/_'); + expect(existsSync(path.join(cwd, 'config', 'hooks', '_', 'runner'))).toBe(true); }); }); diff --git a/packages/rstack/tests/setup/helpers.ts b/packages/rstack/tests/setup/helpers.ts index 533cb089..206e54c6 100644 --- a/packages/rstack/tests/setup/helpers.ts +++ b/packages/rstack/tests/setup/helpers.ts @@ -70,6 +70,9 @@ export const writeInit = (cwd: string, content: string): void => { export const runHook = (cwd: string, value?: string): SpawnSyncReturns => git(cwd, ['hook', 'run', 'pre-commit'], hookEnv(cwd, value)); +export const runGitHook = (cwd: string, name: string, args: string[]): SpawnSyncReturns => + git(cwd, ['hook', 'run', name, '--', ...args], hookEnv(cwd)); + export const withRepository = (callback: (cwd: string) => void): void => withDirectory((cwd) => { const globalConfig = process.env.GIT_CONFIG_GLOBAL; diff --git a/packages/rstack/tests/setup/hooks.test.ts b/packages/rstack/tests/setup/hooks.test.ts index 882e1b60..3baa5348 100644 --- a/packages/rstack/tests/setup/hooks.test.ts +++ b/packages/rstack/tests/setup/hooks.test.ts @@ -5,10 +5,8 @@ import { expect, test } from 'rstack/test'; import { createHookFiles } from '../../src/setup/hooks.ts'; import { withDirectory } from './helpers.ts'; -test('generates the dispatcher and all client-side Git hook shims', () => { - const { runner, ...shims } = createHookFiles(); - - expect(Object.keys(shims)).toEqual([ +test('generates the runner and all client-side Git hook shims', () => { + expect(Object.keys(createHookFiles()).filter((name) => name !== 'runner')).toEqual([ 'pre-commit', 'pre-merge-commit', 'prepare-commit-msg', @@ -24,21 +22,19 @@ test('generates the dispatcher and all client-side Git hook shims', () => { 'pre-push', 'pre-auto-gc', ]); - expect(runner).toBeTruthy(); - expect(new Set(Object.values(shims)).size).toBe(1); }); test.runIf(process.platform === 'win32')('converts Windows Node paths', () => { const { runner } = createHookFiles(String.raw`C:\Program Files\nodejs\node.exe`); - expect(runner).toContain("node_fallback='/c/Program Files/nodejs/node.exe'"); + expect(runner).toContain("rs_node_fallback='/c/Program Files/nodejs/node.exe'"); }); test.runIf(process.platform !== 'win32')('preserves backslashes in POSIX Node paths', () => { const nodeExecutable = String.raw`/opt/node\24/bin/node`; const { runner } = createHookFiles(nodeExecutable); - expect(runner).toContain(`node_fallback='${nodeExecutable}'`); + expect(runner).toContain(`rs_node_fallback='${nodeExecutable}'`); }); test.runIf(process.platform !== 'win32')('runs generated hooks', () => { @@ -58,10 +54,11 @@ test.runIf(process.platform !== 'win32')('runs generated hooks', () => { }; mkdirSync(generatedDirectory, { recursive: true }); + writeFileSync(path.join(generatedDirectory, '.owner'), '.\n'); writeFileSync(path.join(generatedDirectory, 'runner'), files.runner); writeFileSync(generatedHook, files['pre-commit']); - expect(spawnSync('sh', [generatedHook], { env }).status).toBe(0); + expect(spawnSync('sh', [generatedHook], { cwd: directory, env }).status).toBe(0); writeFileSync( userHook, @@ -70,6 +67,7 @@ printf '%s\\n' "$1|$input" `, ); const result = spawnSync('sh', [generatedHook, 'argument with spaces'], { + cwd: directory, encoding: 'utf8', env, input: 'standard input\n', @@ -84,7 +82,11 @@ printf '%s\\n' "$1|$input" printf 'unreachable\\n' `, ); - const errexitResult = spawnSync('sh', [generatedHook], { encoding: 'utf8', env }); + const errexitResult = spawnSync('sh', [generatedHook], { + cwd: directory, + encoding: 'utf8', + env, + }); expect(errexitResult.status).toBe(1); expect(errexitResult.stdout).toBe('Rstack - pre-commit hook failed (code 1)\n'); @@ -95,13 +97,21 @@ printf 'unreachable\\n' symlinkSync('/bin/sh', path.join(runtimeDirectory, 'sh')); symlinkSync('/bin/sh', fallbackNode); - const fallbackResult = spawnSync('sh', [generatedHook], { encoding: 'utf8', env }); + const fallbackResult = spawnSync('sh', [generatedHook], { + cwd: directory, + encoding: 'utf8', + env, + }); expect(fallbackResult.stdout).toBe(`${fallbackNode}\n`); const activeNode = path.join(runtimeDirectory, 'node'); symlinkSync('/bin/sh', activeNode); - const activeResult = spawnSync('sh', [generatedHook], { encoding: 'utf8', env }); + const activeResult = spawnSync('sh', [generatedHook], { + cwd: directory, + encoding: 'utf8', + env, + }); expect(activeResult.stdout).toBe(`${activeNode}\n`); }); }); diff --git a/packages/rstack/tests/setup/install.test.ts b/packages/rstack/tests/setup/install.test.ts index 44aef2a6..355d77c7 100644 --- a/packages/rstack/tests/setup/install.test.ts +++ b/packages/rstack/tests/setup/install.test.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import { expect, test } from 'rstack/test'; import { createHookFiles } from '../../src/setup/hooks.ts'; import { installHooks } from '../../src/setup/install.ts'; -import { git, hooksPath, restoreEnv, runGit, withDirectory, withRepository } from './helpers.ts'; +import { git, hooksPath, restoreEnv, runGit, withRepository } from './helpers.ts'; test('installs generated hooks and configures the repository', () => { withRepository((cwd) => { @@ -12,6 +12,7 @@ test('installs generated hooks and configures the repository', () => { const directory = path.join(cwd, hooksPath); expect(readFileSync(path.join(directory, '.gitignore'), 'utf8')).toBe('*\n'); + expect(readFileSync(path.join(directory, '.owner'), 'utf8')).toBe('.\n'); expect(runGit(cwd, ['status', '--short', '--untracked-files=all'])).toBe(''); for (const [name, content] of Object.entries(createHookFiles())) { @@ -62,29 +63,26 @@ test('repairs generated files without rewriting an unchanged hooksPath', () => { }); }); -test('skips non-Git directories without creating files', () => { - withDirectory((cwd) => { - expect(installHooks({ cwd })).toEqual({ - status: 'skipped', - reason: 'not-git-repository', - }); - expect(existsSync(path.join(cwd, '.rstack'))).toBe(false); - }); -}); +test('resolves repository context with a single Git process when unchanged', () => { + withRepository((cwd) => { + expect(installHooks({ cwd }).status).toBe('installed'); + const tracePath = path.join(cwd, 'git-trace.json'); + const originalTrace = process.env.GIT_TRACE2_EVENT; + process.env.GIT_TRACE2_EVENT = tracePath; -test('reports when Git is unavailable', () => { - withDirectory((cwd) => { - const originalPath = process.env.PATH; - process.env.PATH = ''; try { - expect(installHooks({ cwd })).toEqual({ - status: 'failed', - reason: 'git-not-found', - message: 'Git command not found.', - }); + expect(installHooks({ cwd })).toEqual({ status: 'unchanged', hooksPath }); } finally { - restoreEnv('PATH', originalPath); + restoreEnv('GIT_TRACE2_EVENT', originalTrace); } + + const starts = readFileSync(tracePath, 'utf8') + .trim() + .split('\n') + .map((line) => JSON.parse(line)) + .filter((event) => event.event === 'start'); + expect(starts).toHaveLength(1); + expect(starts[0].argv).toContain('rev-parse'); }); }); @@ -112,3 +110,31 @@ test('reports Git configuration failures without changing hooksPath', () => { expect(existsSync(path.join(cwd, hooksPath, 'runner'))).toBe(true); }); }); + +test('does not replace another Git hooks path', () => { + withRepository((cwd) => { + runGit(cwd, ['config', '--local', 'core.hooksPath', '.husky/_']); + + expect(installHooks({ cwd })).toMatchObject({ + status: 'skipped', + reason: 'hooks-path-conflict', + }); + expect(runGit(cwd, ['config', '--local', '--get', 'core.hooksPath'])).toBe('.husky/_'); + expect(existsSync(path.join(cwd, hooksPath))).toBe(false); + }); +}); + +test('does not bypass existing Git hooks', () => { + withRepository((cwd) => { + const existingHook = path.join(cwd, '.git', 'hooks', 'pre-commit'); + writeFileSync(existingHook, '#!/usr/bin/env sh\n'); + + expect(installHooks({ cwd })).toEqual({ + status: 'skipped', + reason: 'existing-git-hooks', + message: 'existing Git hooks were found: pre-commit', + }); + expect(git(cwd, ['config', '--local', '--get', 'core.hooksPath']).status).toBe(1); + expect(readFileSync(existingHook, 'utf8')).toBe('#!/usr/bin/env sh\n'); + }); +}); diff --git a/packages/rstack/tests/setup/runtime.test.ts b/packages/rstack/tests/setup/runtime.test.ts index 4efb84e9..455f1a88 100644 --- a/packages/rstack/tests/setup/runtime.test.ts +++ b/packages/rstack/tests/setup/runtime.test.ts @@ -2,11 +2,12 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'n import path from 'node:path'; import { expect, test } from 'rstack/test'; import { installHooks } from '../../src/setup/install.ts'; -import { runHook, withRepository, writeHook, writeInit } from './helpers.ts'; +import { runGitHook, runHook, withRepository, writeHook, writeInit } from './helpers.ts'; test('loads user init and project binaries', () => { withRepository((cwd) => { - const binDirectory = path.join(cwd, 'node_modules', '.bin'); + const projectDirectory = path.join(cwd, 'frontend'); + const binDirectory = path.join(projectDirectory, 'node_modules', '.bin'); mkdirSync(binDirectory, { recursive: true }); writeInit(cwd, 'set -u\nexport RSTACK_INIT=loaded\n'); @@ -26,11 +27,41 @@ rstack-hook-command `, ); - expect(installHooks({ cwd }).status).toBe('installed'); + expect(installHooks({ cwd: projectDirectory }).status).toBe('installed'); expect(runHook(cwd).status).toBe(0); - expect(readFileSync(path.join(cwd, 'init-ran'), 'utf8')).toBe('loaded\n'); - expect(readFileSync(path.join(cwd, 'project-bin-ran'), 'utf8')).toBe('ran\n'); + expect(readFileSync(path.join(projectDirectory, 'init-ran'), 'utf8')).toBe('loaded\n'); + expect(readFileSync(path.join(projectDirectory, 'project-bin-ran'), 'utf8')).toBe('ran\n'); + }); +}); + +test('preserves cwd-sensitive hook arguments for a nested project', () => { + withRepository((cwd) => { + const projectDirectory = path.join(cwd, 'frontend'); + const messagePath = '.git/COMMIT_EDITMSG'; + mkdirSync(projectDirectory); + writeFileSync(path.join(cwd, messagePath), 'commit message\n'); + + expect(installHooks({ cwd: projectDirectory }).status).toBe('installed'); + + for (const name of ['applypatch-msg', 'commit-msg', 'prepare-commit-msg']) { + writeFileSync(path.join(cwd, '.rstack', 'hooks', name), 'cat "$1"\n'); + + expect(runGitHook(cwd, name, [messagePath])).toMatchObject({ + status: 0, + stderr: 'commit message\n', + }); + } + + mkdirSync(path.join(cwd, 'remote.git')); + writeFileSync( + path.join(cwd, '.rstack', 'hooks', 'pre-push'), + '[ -d "$2" ] || [ "$2" = "git@example.com:repo.git" ]\n', + ); + + for (const remote of ['remote.git', 'git@example.com:repo.git']) { + expect(runGitHook(cwd, 'pre-push', ['origin', remote]).status).toBe(0); + } }); }); @@ -46,5 +77,8 @@ test('skips user hooks when disabled by the environment or init', () => { expect(runHook(cwd).status).toBe(0); expect(existsSync(path.join(cwd, 'hook-ran'))).toBe(false); + + writeFileSync(path.join(cwd, '.rstack', 'hooks', 'commit-msg'), 'exit 1\n'); + expect(runGitHook(cwd, 'commit-msg', []).status).toBe(0); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e7d4b033..e26f1fa5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,9 @@ settings: catalogs: default: + '@napi-rs/cli': + specifier: ^3.8.3 + version: 3.8.3 '@rsbuild/core': specifier: ~2.1.10 version: 2.1.10 @@ -35,8 +38,8 @@ catalogs: specifier: 1.14.7 version: 1.14.7 '@rstackjs/create-toolkit': - specifier: 2.1.5 - version: 2.1.5 + specifier: 2.2.3 + version: 2.2.3 '@rstackjs/load-config': specifier: ^0.1.2 version: 0.1.2 @@ -44,17 +47,17 @@ catalogs: specifier: ^0.2.0 version: 0.2.0 '@rstest/adapter-rsbuild': - specifier: ~0.11.5 - version: 0.11.5 + specifier: ~0.11.6 + version: 0.11.6 '@rstest/adapter-rslib': - specifier: ~0.11.5 - version: 0.11.5 + specifier: ~0.11.6 + version: 0.11.6 '@rstest/core': - specifier: ~0.11.5 - version: 0.11.5 + specifier: ~0.11.6 + version: 0.11.6 '@shikijs/transformers': - specifier: ^4.4.1 - version: 4.4.1 + specifier: ^4.4.2 + version: 4.4.2 '@testing-library/dom': specifier: ^10.4.1 version: 10.4.1 @@ -83,14 +86,11 @@ catalogs: specifier: 2.1.0 version: 2.1.0 happy-dom: - specifier: ^20.11.1 - version: 20.11.1 + specifier: ^20.11.2 + version: 20.11.2 heading-case: - specifier: ^1.1.4 - version: 1.1.4 - ignore: - specifier: 7.0.6 - version: 7.0.6 + specifier: ^1.1.5 + version: 1.1.5 import-meta-resolve: specifier: 4.2.0 version: 4.2.0 @@ -134,8 +134,8 @@ catalogs: specifier: ^7.0.2 version: 7.0.2 yuku-parser: - specifier: 0.8.3 - version: 0.8.3 + specifier: 0.8.4 + version: 0.8.4 importers: @@ -149,7 +149,7 @@ importers: version: 0.0.4 heading-case: specifier: 'catalog:' - version: 1.1.4 + version: 1.1.5 prettier: specifier: 'catalog:' version: 3.9.6 @@ -189,7 +189,7 @@ importers: version: 19.2.4(@types/react@19.2.18) happy-dom: specifier: 'catalog:' - version: 20.11.1 + version: 20.11.2 rstack: specifier: workspace:* version: link:../../packages/rstack @@ -210,7 +210,7 @@ importers: version: 24.13.3 happy-dom: specifier: 'catalog:' - version: 20.11.1 + version: 20.11.2 rstack: specifier: workspace:* version: link:../../packages/rstack @@ -277,7 +277,7 @@ importers: version: 19.2.4(@types/react@19.2.18) happy-dom: specifier: 'catalog:' - version: 20.11.1 + version: 20.11.2 react: specifier: 'catalog:' version: 19.2.8 @@ -320,7 +320,7 @@ importers: version: 19.2.4(@types/react@19.2.18) happy-dom: specifier: 'catalog:' - version: 20.11.1 + version: 20.11.2 rstack: specifier: workspace:* version: link:../../packages/rstack @@ -332,7 +332,7 @@ importers: dependencies: '@rstackjs/create-toolkit': specifier: 'catalog:' - version: 2.1.5 + version: 2.2.3 devDependencies: '@types/node': specifier: 'catalog:' @@ -357,7 +357,7 @@ importers: version: 0.7.3 '@rstest/core': specifier: 'catalog:' - version: 0.11.5(happy-dom@20.11.1) + version: 0.11.6(happy-dom@20.11.2) prettier: specifier: 'catalog:' version: 3.9.6 @@ -366,8 +366,11 @@ importers: version: 2.1.0 yuku-parser: specifier: 'catalog:' - version: 0.8.3 + version: 0.8.4 devDependencies: + '@napi-rs/cli': + specifier: 'catalog:' + version: 3.8.3(@types/node@24.13.3)(node-addon-api@7.1.1)(supports-color@8.1.1) '@rspress/core': specifier: 'catalog:' version: 2.0.19(micromark-util-types@2.0.2)(micromark@4.0.2)(supports-color@8.1.1) @@ -379,10 +382,10 @@ importers: version: 0.2.0 '@rstest/adapter-rsbuild': specifier: 'catalog:' - version: 0.11.5(@rsbuild/core@2.1.10)(@rstest/core@0.11.5) + version: 0.11.6(@rsbuild/core@2.1.10)(@rstest/core@0.11.6) '@rstest/adapter-rslib': specifier: 'catalog:' - version: 0.11.5(@rslib/core@1.0.0-beta.2)(@rstest/core@0.11.5)(typescript@7.0.2) + version: 0.11.6(@rslib/core@1.0.0-beta.2)(@rstest/core@0.11.6)(typescript@7.0.2) '@types/micromatch': specifier: 'catalog:' version: 4.0.10 @@ -392,9 +395,6 @@ importers: fast-json-stable-stringify: specifier: 'catalog:' version: 2.1.0 - ignore: - specifier: 'catalog:' - version: 7.0.6 import-meta-resolve: specifier: 'catalog:' version: 4.2.0 @@ -439,7 +439,7 @@ importers: version: 1.14.7(@rspress/core@2.0.19) '@shikijs/transformers': specifier: 'catalog:' - version: 4.4.1 + version: 4.4.2 '@types/node': specifier: 'catalog:' version: 24.13.3 @@ -550,15 +550,167 @@ packages: '@bufbuild/protobuf@2.12.1': resolution: {integrity: sha512-BvAMfS6LrgZiryOAZ4pBYucu4wG/Ei/9o9DZ9akbREnMLbPJiom2i8b9C8IsKErQoiKqVhrerzt3kOT/RrzLHg==} + '@emnapi/core@1.11.2': + resolution: {integrity: sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==} + '@emnapi/core@1.11.3': resolution: {integrity: sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==} + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + + '@emnapi/runtime@1.11.2': + resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + '@emnapi/runtime@1.11.3': resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@emnapi/wasi-threads@1.2.3': resolution: {integrity: sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==} + '@inquirer/ansi@2.0.7': + resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + + '@inquirer/checkbox@5.2.1': + resolution: {integrity: sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@6.1.1': + resolution: {integrity: sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@11.2.1': + resolution: {integrity: sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@5.2.2': + resolution: {integrity: sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@5.1.1': + resolution: {integrity: sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@3.0.3': + resolution: {integrity: sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@2.0.7': + resolution: {integrity: sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + + '@inquirer/input@5.1.2': + resolution: {integrity: sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@4.1.1': + resolution: {integrity: sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@5.1.1': + resolution: {integrity: sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@8.5.2': + resolution: {integrity: sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@5.3.1': + resolution: {integrity: sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@4.2.1': + resolution: {integrity: sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@5.2.1': + resolution: {integrity: sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@4.0.7': + resolution: {integrity: sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} @@ -568,12 +720,417 @@ packages: '@types/react': '>=16' react: '>=16' + '@napi-rs/cli@3.8.3': + resolution: {integrity: sha512-f5vr9ih+ROvX5x9yZ4ywGj+kqcMXTzc4TsXUT4KUmfYlcdKTJ0uROuzeDP6rfDKhCqWo7EL6nBvfMWkhv5TMeQ==} + engines: {node: ^20.17.0 || ^22.13.0 || >= 23.5.0} + hasBin: true + peerDependencies: + '@emnapi/runtime': 2.0.0-alpha.3 + peerDependenciesMeta: + '@emnapi/runtime': + optional: true + + '@napi-rs/cross-toolchain@1.0.3': + resolution: {integrity: sha512-ENPfLe4937bsKVTDA6zdABx4pq9w0tHqRrJHyaGxgaPq03a2Bd1unD5XSKjXJjebsABJ+MjAv1A2OvCgK9yehg==} + peerDependencies: + '@napi-rs/cross-toolchain-arm64-target-aarch64': ^1.0.3 + '@napi-rs/cross-toolchain-arm64-target-armv7': ^1.0.3 + '@napi-rs/cross-toolchain-arm64-target-ppc64le': ^1.0.3 + '@napi-rs/cross-toolchain-arm64-target-s390x': ^1.0.3 + '@napi-rs/cross-toolchain-arm64-target-x86_64': ^1.0.3 + '@napi-rs/cross-toolchain-x64-target-aarch64': ^1.0.3 + '@napi-rs/cross-toolchain-x64-target-armv7': ^1.0.3 + '@napi-rs/cross-toolchain-x64-target-ppc64le': ^1.0.3 + '@napi-rs/cross-toolchain-x64-target-s390x': ^1.0.3 + '@napi-rs/cross-toolchain-x64-target-x86_64': ^1.0.3 + peerDependenciesMeta: + '@napi-rs/cross-toolchain-arm64-target-aarch64': + optional: true + '@napi-rs/cross-toolchain-arm64-target-armv7': + optional: true + '@napi-rs/cross-toolchain-arm64-target-ppc64le': + optional: true + '@napi-rs/cross-toolchain-arm64-target-s390x': + optional: true + '@napi-rs/cross-toolchain-arm64-target-x86_64': + optional: true + '@napi-rs/cross-toolchain-x64-target-aarch64': + optional: true + '@napi-rs/cross-toolchain-x64-target-armv7': + optional: true + '@napi-rs/cross-toolchain-x64-target-ppc64le': + optional: true + '@napi-rs/cross-toolchain-x64-target-s390x': + optional: true + '@napi-rs/cross-toolchain-x64-target-x86_64': + optional: true + + '@napi-rs/lzma-android-arm-eabi@1.5.1': + resolution: {integrity: sha512-sahBe4ko2Z69NPTddaX6ZgbQZu9SDoITxw1S3dWl1gAGynZG34qHHCT8UaUMFxf3h3zMhCJjEzz4basaBxiTuQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [arm] + os: [android] + + '@napi-rs/lzma-android-arm64@1.5.1': + resolution: {integrity: sha512-7tkQAJJuBHxAxiEBNFgSTpvrtGpbwZYYJUSOmGEK3OfbdbNeoT2rdBxpM/gY1s+itEVbtOSlpaRPPG19MnwOzA==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [arm64] + os: [android] + + '@napi-rs/lzma-darwin-arm64@1.5.1': + resolution: {integrity: sha512-XWX8gtF+GHGk3nH3Wm3QUZNcxw9QHsFVZz3MzVLhWWHhceede1J4/vD+3dj3E1iKB9G6mualaZxOoD08R3E+7g==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [arm64] + os: [darwin] + + '@napi-rs/lzma-darwin-x64@1.5.1': + resolution: {integrity: sha512-CfsqUpMTI1z8enrA/b+GcHM6YDI8D0kqCiqPYEnst4rbOABQ9KZ92ybTTNnlnZ7A017WoMZKUEWc36KXDwi0xg==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [darwin] + + '@napi-rs/lzma-freebsd-x64@1.5.1': + resolution: {integrity: sha512-bTyNfg90FXIgE61U7l14aMmVOqRQ6AyP5JMT3jmCStaZI18apLNPdzZ8i7yqxZfKvRMVfPjE2brXIw27c+RRgA==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [freebsd] + + '@napi-rs/lzma-linux-arm-gnueabihf@1.5.1': + resolution: {integrity: sha512-vNE+D8nrw+eOkBsdKCsmDhowDV3pIMKXEhedvXfbgrWbrO7GlZJH+RXL+X+RYLxGwi8Ym61ZMt15sIOnNmh9Sw==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [arm] + os: [linux] + + '@napi-rs/lzma-linux-arm64-gnu@1.5.1': + resolution: {integrity: sha512-csUem4WgoKGTprv/pOPm9UIWbb+hrfUwYXefpTHPAEGVFLl5behEFabisJ7FtihCa3yG2Efcl+yw25rlhhrIYw==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@napi-rs/lzma-linux-arm64-musl@1.5.1': + resolution: {integrity: sha512-kB/xhlVN1eLvVmDJSKZEjp5Gg2xDYexNrB5jwpSMbOkeGS6N9AasByPBg5VqCpMYC+zZi7DM458DRhtWYhqXTQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@napi-rs/lzma-linux-ppc64-gnu@1.5.1': + resolution: {integrity: sha512-s28RW0W1yBWQc1nbPdF7tp14koqslY3ZWLVI8uaanX292Dc6ezd4NPVwxEoCNBVON/oD7BmUbWGtyFvmm7dQ5A==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@napi-rs/lzma-linux-riscv64-gnu@1.5.1': + resolution: {integrity: sha512-+lGNwYlIN14YPMTNvYtIJJqHFevDTd6Juw/1NmXbWx/iRd/LLrjhlM/yluMX6pxs6NkOGsuuEXJJrbbEUS59OQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@napi-rs/lzma-linux-s390x-gnu@1.5.1': + resolution: {integrity: sha512-PB44FFWWFrLeQowhcep1hPD1YcLqKlnnY60RMU74qrxTlr4YGEyzeMItJqh2uivBfv9kQScOF/B0J9+Vab/oyw==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@napi-rs/lzma-linux-x64-musl@1.5.1': + resolution: {integrity: sha512-I3nsYrWtrW9JpeCr+mkJIVDt0HY3m6qVUBs5vTtoIvJQxwqf1PBXSy5IS7T53ksQFH2kd2UX8rLxJ7B4WISpZg==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [musl] + + '@napi-rs/lzma-wasm32-wasi@1.5.1': + resolution: {integrity: sha512-gy3wwPBa6+XEyA4fUzq6CClrXA1ajXjuVf5zbnHytJRgoHznj+mvpU3+co2fxXwqTCmIpn6KrzqH5bRDztBPhA==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [wasm32] + + '@napi-rs/lzma-win32-arm64-msvc@1.5.1': + resolution: {integrity: sha512-dK+huOsHiyH6oJjij+cnjqFCakk2HgWmpI12Xm4pLUyPphe4ebYoJBgehaNAxprmjFqBQ7nL95YPVz9BHyqmPg==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [arm64] + os: [win32] + + '@napi-rs/lzma-win32-ia32-msvc@1.5.1': + resolution: {integrity: sha512-dGE8L+0EQ+GyU9ap9InqB/t/PmPG/bLj918q7OsJ29FuTdn8fK4OX3U4IQZhylHIA+/dQ/SXJk5n4yfah2XVvA==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [ia32] + os: [win32] + + '@napi-rs/lzma-win32-x64-msvc@1.5.1': + resolution: {integrity: sha512-EKW4t/iqdCT/xnd5t9oXLvVER/PMNAWXKqUAl3fgvUcOILeZIIht77/dVnfFcc9htA/DCBXC/6YQWdW+LusjFA==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [win32] + + '@napi-rs/lzma@1.5.1': + resolution: {integrity: sha512-sgOZ89+y8cDbY+3WbzR8CtIhCuFRWotZ9/2PjPVDJHz6np5KFTAev0DrwiyTJTgFsCRDhfGlbmhMgyhHbWdZ6g==} + engines: {node: ^22.20 || ^24.12 || >=25} + + '@napi-rs/tar-android-arm-eabi@1.1.1': + resolution: {integrity: sha512-cAhnA10cSusAUbcE9HtjQY/tZ9BH/0w2sKtRcQc94TzIlnm7QSr1htJSd/PPrbWNPtrv1orXb2CkrHlVlbnlHA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@napi-rs/tar-android-arm64@1.1.1': + resolution: {integrity: sha512-EslUWHCDBY/g5abTPBiHLsMaML4GagV0TXLm5WL9hAjx/DDtlxz9fegMb77RJ+f7nFLOIsUxF/3QWFvgOT0sMQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/tar-darwin-arm64@1.1.1': + resolution: {integrity: sha512-+A42/6ES5G9CQ35BOwzwA+WBjLID28r2jNPgc0dteD2hhClIhng0mva7D2ujUlXBNmgNOsr1LHn3stA4uTf4NQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/tar-darwin-x64@1.1.1': + resolution: {integrity: sha512-RYtE8w1dkEvj8hSJCDV5Jw0Rz2i13fsM7u893zv5O9n/4Ad5GNsw/f4RQ7/0YGSFaenkVxqPFrjmEvUHlKzsrg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/tar-freebsd-x64@1.1.1': + resolution: {integrity: sha512-rEepBvCJUwcuvUYkY83e8aot8RsR5Jcnal4PsG3tbWGKW1yAvcXhyMXf0fN6ZGpVRZFnB+FJqDyBxvsCPEXKhw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@napi-rs/tar-linux-arm-gnueabihf@1.1.1': + resolution: {integrity: sha512-an1bJdfyhI5FpZYyTQ20mrqwR+a676i8GkaYc4Uy12dH/a7TJIfrK6Qa2Gm46arZvxUvx56qxoRKXbpOjUPvwA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/tar-linux-arm64-gnu@1.1.1': + resolution: {integrity: sha512-w++Vtx36T2yHTKws7GVnmHHcUT1ybB59xLWSh9A8bwEpJVG4dG7Qub9mFe5cpcbfrJ+XP2mKKxC3oUJSunK3iQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@napi-rs/tar-linux-arm64-musl@1.1.1': + resolution: {integrity: sha512-Rh6UFhNtj3i4deJHOBINFIeRL0072mgbeyuK5rl1HokKnNoMKx8qKIZNEzBTTqpogMfDHWGvzyTQdnVxes5dpA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@napi-rs/tar-linux-ppc64-gnu@1.1.1': + resolution: {integrity: sha512-Cp+AxFbv9zcyAXtnzQi0OzmgDnQgy2w9D4Ubr+iwzMtVgJcztzcEoCcCrN1k2ATdEB01LX2Vb49IaocGOZhC9Q==} + engines: {node: '>= 10'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@napi-rs/tar-linux-s390x-gnu@1.1.1': + resolution: {integrity: sha512-ZyscC3SYKTBWyDRYjLOKAd5TyJ7q0KACRdQ8bWrb3rgrra1CCIJD66CsGTH6Dh0AVSdfLwZ8MfIIXU6+14BMjQ==} + engines: {node: '>= 10'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@napi-rs/tar-linux-x64-gnu@1.1.1': + resolution: {integrity: sha512-LlIv+zg4fiOQge9LQX/ieBdRWE2fhVDjCTHxnunZkbugNmdhdelxWf1RpZb/6ZujWpNF4LPu4N/MW7ygg2oYAQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@napi-rs/tar-linux-x64-musl@1.1.1': + resolution: {integrity: sha512-gZBeoKLjanOVj55qk4EMu13P2i9M0SuINmlGQkOxm1niIJofexzddHUYtqO5o/5QqtyL8lADmAcZplLILMLhHA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@napi-rs/tar-wasm32-wasi@1.1.1': + resolution: {integrity: sha512-rwtQ1Mdt/ft6g6I54fJzbUeLspl4yTwj6I3UJ6mitKnrN42soJkcDrdh3Y/FGvlpqZTad2YMQ96fGJl3EtAm2Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@napi-rs/tar-win32-arm64-msvc@1.1.1': + resolution: {integrity: sha512-30PVp1AehRpfwxmv5wI4cg0yj3WmWBsZ+1QnLGnvEELu7Eu/+dhNU0nrmhI7VfPgLwSRK2eg9DQTB3tP7Wv9bA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@napi-rs/tar-win32-ia32-msvc@1.1.1': + resolution: {integrity: sha512-aI3/rmz+izUChiSeaPxcasAOxhf3FpJNuIHMXlxS/vpW+HIxUsSDR5+XV61PEG5DL4L/75iENVUxmSGM5l2yaw==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@napi-rs/tar-win32-x64-msvc@1.1.1': + resolution: {integrity: sha512-yJsB2IsrODQVLKbm2Fg1nHiVRbEj49mSPbj4x7JPZWJI0jGVPjohE2Sif0FBbx8OxsVoUODvS0BwksZZ8jl/OA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/tar@1.1.1': + resolution: {integrity: sha512-p6q2HhUc5vwH1CNwfOcrhLoxfgn8ust8Sqlfx+sA4VzAcp1cMbvbkl99tZZlDqOjCHgQNSiTfk/yWPjl/D42qA==} + engines: {node: '>= 10'} + '@napi-rs/wasm-runtime@1.1.6': resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@napi-rs/wasm-tools-android-arm-eabi@1.1.0': + resolution: {integrity: sha512-p6J8PB59I8d/XItXB/go5JH6nKW+xIbpzaL43EBTV0hi7mrS/Z4gs+MsB04ZrlqZN29BdZV8fChRyasuXLhRaA==} + engines: {node: '>= 12.22.0'} + cpu: [arm] + os: [android] + + '@napi-rs/wasm-tools-android-arm64@1.1.0': + resolution: {integrity: sha512-lWoKN3suypeBSCIRPIw+++sH9V2K6nQkhtdt1opu7XY3v9JwLs6Gw063HWRqkNjphlYpkd/Qy8XcfSPGbJj7nQ==} + engines: {node: '>= 12.22.0'} + cpu: [arm64] + os: [android] + + '@napi-rs/wasm-tools-darwin-arm64@1.1.0': + resolution: {integrity: sha512-jfw5vyNDUf6oe0kP8lMveFN9U7cLk1cUosS7uMIfw/xmqmopYfKQ198DAx2g/6aEF7Tm+CqER2gpMpYKui30LA==} + engines: {node: '>= 12.22.0'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/wasm-tools-darwin-x64@1.1.0': + resolution: {integrity: sha512-R+pjeudAB7BYdH1vKkOJM61Tfv5jB6uXkxmFscYd+KKpdUpWBlNG+s4hr0w4i1rMBM91VhIAETZn2pz+MDHK9A==} + engines: {node: '>= 12.22.0'} + cpu: [x64] + os: [darwin] + + '@napi-rs/wasm-tools-freebsd-x64@1.1.0': + resolution: {integrity: sha512-hQJTe+aazrT++Vgm6I4lUd9099ItUCFYdd+aKg6Ys6nax6d/cZ1barDLTwA2lwOoVDsXMekJI/FOL6ZvVlIYBg==} + engines: {node: '>= 12.22.0'} + cpu: [x64] + os: [freebsd] + + '@napi-rs/wasm-tools-linux-arm64-gnu@1.1.0': + resolution: {integrity: sha512-1TAXJxUHsWGar90k3W/MknavvBMwOWzjh7Q6Spxo8twRcWJbBD5Kow/Q2KhhDq5hxh2sKGDXn3uLc1tdtz4WUg==} + engines: {node: '>= 12.22.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@napi-rs/wasm-tools-linux-arm64-musl@1.1.0': + resolution: {integrity: sha512-7rw3nlubTjNAVRH2LwphCxHy1b/N2/TerXocQ6XRn4Q+buaY1Z7P/hbdALy1i1ex2yfOU2Xcij7ib7ZLi/lKfw==} + engines: {node: '>= 12.22.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@napi-rs/wasm-tools-linux-x64-gnu@1.1.0': + resolution: {integrity: sha512-1sel0t9MRjI/tdT89M8Dd6gPfANeeFP24Xa46R11WeHNwhjsXXZh+xUk50uWCRTSGcaCy3ugm3AMK/lmHYQJkg==} + engines: {node: '>= 12.22.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@napi-rs/wasm-tools-linux-x64-musl@1.1.0': + resolution: {integrity: sha512-o2jH5AMfor4EKF2HII1LBnMQxoWu7+usPifTEY8Zk6e9OiSi4EJkAXf9v3ANlX7TI2V/cUEV34OEW7r10GiVIA==} + engines: {node: '>= 12.22.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@napi-rs/wasm-tools-wasm32-wasi@1.1.0': + resolution: {integrity: sha512-s6YDtDR1UWrsqJPtaxf+JLYLceWVyn3l8OpQYElHkDhf3Qfz9R6Ba3S0OgznTBv38L5/TIHysQ9Q4yO73Z0csg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@napi-rs/wasm-tools-win32-arm64-msvc@1.1.0': + resolution: {integrity: sha512-x+NuxbG84VxU68tU8w7Rf5lSyq0l584M6dVlke5DTweHYFZoMyeqkpbwEq+qsyAX6ivfipK8xRsmFwamb5uDnA==} + engines: {node: '>= 12.22.0'} + cpu: [arm64] + os: [win32] + + '@napi-rs/wasm-tools-win32-ia32-msvc@1.1.0': + resolution: {integrity: sha512-mdD96QDEp70SX67rXFTY6c725nVYeqEEjyDqzzbNh6u1APj7CI7IMNpMmvE75XbCRl4C2MHZVU4U6AWdAzvyQQ==} + engines: {node: '>= 12.22.0'} + cpu: [ia32] + os: [win32] + + '@napi-rs/wasm-tools-win32-x64-msvc@1.1.0': + resolution: {integrity: sha512-bVVjuvhlyVX++3eJXfDR63cXdw1ay5QYac6iq0MKQw8wZARInTM+bXCtByDT4fzVFI3+7ZthYb/ERWRdBNIqgQ==} + engines: {node: '>= 12.22.0'} + cpu: [x64] + os: [win32] + + '@napi-rs/wasm-tools@1.1.0': + resolution: {integrity: sha512-VjHyKEqXAwYZK+HY7iJctYvRm3TFEbaQxeZwvAG1QRkoo1a39phMY8J6x9tUEqJI03W6MysB8F2jacI6wvcx+w==} + engines: {node: '>= 12.22.0'} + + '@octokit/auth-token@6.0.0': + resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==} + engines: {node: '>= 20'} + + '@octokit/core@7.0.7': + resolution: {integrity: sha512-DcB0M3KFgr9ECI328lhBMVsyFT2DnmNucSBTqEN3exyNKUzkkpUSCHmTRcunF41Eou2TIQKW4seewri8ON9bSA==} + engines: {node: '>= 20'} + + '@octokit/endpoint@11.0.4': + resolution: {integrity: sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA==} + engines: {node: '>= 20'} + + '@octokit/graphql@9.0.4': + resolution: {integrity: sha512-5s15CCiY8XXQ+FG+b1YQcl6Z2FA++nwAz/tg2VUrTmnMncP+2nnGUEYANImdnxsA2Fnq+Mbl7hDjUTw7cFAwcg==} + engines: {node: '>= 20'} + + '@octokit/openapi-types@27.0.0': + resolution: {integrity: sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==} + + '@octokit/openapi-types@28.0.0': + resolution: {integrity: sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==} + + '@octokit/plugin-paginate-rest@14.0.0': + resolution: {integrity: sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-request-log@6.0.0': + resolution: {integrity: sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-rest-endpoint-methods@17.0.0': + resolution: {integrity: sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/request-error@7.1.1': + resolution: {integrity: sha512-+eaY7G2VVpSf2pc5Gn1+mph837V/d/TYTJAgWL9Tb0ogGYcpN3IlAVFgjL+Vv93F/sevrxkvsYCedtpLdcFLzA==} + engines: {node: '>= 20'} + + '@octokit/request@10.0.13': + resolution: {integrity: sha512-v2269YxL9Yf+x3d+gRI63FP0vFQEiWgLyBzxe/Y+0yFDg2B/Tzf5dhh9VNfccVAQnfcfwQWyk/y6Bn7rUXXs7A==} + engines: {node: '>= 20'} + + '@octokit/rest@22.0.1': + resolution: {integrity: sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==} + engines: {node: '>= 20'} + + '@octokit/types@16.0.0': + resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==} + + '@octokit/types@17.0.0': + resolution: {integrity: sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q==} + '@parcel/watcher-android-arm64@2.5.6': resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} engines: {node: '>= 10.0.0'} @@ -871,8 +1428,8 @@ packages: '@rspress/core': optional: true - '@rstackjs/create-toolkit@2.1.5': - resolution: {integrity: sha512-55hmJqJJbyvdz9ET2NLXpQ+AF1+keb75duvLXMEU423sEPM1e8GdIE7aS3ODsXYKJ7CoYphDjVkcaDeB0TS5uw==} + '@rstackjs/create-toolkit@2.2.3': + resolution: {integrity: sha512-0I1U/BGGLXBkvE+C9oDKYfmg1wjFMHCPQlG2BJF+f41kL0BmXZaQMa/m515lo8Q349otqyAEam4A5BMuxetTVw==} engines: {node: ^20.19.0 || >=22.12.0} '@rstackjs/load-config@0.1.2': @@ -886,29 +1443,29 @@ packages: '@rstackjs/test-utils@0.2.0': resolution: {integrity: sha512-P+LOo1WE3xYeGkHmEthyq2cIpN69k4LhiB/4UBSceD+nW9hDlhWv8MC0LTLWokZXccWl4ntcfOBjQFllkcBlPA==} - '@rstest/adapter-rsbuild@0.11.5': - resolution: {integrity: sha512-v2iHYkELfLEVFhJcT9n/ndXkGjLeTdawll/sfy9GGD8ZajZGnZ0EqmziGVKlAexjAESyzmzv8NzBhOkYH4cbow==} + '@rstest/adapter-rsbuild@0.11.6': + resolution: {integrity: sha512-l2bKftH1IEuY3Sj7ZEb+k6OoZf2FO0vTeKfk1Xxo2ons9fL1LHsbNDWEXNw9lNHg0fv92sai6ygQkGkvCpxkjg==} peerDependencies: '@rsbuild/core': ^1.0.0 || ^2.0.0 - '@rstest/core': ^0.11.5 + '@rstest/core': ^0.11.0 - '@rstest/adapter-rslib@0.11.5': - resolution: {integrity: sha512-ijVBmQ+uyJF4yw5HVbGhoXUEepnlb7mFXhuUm+C/OxKJim+IYUHGhDB9Mz6Zzo+ltgPVFqGpP4QQNGPAxJXXVQ==} + '@rstest/adapter-rslib@0.11.6': + resolution: {integrity: sha512-0NOU3W63TWtbWExgT/gvpbQ5ZtWqW5HepvJ/mNne7FgZfjm2bNwDF2Saglpg/M83KuBpA9xmnrOUQXNosJkCBQ==} peerDependencies: '@rslib/core': '>=0.18.6 || ^1.0.0-0' - '@rstest/core': ^0.11.5 + '@rstest/core': ^0.11.0 typescript: ^5.0.0 || ^6.0.0 || ^7.0.0 peerDependenciesMeta: typescript: optional: true - '@rstest/core@0.11.5': - resolution: {integrity: sha512-ySXZaFqU1mJonm79ko7OwagG2AurULg1dLDErJguhv/sepEyjt39sq2Bpt42mOxHehiFcl0Pr0PrlaPltmYbbA==} + '@rstest/core@0.11.6': + resolution: {integrity: sha512-P3wgYGDF3JmhapwN3p4DnbNV9N6+E+dlbp0coT1lAuXN5Po6bHeP/rduGLsTUIX85qWxmJD7ekF7nlOKm9RoOA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: happy-dom: ^20.8.3 - jsdom: '*' + jsdom: '>=15.0.0' peerDependenciesMeta: happy-dom: optional: true @@ -919,8 +1476,8 @@ packages: resolution: {integrity: sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==} engines: {node: '>=20'} - '@shikijs/core@4.4.1': - resolution: {integrity: sha512-VeR2CY6Nn9/WbisoYLOQZ7HZOnwTrpBuOw4wExjqLnBCi62BNWynBUO6K2uPIASPFJwAv7cX1fUu+LrPlSstcw==} + '@shikijs/core@4.4.2': + resolution: {integrity: sha512-StyzbAyxg2/tBGf78gwbBkGyeQ73lf8UiJArFaQhTQIDqQOCKPCQFanvrs4/Yv3Yfyc+ONInJM6K+FMIf+P+kA==} engines: {node: '>=20'} '@shikijs/engine-javascript@4.3.1': @@ -939,8 +1496,8 @@ packages: resolution: {integrity: sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==} engines: {node: '>=20'} - '@shikijs/primitive@4.4.1': - resolution: {integrity: sha512-ko2OfDoG89YuQ7xL5LtcQiWKb7NIv1Ephb7g48TVU198OzAMLC8lXVEwaJGHK4sUMYrfAGJDqYmNLOLiW/Kz8w==} + '@shikijs/primitive@4.4.2': + resolution: {integrity: sha512-l6fQQKsOMlz72n38fztmSgZ76MO6KSWuw8o+GJ+FhmqrpC9pIOJNQNXGgbb5yX2AwpzlEHwsaLPnk/8o4Fm+rA==} engines: {node: '>=20'} '@shikijs/rehype@4.3.1': @@ -951,16 +1508,16 @@ packages: resolution: {integrity: sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==} engines: {node: '>=20'} - '@shikijs/transformers@4.4.1': - resolution: {integrity: sha512-Sb9Eehas+5EhClpFgNuklwY3aWf354FLaKRCiAWmjdNbHAjoQUpv6WmSj+N19eTXO6GLIWh1dIOH9dxyauhVWw==} + '@shikijs/transformers@4.4.2': + resolution: {integrity: sha512-d81PJ9KkR1tVP95FH/9296HTtDo0mh76wv10u9T1YmsZq/UcXgt0OLdBszfUQ1i+umkRMCjDnFbFZU7/tCODTQ==} engines: {node: '>=20'} '@shikijs/types@4.3.1': resolution: {integrity: sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==} engines: {node: '>=20'} - '@shikijs/types@4.4.1': - resolution: {integrity: sha512-GOwCLQDHM5EjGUWNPrhzJbr6JP8V/Dx/CDVkWvbZ1Avw5JFnNUckrgbLmE07qtg4WlW7Q7QFndhjIkeU9XMPvw==} + '@shikijs/types@4.4.2': + resolution: {integrity: sha512-PFYitV4vpDr/iPCIhnHp+Q4ftic5N5VeNJ3KQ1O8gn3h2ar8qgwMAXF7tq4m1CWaMS60fV4VqF6vfnWH4F7vqQ==} engines: {node: '>=20'} '@shikijs/vscode-textmate@10.0.2': @@ -1184,74 +1741,74 @@ packages: peerDependencies: react: '>=18.3.1' - '@yuku-parser/binding-android-arm64@0.8.3': - resolution: {integrity: sha512-vySYRsMeul9ssvxeHdxgS9ZUIcq7gqljWNqgokjJE0uQWvVvOprihJ6hOsiifVqWsla0BMc3vAFBvNS9QqCw7g==} + '@yuku-parser/binding-android-arm64@0.8.4': + resolution: {integrity: sha512-+HIMmv08Zrh9ugIAEMnKBMMePOl7CDxrjc8Vui1+GG2TJHM1yI1+3wo1pnXB6Nj2IiHugDkQw8ycUI6SA2EUkQ==} cpu: [arm64] os: [android] - '@yuku-parser/binding-darwin-arm64@0.8.3': - resolution: {integrity: sha512-+wpB/wqhiZ685Y77I+lj6v9pHSAJ3Y+QMHJmvch0Q0ahIMbNwtKk3s54MhtjCMKO1qpjPbyN/PjuHDg2hbKaVQ==} + '@yuku-parser/binding-darwin-arm64@0.8.4': + resolution: {integrity: sha512-Elf/B/2m3OsyvxoQnBk8Dtu+9csHkzBNs5Yv9GbHjT3x0kVKNWjFusyZgm41VwxcPDqdpRi8tWxNX7OqXkmf/A==} cpu: [arm64] os: [darwin] - '@yuku-parser/binding-darwin-x64@0.8.3': - resolution: {integrity: sha512-jKqiWejj4zVy7pPtEGu4/Ty+pG1h7ooQOXIkm7shKZTSwTU9X8X+eoH11uIeKHZi2SQWV0GhNz0J56eerseysQ==} + '@yuku-parser/binding-darwin-x64@0.8.4': + resolution: {integrity: sha512-CjZuMoXnL5XUkVpDqh4WDPwpAw8CwmtHHnTerGkS45So/sNuwkXdyIAEqqIZfaLopi5W/V9NApAT2md9XizjsQ==} cpu: [x64] os: [darwin] - '@yuku-parser/binding-freebsd-x64@0.8.3': - resolution: {integrity: sha512-FC7zSwzFzd4z9bsId07CiHLR+Iw6yW/LzIQhL5AUtPUuVXLgEyx0rilgbRUYkl1CT3GJcLpkh63WuPZUSgCDzw==} + '@yuku-parser/binding-freebsd-x64@0.8.4': + resolution: {integrity: sha512-ibLKORdz71iI4Vs+fyFgvwQ51P5XcxJIyQLa8cSEWqwptRdo+BTcZHIQEcZnFDPUuvmJ19RRH9CoiJdfhr7pZw==} cpu: [x64] os: [freebsd] - '@yuku-parser/binding-linux-arm-gnu@0.8.3': - resolution: {integrity: sha512-So61j88b9/ygDnUPlWCm1EUPw4HSxAyDjrNHKgud5N3aRDQ3kw94nW7TriXbo7GBXID9oBHCMNm1r1Fof/Df5Q==} + '@yuku-parser/binding-linux-arm-gnu@0.8.4': + resolution: {integrity: sha512-Fo3r5fYhGDcFnl+KN+L9PgtiQPS4AIE1n1mG1o5jZ11p7g5yZ/1EjLFmSHAUsoXreun8KjTsFjEL7P1Sb93PZQ==} cpu: [arm] os: [linux] libc: [glibc] - '@yuku-parser/binding-linux-arm-musl@0.8.3': - resolution: {integrity: sha512-Nmnn20yJvSSKL8ZdtqReBRSGCDkSMqR5jEk/Sk/cdIdZmqVD49Z6M7w2GbMjdrxMI1MBPbsWFMMWxa93cd5t5g==} + '@yuku-parser/binding-linux-arm-musl@0.8.4': + resolution: {integrity: sha512-BEB31vUEgXPWf7WkoMPSzzJhpC/wWCBXyysRCCPsw47BJ/OtbQsvJbxX9fFDuSRLy3kbyIV/WbdUTgbQ9COxiw==} cpu: [arm] os: [linux] libc: [musl] - '@yuku-parser/binding-linux-arm64-gnu@0.8.3': - resolution: {integrity: sha512-Lfgw7AXJ0rxu6BMPGgfc8HLJWEIr8BHhCzcQp/75k+NM90uCLkHlBNqIg/K42KlSvBgAvu9euOvjdswib+4qJA==} + '@yuku-parser/binding-linux-arm64-gnu@0.8.4': + resolution: {integrity: sha512-xGLCRcHn9xVz7JVNyyKtiNJSf503qtUmih9XVSsghgzOmiKUMnHObs69OMMwXN3788tg1jsl11fNjjQlB8idMA==} cpu: [arm64] os: [linux] libc: [glibc] - '@yuku-parser/binding-linux-arm64-musl@0.8.3': - resolution: {integrity: sha512-cfRyu87xsJ0tFkHNsnMC4Rq6+xsFJ6i2dc4VAH52d2qLvykEJU/Mdi3ul1O2PyOApX/LoLT3uQZ0fWs3D5XE4w==} + '@yuku-parser/binding-linux-arm64-musl@0.8.4': + resolution: {integrity: sha512-3kNRi8NJT2q6FQRVCUFHIQ99+kXdi8cVJEEUi6+xtFqpMgNrZNnbgAj28ILsDC7zmclaU+v47eUCeJoKLSCbww==} cpu: [arm64] os: [linux] libc: [musl] - '@yuku-parser/binding-linux-x64-gnu@0.8.3': - resolution: {integrity: sha512-GcQQCUuYxbm6P1n+io/A50rvWKDeWHutIp6rW0ycDOZuEQjOb8hDVgS88+NDyOnd9FfS0/Z6GXopcRFDyKpzOg==} + '@yuku-parser/binding-linux-x64-gnu@0.8.4': + resolution: {integrity: sha512-isi62oMy94Z3OXwGs2l2rkqRiRyqLmfHeTRHpA/uWZbsNhnm7IdVvkF7e7wHNKAtd5jwGzOqYSroxKv2fOm7Cw==} cpu: [x64] os: [linux] libc: [glibc] - '@yuku-parser/binding-linux-x64-musl@0.8.3': - resolution: {integrity: sha512-rMkImBGZzg7GZlj8krYtdiezyjYI4igjKWMut5T65jHyNWFigMQrEpn9mDIBflloW9FKhGE3mN6yTZ/N+4HRwg==} + '@yuku-parser/binding-linux-x64-musl@0.8.4': + resolution: {integrity: sha512-9RsEw2xYHqU/pjSRBTOupWN3sF8uz9stjJdARfz6o0llvF+yfrj5QHmTiocGGBeAI80fvKmDtr2RIQClUzAPcA==} cpu: [x64] os: [linux] libc: [musl] - '@yuku-parser/binding-win32-arm64@0.8.3': - resolution: {integrity: sha512-/2Pl2cAzCXWxah8FqJapEj/ikpt9cEutEZFCa0hnbfrshkn5+C+aBM3ZDq62d1jsgQjBMmqr5HVhJUA4OAG/Tg==} + '@yuku-parser/binding-win32-arm64@0.8.4': + resolution: {integrity: sha512-VEZHo9rEGOBKR20sA3vCO00aQvwWND5aLu7YxeX+YupMZJh9hd1f17AbClJN37Q2iL1PCHht+wDTOKX6tZYqXg==} cpu: [arm64] os: [win32] - '@yuku-parser/binding-win32-x64@0.8.3': - resolution: {integrity: sha512-Ntnvjoan9jnfLhn7Kn3h8j/bhsbVdQSVmKUqFULKtmwImLCJVHOJbLL4qbEJyrOQ7r/FBL1/c/dRvx/AQWzzXg==} + '@yuku-parser/binding-win32-x64@0.8.4': + resolution: {integrity: sha512-PeH3VzN1feGjPtDpVEAqf000fPT+nxtw/696LKp/5Z9RJi/MaXpB636QC+5QtrAPSoEnIkyEe+c+mq2QLZPWBA==} cpu: [x64] os: [win32] - '@yuku-toolchain/types@0.8.3': - resolution: {integrity: sha512-9LN3HYs3A9qSPVFunsxlbfwBcUgexti3TmhOzIxB/UH8zFuaHQJXTRDcN17DW6cp1GsyZtiZA7f18uIra36Jag==} + '@yuku-toolchain/types@0.8.4': + resolution: {integrity: sha512-p7JE8flrj7ijZ/qLjHi4UwKqMarMD6zumbKXhrjp2I2iLJOuTYiQyci2U36VlXcUlNyzsY7E/mLnKCHotbzJVw==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -1271,6 +1828,9 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} @@ -1289,6 +1849,9 @@ packages: bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + before-after-hook@4.0.0: + resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} + big.js@5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} @@ -1322,10 +1885,20 @@ packages: character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chardet@2.2.0: + resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} + chokidar@5.0.0: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + clipanion@4.0.0-rc.4: + resolution: {integrity: sha512-CXkMQxU6s9GklO/1f714dkKBMu1lopS1WFF0B8o4AxPykR1hpozxSiUZ5ZUeBjfPgCWqbcNOtZVFhB8Lkfp1+Q==} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -1333,6 +1906,9 @@ packages: collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + colorjs.io@0.5.2: resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==} @@ -1342,6 +1918,10 @@ packages: compute-scroll-into-view@3.1.1: resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==} + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} @@ -1399,6 +1979,14 @@ packages: dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + emnapi@2.0.0-alpha.3: + resolution: {integrity: sha512-K9bc9Xx4OwSfhJpdSOpcfIKzn7/6emuubaIorf6I5e7WBAM79665rf6iHr9y50NL4qYMUP/AheTpD1Z4yU1EBw==} + peerDependencies: + node-addon-api: '>= 6.1.0' + peerDependenciesMeta: + node-addon-api: + optional: true + emojis-list@3.0.0: resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} engines: {node: '>= 4'} @@ -1411,6 +1999,9 @@ packages: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} + es-toolkit@1.50.0: + resolution: {integrity: sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==} + esast-util-from-estree@2.0.0: resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} @@ -1448,6 +2039,15 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1471,8 +2071,8 @@ packages: git-hooks-list@4.2.1: resolution: {integrity: sha512-WNvqJjOxxs/8ZP9+DWdwWJ7cDsd60NHf39XnD82pDVrKO5q7xfPqpkK6hwEAmBa/ZSEE4IOoR75EzbbIuwGlMw==} - happy-dom@20.11.1: - resolution: {integrity: sha512-XSt8tMzbW9ymE7687xztkO1ckR7qJNQ3LywY9vlYGhGi3zXrGBHuUo2Cl1ztZaICW+1eAGdkLbj6iwVqDT33kg==} + happy-dom@20.11.2: + resolution: {integrity: sha512-7MB+bJLkxu3SowAfBJbjW+c55kNz5tkR45gu2qzrxznezhLeN5YIlJbwUgSzlGc+qWoZ8Ykg71H5ezz69xixrw==} engines: {node: '>=20.0.0'} has-flag@4.0.0: @@ -1515,8 +2115,8 @@ packages: hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} - heading-case@1.1.4: - resolution: {integrity: sha512-UwPg0xcqLrmd6kSW93kxjy4FMqcb0o97IoTUy+TMvjCF8mJvi6X0h733DEaJO/Ch9h9dOyv0I0ckGoSsuUp9KA==} + heading-case@1.1.5: + resolution: {integrity: sha512-dVZmEJLF5vK2IV3LLpEWgyy0wbe4W+vsUzuHy6KE440IClTXR5GIGDGwiawHqCqunsqos4nvOBpnLGUrUv3BUg==} hasBin: true hookable@6.1.1: @@ -1525,9 +2125,9 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} - ignore@7.0.6: - resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} - engines: {node: '>= 4'} + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} immutable@5.1.9: resolution: {integrity: sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==} @@ -1581,6 +2181,13 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-yaml@4.3.1: + resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} + hasBin: true + + json-with-bigint@3.5.10: + resolution: {integrity: sha512-Vcx+JVNEBts/xfcoCS69sKrOhOk/3TVlvlT+XzUOefVKnnrbYSCKpDCm10pohsJFtsJVYnwa/cXRZ4eElzaM6w==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -1823,6 +2430,10 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} + nanoid@3.3.16: resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1834,6 +2445,10 @@ packages: nprogress@0.2.0: resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==} + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + oniguruma-parser@0.12.2: resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} @@ -2037,6 +2652,9 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sass-embedded-all-unknown@1.100.0: resolution: {integrity: sha512-auFtXY/kwYILmSVjtBDwyj0axcLbYYiffOKWoaXHnI5bsYwiRbBh3EneR1rpbX2ZIZCrwX93i5pxKLTZF/662Q==} cpu: ['!arm', '!arm64', '!riscv64', '!x64'] @@ -2177,6 +2795,10 @@ packages: resolution: {integrity: sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==} engines: {node: '>=20'} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + sort-object-keys@2.1.0: resolution: {integrity: sha512-SOiEnthkJKPv2L6ec6HMwhUcN0/lppkeYuN1x63PbyPRrgSPIuBJCiYxYyvWRTtjMlOi14vQUCGUJqS6PLVm8g==} @@ -2256,6 +2878,14 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + typanion@3.14.0: + resolution: {integrity: sha512-ZW/lVMRabETuYCd9O9ZvMhAh8GslSqaUjxmK/JLPCh6l73CvLBiuXswj/+7LdnWOgYsQ130FqLzFz5aGT4I3Ug==} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + typescript@7.0.2: resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} engines: {node: '>=16.20.0'} @@ -2291,6 +2921,9 @@ packages: unist-util-visit@5.1.0: resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + universal-user-agent@7.0.3: + resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} + varint@6.0.0: resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} @@ -2327,11 +2960,11 @@ packages: engines: {node: '>= 14.6'} hasBin: true - yuku-ast@0.8.3: - resolution: {integrity: sha512-8x34yU5uhHUnJXzy2Qvjvec/vE9BzS0/2khVT1MsLmSLO/P8Q1Wp8IxHv+IhD+HMYETk6kherOSvP4JPWw2joQ==} + yuku-ast@0.8.4: + resolution: {integrity: sha512-s7EWfWIQkaGmsGnyr/BU0jli9YTN5TvrKIsSmALyRD9elumDQInuhv0BrVObENKVCxr9W3Ikmnx5u02KvfuUmw==} - yuku-parser@0.8.3: - resolution: {integrity: sha512-KPQcpF9aj77ywlJBIkQWCQ9DObdxnCA8AJdUOmA5CZZx042Xt4+dvbQmPJfWxF3E+KG5dVAZ2fBKuDJ8VsKWgA==} + yuku-parser@0.8.4: + resolution: {integrity: sha512-sw41wouvT5rUmLIp87hmvm5vtF+MRSI3x6yjq6xqpYmtkQj+Ht6N7xRQ8lMhLv8N7JAzughGj0Rfi0jQRSu9HQ==} zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -2391,22 +3024,173 @@ snapshots: '@bufbuild/protobuf@2.12.1': {} + '@emnapi/core@1.11.2': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + '@emnapi/core@1.11.3': dependencies: '@emnapi/wasi-threads': 1.2.3 tslib: 2.8.1 optional: true + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.2': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.11.3': dependencies: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/wasi-threads@1.2.3': dependencies: tslib: 2.8.1 optional: true + '@inquirer/ansi@2.0.7': {} + + '@inquirer/checkbox@5.2.1(@types/node@24.13.3)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/core': 11.2.1(@types/node@24.13.3) + '@inquirer/figures': 2.0.7 + '@inquirer/type': 4.0.7(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/confirm@6.1.1(@types/node@24.13.3)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@24.13.3) + '@inquirer/type': 4.0.7(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/core@11.2.1(@types/node@24.13.3)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/figures': 2.0.7 + '@inquirer/type': 4.0.7(@types/node@24.13.3) + cli-width: 4.1.0 + fast-wrap-ansi: 0.2.2 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/editor@5.2.2(@types/node@24.13.3)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@24.13.3) + '@inquirer/external-editor': 3.0.3(@types/node@24.13.3) + '@inquirer/type': 4.0.7(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/expand@5.1.1(@types/node@24.13.3)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@24.13.3) + '@inquirer/type': 4.0.7(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/external-editor@3.0.3(@types/node@24.13.3)': + dependencies: + chardet: 2.2.0 + iconv-lite: 0.7.3 + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/figures@2.0.7': {} + + '@inquirer/input@5.1.2(@types/node@24.13.3)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@24.13.3) + '@inquirer/type': 4.0.7(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/number@4.1.1(@types/node@24.13.3)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@24.13.3) + '@inquirer/type': 4.0.7(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/password@5.1.1(@types/node@24.13.3)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/core': 11.2.1(@types/node@24.13.3) + '@inquirer/type': 4.0.7(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/prompts@8.5.2(@types/node@24.13.3)': + dependencies: + '@inquirer/checkbox': 5.2.1(@types/node@24.13.3) + '@inquirer/confirm': 6.1.1(@types/node@24.13.3) + '@inquirer/editor': 5.2.2(@types/node@24.13.3) + '@inquirer/expand': 5.1.1(@types/node@24.13.3) + '@inquirer/input': 5.1.2(@types/node@24.13.3) + '@inquirer/number': 4.1.1(@types/node@24.13.3) + '@inquirer/password': 5.1.1(@types/node@24.13.3) + '@inquirer/rawlist': 5.3.1(@types/node@24.13.3) + '@inquirer/search': 4.2.1(@types/node@24.13.3) + '@inquirer/select': 5.2.1(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/rawlist@5.3.1(@types/node@24.13.3)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@24.13.3) + '@inquirer/type': 4.0.7(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/search@4.2.1(@types/node@24.13.3)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@24.13.3) + '@inquirer/figures': 2.0.7 + '@inquirer/type': 4.0.7(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/select@5.2.1(@types/node@24.13.3)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/core': 11.2.1(@types/node@24.13.3) + '@inquirer/figures': 2.0.7 + '@inquirer/type': 4.0.7(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/type@4.0.7(@types/node@24.13.3)': + optionalDependencies: + '@types/node': 24.13.3 + '@mdx-js/mdx@3.1.1(supports-color@8.1.1)': dependencies: '@types/estree': 1.0.9 @@ -2443,6 +3227,197 @@ snapshots: '@types/react': 19.2.18 react: 19.2.8 + '@napi-rs/cli@3.8.3(@types/node@24.13.3)(node-addon-api@7.1.1)(supports-color@8.1.1)': + dependencies: + '@inquirer/prompts': 8.5.2(@types/node@24.13.3) + '@napi-rs/cross-toolchain': 1.0.3(supports-color@8.1.1) + '@napi-rs/wasm-tools': 1.1.0 + '@octokit/rest': 22.0.1 + clipanion: 4.0.0-rc.4 + colorette: 2.0.20 + emnapi: 2.0.0-alpha.3(node-addon-api@7.1.1) + es-toolkit: 1.50.0 + js-yaml: 4.3.1 + obug: 2.1.4 + semver: 7.8.5 + typanion: 3.14.0 + typescript: 6.0.3 + transitivePeerDependencies: + - '@napi-rs/cross-toolchain-arm64-target-aarch64' + - '@napi-rs/cross-toolchain-arm64-target-armv7' + - '@napi-rs/cross-toolchain-arm64-target-ppc64le' + - '@napi-rs/cross-toolchain-arm64-target-s390x' + - '@napi-rs/cross-toolchain-arm64-target-x86_64' + - '@napi-rs/cross-toolchain-x64-target-aarch64' + - '@napi-rs/cross-toolchain-x64-target-armv7' + - '@napi-rs/cross-toolchain-x64-target-ppc64le' + - '@napi-rs/cross-toolchain-x64-target-s390x' + - '@napi-rs/cross-toolchain-x64-target-x86_64' + - '@types/node' + - node-addon-api + - supports-color + + '@napi-rs/cross-toolchain@1.0.3(supports-color@8.1.1)': + dependencies: + '@napi-rs/lzma': 1.5.1 + '@napi-rs/tar': 1.1.1 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + '@napi-rs/lzma-android-arm-eabi@1.5.1': + optional: true + + '@napi-rs/lzma-android-arm64@1.5.1': + optional: true + + '@napi-rs/lzma-darwin-arm64@1.5.1': + optional: true + + '@napi-rs/lzma-darwin-x64@1.5.1': + optional: true + + '@napi-rs/lzma-freebsd-x64@1.5.1': + optional: true + + '@napi-rs/lzma-linux-arm-gnueabihf@1.5.1': + optional: true + + '@napi-rs/lzma-linux-arm64-gnu@1.5.1': + optional: true + + '@napi-rs/lzma-linux-arm64-musl@1.5.1': + optional: true + + '@napi-rs/lzma-linux-ppc64-gnu@1.5.1': + optional: true + + '@napi-rs/lzma-linux-riscv64-gnu@1.5.1': + optional: true + + '@napi-rs/lzma-linux-s390x-gnu@1.5.1': + optional: true + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@napi-rs/lzma-linux-x64-musl@1.5.1': + optional: true + + '@napi-rs/lzma-wasm32-wasi@1.5.1': + dependencies: + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2) + optional: true + + '@napi-rs/lzma-win32-arm64-msvc@1.5.1': + optional: true + + '@napi-rs/lzma-win32-ia32-msvc@1.5.1': + optional: true + + '@napi-rs/lzma-win32-x64-msvc@1.5.1': + optional: true + + '@napi-rs/lzma@1.5.1': + optionalDependencies: + '@napi-rs/lzma-android-arm-eabi': 1.5.1 + '@napi-rs/lzma-android-arm64': 1.5.1 + '@napi-rs/lzma-darwin-arm64': 1.5.1 + '@napi-rs/lzma-darwin-x64': 1.5.1 + '@napi-rs/lzma-freebsd-x64': 1.5.1 + '@napi-rs/lzma-linux-arm-gnueabihf': 1.5.1 + '@napi-rs/lzma-linux-arm64-gnu': 1.5.1 + '@napi-rs/lzma-linux-arm64-musl': 1.5.1 + '@napi-rs/lzma-linux-ppc64-gnu': 1.5.1 + '@napi-rs/lzma-linux-riscv64-gnu': 1.5.1 + '@napi-rs/lzma-linux-s390x-gnu': 1.5.1 + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@napi-rs/lzma-linux-x64-musl': 1.5.1 + '@napi-rs/lzma-wasm32-wasi': 1.5.1 + '@napi-rs/lzma-win32-arm64-msvc': 1.5.1 + '@napi-rs/lzma-win32-ia32-msvc': 1.5.1 + '@napi-rs/lzma-win32-x64-msvc': 1.5.1 + + '@napi-rs/tar-android-arm-eabi@1.1.1': + optional: true + + '@napi-rs/tar-android-arm64@1.1.1': + optional: true + + '@napi-rs/tar-darwin-arm64@1.1.1': + optional: true + + '@napi-rs/tar-darwin-x64@1.1.1': + optional: true + + '@napi-rs/tar-freebsd-x64@1.1.1': + optional: true + + '@napi-rs/tar-linux-arm-gnueabihf@1.1.1': + optional: true + + '@napi-rs/tar-linux-arm64-gnu@1.1.1': + optional: true + + '@napi-rs/tar-linux-arm64-musl@1.1.1': + optional: true + + '@napi-rs/tar-linux-ppc64-gnu@1.1.1': + optional: true + + '@napi-rs/tar-linux-s390x-gnu@1.1.1': + optional: true + + '@napi-rs/tar-linux-x64-gnu@1.1.1': + optional: true + + '@napi-rs/tar-linux-x64-musl@1.1.1': + optional: true + + '@napi-rs/tar-wasm32-wasi@1.1.1': + dependencies: + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2) + optional: true + + '@napi-rs/tar-win32-arm64-msvc@1.1.1': + optional: true + + '@napi-rs/tar-win32-ia32-msvc@1.1.1': + optional: true + + '@napi-rs/tar-win32-x64-msvc@1.1.1': + optional: true + + '@napi-rs/tar@1.1.1': + optionalDependencies: + '@napi-rs/tar-android-arm-eabi': 1.1.1 + '@napi-rs/tar-android-arm64': 1.1.1 + '@napi-rs/tar-darwin-arm64': 1.1.1 + '@napi-rs/tar-darwin-x64': 1.1.1 + '@napi-rs/tar-freebsd-x64': 1.1.1 + '@napi-rs/tar-linux-arm-gnueabihf': 1.1.1 + '@napi-rs/tar-linux-arm64-gnu': 1.1.1 + '@napi-rs/tar-linux-arm64-musl': 1.1.1 + '@napi-rs/tar-linux-ppc64-gnu': 1.1.1 + '@napi-rs/tar-linux-s390x-gnu': 1.1.1 + '@napi-rs/tar-linux-x64-gnu': 1.1.1 + '@napi-rs/tar-linux-x64-musl': 1.1.1 + '@napi-rs/tar-wasm32-wasi': 1.1.1 + '@napi-rs/tar-win32-arm64-msvc': 1.1.1 + '@napi-rs/tar-win32-ia32-msvc': 1.1.1 + '@napi-rs/tar-win32-x64-msvc': 1.1.1 + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)': + dependencies: + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 + '@tybys/wasm-util': 0.10.3 + optional: true + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': dependencies: '@emnapi/core': 1.11.3 @@ -2450,6 +3425,141 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@napi-rs/wasm-tools-android-arm-eabi@1.1.0': + optional: true + + '@napi-rs/wasm-tools-android-arm64@1.1.0': + optional: true + + '@napi-rs/wasm-tools-darwin-arm64@1.1.0': + optional: true + + '@napi-rs/wasm-tools-darwin-x64@1.1.0': + optional: true + + '@napi-rs/wasm-tools-freebsd-x64@1.1.0': + optional: true + + '@napi-rs/wasm-tools-linux-arm64-gnu@1.1.0': + optional: true + + '@napi-rs/wasm-tools-linux-arm64-musl@1.1.0': + optional: true + + '@napi-rs/wasm-tools-linux-x64-gnu@1.1.0': + optional: true + + '@napi-rs/wasm-tools-linux-x64-musl@1.1.0': + optional: true + + '@napi-rs/wasm-tools-wasm32-wasi@1.1.0': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@napi-rs/wasm-tools-win32-arm64-msvc@1.1.0': + optional: true + + '@napi-rs/wasm-tools-win32-ia32-msvc@1.1.0': + optional: true + + '@napi-rs/wasm-tools-win32-x64-msvc@1.1.0': + optional: true + + '@napi-rs/wasm-tools@1.1.0': + optionalDependencies: + '@napi-rs/wasm-tools-android-arm-eabi': 1.1.0 + '@napi-rs/wasm-tools-android-arm64': 1.1.0 + '@napi-rs/wasm-tools-darwin-arm64': 1.1.0 + '@napi-rs/wasm-tools-darwin-x64': 1.1.0 + '@napi-rs/wasm-tools-freebsd-x64': 1.1.0 + '@napi-rs/wasm-tools-linux-arm64-gnu': 1.1.0 + '@napi-rs/wasm-tools-linux-arm64-musl': 1.1.0 + '@napi-rs/wasm-tools-linux-x64-gnu': 1.1.0 + '@napi-rs/wasm-tools-linux-x64-musl': 1.1.0 + '@napi-rs/wasm-tools-wasm32-wasi': 1.1.0 + '@napi-rs/wasm-tools-win32-arm64-msvc': 1.1.0 + '@napi-rs/wasm-tools-win32-ia32-msvc': 1.1.0 + '@napi-rs/wasm-tools-win32-x64-msvc': 1.1.0 + + '@octokit/auth-token@6.0.0': {} + + '@octokit/core@7.0.7': + dependencies: + '@octokit/auth-token': 6.0.0 + '@octokit/graphql': 9.0.4 + '@octokit/request': 10.0.13 + '@octokit/request-error': 7.1.1 + '@octokit/types': 17.0.0 + before-after-hook: 4.0.0 + universal-user-agent: 7.0.3 + + '@octokit/endpoint@11.0.4': + dependencies: + '@octokit/types': 17.0.0 + universal-user-agent: 7.0.3 + + '@octokit/graphql@9.0.4': + dependencies: + '@octokit/request': 10.0.13 + '@octokit/types': 17.0.0 + universal-user-agent: 7.0.3 + + '@octokit/openapi-types@27.0.0': {} + + '@octokit/openapi-types@28.0.0': {} + + '@octokit/plugin-paginate-rest@14.0.0(@octokit/core@7.0.7)': + dependencies: + '@octokit/core': 7.0.7 + '@octokit/types': 16.0.0 + + '@octokit/plugin-request-log@6.0.0(@octokit/core@7.0.7)': + dependencies: + '@octokit/core': 7.0.7 + + '@octokit/plugin-rest-endpoint-methods@17.0.0(@octokit/core@7.0.7)': + dependencies: + '@octokit/core': 7.0.7 + '@octokit/types': 16.0.0 + + '@octokit/request-error@7.1.1': + dependencies: + '@octokit/types': 17.0.0 + + '@octokit/request@10.0.13': + dependencies: + '@octokit/endpoint': 11.0.4 + '@octokit/request-error': 7.1.1 + '@octokit/types': 17.0.0 + content-type: 2.0.0 + json-with-bigint: 3.5.10 + universal-user-agent: 7.0.3 + + '@octokit/rest@22.0.1': + dependencies: + '@octokit/core': 7.0.7 + '@octokit/plugin-paginate-rest': 14.0.0(@octokit/core@7.0.7) + '@octokit/plugin-request-log': 6.0.0(@octokit/core@7.0.7) + '@octokit/plugin-rest-endpoint-methods': 17.0.0(@octokit/core@7.0.7) + + '@octokit/types@16.0.0': + dependencies: + '@octokit/openapi-types': 27.0.0 + + '@octokit/types@17.0.0': + dependencies: + '@octokit/openapi-types': 28.0.0 + '@parcel/watcher-android-arm64@2.5.6': optional: true @@ -2723,30 +3833,30 @@ snapshots: optionalDependencies: '@rspress/core': 2.0.19(micromark-util-types@2.0.2)(micromark@4.0.2)(supports-color@8.1.1) - '@rstackjs/create-toolkit@2.1.5': {} + '@rstackjs/create-toolkit@2.2.3': {} '@rstackjs/load-config@0.1.2': {} '@rstackjs/test-utils@0.2.0': {} - '@rstest/adapter-rsbuild@0.11.5(@rsbuild/core@2.1.10)(@rstest/core@0.11.5)': + '@rstest/adapter-rsbuild@0.11.6(@rsbuild/core@2.1.10)(@rstest/core@0.11.6)': dependencies: '@rsbuild/core': 2.1.10 - '@rstest/core': 0.11.5(happy-dom@20.11.1) + '@rstest/core': 0.11.6(happy-dom@20.11.2) - '@rstest/adapter-rslib@0.11.5(@rslib/core@1.0.0-beta.2)(@rstest/core@0.11.5)(typescript@7.0.2)': + '@rstest/adapter-rslib@0.11.6(@rslib/core@1.0.0-beta.2)(@rstest/core@0.11.6)(typescript@7.0.2)': dependencies: '@rslib/core': 1.0.0-beta.2(typescript@7.0.2) - '@rstest/core': 0.11.5(happy-dom@20.11.1) + '@rstest/core': 0.11.6(happy-dom@20.11.2) optionalDependencies: typescript: 7.0.2 - '@rstest/core@0.11.5(happy-dom@20.11.1)': + '@rstest/core@0.11.6(happy-dom@20.11.2)': dependencies: '@rsbuild/core': 2.1.10 '@types/chai': 5.2.3 optionalDependencies: - happy-dom: 20.11.1 + happy-dom: 20.11.2 transitivePeerDependencies: - '@module-federation/runtime-tools' - core-js @@ -2759,10 +3869,10 @@ snapshots: '@types/hast': 3.0.5 hast-util-to-html: 9.0.5 - '@shikijs/core@4.4.1': + '@shikijs/core@4.4.2': dependencies: - '@shikijs/primitive': 4.4.1 - '@shikijs/types': 4.4.1 + '@shikijs/primitive': 4.4.2 + '@shikijs/types': 4.4.2 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 hast-util-to-html: 9.0.5 @@ -2788,9 +3898,9 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 - '@shikijs/primitive@4.4.1': + '@shikijs/primitive@4.4.2': dependencies: - '@shikijs/types': 4.4.1 + '@shikijs/types': 4.4.2 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 @@ -2807,17 +3917,17 @@ snapshots: dependencies: '@shikijs/types': 4.3.1 - '@shikijs/transformers@4.4.1': + '@shikijs/transformers@4.4.2': dependencies: - '@shikijs/core': 4.4.1 - '@shikijs/types': 4.4.1 + '@shikijs/core': 4.4.2 + '@shikijs/types': 4.4.2 '@shikijs/types@4.3.1': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 - '@shikijs/types@4.4.1': + '@shikijs/types@4.4.2': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 @@ -2990,43 +4100,43 @@ snapshots: react: 19.2.8 unhead: 2.1.16 - '@yuku-parser/binding-android-arm64@0.8.3': + '@yuku-parser/binding-android-arm64@0.8.4': optional: true - '@yuku-parser/binding-darwin-arm64@0.8.3': + '@yuku-parser/binding-darwin-arm64@0.8.4': optional: true - '@yuku-parser/binding-darwin-x64@0.8.3': + '@yuku-parser/binding-darwin-x64@0.8.4': optional: true - '@yuku-parser/binding-freebsd-x64@0.8.3': + '@yuku-parser/binding-freebsd-x64@0.8.4': optional: true - '@yuku-parser/binding-linux-arm-gnu@0.8.3': + '@yuku-parser/binding-linux-arm-gnu@0.8.4': optional: true - '@yuku-parser/binding-linux-arm-musl@0.8.3': + '@yuku-parser/binding-linux-arm-musl@0.8.4': optional: true - '@yuku-parser/binding-linux-arm64-gnu@0.8.3': + '@yuku-parser/binding-linux-arm64-gnu@0.8.4': optional: true - '@yuku-parser/binding-linux-arm64-musl@0.8.3': + '@yuku-parser/binding-linux-arm64-musl@0.8.4': optional: true - '@yuku-parser/binding-linux-x64-gnu@0.8.3': + '@yuku-parser/binding-linux-x64-gnu@0.8.4': optional: true - '@yuku-parser/binding-linux-x64-musl@0.8.3': + '@yuku-parser/binding-linux-x64-musl@0.8.4': optional: true - '@yuku-parser/binding-win32-arm64@0.8.3': + '@yuku-parser/binding-win32-arm64@0.8.4': optional: true - '@yuku-parser/binding-win32-x64@0.8.3': + '@yuku-parser/binding-win32-x64@0.8.4': optional: true - '@yuku-toolchain/types@0.8.3': {} + '@yuku-toolchain/types@0.8.4': {} acorn-jsx@5.3.2(acorn@8.17.0): dependencies: @@ -3038,6 +4148,8 @@ snapshots: ansi-styles@5.2.0: {} + argparse@2.0.1: {} + aria-query@5.3.0: dependencies: dequal: 2.0.3 @@ -3050,6 +4162,8 @@ snapshots: bail@2.0.2: {} + before-after-hook@4.0.0: {} + big.js@5.2.2: {} binary-extensions@3.1.0: {} @@ -3074,21 +4188,33 @@ snapshots: character-reference-invalid@2.0.1: {} + chardet@2.2.0: {} + chokidar@5.0.0: dependencies: readdirp: 5.0.0 optional: true + cli-width@4.1.0: {} + + clipanion@4.0.0-rc.4: + dependencies: + typanion: 3.14.0 + clsx@2.1.1: {} collapse-white-space@2.1.0: {} + colorette@2.0.20: {} + colorjs.io@0.5.2: {} comma-separated-tokens@2.0.3: {} compute-scroll-into-view@3.1.1: {} + content-type@2.0.0: {} + cookie@1.1.1: {} copy-to-clipboard@3.3.3: @@ -3130,12 +4256,18 @@ snapshots: dom-accessibility-api@0.6.3: {} + emnapi@2.0.0-alpha.3(node-addon-api@7.1.1): + optionalDependencies: + node-addon-api: 7.1.1 + emojis-list@3.0.0: {} entities@6.0.1: {} entities@7.0.1: {} + es-toolkit@1.50.0: {} + esast-util-from-estree@2.0.0: dependencies: '@types/estree-jsx': 1.0.5 @@ -3189,6 +4321,16 @@ snapshots: fast-json-stable-stringify@2.1.0: {} + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + fdir@6.5.0(picomatch@4.0.5): optionalDependencies: picomatch: 4.0.5 @@ -3203,7 +4345,7 @@ snapshots: git-hooks-list@4.2.1: {} - happy-dom@20.11.1: + happy-dom@20.11.2: dependencies: '@types/node': 24.13.3 '@types/whatwg-mimetype': 3.0.2 @@ -3338,13 +4480,15 @@ snapshots: property-information: 7.2.0 space-separated-tokens: 2.0.2 - heading-case@1.1.4: {} + heading-case@1.1.5: {} hookable@6.1.1: {} html-void-elements@3.0.0: {} - ignore@7.0.6: {} + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 immutable@5.1.9: {} @@ -3385,6 +4529,12 @@ snapshots: js-tokens@4.0.0: {} + js-yaml@4.3.1: + dependencies: + argparse: 2.0.1 + + json-with-bigint@3.5.10: {} + json5@2.2.3: {} lint-staged@17.3.0: @@ -3901,6 +5051,8 @@ snapshots: ms@2.1.3: {} + mute-stream@3.0.0: {} + nanoid@3.3.16: {} node-addon-api@7.1.1: @@ -3908,6 +5060,8 @@ snapshots: nprogress@0.2.0: {} + obug@2.1.4: {} + oniguruma-parser@0.12.2: {} oniguruma-to-es@4.3.6: @@ -4152,6 +5306,8 @@ snapshots: dependencies: tslib: 2.8.1 + safer-buffer@2.1.2: {} + sass-embedded-all-unknown@1.100.0: dependencies: sass: 1.100.0 @@ -4269,6 +5425,8 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 + signal-exit@4.1.0: {} + sort-object-keys@2.1.0: {} sort-package-json@4.0.0: @@ -4341,6 +5499,10 @@ snapshots: tslib@2.8.1: {} + typanion@3.14.0: {} + + typescript@6.0.3: {} + typescript@7.0.2: optionalDependencies: '@typescript/typescript-aix-ppc64': 7.0.2 @@ -4411,6 +5573,8 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 + universal-user-agent@7.0.3: {} + varint@6.0.0: {} vfile-location@5.0.3: @@ -4437,26 +5601,26 @@ snapshots: yaml@2.9.0: optional: true - yuku-ast@0.8.3: + yuku-ast@0.8.4: dependencies: - '@yuku-toolchain/types': 0.8.3 + '@yuku-toolchain/types': 0.8.4 - yuku-parser@0.8.3: + yuku-parser@0.8.4: dependencies: - '@yuku-toolchain/types': 0.8.3 - yuku-ast: 0.8.3 + '@yuku-toolchain/types': 0.8.4 + yuku-ast: 0.8.4 optionalDependencies: - '@yuku-parser/binding-android-arm64': 0.8.3 - '@yuku-parser/binding-darwin-arm64': 0.8.3 - '@yuku-parser/binding-darwin-x64': 0.8.3 - '@yuku-parser/binding-freebsd-x64': 0.8.3 - '@yuku-parser/binding-linux-arm-gnu': 0.8.3 - '@yuku-parser/binding-linux-arm-musl': 0.8.3 - '@yuku-parser/binding-linux-arm64-gnu': 0.8.3 - '@yuku-parser/binding-linux-arm64-musl': 0.8.3 - '@yuku-parser/binding-linux-x64-gnu': 0.8.3 - '@yuku-parser/binding-linux-x64-musl': 0.8.3 - '@yuku-parser/binding-win32-arm64': 0.8.3 - '@yuku-parser/binding-win32-x64': 0.8.3 + '@yuku-parser/binding-android-arm64': 0.8.4 + '@yuku-parser/binding-darwin-arm64': 0.8.4 + '@yuku-parser/binding-darwin-x64': 0.8.4 + '@yuku-parser/binding-freebsd-x64': 0.8.4 + '@yuku-parser/binding-linux-arm-gnu': 0.8.4 + '@yuku-parser/binding-linux-arm-musl': 0.8.4 + '@yuku-parser/binding-linux-arm64-gnu': 0.8.4 + '@yuku-parser/binding-linux-arm64-musl': 0.8.4 + '@yuku-parser/binding-linux-x64-gnu': 0.8.4 + '@yuku-parser/binding-linux-x64-musl': 0.8.4 + '@yuku-parser/binding-win32-arm64': 0.8.4 + '@yuku-parser/binding-win32-x64': 0.8.4 zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8215985a..c57d25d7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -12,6 +12,7 @@ catalogMode: prefer cleanupUnusedCatalogs: true catalog: + '@napi-rs/cli': '^3.8.3' '@rsbuild/core': '~2.1.10' '@rsbuild/plugin-react': '^2.1.0' '@rsbuild/plugin-sass': '^2.0.1' @@ -21,12 +22,12 @@ catalog: '@rspress/plugin-client-redirects': '^2.0.19' '@rspress/plugin-sitemap': '^2.0.19' '@rstack-dev/doc-ui': '1.14.7' - '@rstackjs/create-toolkit': '2.1.5' + '@rstackjs/create-toolkit': '2.2.3' '@rstackjs/load-config': ^0.1.2 '@rstackjs/test-utils': ^0.2.0 - '@rstest/adapter-rsbuild': '~0.11.5' - '@rstest/adapter-rslib': '~0.11.5' - '@rstest/core': '~0.11.5' + '@rstest/adapter-rsbuild': '~0.11.6' + '@rstest/adapter-rslib': '~0.11.6' + '@rstest/core': '~0.11.6' '@testing-library/dom': '^10.4.1' '@testing-library/jest-dom': '^7.0.0' '@testing-library/react': '^16.3.2' @@ -34,12 +35,11 @@ catalog: '@types/node': '^24.13.3' '@types/react': '^19.2.18' '@types/react-dom': '^19.2.4' - '@shikijs/transformers': '^4.4.1' + '@shikijs/transformers': '^4.4.2' 'cspell-ban-words': '^0.0.4' 'fast-json-stable-stringify': '2.1.0' - 'happy-dom': '^20.11.1' - 'heading-case': '^1.1.4' - ignore: 7.0.6 + 'happy-dom': '^20.11.2' + 'heading-case': '^1.1.5' 'import-meta-resolve': '4.2.0' is-binary-path: 3.0.0 'lint-staged': '^17.3.0' @@ -54,7 +54,7 @@ catalog: tinypool: '2.1.0' tiny-readdir: 3.1.1 'typescript': '^7.0.2' - yuku-parser: '0.8.3' + yuku-parser: '0.8.4' dedupePeers: true diff --git a/rstack.config.ts b/rstack.config.ts index f451cc2a..9ffac863 100644 --- a/rstack.config.ts +++ b/rstack.config.ts @@ -40,6 +40,15 @@ define.lint(async () => { }); define.fmt({ + ignorePatterns: ['packages/rstack/binding.cjs', 'packages/rstack/binding.d.cts'], + overrides: [ + { + files: 'packages/create-rstack/template-*/**/*', + options: { + printWidth: 80, + }, + }, + ], printWidth: 100, singleQuote: true, sortPackageJson: true, diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..6f8397f5 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,5 @@ +[toolchain] +# Required by the release-only -Zlocation-detail=none flag. +channel = "nightly-2026-04-16" +components = ["clippy", "rustfmt"] +profile = "minimal" diff --git a/scripts/dictionary.txt b/scripts/dictionary.txt index af854b59..06b17167 100644 --- a/scripts/dictionary.txt +++ b/scripts/dictionary.txt @@ -1,12 +1,18 @@ # Custom Dictionary Words applypatch +cdpath +clippy dirents +editmsg errexit +esac extglob fnames huskyrc indentable +jsonline llms +napi noformat noprettier nosystem @@ -23,7 +29,9 @@ rstackjs rstest shiki shikijs +solidjs turborepo typicode worktank +worktree yuku diff --git a/scripts/prepare-release.js b/scripts/prepare-release.js index 0cdec3b7..cab8f15b 100644 --- a/scripts/prepare-release.js +++ b/scripts/prepare-release.js @@ -6,7 +6,7 @@ import path from 'node:path'; const rootDir = path.resolve(import.meta.dirname, '..'); const websiteDir = path.join(rootDir, 'website'); const websiteDistDir = path.join(websiteDir, 'doc_build'); -const packageDocsDir = path.join(rootDir, 'packages/rstack/dist/docs'); +const packageDocsDir = path.join(rootDir, 'packages/rstack/docs'); const run = (command, args) => new Promise((resolve, reject) => { diff --git a/website/docs/en/guide/_meta.json b/website/docs/en/guide/_meta.json index 775ed81b..630bde07 100644 --- a/website/docs/en/guide/_meta.json +++ b/website/docs/en/guide/_meta.json @@ -13,6 +13,11 @@ "name": "configuration", "label": "Configuration" }, + { + "type": "file", + "name": "ai", + "label": "AI" + }, { "type": "file", "name": "api-reference", diff --git a/website/docs/en/guide/ai.mdx b/website/docs/en/guide/ai.mdx new file mode 100644 index 00000000..80ef7217 --- /dev/null +++ b/website/docs/en/guide/ai.mdx @@ -0,0 +1,81 @@ +--- +description: 'Use Rstack CLI with coding agents through Agent Skills, llms.txt, Markdown docs, and AGENTS.md.' +--- + +import { PackageManagerTabs } from '@rspress/core/theme'; + +# AI + +To help coding agents understand Rstack CLI commands, configuration, and best practices, Rstack CLI provides the following resources: + +- [AGENTS.md](#agentsmd) +- [Agent Skills](#agent-skills) +- [llms.txt](#llmstxt) +- [Markdown docs](#markdown-docs) + +## AGENTS.md + +Projects created with [create-rstack](https://www.npmjs.com/package/create-rstack) include an [`AGENTS.md`](https://agents.md/) file that gives coding agents the key context for working with Rstack CLI. + +You can also copy the following content into your own `AGENTS.md`: + +```markdown wrapCode title="AGENTS.md" +This project uses Rstack CLI as its JavaScript toolchain. + +- Before working with `rs` commands, `rstack.config.*` files, or imports from `rstack`, start with `node_modules/rstack/docs/llms.txt`, then read only the linked pages relevant to the task. +- For command details, use `rs -h` or `rs -h`. +- If the local documentation is unavailable, use https://rstack.rs/llms.txt and `rs -h`. +``` + +This content serves a similar purpose to the [rstack-cli-best-practices](#rstack-cli-best-practices) Skill, helping coding agents use Rstack CLI and find relevant documentation. Add it to `AGENTS.md` or install the Skill; either is sufficient. + +## Agent Skills + +Rstack CLI provides domain-specific Agent Skills that help coding agents give more accurate guidance and perform relevant tasks. + +### rstack-cli-best-practices + +The [rstack-cli-best-practices](https://github.com/rstackjs/rstack-cli/tree/main/.agents/skills/rstack-cli-best-practices) Skill provides guidance and best practices for using Rstack CLI. + +Install it with the [skills](https://www.npmjs.com/package/skills) package: + + + +### migrate-to-rstack-cli + +The [migrate-to-rstack-cli](https://github.com/rstackjs/rstack-cli/tree/main/.agents/skills/migrate-to-rstack-cli) Skill migrates projects from standalone Rstack tools and related development tools to Rstack CLI. + +To migrate an existing project, install the Skill: + + + +For supported tools and migration instructions, see [Migrate to Rstack CLI](./migration). + +## llms.txt + +[llms.txt](https://llmstxt.org/) is a standard that helps LLMs discover and use project documentation. The Rstack CLI documentation site provides the following files: + +- [llms.txt](https://rstack.rs/llms.txt): A structured index containing the title, link, and description of each documentation page. + +```text +https://rstack.rs/llms.txt +``` + +- [llms-full.txt](https://rstack.rs/llms-full.txt): A single file containing the full content of all documentation pages. + +```text +https://rstack.rs/llms-full.txt +``` + +Use `llms.txt` when the agent can follow links and load only the pages relevant to a task. Use `llms-full.txt` when the agent needs the complete documentation in one context and the larger token cost is acceptable. + +## Markdown docs + +Every Rstack CLI documentation page has a corresponding `.md` plain-text version that can be provided directly to an agent. On any documentation page, use “Copy Markdown” or “Copy Markdown Link” under the title to copy its content or URL. + +```text +https://rstack.rs/guide/quick-start.md +``` diff --git a/website/docs/en/guide/cli/_meta.json b/website/docs/en/guide/cli/_meta.json index 0f31a84a..acdaffbe 100644 --- a/website/docs/en/guide/cli/_meta.json +++ b/website/docs/en/guide/cli/_meta.json @@ -1 +1 @@ -["dev", "build", "preview", "lib", "doc", "test", "lint", "fmt", "setup", "staged"] +["dev", "build", "preview", "lib", "doc", "test", "check", "lint", "fmt", "setup", "staged"] diff --git a/website/docs/en/guide/cli/check.mdx b/website/docs/en/guide/cli/check.mdx new file mode 100644 index 00000000..59679821 --- /dev/null +++ b/website/docs/en/guide/cli/check.mdx @@ -0,0 +1,53 @@ +--- +description: 'Run linting and formatting checks together, with optional TypeScript type checking.' +--- + +# check + +The `rs check` command combines linting, formatting, and optional TypeScript type checking for the current project. By default, it runs [`rs lint`](./lint) followed by [`rs fmt --check`](./fmt#--check). + +## Usage + +```bash +rs check [options] +``` + +## Checks + +Running `rs check` is equivalent to: + +```bash +rs lint && rs fmt --check +``` + +Checks run sequentially. If linting fails, `rs check` stops without running the formatting check. The command exits successfully only when every enabled check passes. + +## Options + +### `--type-check` + +Enable TypeScript type checking as part of linting: + +```bash +rs check --type-check +``` + +This is equivalent to: + +```bash +rs lint --type-check && rs fmt --check +``` + +Type checking is disabled when this option is omitted. + +### `-h, --help` + +Display usage and option information without running checks: + +```bash +rs check --help +``` + +## Configuration + +`rs check` has no separate `define.check()` configuration. It uses the linting configuration from [`define.lint()`](../configuration#define-lint) and the formatting configuration from [`define.fmt()`](../configuration#define-fmt). diff --git a/website/docs/en/guide/cli/setup.mdx b/website/docs/en/guide/cli/setup.mdx index 186a46dd..e61403d4 100644 --- a/website/docs/en/guide/cli/setup.mdx +++ b/website/docs/en/guide/cli/setup.mdx @@ -2,7 +2,7 @@ import { PackageManagerTabs } from '@rspress/core/theme'; -The `rs setup` command installs project-local [Git hooks](https://git-scm.com/docs/githooks) in the current repository. +The `rs setup` command installs repository-level [Git hooks](https://git-scm.com/docs/githooks) and runs them in the project that invokes the command. ## Usage @@ -10,9 +10,9 @@ The `rs setup` command installs project-local [Git hooks](https://git-scm.com/do rs setup [options] ``` -By default, project hook scripts are stored in `.rstack/hooks`. If the current directory is not inside a Git repository, the command skips installation. +By default, hook scripts are stored in `.rstack/hooks`, relative to the Git repository root. If the current directory is not inside a Git repository, the command skips installation. -Add `rs setup` to the `prepare` script in the root `package.json` to automatically generate hook files when dependencies are installed: +Add `rs setup` to the `prepare` script of the project that should manage the repository hooks: ```json title="package.json" { @@ -41,7 +41,7 @@ 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). If the repository already uses Husky or another Git hook manager, move the required hooks 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. Migrate the required hooks and remove the existing hooks configuration before running the command. ::: @@ -49,7 +49,7 @@ rs staged ### `--hooks-dir` -Sets the directory for project hook scripts, relative to the current directory. +Sets the directory for hook scripts, relative to the Git repository root. ```bash rs setup --hooks-dir config/git-hooks @@ -58,7 +58,7 @@ rs setup --hooks-dir config/git-hooks rs setup --hooks-dir "config/git hooks" ``` -When using a custom directory, add the full command to the `prepare` script in the root `package.json`: +When using a custom directory, add the full command to the `prepare` script of the project that manages hooks: ```json title="package.json" { @@ -68,7 +68,7 @@ When using a custom directory, add the full command to the `prepare` script in t } ``` -> To prevent Git hook files from being created or overwritten outside the current project through parent directory paths, the path must not contain `..`. +> To prevent Git hook files from being created or overwritten outside the repository through parent directory paths, the path must not contain `..`. ### `--help` @@ -85,16 +85,17 @@ The default directory structure is: ```text .rstack/ └── hooks/ - ├── pre-commit # Project hook script: edit and commit + ├── pre-commit # Repository hook script: edit and commit └── _/ # Generated by rs setup; ignored by Git ├── .gitignore + ├── .owner ├── runner ├── pre-commit ├── commit-msg └── ... ``` -Files next to `_` are project hook scripts. The `_` directory contains generated files and is ignored by Git. `rs setup` points `core.hooksPath` to `.rstack/hooks/_`; rerun it after cloning the repository or when generated files are missing. +Files next to `_` are repository hook scripts. The `_` directory contains generated files and is ignored by Git. `rs setup` points `core.hooksPath` to `.rstack/hooks/_`; rerun it after cloning the repository or when generated files are missing. ## Supported hooks @@ -119,7 +120,7 @@ Create a file with the matching name next to the `_` directory. ## Hook runtime -Rstack runs hook scripts with POSIX `sh -e`, forwards Git's arguments and standard input, and returns the hook's exit code. It also prepends `node_modules/.bin` to `PATH`. +Rstack runs hook scripts with POSIX `sh -e`, forwards Git's arguments and standard input, and returns the hook's exit code. Before running a hook, it changes to the project that installed the hooks and prepends that project's `node_modules/.bin` to `PATH`. ### Disable and debug @@ -147,22 +148,23 @@ Use it to initialize a Node.js version manager, update `PATH`, or set `RSTACK_HO ## Monorepo -In a monorepo, a project may be located in a Git repository subdirectory, such as `frontend/`. When run from that directory, `rs setup` creates the hooks directory relative to the project and includes the project path in `core.hooksPath`: +In a monorepo, the project that provides Rstack may be located in a subdirectory such as `frontend/`. Running `rs setup` from that directory still installs hooks at the Git repository root: ```text -frontend/.rstack/hooks/ -frontend/.rstack/hooks/_/ -core.hooksPath=frontend/.rstack/hooks/_ +repo/.rstack/hooks/ +repo/.rstack/hooks/_/ +core.hooksPath=.rstack/hooks/_ ``` -Git runs hooks from the repository root. If the project is in a subdirectory, change to that directory in the hook script before running project commands: +Rstack records `frontend` as the project that owns the hooks. Hook scripts remain at the repository root, but run from `frontend`, so they can use its configuration and dependencies without an explicit `cd`: -```sh title="frontend/.rstack/hooks/pre-commit" -cd frontend -pnpm test +```sh title=".rstack/hooks/pre-commit" +rs staged ``` -A Git repository has one `core.hooksPath`, so choose either the repository root or one subproject to manage hooks. +A Git repository has one hooks owner. Only that project should include `rs setup` in its `prepare` script. Calls from another project are skipped with a warning. + +To change the owner, remove `rs setup` from the previous project's `prepare` script, delete the generated `_` directory, and then run `rs setup` from the new project. ## Remove hooks @@ -185,6 +187,8 @@ To remove Rstack-managed hooks: - Run `git config --local --get core.hooksPath` and verify the configured 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 Rstack owner is reported, follow the ownership transfer steps in [Monorepo](#monorepo). Hook scripts do not need to be executable because Rstack runs them with `sh`. diff --git a/website/docs/en/guide/monorepo.mdx b/website/docs/en/guide/monorepo.mdx index 141e96b3..d8b4d501 100644 --- a/website/docs/en/guide/monorepo.mdx +++ b/website/docs/en/guide/monorepo.mdx @@ -1,3 +1,7 @@ +--- +description: 'Configure shared Rstack checks, formatting, staged tasks, and project workflows in a monorepo.' +--- + # Monorepo This guide explains how to use Rstack CLI in a monorepo, including how it works with task orchestrators such as [Turborepo](https://turborepo.com/docs) and [Nx](https://nx.dev/docs/getting-started/intro). @@ -65,9 +69,9 @@ Expose these tasks through scripts in the root `package.json`: { "private": true, "scripts": { - "lint": "rs lint", + "check": "rs check --type-check", "format": "rs fmt", - "check:format": "rs fmt --check", + "lint": "rs lint", "staged": "rs staged" } } diff --git a/website/docs/en/guide/quick-start.mdx b/website/docs/en/guide/quick-start.mdx index bee941fa..cadaf016 100644 --- a/website/docs/en/guide/quick-start.mdx +++ b/website/docs/en/guide/quick-start.mdx @@ -1,8 +1,12 @@ +--- +description: 'Create a Rstack project or add Rstack CLI to an existing project and configure the Rstack JavaScript toolchain.' +--- + # Quick start import { PackageManagerTabs } from '@rspress/core/theme'; -Rstack CLI brings the Rstack toolchain together with one CLI and one configuration file. This guide adds Rstack to an existing project and introduces the available workflows. +Rstack CLI brings the Rstack toolchain together with one CLI and one configuration file. This guide shows how to create a new Rstack project or add Rstack to an existing project, and introduces the available workflows. ## Environment preparation @@ -20,6 +24,86 @@ Rstack requires Node.js 22.12.0 or higher when using Node.js as the runtime. ::: +## Create a Rstack project + +[create-rstack](https://www.npmjs.com/package/create-rstack) lets you quickly create an application, library, or documentation site with Rstack configured. We recommend using [pnpm](https://pnpm.io/) as your package manager. + + + +Follow the prompts to complete the setup. + +After creating the project, do the following: + +- Run `pnpm install` (or your package manager's install command) to install dependencies. +- Run `pnpm run dev` to start the development server or watch mode. + +### Templates + +When creating a project, choose from the following templates provided by `create-rstack`: + +| Project type | Templates | +| ------------------ | ---------------------------------------------------------- | +| Web application | Vanilla JavaScript, React, Preact, Vue, Lit, Svelte, Solid | +| Library | Node.js, React, Vue, Svelte, Solid | +| Documentation site | Single language, multilingual | + +All application and library templates are available in both JavaScript and TypeScript. + +### Current directory + +To create a project in the current directory, set the project name or path to `.`: + +``` +◆ Create Rstack Project +│ +◇ Project name or path +│ . +│ +◇ "." is not empty, please choose: +│ Continue and override files +``` + +### Non-interactive mode + +[create-rstack](https://www.npmjs.com/package/create-rstack) supports a non-interactive mode via command-line options. This mode skips prompts and creates the project directly, which is useful for scripts, CI, and automation. + +For example, the following command creates a TypeScript React application in the `my-project` directory: + +```bash +npx -y create-rstack@latest my-project --template app-react-ts + +# Using abbreviations +npx -y create-rstack@latest my-project -t app-react-ts + +# Skip Git repository initialization +npx -y create-rstack@latest my-project -t app-react-ts --no-git +``` + +All CLI flags supported by `create-rstack`: + +```text wrapCode +Usage: create-rstack [dir] [options] + +Options: + -h, --help display help for command + -d, --dir create project in specified directory + -t, --template specify the template to use + --no-git skip Git repository initialization + --override override files in target directory + --package-name specify the package name + --template-version specify the npm template version + +Available templates: app-vanilla, app-vanilla-ts, app-react, app-react-ts, app-preact, app-preact-ts, app-vue, app-vue-ts, app-lit, app-lit-ts, app-svelte, app-svelte-ts, app-solid, app-solid-ts, lib-node, lib-node-ts, lib-react, lib-react-ts, lib-vue, lib-vue-ts, lib-svelte, lib-svelte-ts, lib-solid, lib-solid-ts, doc, doc-i18n +``` + ## Install Rstack Install [`rstack`](https://www.npmjs.com/package/rstack) as a development dependency in a project that has a `package.json`: @@ -44,6 +128,7 @@ Add the commands your project needs to the `scripts` field in `package.json`. Fo "build": "rs build", "preview": "rs preview", "test": "rs test", + "check": "rs check", "lint": "rs lint", "format": "rs fmt" } @@ -60,9 +145,10 @@ The following commands are available: - [`rs lib`](./cli/lib): Build a library with Rslib. - [`rs doc`](./cli/doc): Develop, build, or preview a documentation site with Rspress. - [`rs test`](./cli/test): Run tests with Rstest. +- [`rs check`](./cli/check): Run linting and formatting checks, with optional TypeScript type checking. - [`rs lint`](./cli/lint): Lint source code with Rslint. - [`rs fmt`](./cli/fmt): Format code. -- [`rs setup`](./cli/setup): Install project-local Git hooks. +- [`rs setup`](./cli/setup): Install repository-level Git hooks. - [`rs staged`](./cli/staged): Run tasks against files staged in Git with lint-staged. ## Configure Rstack @@ -88,22 +174,6 @@ define.lint({ See [Configuration](./configuration) for all available configuration APIs. -## Skills - -### Best practice - -Install the Rstack CLI skill so the agent can understand how to use it: - -```bash -npx skills add rstackjs/rstack-cli --skill rstack-cli-best-practices -``` - -### Migration - -Install the migration skill so the agent can migrate existing projects to Rstack CLI: - -```bash -npx skills add rstackjs/rstack-cli --skill migrate-to-rstack-cli -``` +## AI -For supported tools and migration instructions, see [Migrate to Rstack CLI](./migration). +To learn how to use Rstack CLI with coding agents, see the [AI guide](./ai). diff --git a/website/docs/zh/guide/_meta.json b/website/docs/zh/guide/_meta.json index 1763df7c..518da9ca 100644 --- a/website/docs/zh/guide/_meta.json +++ b/website/docs/zh/guide/_meta.json @@ -13,6 +13,11 @@ "name": "configuration", "label": "配置" }, + { + "type": "file", + "name": "ai", + "label": "AI" + }, { "type": "file", "name": "api-reference", diff --git a/website/docs/zh/guide/ai.mdx b/website/docs/zh/guide/ai.mdx new file mode 100644 index 00000000..a9ab69f1 --- /dev/null +++ b/website/docs/zh/guide/ai.mdx @@ -0,0 +1,81 @@ +--- +description: '通过 Agent Skills、llms.txt、Markdown 文档和 AGENTS.md,配合 Coding Agent 使用 Rstack CLI。' +--- + +import { PackageManagerTabs } from '@rspress/core/theme'; + +# AI + +为了帮助 Coding Agent 理解 Rstack CLI 的命令、配置和最佳实践,Rstack CLI 提供了以下资源: + +- [AGENTS.md](#agentsmd) +- [Agent Skills](#agent-skills) +- [llms.txt](#llmstxt) +- [Markdown 文档](#markdown-docs) + +## AGENTS.md + +使用 [create-rstack](https://www.npmjs.com/package/create-rstack) 创建的项目会包含一个遵循 [`AGENTS.md`](https://agents.md/) 规范的文件,为 Coding Agent 提供使用 Rstack CLI 所需的关键上下文。 + +你也可以将以下内容复制到自己的 `AGENTS.md` 中: + +```markdown wrapCode title="AGENTS.md" +This project uses Rstack CLI as its JavaScript toolchain. + +- Before working with `rs` commands, `rstack.config.*` files, or imports from `rstack`, start with `node_modules/rstack/docs/llms.txt`, then read only the linked pages relevant to the task. +- For command details, use `rs -h` or `rs -h`. +- If the local documentation is unavailable, use https://rstack.rs/llms.txt and `rs -h`. +``` + +这段内容与 [rstack-cli-best-practices](#rstack-cli-best-practices) Skill 作用相似,都能指导 Coding Agent 使用 Rstack CLI 并查找相关文档。将其添加到 `AGENTS.md` 或安装该 Skill,任选其一即可。 + +## Agent Skills + +Rstack CLI 提供面向特定领域的 Agent Skills,帮助 Coding Agent 更准确地提供建议并执行相关任务。 + +### rstack-cli-best-practices + +[rstack-cli-best-practices](https://github.com/rstackjs/rstack-cli/tree/main/.agents/skills/rstack-cli-best-practices) Skill 提供 Rstack CLI 的使用指南和最佳实践。 + +使用 [skills](https://www.npmjs.com/package/skills) 包安装该 Skill: + + + +### migrate-to-rstack-cli + +[migrate-to-rstack-cli](https://github.com/rstackjs/rstack-cli/tree/main/.agents/skills/migrate-to-rstack-cli) Skill 可以将使用独立 Rstack 工具及相关开发工具的项目迁移到 Rstack CLI。 + +迁移现有项目时,安装该 Skill: + + + +支持的工具和迁移说明请参阅[迁移到 Rstack CLI](./migration)。 + +## llms.txt + +[llms.txt](https://llmstxt.org/) 是一种帮助 LLM 发现和使用项目文档的标准规范。Rstack CLI 文档站提供了以下文件: + +- [llms.txt](https://rstack.rs/zh/llms.txt):结构化索引文件,包含每篇文档的标题、链接和描述。 + +```text +https://rstack.rs/zh/llms.txt +``` + +- [llms-full.txt](https://rstack.rs/zh/llms-full.txt):包含所有文档完整内容的单个文件。 + +```text +https://rstack.rs/zh/llms-full.txt +``` + +当 Agent 可以按需跟随链接并只加载与任务相关的页面时,使用 `llms.txt`。当 Agent 需要在同一上下文中读取完整文档,并且可以接受更多 token 消耗时,使用 `llms-full.txt`。 + +## Markdown 文档 \{#markdown-docs} + +Rstack CLI 的每篇文档都有对应的 `.md` 纯文本版本,可以直接提供给 Agent。在任意文档页面的标题下方使用「复制 Markdown」或「复制 Markdown 链接」,即可复制文档内容或 URL。 + +```text +https://rstack.rs/zh/guide/quick-start.md +``` diff --git a/website/docs/zh/guide/cli/_meta.json b/website/docs/zh/guide/cli/_meta.json index 0f31a84a..acdaffbe 100644 --- a/website/docs/zh/guide/cli/_meta.json +++ b/website/docs/zh/guide/cli/_meta.json @@ -1 +1 @@ -["dev", "build", "preview", "lib", "doc", "test", "lint", "fmt", "setup", "staged"] +["dev", "build", "preview", "lib", "doc", "test", "check", "lint", "fmt", "setup", "staged"] diff --git a/website/docs/zh/guide/cli/check.mdx b/website/docs/zh/guide/cli/check.mdx new file mode 100644 index 00000000..d64c3c56 --- /dev/null +++ b/website/docs/zh/guide/cli/check.mdx @@ -0,0 +1,53 @@ +--- +description: '同时运行 lint 和格式检查,并可选启用 TypeScript 类型检查。' +--- + +# check + +`rs check` 命令可统一运行当前项目的 lint、格式和可选的 TypeScript 类型检查。默认情况下,它会先运行 [`rs lint`](./lint),再运行 [`rs fmt --check`](./fmt#--check)。 + +## 用法 \{#usage} + +```bash +rs check [options] +``` + +## 检查内容 \{#checks} + +运行 `rs check` 等同于: + +```bash +rs lint && rs fmt --check +``` + +各项检查会按顺序运行。如果 lint 失败,`rs check` 会停止运行,不再检查格式。只有所有已启用的检查均通过时,该命令才会成功退出。 + +## 选项 \{#options} + +### `--type-check` + +在 lint 过程中启用 TypeScript 类型检查: + +```bash +rs check --type-check +``` + +该命令等同于: + +```bash +rs lint --type-check && rs fmt --check +``` + +省略此选项时,不会运行类型检查。 + +### `-h, --help` + +显示命令用法和选项信息,但不运行检查: + +```bash +rs check --help +``` + +## 配置 \{#configuration} + +`rs check` 没有独立的 `define.check()` 配置。它会使用 [`define.lint()`](../configuration#define-lint) 中的 lint 配置,以及 [`define.fmt()`](../configuration#define-fmt) 中的格式化配置。 diff --git a/website/docs/zh/guide/cli/setup.mdx b/website/docs/zh/guide/cli/setup.mdx index ab78d95f..4144e9b6 100644 --- a/website/docs/zh/guide/cli/setup.mdx +++ b/website/docs/zh/guide/cli/setup.mdx @@ -2,7 +2,7 @@ import { PackageManagerTabs } from '@rspress/core/theme'; -`rs setup` 命令用于在当前 Git 仓库中安装项目级 [Git hooks](https://git-scm.com/docs/githooks)。 +`rs setup` 命令用于安装仓库级 [Git hooks](https://git-scm.com/docs/githooks),并在调用该命令的项目中运行 hooks。 ## 用法 \{#usage} @@ -10,9 +10,9 @@ import { PackageManagerTabs } from '@rspress/core/theme'; rs setup [options] ``` -项目 hook 脚本默认存放在 `.rstack/hooks`。如果当前目录不属于 Git 仓库,命令会跳过安装。 +hook 脚本默认存放在 Git 仓库根目录下的 `.rstack/hooks`。如果当前目录不属于 Git 仓库,命令会跳过安装。 -在根目录 `package.json` 的 `prepare` 脚本中添加 `rs setup`,即可在安装依赖时自动生成 hook 文件: +在负责管理仓库 hooks 的项目 `package.json` 中添加 `prepare` 脚本: ```json title="package.json" { @@ -41,7 +41,7 @@ rs staged :::warning 已有 Git hook 管理工具 -`rs setup` 会更新仓库的 [`core.hooksPath`](https://git-scm.com/docs/git-config#Documentation/git-config.txt-corehooksPath)。如果仓库已经使用 Husky 或其他 Git hook 管理工具,请先迁移所需的 hooks,再运行该命令。 +`rs setup` 会更新仓库的 [`core.hooksPath`](https://git-scm.com/docs/git-config#Documentation/git-config.txt-corehooksPath)。检测到其他 hooks 路径或已有 Git hook 时,命令会跳过安装。请先迁移所需的 hooks 并移除已有 hooks 配置,再运行该命令。 ::: @@ -49,7 +49,7 @@ rs staged ### `--hooks-dir` -设置项目 hook 脚本的存放目录,路径相对于命令的当前目录。 +设置 hook 脚本的存放目录,路径相对于 Git 仓库根目录。 ```bash rs setup --hooks-dir config/git-hooks @@ -58,7 +58,7 @@ rs setup --hooks-dir config/git-hooks rs setup --hooks-dir "config/git hooks" ``` -使用自定义目录时,请将完整命令写入根目录 `package.json` 的 `prepare` 脚本: +使用自定义目录时,请将完整命令写入负责管理 hooks 的项目 `package.json`: ```json title="package.json" { @@ -68,7 +68,7 @@ rs setup --hooks-dir "config/git hooks" } ``` -> 为避免通过父目录路径在当前项目之外创建或覆盖 Git hook 文件,路径中不能包含 `..`。 +> 为避免通过父目录路径在仓库之外创建或覆盖 Git hook 文件,路径中不能包含 `..`。 ### `--help` @@ -85,16 +85,17 @@ rs setup --help ```text .rstack/ └── hooks/ - ├── pre-commit # 项目 hook 脚本:编辑并提交 + ├── pre-commit # 仓库 hook 脚本:编辑并提交 └── _/ # 由 rs setup 生成;默认被 Git 忽略 ├── .gitignore + ├── .owner ├── runner ├── pre-commit ├── commit-msg └── ... ``` -与 `_` 同级的文件是项目 hook 脚本。`_` 目录包含生成文件,并由 Git 忽略。`rs setup` 会将 `core.hooksPath` 指向 `.rstack/hooks/_`;克隆仓库后或生成文件缺失时,请重新运行该命令。 +与 `_` 同级的文件是仓库 hook 脚本。`_` 目录包含生成文件,并由 Git 忽略。`rs setup` 会将 `core.hooksPath` 指向 `.rstack/hooks/_`;克隆仓库后或生成文件缺失时,请重新运行该命令。 ## 支持的 hooks \{#supported-hooks} @@ -119,7 +120,7 @@ Rstack 支持以下客户端 Git hooks: ## Hook 运行时 \{#hook-runtime} -Rstack 使用 POSIX `sh -e` 运行 hook 脚本,并转发 Git 提供的参数和标准输入,同时返回 hook 的退出码。运行时还会将 `node_modules/.bin` 添加到 `PATH` 开头。 +Rstack 使用 POSIX `sh -e` 运行 hook 脚本,并转发 Git 提供的参数和标准输入,同时返回 hook 的退出码。运行 hook 前,Rstack 会切换到安装 hooks 的项目,并将该项目的 `node_modules/.bin` 添加到 `PATH` 开头。 ### 禁用与调试 \{#disable-and-debug} @@ -147,22 +148,23 @@ ${XDG_CONFIG_HOME:-$HOME/.config}/rstack/hooks-init.sh ## Monorepo \{#monorepo} -在 monorepo 中,项目可能位于 Git 仓库的子目录,例如 `frontend/`。从该目录运行 `rs setup` 时,hooks 目录会相对于项目创建,`core.hooksPath` 也会包含项目路径: +在 monorepo 中,提供 Rstack 的项目可能位于 `frontend/` 等子目录。从该目录运行 `rs setup` 时,hooks 仍会安装到 Git 仓库根目录: ```text -frontend/.rstack/hooks/ -frontend/.rstack/hooks/_/ -core.hooksPath=frontend/.rstack/hooks/_ +repo/.rstack/hooks/ +repo/.rstack/hooks/_/ +core.hooksPath=.rstack/hooks/_ ``` -Git 会从仓库根目录运行 hook。如果项目位于子目录,请在 hook 脚本中先切换到该目录,再执行项目命令: +Rstack 会将 `frontend` 记录为负责管理 hooks 的项目。hook 脚本仍位于仓库根目录,但会从 `frontend` 目录运行,因此可以直接使用其中的配置和依赖,无需显式执行 `cd`: -```sh title="frontend/.rstack/hooks/pre-commit" -cd frontend -pnpm test +```sh title=".rstack/hooks/pre-commit" +rs staged ``` -一个 Git 仓库只有一个 `core.hooksPath`,因此应选择仓库根目录或其中一个子项目统一管理 hooks。 +一个 Git 仓库只能有一个 hooks owner。只有负责管理 hooks 的项目应在 `prepare` 脚本中调用 `rs setup`。其他项目调用时会收到警告并跳过。 + +如需更换 owner,请先从原项目的 `prepare` 脚本中移除 `rs setup`,删除生成的 `_` 目录,再从新项目运行 `rs setup`。 ## 移除 hooks \{#remove-hooks} @@ -185,6 +187,8 @@ pnpm test - 运行 `git config --local --get core.hooksPath`,检查配置的路径。 - 重新运行 `rs setup`,恢复生成文件及其可执行权限。 - 检查环境变量或初始化文件中是否设置了 `RSTACK_HOOKS=0`。 +- 如果命令提示存在其他 hooks 配置,请先迁移或移除冲突配置,再重新运行该命令。 +- 如果命令提示存在其他 Rstack owner,请按照 [Monorepo](#monorepo) 中的步骤转移 owner。 hook 脚本不需要可执行权限,因为 Rstack 会使用 `sh` 运行它。 diff --git a/website/docs/zh/guide/monorepo.mdx b/website/docs/zh/guide/monorepo.mdx index 66fbedb1..ea1c5ea6 100644 --- a/website/docs/zh/guide/monorepo.mdx +++ b/website/docs/zh/guide/monorepo.mdx @@ -1,3 +1,7 @@ +--- +description: '在 Monorepo 中配置共享的 Rstack 检查、格式化、暂存文件任务和项目工作流。' +--- + # Monorepo 本指南介绍如何在 Monorepo 中使用 Rstack CLI,以及如何让它与 [Turborepo](https://turborepo.com/docs)、[Nx](https://nx.dev/docs/getting-started/intro) 等任务编排工具协同工作。 @@ -65,9 +69,9 @@ define.staged({ { "private": true, "scripts": { - "lint": "rs lint", + "check": "rs check --type-check", "format": "rs fmt", - "check:format": "rs fmt --check", + "lint": "rs lint", "staged": "rs staged" } } diff --git a/website/docs/zh/guide/quick-start.mdx b/website/docs/zh/guide/quick-start.mdx index 6927d123..1c6aaa6c 100644 --- a/website/docs/zh/guide/quick-start.mdx +++ b/website/docs/zh/guide/quick-start.mdx @@ -1,8 +1,12 @@ +--- +description: '创建 Rstack 项目,或在现有项目中安装 Rstack CLI 并配置 Rstack JavaScript 工具链。' +--- + # 快速上手 \{#quick-start} import { PackageManagerTabs } from '@rspress/core/theme'; -Rstack CLI 通过统一的命令行和配置文件整合 Rstack 工具链。本指南将介绍如何在现有项目中添加 Rstack,以及可以使用的工作流。 +Rstack CLI 通过统一的命令行和配置文件整合 Rstack 工具链。本指南将介绍如何创建新的 Rstack 项目或在现有项目中添加 Rstack,以及可以使用的工作流。 ## 环境准备 \{#environment-preparation} @@ -20,6 +24,86 @@ Rstack 支持使用 [Node.js](https://nodejs.org/)、[Deno](https://deno.com/) ::: +## 创建 Rstack 项目 \{#create-a-rstack-project} + +使用 [create-rstack](https://www.npmjs.com/package/create-rstack) 可以快速创建已配置好 Rstack 的应用、库或文档站点。推荐使用 [pnpm](https://pnpm.io/) 作为包管理器。 + + + +按照提示操作即可完成创建。 + +项目创建完成后,执行以下步骤: + +- 执行 `pnpm install`(或其他包管理器的 install 命令)安装依赖。 +- 执行 `pnpm run dev` 启动开发服务器或监听模式。 + +### 模板 \{#templates} + +创建项目时,可选择 `create-rstack` 提供的下列模板: + +| 项目类型 | 模板 | +| -------- | ------------------------------------------------------- | +| Web 应用 | 原生 JavaScript、React、Preact、Vue、Lit、Svelte、Solid | +| 库 | Node.js、React、Vue、Svelte、Solid | +| 文档站点 | 单语言、多语言 | + +所有应用和库模板均提供 JavaScript 和 TypeScript 版本。 + +### 当前目录 \{#current-directory} + +如需在当前目录中创建项目,将项目名称或路径设置为 `.`: + +``` +◆ Create Rstack Project +│ +◇ Project name or path +│ . +│ +◇ "." is not empty, please choose: +│ Continue and override files +``` + +### 非交互模式 \{#non-interactive-mode} + +[create-rstack](https://www.npmjs.com/package/create-rstack) 支持通过命令行选项进入非交互模式。该模式会跳过提示并直接创建项目,适合脚本、CI 和自动化场景。 + +例如,以下命令将在 `my-project` 目录中创建一个 TypeScript React 应用: + +```bash +npx -y create-rstack@latest my-project --template app-react-ts + +# 使用缩写 +npx -y create-rstack@latest my-project -t app-react-ts + +# 跳过 Git 仓库初始化 +npx -y create-rstack@latest my-project -t app-react-ts --no-git +``` + +`create-rstack` 支持的全部 CLI 选项如下: + +```text wrapCode +Usage: create-rstack [dir] [options] + +Options: + -h, --help display help for command + -d, --dir create project in specified directory + -t, --template specify the template to use + --no-git skip Git repository initialization + --override override files in target directory + --package-name specify the package name + --template-version specify the npm template version + +Available templates: app-vanilla, app-vanilla-ts, app-react, app-react-ts, app-preact, app-preact-ts, app-vue, app-vue-ts, app-lit, app-lit-ts, app-svelte, app-svelte-ts, app-solid, app-solid-ts, lib-node, lib-node-ts, lib-react, lib-react-ts, lib-vue, lib-vue-ts, lib-svelte, lib-svelte-ts, lib-solid, lib-solid-ts, doc, doc-i18n +``` + ## 安装 Rstack \{#install-rstack} 在已有 `package.json` 的项目中,将 [`rstack`](https://www.npmjs.com/package/rstack) 安装为开发依赖: @@ -44,6 +128,7 @@ Rstack 支持使用 [Node.js](https://nodejs.org/)、[Deno](https://deno.com/) "build": "rs build", "preview": "rs preview", "test": "rs test", + "check": "rs check", "lint": "rs lint", "format": "rs fmt" } @@ -60,9 +145,10 @@ Rstack 提供以下命令: - [`rs lib`](./cli/lib):使用 Rslib 构建库。 - [`rs doc`](./cli/doc):使用 Rspress 开发、构建或预览文档站点。 - [`rs test`](./cli/test):使用 Rstest 运行测试。 +- [`rs check`](./cli/check):运行 lint 和格式检查,并可选启用 TypeScript 类型检查。 - [`rs lint`](./cli/lint):使用 Rslint 检查源代码。 - [`rs fmt`](./cli/fmt):格式化代码。 -- [`rs setup`](./cli/setup):安装项目本地 Git hooks。 +- [`rs setup`](./cli/setup):安装仓库级 Git hooks。 - [`rs staged`](./cli/staged):使用 lint-staged 对 Git 暂存区中的文件运行任务。 ## 配置 Rstack \{#configure-rstack} @@ -88,22 +174,6 @@ define.lint({ 所有可用的配置 API 请参见[配置](./configuration)。 -## Skills \{#skills} - -### 最佳实践 \{#best-practice} - -安装 Rstack CLI Skill,使 Agent 能够了解如何使用 Rstack CLI: - -```bash -npx skills add rstackjs/rstack-cli --skill rstack-cli-best-practices -``` - -### 迁移 \{#migration} - -安装迁移 Skill,使 Agent 能够将现有项目迁移到 Rstack CLI: - -```bash -npx skills add rstackjs/rstack-cli --skill migrate-to-rstack-cli -``` +## AI -支持的工具和迁移说明请参阅[迁移到 Rstack CLI](./migration)。 +如需了解如何配合 Coding Agent 使用 Rstack CLI,请参阅 [AI 指南](./ai)。