From d785b3eb3f69a9cfc08f1383ae6105df487cb08d Mon Sep 17 00:00:00 2001 From: Andrey Kopylov Date: Thu, 17 Sep 2026 18:45:34 +0500 Subject: [PATCH 1/2] Sync SDK documentation [17.09.2026] --- docs/sdk/API/01-vault/getMaxWithdraw.md | 39 ---------- docs/sdk/API/01-vault/getPositionData.md | 71 +++++++++++++++++++ docs/sdk/API/01-vault/getVault.md | 3 + .../helpers/calculateAllocatorPosition.md | 43 +++++++++++ .../helpers/calculateStakerPosition.md | 41 +++++++++++ .../01-transactions/claimRedeemerExitQueue.md | 31 ++++++++ .../01-transactions/redeemerWithdraw.md | 37 ++++++++++ docs/sdk/API/03-osToken/getMaxMint.md | 37 ---------- docs/sdk/API/03-osToken/getPosition.md | 55 -------------- .../getRedeemerExitQueuePositions.md | 60 ++++++++++++++++ .../API/03-osToken/helpers/getBurnAmount.md | 40 ----------- 11 files changed, 286 insertions(+), 171 deletions(-) delete mode 100644 docs/sdk/API/01-vault/getMaxWithdraw.md create mode 100644 docs/sdk/API/01-vault/getPositionData.md create mode 100644 docs/sdk/API/01-vault/helpers/calculateAllocatorPosition.md create mode 100644 docs/sdk/API/01-vault/helpers/calculateStakerPosition.md create mode 100644 docs/sdk/API/03-osToken/01-transactions/claimRedeemerExitQueue.md create mode 100644 docs/sdk/API/03-osToken/01-transactions/redeemerWithdraw.md delete mode 100644 docs/sdk/API/03-osToken/getMaxMint.md delete mode 100644 docs/sdk/API/03-osToken/getPosition.md create mode 100644 docs/sdk/API/03-osToken/getRedeemerExitQueuePositions.md delete mode 100644 docs/sdk/API/03-osToken/helpers/getBurnAmount.md diff --git a/docs/sdk/API/01-vault/getMaxWithdraw.md b/docs/sdk/API/01-vault/getMaxWithdraw.md deleted file mode 100644 index 03b1f1ff..00000000 --- a/docs/sdk/API/01-vault/getMaxWithdraw.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -id: getMaxWithdraw -title: getMaxWithdraw -slug: /sdk/api/vault/requests/getmaxwithdraw -description: "StakeWise SDK getMaxWithdraw method (deprecated): calculates the maximum withdrawal amount based on staked and minted assets." ---- - -# Deprecated! -Use getMaxWithdrawAmount - -#### Description: - -How much a user can withdraw. Use this method if the user has mintedAssets, if minted balance is null then maxWithdraw will be equal to stakedAssests. - -#### Arguments: - -| Name | Type | Required | Info | -|--------------|----------|----------|--------------------------------------------------------------| -| vaultAddress | `string` | **Yes** | The address of the vault | -| ltvPercent | `bigint` | **Yes** | [sdk.vault.getVault](/sdk/api/vault/requests/getvault) | -| mintedAssets | `bigint` | **Yes** | [sdk.osToken.getPosition](/sdk/api/osToken/requests/getposition) | -| stakedAssets | `bigint` | **Yes** | [sdk.vault.getStakeBalance](/sdk/api/vault/requests/getstakebalance) | - -#### Returns: - -```ts -type Output = bigint -``` - -#### Example: - -```ts -await sdk.vault.getMaxWithdraw({ - ltvPercent: 0n, - mintedAssets: 0n, - stakedAssets: 0n, - vaultAddress: '0x...', -}) -``` diff --git a/docs/sdk/API/01-vault/getPositionData.md b/docs/sdk/API/01-vault/getPositionData.md new file mode 100644 index 00000000..97d162c7 --- /dev/null +++ b/docs/sdk/API/01-vault/getPositionData.md @@ -0,0 +1,71 @@ +--- +id: getPositionData +slug: /sdk/api/vault/requests/getpositiondata +description: Use the StakeWise SDK getPositionData method to fetch the data needed to calculate a user's allocator and staker positions. +--- + +#### Description: + +Fetches the user's position in a vault: stake, minted and boosted osToken, wallet osToken balance and the APY parameters. Pass the result as `data` to `calculateAllocatorPosition` or `calculateStakerPosition`. + +#### Arguments: + +| Name | Type | Required | Description | +|--------------|----------|----------|--------------------------| +| userAddress | `string` | **Yes** | The address of the user | +| vaultAddress | `string` | **Yes** | The address of the vault | + +#### Returns: + +```ts +type Output = { + vault: { + apyData: { + vaultApy: number + borrowApy: number + osTokenApy: number + feePercent: number + ltvPercent: bigint + osTokenRate: bigint + osTokenMintApy: number + allocatorMaxBoostApy: number + leverageMaxMintLtvPercent: bigint + leverageMaxBorrowLtvPercent: bigint + } + isCollateralized: boolean + isOsTokenEnabled: boolean + } | null + stakedAssets: bigint + exitingAssets: bigint + mintedShares: bigint + walletShares: bigint + boostedShares: bigint + boostedAssets: bigint + leverageReward: bigint +} +``` + +| Name | Description | +|----------------|--------------------------------------------------------------------------| +| vault | Vault, osToken and Aave APY parameters, `null` if the vault is not found | +| stakedAssets | Assets staked by the user in the vault | +| exitingAssets | Assets of the user in the exit queue | +| mintedShares | osToken shares minted by the user | +| walletShares | osToken shares in the user's wallet | +| boostedShares | osToken shares in the user's boost position, including exiting | +| boostedAssets | Assets in the user's boost position, including exiting | +| leverageReward | Annual reward of the existing boost position in assets | + +#### Example: + +```ts +const data = await sdk.vault.getPositionData({ + userAddress: '0x...', + vaultAddress: '0x...', +}) + +const { apy, totalAssets } = sdk.vault.helpers.calculateAllocatorPosition({ + data, + boostedSharesDelta: parseEther('1'), +}) +``` diff --git a/docs/sdk/API/01-vault/getVault.md b/docs/sdk/API/01-vault/getVault.md index fa103e7c..57054cbe 100644 --- a/docs/sdk/API/01-vault/getVault.md +++ b/docs/sdk/API/01-vault/getVault.md @@ -54,6 +54,7 @@ type Output = { tokenSymbol: string | null displayName: string | null description: string | null + isStateUpdateRequired: boolean lastFeeUpdateTimestamp: string osTokenConfig: { ltvPercent: string @@ -91,6 +92,7 @@ type Output = { | `tokenName` | ERC20 token name | | `tokenSymbol` | ERC20 token symbol | | `displayName` | Name of vault | +| `isStateUpdateRequired` | Indicates whether the vault state is out of sync with the latest rewards nonce | | `pendingMetaSubVault` | The address of the meta vault that is pending to join as a sub vault | | `ejectingSubVault` | The address of the sub vault currently being ejected (for meta vaults) | | `canHarvest` | Defines whether the vault can harvest new rewards | @@ -98,6 +100,7 @@ type Output = { | `description` | Description of vault | | `whitelist` | List of authorized users for deposits | | `blocklist` | List of blocked users for deposits | +| `lastUpdateStateTimestamp` | The timestamp of the last vault state update. Will be null if the state has never been updated | | `performance` | Vault performance indicator (percent) | | `lastFeeUpdateTimestamp` | The timestamp of the last fee update | | `lastFeePercent` | The vault last fee percent | diff --git a/docs/sdk/API/01-vault/helpers/calculateAllocatorPosition.md b/docs/sdk/API/01-vault/helpers/calculateAllocatorPosition.md new file mode 100644 index 00000000..db58ed10 --- /dev/null +++ b/docs/sdk/API/01-vault/helpers/calculateAllocatorPosition.md @@ -0,0 +1,43 @@ +--- +id: calculateAllocatorPosition +slug: /sdk/api/vault/helpers/calculateallocatorposition +description: Use the StakeWise SDK calculateAllocatorPosition helper to calculate a user's vault APY and total staked assets after staking, minting, burning or boosting. +--- + +#### Description: + +Calculates the user's APY and total staked assets in a vault from the result of `getPositionData` and optional deltas. Zero deltas return the current position. + +A negative `boostedSharesDelta` is ignored: unboosted shares stay in the position while they are exiting. + +#### Arguments: + +| Name | Type | Required | Description | +|--------------------|----------------|----------|--------------------------------------------------------------------| +| data | `PositionData` | **Yes** | Result of `getPositionData` | +| stakedAssetsDelta | `bigint` | No | Change in staked assets (e.g. `+assets` to stake, `-assets` to unstake). Defaults to `0n` | +| mintedSharesDelta | `bigint` | No | Change in minted osToken shares (`+shares` to mint, `-shares` to burn). Defaults to `0n` | +| boostedSharesDelta | `bigint` | No | Change in boosted osToken shares (`+shares` to boost). Defaults to `0n` | + +#### Returns: + +```ts +type Output = { + apy: number + totalAssets: bigint +} +``` + +#### Example: + +```ts +const data = await sdk.vault.getPositionData({ + userAddress: '0x...', + vaultAddress: '0x...', +}) + +const { apy, totalAssets } = sdk.vault.helpers.calculateAllocatorPosition({ + data, + stakedAssetsDelta: parseEther('1'), +}) +``` diff --git a/docs/sdk/API/01-vault/helpers/calculateStakerPosition.md b/docs/sdk/API/01-vault/helpers/calculateStakerPosition.md new file mode 100644 index 00000000..11141b53 --- /dev/null +++ b/docs/sdk/API/01-vault/helpers/calculateStakerPosition.md @@ -0,0 +1,41 @@ +--- +id: calculateStakerPosition +slug: /sdk/api/vault/helpers/calculatestakerposition +description: Use the StakeWise SDK calculateStakerPosition helper to calculate a user's net staker APY and total assets after staking, minting, burning or boosting. +--- + +#### Description: + +Calculates the staker's net APY and total assets across the wallet, mint and boost from the result of `getPositionData` and optional deltas. Synchronous, no requests. Zero deltas return the current position. For a single vault use `calculateAllocatorPosition`. + +#### Arguments: + +| Name | Type | Required | Description | +|--------------------|----------------|----------|---------------------------------------------------| +| data | `PositionData` | **Yes** | Result of `getPositionData` | +| stakedAssetsDelta | `bigint` | No | Change in staked assets. Defaults to `0n` | +| mintedSharesDelta | `bigint` | No | Change in minted osToken shares. Defaults to `0n` | +| boostedSharesDelta | `bigint` | No | Change in boosted osToken shares. Defaults to `0n` | + +#### Returns: + +```ts +type Output = { + apy: number + totalAssets: bigint +} +``` + +#### Example: + +```ts +const data = await sdk.vault.getPositionData({ + userAddress: '0x...', + vaultAddress: '0x...', +}) + +const { apy, totalAssets } = sdk.vault.helpers.calculateStakerPosition({ + data, + stakedAssetsDelta: parseEther('1'), +}) +``` diff --git a/docs/sdk/API/03-osToken/01-transactions/claimRedeemerExitQueue.md b/docs/sdk/API/03-osToken/01-transactions/claimRedeemerExitQueue.md new file mode 100644 index 00000000..83acb0d0 --- /dev/null +++ b/docs/sdk/API/03-osToken/01-transactions/claimRedeemerExitQueue.md @@ -0,0 +1,31 @@ +--- +id: claimRedeemerExitQueue +slug: /sdk/api/osToken/transactions/claimredeemerexitqueue +description: Use the StakeWise SDK claimRedeemerExitQueue method to withdraw exited assets from the OsTokenRedeemer exit queue for a specific user. +--- + +#### Description: + +Withdraws exited assets from the OsTokenRedeemer queue. + +#### Arguments: + +| Name | Type | Required | Description | +|-------------|----------|----------|----------------------------------------------------------------| +| userAddress | `string` | **Yes** | The user address | +| positions | `Array` | **Yes** | Claimable positions (`positionTicket` + `exitQueueIndex`) | + +#### Returns: + +Transaction hash. + +#### Example: + +```ts +const { positions } = await sdk.osToken.getRedeemerExitQueuePositions({ userAddress: '0x...' }) + +await sdk.osToken.claimRedeemerExitQueue({ + userAddress: '0x...', + positions, +}) +``` diff --git a/docs/sdk/API/03-osToken/01-transactions/redeemerWithdraw.md b/docs/sdk/API/03-osToken/01-transactions/redeemerWithdraw.md new file mode 100644 index 00000000..c57c55bb --- /dev/null +++ b/docs/sdk/API/03-osToken/01-transactions/redeemerWithdraw.md @@ -0,0 +1,37 @@ +--- +id: redeemerWithdraw +slug: /sdk/api/osToken/transactions/redeemerwithdraw +description: Use the StakeWise SDK osToken redeemerWithdraw method to redeem osToken through the OsTokenRedeemer exit queue. +--- + +#### Description: + +Enters the OsTokenRedeemer exit queue with your osToken shares. The osToken must be approved to the OsTokenRedeemer contract (or use a permit) before calling. Returns a transaction hash; the position ticket is emitted in the `ExitQueueEntered` event. + +#### Arguments: + +| Name | Type | Required | Description | +|--------------|----------|----------|---------------------------| +| shares | `bigint` | **Yes** | osToken shares to redeem | +| userAddress | `string` | **Yes** | The user address | + +#### Example: + +```ts +const params = { + shares: 0n, + userAddress: '0x...', +} + +// Send transaction +const hash = await sdk.osToken.redeemerWithdraw(params) + +// Wait for the transaction to be confirmed and indexed +await sdk.provider.waitForTransaction(hash) +await sdk.utils.waitForSubgraph({ hash }) + +// When you sign transactions on the backend (for custodians) +const { data, to } = await sdk.osToken.redeemerWithdraw.encode(params) +// Get an approximate gas per transaction +const gas = await sdk.osToken.redeemerWithdraw.estimateGas(params) +``` diff --git a/docs/sdk/API/03-osToken/getMaxMint.md b/docs/sdk/API/03-osToken/getMaxMint.md deleted file mode 100644 index 543454ac..00000000 --- a/docs/sdk/API/03-osToken/getMaxMint.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -id: getMaxMint -title: getMaxMint -slug: /sdk/api/osToken/requests/getmaxmint -description: "StakeWise SDK getMaxMint method (deprecated): returns the maximum number of osToken shares available for minting." ---- - -# Deprecated! -Use getMaxMintAmount - -#### Description: - -Maximum number of **shares** for minting - -#### Arguments: -| Name | Type | Required | Description | -|--------------|----------|----------|--------------------------------------------------------------| -| vaultAddress | `string` | **Yes** | The address of the vault | -| ltvPercent | `bigint` | **Yes** | [sdk.vault.getVault](/sdk/api/vault/requests/getvault) | -| stakedAssets | `bigint` | **Yes** | [sdk.vault.getStakeBalance](/sdk/api/vault/requests/getstakebalance) | -| mintedAssets | `bigint` | **Yes** | [sdk.osToken.getPosition](/sdk/api/osToken/requests/getposition) | - -#### Returns: - -```ts -type Output = bigint -``` -#### Example: - -```ts -await sdk.osToken.getMaxMint({ - ltvPercent: 0n, - mintedAssets: 0n, - stakedAssets: 0n, - vaultAddress: '0x...', -}) -``` diff --git a/docs/sdk/API/03-osToken/getPosition.md b/docs/sdk/API/03-osToken/getPosition.md deleted file mode 100644 index b32cc7a6..00000000 --- a/docs/sdk/API/03-osToken/getPosition.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -id: getPosition -title: getPosition -slug: /sdk/api/osToken/requests/getposition -description: "StakeWise SDK getPosition method (deprecated): retrieves user osToken position data including minted assets and health factor." ---- - -# Deprecated! -Use getBalance - -#### Description: - -User position data - -#### Arguments: -| Name | Type | Required | Description | -|------------------|----------|----------|--------------------------------------------------------------| -| liqThresholdPercent | `bigint` | **Yes** | [sdk.vault.getVault](/sdk/api/vault/requests/getvault) | -| stakedAssets | `bigint` | **Yes** | [sdk.vault.getStakeBalance](/sdk/api/vault/requests/getstakebalance) | -| userAddress | `string` | **Yes** | The user address | -| vaultAddress | `string` | **Yes** | The address of the vault | - -#### Returns: - -```ts -type Output = { - minted: { - assets: bigint - shares: bigint - } - healthFactor: { - value: number - health: OsTokenPositionHealth - } - protocolFeePercent: bigint -} -``` - -| Name | Description | -|----------------------|-----------------------------------------------------------------| -| `minted.shares` | Balance | -| `minted.assets` | Balance in ETH | -| `healthFactor` | [sdk.osToken.getHealthFactor](/sdk/api/osToken/helpers/gethealthfactor) | -| `protocolFeePercent` | Usage fee percent | - -#### Example: - -```ts -await sdk.osToken.getPosition({ - stakedAssets: 0n, - userAddress: '0x...', - vaultAddress: '0x...', - liqThresholdPercent: 0n, -}) -``` diff --git a/docs/sdk/API/03-osToken/getRedeemerExitQueuePositions.md b/docs/sdk/API/03-osToken/getRedeemerExitQueuePositions.md new file mode 100644 index 00000000..47c35708 --- /dev/null +++ b/docs/sdk/API/03-osToken/getRedeemerExitQueuePositions.md @@ -0,0 +1,60 @@ +--- +id: getRedeemerExitQueuePositions +slug: /sdk/api/osToken/requests/getredeemerexitqueuepositions +description: Use the StakeWise SDK getRedeemerExitQueuePositions method to retrieve a user's OsTokenRedeemer exit queue positions and claimable amounts. +--- + +#### Description: + +Returns the OsTokenRedeemer exit queue positions for a specific user. + +#### Arguments: + +| Name | Type | Required | Description | +|-------------|-----------|----------|-----------------------------------------| +| userAddress | `string` | **Yes** | The user address | +| isClaimed | `boolean` | **No** | Whether the exit request is claimed. Defaults to `false` | + +#### Returns: + +```ts +type ExitRequest = { + owner: string + receiver: string + isClaimed: boolean + timestamp: string + isClaimable: boolean + totalShares: bigint + totalAssets: bigint + exitedAssets: bigint + positionTicket: string + exitQueueIndex: string | null +} + +type Position = { + positionTicket: string + exitQueueIndex: string +} + +type Output = { + total: bigint + withdrawable: bigint + positions: Position[] + requests: ExitRequest[] +} +``` + +| Name | Description | +|----------------|--------------------------------------------------------------| +| `requests` | Array of all requests | +| `positions` | Positions in a special format that are required for claiming | +| `total` | Total queued assets (e.g. ETH) | +| `withdrawable` | Assets available for withdrawal (e.g. ETH) | + +#### Example: + +```ts +await sdk.osToken.getRedeemerExitQueuePositions({ + userAddress: '0x...', +}) +``` diff --git a/docs/sdk/API/03-osToken/helpers/getBurnAmount.md b/docs/sdk/API/03-osToken/helpers/getBurnAmount.md deleted file mode 100644 index c5785066..00000000 --- a/docs/sdk/API/03-osToken/helpers/getBurnAmount.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -id: getBurnAmount -title: getBurnAmount -slug: /sdk/api/osToken/helpers/getburnamount -description: "StakeWise SDK getBurnAmount helper (deprecated): calculates how many osTokens to burn to fully withdraw your deposit." ---- - -# Deprecated! -Use getBurnAmountForUnstake - -#### Description: - -How many osToken burn do you need to make to withdraw all deposit. - -#### Arguments: -| Name | Type | Required | Description | -|-----------------|----------|----------|--------------------------------------------------------------| -| vaultAddress | `string` | **Yes** | The address of the vault | -| ltvPercent | `bigint` | **Yes** | [sdk.vault.getVault](/sdk/api/vault/requests/getvault) | -| mintedAssets | `bigint` | **Yes** | [sdk.osToken.getPosition](/sdk/api/osToken/requests/getposition) | -| stakedAssets | `bigint` | **Yes** | [sdk.vault.getStakeBalance](/sdk/api/vault/requests/getstakebalance) | -| newStakedAssets | `bigint` | **Yes** | The future amount of stake after the deposit | - -#### Returns: - -```ts -type Output = bigint -``` - -#### Example: - -```ts -sdk.osToken.getBurnAmount({ - ltvPercent: 0n, - mintedAssets: 0n, - stakedAssets: 0n, - newStakedAssets: 0n, - vaultAddress: '0x...', -}) -``` From e93057c33cc9425502210f15003c71df584303aa Mon Sep 17 00:00:00 2001 From: Andrey Kopylov Date: Mon, 21 Sep 2026 19:13:02 +0500 Subject: [PATCH 2/2] change redirects script --- CLAUDE.md | 2 +- redirects.ts | 16 ++++ scripts/checkRedirects/index.ts | 19 ++--- scripts/checkRedirects/util/anchors.ts | 3 +- scripts/checkRedirects/util/index.ts | 1 + scripts/checkRedirects/util/slugs.ts | 103 +++++++++++++++++++++++++ 6 files changed, 133 insertions(+), 11 deletions(-) create mode 100644 scripts/checkRedirects/util/slugs.ts diff --git a/CLAUDE.md b/CLAUDE.md index f1b66d66..f1141223 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,7 +47,7 @@ Adding a page to a manually-listed sidebar (`docsSidebar`, `operatorSidebar`, `s Moving, renaming, or deleting any file under `docs/`, `operator/`, or `staker/` requires an entry in `redirects.ts`. This is enforced twice: the husky `pre-commit` hook runs `yarn checkRedirects`, and `.github/workflows/check-redirects.yml` pipes a rename-aware `git diff` into `yarn checkRedirects --stdin`. -`scripts/checkRedirects/` also rejects duplicate `from` values and verifies that any `#anchor` in a `to` target actually exists as a heading in the destination file. URL derivation (`util/contentRoots.ts`): strip the extension, lowercase, drop a trailing `/index`, and map `docs/` → ``, `operator/` → `/operator`, `staker/` → `/staker`. +`scripts/checkRedirects/` also rejects duplicate `from` values and verifies that any `#anchor` in a `to` target actually exists as a heading in the destination file. URL derivation: a page with an explicit `slug` in its frontmatter (all synced SDK pages) uses that slug verbatim — `util/slugs.ts` reads the old frontmatter back out of git for deleted/renamed files, and a move that keeps the slug needs no redirect. Otherwise (`util/contentRoots.ts`) strip the extension, drop a trailing `/index`, and map `docs/` → ``, `operator/` → `/operator`, `staker/` → `/staker`; casing is preserved. ## MDX conventions diff --git a/redirects.ts b/redirects.ts index bba0e8c8..148a317c 100644 --- a/redirects.ts +++ b/redirects.ts @@ -59,4 +59,20 @@ export default [ from: '/contracts/api/vaults/ethereum/custom/EthMetaVaultFactory', to: '/contracts/api/vaults/ethereum/EthMetaVaultFactory', }, + { + from: '/sdk/api/vault/requests/getmaxwithdraw', + to: '/sdk/api/vault/requests/getmaxwithdrawamount', + }, + { + from: '/sdk/api/osToken/requests/getmaxmint', + to: '/sdk/api/osToken/requests/getmaxmintamount', + }, + { + from: '/sdk/api/osToken/requests/getposition', + to: '/sdk/api/osToken/requests/getbalance', + }, + { + from: '/sdk/api/osToken/helpers/getburnamount', + to: '/sdk/api/osToken/helpers/getburnamountforunstake', + }, ] diff --git a/scripts/checkRedirects/index.ts b/scripts/checkRedirects/index.ts index ae9bf3c9..68c6a041 100644 --- a/scripts/checkRedirects/index.ts +++ b/scripts/checkRedirects/index.ts @@ -1,6 +1,6 @@ import { - green, pathToUrl, validateAnchors, printMissingRedirects, readDiff, parseDiff, isContentFile, - readRedirectsSource, parseRedirectFroms, checkDuplicates, log, + green, previousUrl, currentUrl, validateAnchors, printMissingRedirects, readDiff, parseDiff, + isContentFile, readRedirectsSource, parseRedirectFroms, checkDuplicates, log, } from './util' import type { Missing } from './util' @@ -24,15 +24,16 @@ const main = (): void => { const missing: Missing[] = [] for (const { oldPath, newPath } of entries) { - const expectedUrl = pathToUrl(oldPath) + const expectedUrl = previousUrl(oldPath) + const toUrl = newPath ? currentUrl(newPath) : 'TODO_DESTINATION' + + // A file can move on disk while keeping its `slug`, which leaves the route untouched. + if (newPath && toUrl === expectedUrl) { + continue + } if (!redirectFroms.includes(expectedUrl)) { - missing.push({ - oldPath, - expectedUrl, - newPath, - toUrl: newPath ? pathToUrl(newPath) : 'TODO_DESTINATION', - }) + missing.push({ oldPath, expectedUrl, newPath, toUrl }) } } diff --git a/scripts/checkRedirects/util/anchors.ts b/scripts/checkRedirects/util/anchors.ts index a7c871d3..c9fb12f3 100644 --- a/scripts/checkRedirects/util/anchors.ts +++ b/scripts/checkRedirects/util/anchors.ts @@ -2,6 +2,7 @@ import { readFileSync } from 'fs' import { resolveToFilePath } from './contentRoots' import { printBrokenAnchors } from './output' +import { findFileBySlug } from './slugs' import type { BrokenAnchor } from './output' @@ -59,7 +60,7 @@ export const validateAnchors = (redirectsSource: string): void => { for (const [ target, fromList ] of Object.entries(fromByTarget)) { const [ pathPart, anchor ] = target.split('#') - const filePath = resolveToFilePath(pathPart) + const filePath = resolveToFilePath(pathPart) || findFileBySlug(pathPart) if (!filePath) { broken.push({ target, anchor, filePath: null, fromList }) diff --git a/scripts/checkRedirects/util/index.ts b/scripts/checkRedirects/util/index.ts index dc1cf58c..0f990906 100644 --- a/scripts/checkRedirects/util/index.ts +++ b/scripts/checkRedirects/util/index.ts @@ -1,5 +1,6 @@ export * from './colors' export * from './contentRoots' +export * from './slugs' export * from './diff' export * from './log' export * from './parser' diff --git a/scripts/checkRedirects/util/slugs.ts b/scripts/checkRedirects/util/slugs.ts new file mode 100644 index 00000000..0525cb76 --- /dev/null +++ b/scripts/checkRedirects/util/slugs.ts @@ -0,0 +1,103 @@ +import { execFileSync } from 'child_process' +import { existsSync, readdirSync, readFileSync } from 'fs' + +import { contentRoots, pathToUrl } from './contentRoots' + + +let slugIndex: Record | null = null + +const runGit = (args: string[]): string | null => { + try { + return execFileSync('git', args, { encoding: 'utf8', stdio: [ 'ignore', 'pipe', 'ignore' ] }) + } + catch { + return null + } +} + +const normalizeUrl = (value: string): string => { + const url = value.startsWith('/') ? value : `/${value}` + + return url.replace(/\/$/, '') +} + +export const extractSlug = (source: string): string | null => { + const frontmatter = source.match(/^---\r?\n([\s\S]*?)\r?\n---/) + + if (!frontmatter) { + return null + } + + const match = frontmatter[1].match(/^slug:\s*['"]?([^'"\s]+)['"]?\s*$/m) + + return match ? match[1] : null +} + +const readPreviousSource = (filePath: string): string | null => { + const head = runGit([ 'show', `HEAD:${filePath}` ]) + + if (head !== null) { + return head + } + + const sha = runGit([ 'rev-list', '-n', '1', 'HEAD', '--', filePath ])?.trim() + + return sha ? runGit([ 'show', `${sha}^:${filePath}` ]) : null +} + +const readCurrentSource = (filePath: string): string | null => { + const staged = runGit([ 'show', `:${filePath}` ]) + + if (staged !== null) { + return staged + } + + return existsSync(filePath) ? readFileSync(filePath, 'utf8') : null +} + +const toUrl = (filePath: string, source: string | null): string => { + const slug = source ? extractSlug(source) : null + + return slug ? normalizeUrl(slug) : pathToUrl(filePath) +} + +export const previousUrl = (filePath: string): string => ( + toUrl(filePath, readPreviousSource(filePath)) +) + +export const currentUrl = (filePath: string): string => ( + toUrl(filePath, readCurrentSource(filePath)) +) + +const buildSlugIndex = (): Record => { + const index: Record = {} + + for (const { dir } of contentRoots) { + if (!existsSync(dir)) { + continue + } + + const files = readdirSync(dir, { recursive: true, encoding: 'utf8' }) + + for (const file of files) { + if (!/\.(mdx|md)$/.test(file)) { + continue + } + + const filePath = `${dir}/${file}` + const slug = extractSlug(readFileSync(filePath, 'utf8')) + + if (slug) { + index[normalizeUrl(slug)] = filePath + } + } + } + + return index +} + +export const findFileBySlug = (url: string): string | null => { + slugIndex = slugIndex || buildSlugIndex() + + return slugIndex[normalizeUrl(url)] || null +}