diff --git a/.gitignore b/.gitignore index a99afee7e91..a5b5fd8c9a8 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* +server.log # environment variables .env diff --git a/src/content/ccip/api-reference/v1.5.0/burn-from-mint-token-pool.mdx b/src/content/ccip/api-reference/v1.5.0/burn-from-mint-token-pool.mdx index 4f89b016cd5..32a81f403c3 100644 --- a/src/content/ccip/api-reference/v1.5.0/burn-from-mint-token-pool.mdx +++ b/src/content/ccip/api-reference/v1.5.0/burn-from-mint-token-pool.mdx @@ -14,7 +14,7 @@ import CcipCommon from "@features/ccip/CcipCommon.astro" ## BurnFromMintTokenPool -[`BurnFromMintTokenPool`](https://github.com/smartcontractkit/ccip/tree/release/contracts-ccip-1.5.0/contracts/src/v0.8/ccip/pools/BurnFromMintTokenPool.sol) is a concrete implementation that inherits from [`BurnMintTokenPoolAbstract`](/ccip/api-reference/burn-mint-token-pool-abstract). It is designed to mint and burn a 3rd-party token, using the `burnFrom(address, amount)` function for burning tokens, where the contract burns tokens from its own balance. When allowlist is enabled, it ensures only token-developer specified addresses can transfer tokens. +[`BurnFromMintTokenPool`](https://github.com/smartcontractkit/ccip/tree/release/contracts-ccip-1.5.0/contracts/src/v0.8/ccip/pools/BurnFromMintTokenPool.sol) is a concrete implementation that inherits from [`BurnMintTokenPoolAbstract`](/ccip/api-reference/v1.5.0/burn-mint-token-pool-abstract). It is designed to mint and burn a 3rd-party token, using the `burnFrom(address, amount)` function for burning tokens, where the contract burns tokens from its own balance. When allowlist is enabled, it ensures only token-developer specified addresses can transfer tokens. ## Variables diff --git a/src/content/ccip/api-reference/v1.5.0/burn-mint-token-pool-abstract.mdx b/src/content/ccip/api-reference/v1.5.0/burn-mint-token-pool-abstract.mdx index 55cac8cadb0..4ae73cf1805 100644 --- a/src/content/ccip/api-reference/v1.5.0/burn-mint-token-pool-abstract.mdx +++ b/src/content/ccip/api-reference/v1.5.0/burn-mint-token-pool-abstract.mdx @@ -14,7 +14,7 @@ import CcipCommon from "@features/ccip/CcipCommon.astro" ## BurnMintTokenPoolAbstract -[`BurnMintTokenPoolAbstract`](https://github.com/smartcontractkit/ccip/tree/release/contracts-ccip-1.5.0/contracts/src/v0.8/ccip/pools/BurnMintTokenPoolAbstract.sol) is an abstract contract that extends the [`TokenPool`](/ccip/api-reference/token-pool) contract. It defines the common logic for burning tokens in the pool. This contract contains validation mechanisms and logic for burning tokens in the context of a cross-chain token transfer. When allowlist is enabled, it ensures only token-developer specified addresses can transfer tokens. +[`BurnMintTokenPoolAbstract`](https://github.com/smartcontractkit/ccip/tree/release/contracts-ccip-1.5.0/contracts/src/v0.8/ccip/pools/BurnMintTokenPoolAbstract.sol) is an abstract contract that extends the [`TokenPool`](/ccip/api-reference/v1.5.0/token-pool) contract. It defines the common logic for burning tokens in the pool. This contract contains validation mechanisms and logic for burning tokens in the context of a cross-chain token transfer. When allowlist is enabled, it ensures only token-developer specified addresses can transfer tokens. ## Functions diff --git a/src/content/ccip/api-reference/v1.5.0/burn-mint-token-pool.mdx b/src/content/ccip/api-reference/v1.5.0/burn-mint-token-pool.mdx index 59efa5dd9cf..74e1799bccd 100644 --- a/src/content/ccip/api-reference/v1.5.0/burn-mint-token-pool.mdx +++ b/src/content/ccip/api-reference/v1.5.0/burn-mint-token-pool.mdx @@ -14,7 +14,7 @@ import CcipCommon from "@features/ccip/CcipCommon.astro" ## BurnMintTokenPool -[`BurnMintTokenPool`](https://github.com/smartcontractkit/ccip/tree/release/contracts-ccip-1.5.0/contracts/src/v0.8/ccip/pools/BurnMintTokenPool.sol) is a concrete implementation that inherits from [`BurnMintTokenPoolAbstract`](/ccip/api-reference/burn-mint-token-pool-abstract). It is designed to mint and burn a 3rd-party token, using the `burn(amount)` function for burning tokens. Pool whitelisting mode is set during the contract's deployment and cannot be modified later. +[`BurnMintTokenPool`](https://github.com/smartcontractkit/ccip/tree/release/contracts-ccip-1.5.0/contracts/src/v0.8/ccip/pools/BurnMintTokenPool.sol) is a concrete implementation that inherits from [`BurnMintTokenPoolAbstract`](/ccip/api-reference/v1.5.0/burn-mint-token-pool-abstract). It is designed to mint and burn a 3rd-party token, using the `burn(amount)` function for burning tokens. Pool whitelisting mode is set during the contract's deployment and cannot be modified later. ## Variables diff --git a/src/content/ccip/api-reference/v1.5.0/ccip-receiver.mdx b/src/content/ccip/api-reference/v1.5.0/ccip-receiver.mdx index 92de053a9b3..a88b2ec8285 100644 --- a/src/content/ccip/api-reference/v1.5.0/ccip-receiver.mdx +++ b/src/content/ccip/api-reference/v1.5.0/ccip-receiver.mdx @@ -61,9 +61,9 @@ will move to a FAILED state and become available for manual execution. #### Parameters -| Name | Type | Description | -| ------- | ------------------------------------------------------------------------- | ------------ | -| message | struct [Client.Any2EVMMessage](/ccip/api-reference/client#any2evmmessage) | CCIP Message | +| Name | Type | Description | +| ------- | -------------------------------------------------------------------------------- | ------------ | +| message | struct [Client.Any2EVMMessage](/ccip/api-reference/v1.5.0/client#any2evmmessage) | CCIP Message | ### \_ccipReceive @@ -75,9 +75,9 @@ Override this function in your implementation. #### Parameters -| Name | Type | Description | -| ------- | ------------------------------------------------------------------------- | -------------- | -| message | struct [Client.Any2EVMMessage](/ccip/api-reference/client#any2evmmessage) | Any2EVMMessage | +| Name | Type | Description | +| ------- | -------------------------------------------------------------------------------- | -------------- | +| message | struct [Client.Any2EVMMessage](/ccip/api-reference/v1.5.0/client#any2evmmessage) | Any2EVMMessage | ### getRouter diff --git a/src/content/ccip/api-reference/v1.5.0/i-router-client.mdx b/src/content/ccip/api-reference/v1.5.0/i-router-client.mdx index ef9cb0fcbd1..c7ec68c4659 100644 --- a/src/content/ccip/api-reference/v1.5.0/i-router-client.mdx +++ b/src/content/ccip/api-reference/v1.5.0/i-router-client.mdx @@ -102,10 +102,10 @@ _returns 0 fees on invalid message._ #### Parameters -| Name | Type | Description | -| ------------------------ | ------------------------------------------------------------------------- | ---------------------------------------------------------- | -| destinationChainSelector | uint64 | The destination chainSelector | -| message | struct [Client.EVM2AnyMessage](/ccip/api-reference/client#evm2anymessage) | The cross-chain CCIP message, including data and/or tokens | +| Name | Type | Description | +| ------------------------ | -------------------------------------------------------------------------------- | ---------------------------------------------------------- | +| destinationChainSelector | uint64 | The destination chainSelector | +| message | struct [Client.EVM2AnyMessage](/ccip/api-reference/v1.5.0/client#evm2anymessage) | The cross-chain CCIP message, including data and/or tokens | #### Return Values @@ -127,10 +127,10 @@ Request a message to be sent to the destination chain. #### Parameters -| Name | Type | Description | -| ------------------------ | ------------------------------------------------------------------------- | ---------------------------------------------------------- | -| destinationChainSelector | uint64 | The destination chain ID | -| message | struct [Client.EVM2AnyMessage](/ccip/api-reference/client#evm2anymessage) | The cross-chain CCIP message, including data and/or tokens | +| Name | Type | Description | +| ------------------------ | -------------------------------------------------------------------------------- | ---------------------------------------------------------- | +| destinationChainSelector | uint64 | The destination chain ID | +| message | struct [Client.EVM2AnyMessage](/ccip/api-reference/v1.5.0/client#evm2anymessage) | The cross-chain CCIP message, including data and/or tokens | #### Return Values diff --git a/src/content/ccip/api-reference/v1.5.0/lock-release-token-pool.mdx b/src/content/ccip/api-reference/v1.5.0/lock-release-token-pool.mdx index f8e35885c7c..cd77c527fbf 100644 --- a/src/content/ccip/api-reference/v1.5.0/lock-release-token-pool.mdx +++ b/src/content/ccip/api-reference/v1.5.0/lock-release-token-pool.mdx @@ -16,7 +16,7 @@ import CcipCommon from "@features/ccip/CcipCommon.astro" The [`LockReleaseTokenPool`](https://github.com/smartcontractkit/ccip/tree/release/contracts-ccip-1.5.0/contracts/src/v0.8/ccip/pools/LockReleaseTokenPool.sol) contract is used for handling tokens on their native chain using a lock and release mechanism. It allows tokens to be locked in the pool, facilitating their transfer across blockchains, and then released to the recipient on the destination chain. When allowlist is enabled, it ensures only token-developer specified addresses can transfer tokens. -This contract inherits from the [`TokenPool`](/ccip/api-reference/token-pool) contract and implements additional functions for liquidity management. +This contract inherits from the [`TokenPool`](/ccip/api-reference/v1.5.0/token-pool) contract and implements additional functions for liquidity management. ### typeAndVersion diff --git a/src/content/ccip/api-reference/v1.5.0/registry-module-owner-custom.mdx b/src/content/ccip/api-reference/v1.5.0/registry-module-owner-custom.mdx index ef71761f7ac..44481e9bbc7 100644 --- a/src/content/ccip/api-reference/v1.5.0/registry-module-owner-custom.mdx +++ b/src/content/ccip/api-reference/v1.5.0/registry-module-owner-custom.mdx @@ -12,7 +12,7 @@ import CcipCommon from "@features/ccip/CcipCommon.astro" -The [`RegistryModuleOwnerCustom`](https://github.com/smartcontractkit/ccip/tree/release/contracts-ccip-1.5.0/contracts/src/v0.8/ccip/tokenAdminRegistry/RegistryModuleOwnerCustom.sol) contract is responsible for registering the administrator of a token in the [`TokenAdminRegistry`](/ccip/api-reference/token-admin-registry). This contract allows for the registration of token administrators through either the `getCCIPAdmin()` method (for tokens with a CCIP admin) or the `owner()` method (for standard tokens with an owner). The contract enforces that only the rightful administrator of the token can register themselves, ensuring secure and accurate registration within the `TokenAdminRegistry`. The contract also emits an event, `AdministratorRegistered`, whenever a token administrator is successfully registered. +The [`RegistryModuleOwnerCustom`](https://github.com/smartcontractkit/ccip/tree/release/contracts-ccip-1.5.0/contracts/src/v0.8/ccip/tokenAdminRegistry/RegistryModuleOwnerCustom.sol) contract is responsible for registering the administrator of a token in the [`TokenAdminRegistry`](/ccip/api-reference/v1.5.0/token-admin-registry). This contract allows for the registration of token administrators through either the `getCCIPAdmin()` method (for tokens with a CCIP admin) or the `owner()` method (for standard tokens with an owner). The contract enforces that only the rightful administrator of the token can register themselves, ensuring secure and accurate registration within the `TokenAdminRegistry`. The contract also emits an event, `AdministratorRegistered`, whenever a token administrator is successfully registered. ## Errors diff --git a/src/scripts/link-check/linkcheck.ts b/src/scripts/link-check/linkcheck.ts index 395ce4657ee..a2fc0dcaf04 100644 --- a/src/scripts/link-check/linkcheck.ts +++ b/src/scripts/link-check/linkcheck.ts @@ -1,44 +1,80 @@ -import { ChildProcessByStdio, execSync, spawn } from "child_process" +import { execSync, spawn, SpawnOptions } from "child_process" import { existsSync, readFileSync, writeFileSync, createWriteStream, statSync, mkdirSync } from "fs" -import { Readable } from "node:stream" -import { exit, cwd, stdout } from "process" +import { exit, cwd } from "process" -const tempDir = `${cwd()}/temp` -if (!existsSync(tempDir)) { - mkdirSync(tempDir) +declare global { + function fetch(input: RequestInfo, init?: RequestInit): Promise } -const logFile = `${tempDir}/link-checker.log` -const logStream = createWriteStream(logFile, { flags: "w" }) -const displayLogFile = () => { - if (existsSync(logFile) && statSync(logFile).size > 0) { - const content = readFileSync(logFile, "utf8") +// ================================ +// CONFIGURABLE CONSTANTS +// ================================ +const BASE_URL = "http://localhost:4321" +const TEMP_DIR = `${cwd()}/temp` +const LOG_FILE = `${TEMP_DIR}/link-checker.log` + +const LINK_CHUNK_SIZE = 300 + +// ================================ +// HELPER FUNCTIONS +// ================================ + +/** + * Create temp directory and log file + */ +function ensureTempSetup() { + if (!existsSync(TEMP_DIR)) { + mkdirSync(TEMP_DIR) + } +} + +/** + * Show log file content at the end, if any + */ +function displayLogFile() { + if (existsSync(LOG_FILE) && statSync(LOG_FILE).size > 0) { + const content = readFileSync(LOG_FILE, "utf8") console.log("\n------ Log File Content ------") console.log(content) console.log("------------------------------") } } -// eslint-disable-next-line prefer-const -let server: ChildProcessByStdio -// eslint-disable-next-line prefer-const -let siteMapChecker: ChildProcessByStdio - -const cleanup = () => { - displayLogFile() - server?.kill("SIGTERM") - siteMapChecker?.kill("SIGTERM") +/** + * Wait for the dev server to be "ready" by checking for a 2xx status on BASE_URL. + */ +async function waitForServerReadiness(url: string, attempts = 20) { + for (let i = 1; i <= attempts; i++) { + try { + const response = await fetch(url) + if (response.ok) { + console.log(`Server is ready at ${url}`) + return + } + } catch { + // Connection error or not ready yet + } + console.log(`Waiting for server to be ready... Attempt ${i}/${attempts}`) + await new Promise((resolve) => setTimeout(resolve, 1000)) + } + throw new Error(`Server not ready after ${attempts} attempts at ${url}`) } -process.on("uncaughtException", (err) => { - console.error("Uncaught Exception: ", err) - cleanup() - exit(1) -}) - -process.on("exit", cleanup) +/** + * Split an array into chunks of size `chunkSize`. + */ +function chunkArray(arr: T[], chunkSize: number): T[][] { + const result: T[][] = [] + for (let i = 0; i < arr.length; i += chunkSize) { + result.push(arr.slice(i, i + chunkSize)) + } + return result +} -const getModeFromArgs = (): "internal" | "external" => { +/** + * Parse command line mode: "internal" or "external" + */ +function getModeFromArgs(): "internal" | "external" { const modeArg = process.argv.find((arg) => arg.startsWith("--mode=")) if (!modeArg) return "internal" @@ -49,21 +85,17 @@ const getModeFromArgs = (): "internal" | "external" => { return modeValue as "internal" | "external" } -const parseBaseUrl = (data: string): string => { - const regex = /(Local.*?)(?http:\/\/.*?)\// - const match = data.toString().match(regex) - return match?.groups?.baseUrl || "" -} - -const processSiteMap = (baseUrl: string): string => { +/** + * Process the sitemap and split it into chunked files. + */ +async function processSiteMap(mode: "internal" | "external"): Promise { const siteMap = `${cwd()}/.vercel/output/static/sitemap-0.xml` - const linksFile = `${tempDir}/sitemap-urls.txt` + const data = readFileSync(siteMap, "utf8") - const data = readFileSync(siteMap, { encoding: "utf8" }) const regex = /(?.*?)<\/loc>/gm - const links: string[] = [] + const rawLinks: string[] = [] - // Use the appropriate ignore file based on mode + // Use the appropriate ignore file const ignoredPatternsFile = mode === "external" ? `${cwd()}/src/scripts/link-check/ignoredfiles-external.txt` @@ -74,52 +106,40 @@ const processSiteMap = (baseUrl: string): string => { .filter((line) => line && !line.startsWith("#")) .map((pattern) => new RegExp(pattern)) + // Collect all relevant links for (const loc of data.matchAll(regex)) { const link = loc.groups?.link if (link) { - const normalizedLink = link.replace("https://docs.chain.link", baseUrl) - // Only add the link if it doesn't match any of the ignored patterns + // Replace production domain with the local base + const normalizedLink = link.replace("https://docs.chain.link", BASE_URL) const shouldInclude = !ignoredPatterns.some((pattern) => pattern.test(normalizedLink)) if (shouldInclude) { - links.push(normalizedLink) + rawLinks.push(normalizedLink) } } } - writeFileSync(linksFile, links.join("\n")) - return linksFile -} - -let mode: "internal" | "external" -try { - mode = getModeFromArgs() -} catch (error) { - console.error(error.message) - exit(1) -} - -console.log("Generating a production build...") -try { - execSync("npm run build") - console.log("Build finished.") -} catch (error) { - console.error("Failed to generate the build.", error) - exit(1) -} - -server = spawn("npm", ["run", "dev"], { - stdio: ["ignore", "pipe", "pipe"], -}) + // Split into chunks + const chunkedLinks = chunkArray(rawLinks, LINK_CHUNK_SIZE) -server.stdout.on("data", (data) => { - const baseUrl = parseBaseUrl(data) - stdout.write(data.toString()) + // Write each chunk to its own file + const chunkFiles: string[] = [] - if (baseUrl) { - const linksFile = processSiteMap(baseUrl) + chunkedLinks.forEach((linksArr, index) => { + const linksFile = `${TEMP_DIR}/sitemap-urls-part${index + 1}.txt` + writeFileSync(linksFile, linksArr.join("\n")) + chunkFiles.push(linksFile) + }) - console.log("Checking sitemap links:", linksFile) + return chunkFiles +} +/** + * Run the link check on a single chunk file. + * Return 0 on success, or the child exit code on failure. + */ +function checkChunkFile(linksFile: string, mode: "internal" | "external"): Promise { + return new Promise((resolve) => { const args = mode === "external" ? [ @@ -130,7 +150,7 @@ server.stdout.on("data", (data) => { "--skip-file", `${cwd()}/src/scripts/link-check/ignoredfiles-external.txt`, "--hosts", - baseUrl, + BASE_URL, "-e", ] : [ @@ -139,36 +159,114 @@ server.stdout.on("data", (data) => { "--skip-file", `${cwd()}/src/scripts/link-check/ignoredfiles-internal.txt`, "--hosts", - baseUrl, + BASE_URL, ] - siteMapChecker = spawn("npm", ["run", "linkcheckWrapper", "--", ...args], { - stdio: ["ignore", "pipe", "pipe"], - }) + // We'll just spawn the linkcheck wrapper + const checker = spawn("npm", ["run", "linkcheckWrapper", "--", ...args], { + stdio: ["pipe", "pipe", "pipe"], + } as SpawnOptions) - siteMapChecker.stdout.on("data", (checkerData) => { - logStream.write(checkerData) - }) + // Write stdout to log file + const logStream = createWriteStream(LOG_FILE, { flags: "a" }) + if (checker.stdout) { + checker.stdout.on("data", (data) => { + logStream.write(data) + }) + } + + // Write stderr to console and log + if (checker.stderr) { + checker.stderr.on("data", (data) => { + process.stderr.write(data.toString()) + }) + } + + // When process exits + checker.on("exit", (code) => { + logStream.end() - siteMapChecker.stderr.on("data", (errorData) => { - console.error("Linkcheck error:", errorData.toString()) + // if exit code is 1 (warnings only), treat as success (0). + if (code === 1) { + console.log("Link checker reported warnings only. Treating as success.") + code = 0 + } + + resolve(code ?? 1) // if code is null, treat as error }) + }) +} + +// ================================ +// MAIN LOGIC +// ================================ - siteMapChecker.on("exit", (code) => { +async function main() { + ensureTempSetup() + + // 1) Determine mode from command line + const mode = getModeFromArgs() + + // 2) Build site once (production build) + console.log("Generating a production build...") + try { + execSync("npm run build", { stdio: "inherit" }) + console.log("Build finished.") + } catch (error) { + console.error("Failed to generate the build.", error) + exit(1) + } + + // 3) Serve the static build in the background + console.log("Starting production server in background...") + execSync("nohup npx serve .vercel/output/static --listen 4321 > server.log 2>&1 &", { stdio: "inherit" }) + + // 4) Wait for readiness + try { + await waitForServerReadiness(BASE_URL, 30) // Wait up to 30 attempts + } catch (err) { + console.error("Server did not become ready in time.", err) + exit(1) + } + + // 5) Process site map: get chunk files + const chunkFiles = await processSiteMap(mode) + console.log(`Created ${chunkFiles.length} chunk files.`) + + // 6) Run link check on each chunk in parallel, collecting any failures + const checkPromises = chunkFiles.map((chunkFile, index) => { + const chunkNumber = index + 1 + console.log(`\n>>> Checking chunk ${chunkNumber} of ${chunkFiles.length}: ${chunkFile}\n`) + + return checkChunkFile(chunkFile, mode).then((code) => { if (code !== 0) { - console.error(`Sitemap link checker exited with code ${code}`) - exit(2) + console.error(`Link checker failed on chunk ${chunkNumber} (exit code: ${code})`) + // We'll store chunkNumber with the failure + return { chunkNumber, failed: true } } - exit(0) + return { chunkNumber, failed: false } }) + }) + + const results = await Promise.all(checkPromises) + const failedChunks = results.filter((r) => r.failed).map((r) => r.chunkNumber) + + // 7) If any chunks failed, exit with error after we've checked them all + if (failedChunks.length > 0) { + console.error(`Some chunks failed: ${failedChunks.join(", ")}`) + displayLogFile() + exit(2) } -}) -server.stderr.on("data", (errorData) => { - console.error("Server error:", errorData.toString()) -}) + // 8) Otherwise, success + console.log(`All chunks succeeded!`) + displayLogFile() + exit(0) +} -server.on("error", (error) => { - console.error("Failed to start the server.", error) +// Run main, and handle uncaught rejections +main().catch((err) => { + console.error("Uncaught error in main():", err) + displayLogFile() exit(1) })