diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml new file mode 100644 index 00000000..1053fe7d --- /dev/null +++ b/.github/workflows/check-links.yml @@ -0,0 +1,26 @@ +name: Check Link Validity in Documentation + +on: + pull_request: + branches: + - master + +jobs: + check-links: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '16' # or the version of Node.js you're using + + - name: Install dependencies + run: | + npm install + + - name: Run link check + run: | + npm run check-links diff --git a/.github/workflows/validate-nav-build.yml b/.github/workflows/validate-nav-build.yml new file mode 100644 index 00000000..b76378d6 --- /dev/null +++ b/.github/workflows/validate-nav-build.yml @@ -0,0 +1,40 @@ +name: Validate nav.ts Matches nav.js + +on: + pull_request: + branches: + - master + +jobs: + validate-build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '16' + + - name: Install dependencies + run: | + npm install + + - name: Backup existing nav.js + run: | + mv docs/nav.js docs/nav.js.original + + - name: Build nav.ts + run: | + npm run build + + - name: Compare generated nav.js with original nav.js + run: | + diff -q docs/nav.js docs/nav.js.original || (echo "Generated nav.js differs from committed version. Run 'npm run build' and commit the updated file." && exit 1) + + - name: Restore original nav.js + if: success() || failure() + run: | + mv docs/nav.js.original docs/nav.js diff --git a/README.md b/README.md index c31b2c3f..2165ae62 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,10 @@ git push -u origin a-branch-name-that-describes-my-change > NOTE! If you make a change to `nav.ts` you will have to run `npm run build` to generate a new `docs/nav.js` file. +### Checking Links + +We have a CI job which validates internal links. You can run it locally with `npm run check-links`. This will print any internal links (i.e. links to other docs pages) whose targets do not exist, including fragment links (i.e. `#`-ey links to anchors). + ## License This documentation repository is licensed under Apache 2.0. See LICENSE.txt for more details. diff --git a/docs/bsatn.md b/docs/bsatn.md index 0da55ce7..e8e6d945 100644 --- a/docs/bsatn.md +++ b/docs/bsatn.md @@ -24,12 +24,12 @@ To do this, we use inductive definitions, and define the following notation: ### At a glance -| Type | Description | -| ---------------- | ---------------------------------------------------------------- | -| `AlgebraicValue` | A value whose type may be any [`AlgebraicType`](#algebraictype). | -| `SumValue` | A value whose type is a [`SumType`](#sumtype). | -| `ProductValue` | A value whose type is a [`ProductType`](#producttype). | -| `BuiltinValue` | A value whose type is a [`BuiltinType`](#builtintype). | +| Type | Description | +|-------------------------------------|-----------------------------------------------------------------------| +| [`AlgebraicValue`](#algebraicvalue) | A value of any type. | +| [`SumValue`](#sumvalue) | A value of a sum type, i.e. an enum or tagged union. | +| [`ProductValue`](#productvalue) | A value of a product type, i.e. a struct or tuple. | +| [`BuiltinValue`](#builtinvalue) | A value of a builtin type, including numbers, booleans and sequences. | ### `AlgebraicValue` @@ -41,17 +41,17 @@ bsatn(AlgebraicValue) = bsatn(SumValue) | bsatn(ProductValue) | bsatn(BuiltinVal ### `SumValue` -An instance of a [`SumType`](#sumtype). +An instance of a sum type, i.e. an enum or tagged union. `SumValue`s are binary-encoded as `bsatn(tag) ++ bsatn(variant_data)` -where `tag: u8` is an index into the [`SumType.variants`](#sumtype) -array of the value's [`SumType`](#sumtype), +where `tag: u8` is an index into the `SumType.variants` +array of the value's `SumType`, and where `variant_data` is the data of the variant. For variants holding no data, i.e., of some zero sized type, `bsatn(variant_data) = []`. ### `ProductValue` -An instance of a [`ProductType`](#producttype). +An instance of a product type, i.e. a struct or tuple. `ProductValue`s are binary encoded as: ```fsharp @@ -62,7 +62,8 @@ Field names are not encoded. ### `BuiltinValue` -An instance of a [`BuiltinType`](#builtintype). +An instance of a buil-in type. +Built-in types include booleans, integers, floats, strings and arrays. The BSATN encoding of `BuiltinValue`s defers to the encoding of each variant: ```fsharp @@ -73,7 +74,6 @@ bsatn(BuiltinValue) | bsatn(F32) | bsatn(F64) | bsatn(String) | bsatn(Array) - | bsatn(Map) bsatn(Bool(b)) = bsatn(b as u8) bsatn(U8(x)) = [x] @@ -91,10 +91,6 @@ bsatn(F64(x: f64)) = bsatn(f64_to_raw_bits(x)) // lossless conversion bsatn(String(s)) = bsatn(len(s) as u32) ++ bsatn(bytes(s)) bsatn(Array(a)) = bsatn(len(a) as u32) ++ bsatn(normalize(a)_0) ++ .. ++ bsatn(normalize(a)_n) -bsatn(Map(map)) = bsatn(len(m) as u32) - ++ bsatn(key(map_0)) ++ bsatn(value(map_0)) - .. - ++ bsatn(key(map_n)) ++ bsatn(value(map_n)) ``` Where @@ -102,14 +98,12 @@ Where - `f32_to_raw_bits(x)` is the raw transmute of `x: f32` to `u32` - `f64_to_raw_bits(x)` is the raw transmute of `x: f64` to `u64` - `normalize(a)` for `a: ArrayValue` converts `a` to a list of `AlgebraicValue`s -- `key(map_i)` extracts the key of the `i`th entry of `map` -- `value(map_i)` extracts the value of the `i`th entry of `map` ## Types All SATS types are BSATN-encoded by converting them to an `AlgebraicValue`, then BSATN-encoding that meta-value. -See [the SATN JSON Format](/docs/satn-reference-json-format) +See [the SATN JSON Format](/docs/satn) for more details of the conversion to meta values. Note that these meta values are converted to BSATN and _not JSON_. diff --git a/docs/http/database.md b/docs/http/database.md index 9b6e0488..b23701e8 100644 --- a/docs/http/database.md +++ b/docs/http/database.md @@ -11,8 +11,6 @@ The HTTP endpoints in `/database` allow clients to interact with Spacetime datab | [`/database/set_name GET`](#databaseset_name-get) | Set a database's name, given its address. | | [`/database/ping GET`](#databaseping-get) | No-op. Used to determine whether a client can connect. | | [`/database/register_tld GET`](#databaseregister_tld-get) | Register a top-level domain. | -| [`/database/request_recovery_code GET`](#databaserequest_recovery_code-get) | Request a recovery code to the email associated with an identity. | -| [`/database/confirm_recovery_code GET`](#databaseconfirm_recovery_code-get) | Recover a login token from a recovery code. | | [`/database/publish POST`](#databasepublish-post) | Publish a database given its module code. | | [`/database/delete/:address POST`](#databasedeleteaddress-post) | Delete a database. | | [`/database/subscribe/:name_or_address GET`](#databasesubscribename_or_address-get) | Begin a [WebSocket connection](/docs/ws). | @@ -175,43 +173,6 @@ If the domain is already registered to another identity, returns JSON in the for } } ``` -## `/database/request_recovery_code GET` - -Request a recovery code or link via email, in order to recover the token associated with an identity. - -Accessible through the CLI as `spacetime identity recover `. - -#### Query Parameters - -| Name | Value | -| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `identity` | The identity whose token should be recovered. | -| `email` | The email to send the recovery code or link to. This email must be associated with the identity, either during creation via [`/identity`](/docs/http/identity#identity-post) or afterwards via [`/identity/:identity/set-email`](/docs/http/identity#identityidentityset_email-post). | -| `link` | A boolean; whether to send a clickable link rather than a recovery code. | - -## `/database/confirm_recovery_code GET` - -Confirm a recovery code received via email following a [`/database/request_recovery_code GET`](#-database-request_recovery_code-get) request, and retrieve the identity's token. - -Accessible through the CLI as `spacetime identity recover `. - -#### Query Parameters - -| Name | Value | -| ---------- | --------------------------------------------- | -| `identity` | The identity whose token should be recovered. | -| `email` | The email which received the recovery code. | -| `code` | The recovery code received via email. | - -On success, returns JSON in the form: - -```typescript -{ - "identity": string, - "token": string -} -``` - ## `/database/publish POST` Publish a database. diff --git a/docs/modules/c-sharp/quickstart.md b/docs/modules/c-sharp/quickstart.md index 5d8c873d..571351c1 100644 --- a/docs/modules/c-sharp/quickstart.md +++ b/docs/modules/c-sharp/quickstart.md @@ -312,6 +312,6 @@ spacetime sql "SELECT * FROM Message" ## What's next? -You've just set up your first database in SpacetimeDB! The next step would be to create a client module that interacts with this module. You can use any of SpacetimDB's supported client languages to do this. Take a look at the quick start guide for your client language of choice: [Rust](/docs/languages/rust/rust-sdk-quickstart-guide), [C#](/docs/languages/csharp/csharp-sdk-quickstart-guide), or [TypeScript](/docs/languages/typescript/typescript-sdk-quickstart-guide). +You've just set up your first database in SpacetimeDB! The next step would be to create a client module that interacts with this module. You can use any of SpacetimDB's supported client languages to do this. Take a look at the quick start guide for your client language of choice: [Rust](/docs/sdks/rust/quickstart), [C#](/docs/sdks/c-sharp/quickstart), or [TypeScript](/docs/sdks/typescript/quickstart). If you are planning to use SpacetimeDB with the Unity game engine, you can skip right to the [Unity Comprehensive Tutorial](/docs/unity/part-1) or check out our example game, [BitcraftMini](/docs/unity/part-3). diff --git a/docs/nav.js b/docs/nav.js index a43c2e29..5c3a920e 100644 --- a/docs/nav.js +++ b/docs/nav.js @@ -1,12 +1,23 @@ "use strict"; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; Object.defineProperty(exports, "__esModule", { value: true }); function page(title, slug, path, props) { - return { type: 'page', path, slug, title, ...props }; + return __assign({ type: 'page', path: path, slug: slug, title: title }, props); } function section(title) { - return { type: 'section', title }; + return { type: 'section', title: title }; } -const nav = { +var nav = { items: [ section('Intro'), page('Overview', 'index', 'index.md'), // TODO(BREAKING): For consistency & clarity, 'index' slug should be renamed 'intro'? diff --git a/docs/satn.md b/docs/satn.md index f21e9b30..6fb0ee9f 100644 --- a/docs/satn.md +++ b/docs/satn.md @@ -34,7 +34,7 @@ The tag is an index into the [`SumType.variants`](#sumtype) array of the value's ### `ProductValue` -An instance of a [`ProductType`](#producttype). `ProductValue`s are encoded as JSON arrays. Each element of the `ProductValue` array is of the type of the corresponding index in the [`ProductType.elements`](#productype) array of the value's [`ProductType`](#producttype). +An instance of a [`ProductType`](#producttype). `ProductValue`s are encoded as JSON arrays. Each element of the `ProductValue` array is of the type of the corresponding index in the [`ProductType.elements`](#producttype) array of the value's [`ProductType`](#producttype). ```json array @@ -69,7 +69,7 @@ All SATS types are JSON-encoded by converting them to an `AlgebraicValue`, then | --------------------------------------- | ------------------------------------------------------------------------------------ | | [`AlgebraicType`](#algebraictype) | Any SATS type. | | [`SumType`](#sumtype) | Sum types, i.e. tagged unions. | -| [`ProductType`](#productype) | Product types, i.e. structures. | +| [`ProductType`](#producttype) | Product types, i.e. structures. | | [`BuiltinType`](#builtintype) | Built-in and primitive types, including booleans, numbers, strings, arrays and maps. | | [`AlgebraicTypeRef`](#algebraictyperef) | An indirect reference to a type, used to implement recursive types. | diff --git a/docs/sdks/c-sharp/index.md b/docs/sdks/c-sharp/index.md index d85f5702..a044e4ea 100644 --- a/docs/sdks/c-sharp/index.md +++ b/docs/sdks/c-sharp/index.md @@ -16,10 +16,10 @@ The SpacetimeDB client C# for Rust contains all the tools you need to build nati - [Method `SpacetimeDBClient.Connect`](#method-spacetimedbclientconnect) - [Event `SpacetimeDBClient.onIdentityReceived`](#event-spacetimedbclientonidentityreceived) - [Event `SpacetimeDBClient.onConnect`](#event-spacetimedbclientonconnect) - - [Query subscriptions & one-time actions](#subscribe-to-queries) + - [Subscribe to queries](#subscribe-to-queries) - [Method `SpacetimeDBClient.Subscribe`](#method-spacetimedbclientsubscribe) - [Event `SpacetimeDBClient.onSubscriptionApplied`](#event-spacetimedbclientonsubscriptionapplied) - - [Method `SpacetimeDBClient.OneOffQuery`](#method-spacetimedbclientoneoffquery) + - [Method \[`SpacetimeDBClient.OneOffQuery`\]](#method-spacetimedbclientoneoffquery) - [View rows of subscribed tables](#view-rows-of-subscribed-tables) - [Class `{TABLE}`](#class-table) - [Static Method `{TABLE}.Iter`](#static-method-tableiter) @@ -45,7 +45,6 @@ The SpacetimeDB client C# for Rust contains all the tools you need to build nati - [Static Property `AuthToken.Token`](#static-property-authtokentoken) - [Static Method `AuthToken.SaveToken`](#static-method-authtokensavetoken) - [Class `Identity`](#class-identity) - - [Class `Identity`](#class-identity-1) - [Customizing logging](#customizing-logging) - [Interface `ISpacetimeDBLogger`](#interface-ispacetimedblogger) - [Class `ConsoleLogger`](#class-consolelogger) @@ -104,7 +103,7 @@ The Unity SpacetimeDB SDK relies on there being a `NetworkManager` somewhere in ![Unity-AddNetworkManager](/images/unity-tutorial/Unity-AddNetworkManager.JPG) -This component will handle updating and closing the [`SpacetimeDBClient.instance`](#property-spacetimedbclientinstance) for you, but will not call [`SpacetimeDBClient.Connect`](#method-spacetimedbclientconnect), you still need to handle that yourself. See the [Unity Quickstart](./UnityQuickStart) and [Unity Tutorial](./UnityTutorialPart1) for more information. +This component will handle updating and closing the [`SpacetimeDBClient.instance`](#property-spacetimedbclientinstance) for you, but will not call [`SpacetimeDBClient.Connect`](#method-spacetimedbclientconnect), you still need to handle that yourself. See the [Unity Tutorial](/docs/unity-tutorial) for more information. ### Method `SpacetimeDBClient.Connect` @@ -172,7 +171,7 @@ class SpacetimeDBClient { } ``` -Called when we receive an auth token, [`Identity`](#class-identity) and [`Address`](#class-address) from the server. The [`Identity`](#class-identity) serves as a unique public identifier for a user of the database. It can be for several purposes, such as filtering rows in a database for the rows created by a particular user. The auth token is a private access token that allows us to assume an identity. The [`Address`](#class-address) is opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#class-identity). +Called when we receive an auth token, [`Identity`](#class-identity) and `Address` from the server. The [`Identity`](#class-identity) serves as a unique public identifier for a user of the database. It can be for several purposes, such as filtering rows in a database for the rows created by a particular user. The auth token is a private access token that allows us to assume an identity. The `Address` is opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#class-identity). To store the auth token to the filesystem, use the static method [`AuthToken.SaveToken`](#static-method-authtokensavetoken). You may also want to store the returned [`Identity`](#class-identity) in a local variable. diff --git a/docs/sdks/rust/index.md b/docs/sdks/rust/index.md index 50e8aa9b..d8befe53 100644 --- a/docs/sdks/rust/index.md +++ b/docs/sdks/rust/index.md @@ -149,7 +149,7 @@ impl DbConnection { `frame_tick` will advance the connection until no work remains, then return rather than blocking or `await`-ing. Games might arrange for this message to be called every frame. `frame_tick` returns `Ok` if the connection remains active afterwards, or `Err` if the connection disconnected before or during the call. -## Trait `spacetimedb_sdk::DbContext` +## Trait `DbContext` [`DbConnection`](#type-dbconnection) and [`EventContext`](#type-eventcontext) both implement `DbContext`, which allows @@ -185,7 +185,7 @@ impl SubscriptionBuilder { } ``` -Register a callback to run when the subscription is applied and the matching rows are inserted into the client cache. The [`EventContext`](#type-module_bindings-eventcontext) passed to the callback will have `Event::SubscribeApplied` as its `event`. +Register a callback to run when the subscription is applied and the matching rows are inserted into the client cache. The [`EventContext`](#type-eventcontext) passed to the callback will have `Event::SubscribeApplied` as its `event`. #### Method `subscribe` diff --git a/docs/sdks/typescript/index.md b/docs/sdks/typescript/index.md index 4f4e17da..34d9edef 100644 --- a/docs/sdks/typescript/index.md +++ b/docs/sdks/typescript/index.md @@ -471,7 +471,7 @@ Identity.fromString(str: string): Identity ### Class `Address` -An opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#type-identity). +An opaque identifier for a client connection to a database, intended to differentiate between connections from the same [`Identity`](#class-identity). Defined in [spacetimedb-sdk.address](https://github.com/clockworklabs/spacetimedb-typescript-sdk/blob/main/src/address.ts): @@ -561,9 +561,8 @@ The generated class has a field for each of the table's columns, whose names are | Properties | Description | | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | [`Table.name`](#table-name) | The name of the class. | -| [`Table.tableName`](#table-tableName) | The name of the table in the database. | +| [`Table.tableName`](#table-tablename) | The name of the table in the database. | | Methods | | -| [`Table.isEqual`](#table-isequal) | Method to compare two identities. | | [`Table.all`](#table-all) | Return all the subscribed rows in the table. | | [`Table.filterBy{COLUMN}`](#table-filterbycolumn) | Autogenerated; return subscribed rows with a given value in a particular column. `{COLUMN}` is a placeholder for a column name. | | [`Table.findBy{COLUMN}`](#table-findbycolumn) | Autogenerated; return a subscribed row with a given value in a particular unique column. `{COLUMN}` is a placeholder for a column name. | @@ -857,7 +856,7 @@ Person.onUpdate((oldPerson, newPerson, reducerEvent) => { ### {Table} removeOnUpdate -Unregister a previously-registered [`onUpdate`](#table-onUpdate) callback. +Unregister a previously-registered [`onUpdate`](#table-onupdate) callback. ```ts {Table}.removeOnUpdate(callback: (oldValue: {Table}, newValue: {Table}, reducerEvent: ReducerEvent | undefined) => void): void @@ -912,7 +911,7 @@ Person.onDelete((person, reducerEvent) => { ### {Table} removeOnDelete -Unregister a previously-registered [`onDelete`](#table-onDelete) callback. +Unregister a previously-registered [`onDelete`](#table-ondelete) callback. ```ts {Table}.removeOnDelete(callback: (value: {Table}, reducerEvent: ReducerEvent | undefined) => void): void diff --git a/docs/unity/part-1.md b/docs/unity/part-1.md index 8e0a49e3..10967b33 100644 --- a/docs/unity/part-1.md +++ b/docs/unity/part-1.md @@ -119,5 +119,5 @@ We chose ECS for this example project because it promotes scalability, modularit From here, the tutorial continues with your favorite server module language of choice: -- [Rust](part-2a-rust.md) -- [C#](part-2b-csharp.md) +- [Rust](part-2a-rust) +- [C#](part-2b-c-sharp) diff --git a/docs/ws/index.md b/docs/ws/index.md index 587fbad0..1a3780cc 100644 --- a/docs/ws/index.md +++ b/docs/ws/index.md @@ -1,6 +1,6 @@ # The SpacetimeDB WebSocket API -As an extension of the [HTTP API](/doc/http-api-reference), SpacetimeDB offers a WebSocket API. Clients can subscribe to a database via a WebSocket connection to receive streaming updates as the database changes, and send requests to invoke reducers. Messages received from the server over a WebSocket will follow the same total ordering of transactions as are committed to the database. +As an extension of the [HTTP API](/docs/http), SpacetimeDB offers a WebSocket API. Clients can subscribe to a database via a WebSocket connection to receive streaming updates as the database changes, and send requests to invoke reducers. Messages received from the server over a WebSocket will follow the same total ordering of transactions as are committed to the database. The SpacetimeDB SDKs comminicate with their corresponding database using the WebSocket API. diff --git a/package.json b/package.json index 2c2b9445..e7716fa4 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,13 @@ "main": "index.js", "dependencies": {}, "devDependencies": { + "@types/node": "^22.10.2", + "tsx": "^4.19.2", "typescript": "^5.3.2" }, "scripts": { - "build": "tsc" + "build": "tsc nav.ts --outDir docs", + "check-links": "tsx scripts/checkLinks.ts" }, "author": "Clockwork Labs", "license": "ISC" diff --git a/scripts/checkLinks.ts b/scripts/checkLinks.ts new file mode 100644 index 00000000..78a8daf8 --- /dev/null +++ b/scripts/checkLinks.ts @@ -0,0 +1,231 @@ +import fs from 'fs'; +import path from 'path'; +import nav from '../nav'; // Import the nav object directly + +// Function to map slugs to file paths from nav.ts +function extractSlugToPathMap(nav: { items: any[] }): Map { + const slugToPath = new Map(); + + function traverseNav(items: any[]): void { + items.forEach((item) => { + if (item.type === 'page' && item.slug && item.path) { + const resolvedPath = path.resolve(__dirname, '../docs', item.path); + slugToPath.set(`/docs/${item.slug}`, resolvedPath); + } else if (item.type === 'section' && item.items) { + traverseNav(item.items); // Recursively traverse sections + } + }); + } + + traverseNav(nav.items); + return slugToPath; +} + +// Function to assert that all files in slugToPath exist +function validatePathsExist(slugToPath: Map): void { + slugToPath.forEach((filePath, slug) => { + if (!fs.existsSync(filePath)) { + throw new Error(`File not found: ${filePath} (Referenced by slug: ${slug})`); + } + }); +} + +// Function to extract links from markdown files with line numbers +function extractLinksFromMarkdown(filePath: string): { link: string; line: number }[] { + const fileContent = fs.readFileSync(filePath, 'utf-8'); + const lines = fileContent.split('\n'); + const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; + + const links: { link: string; line: number }[] = []; + lines.forEach((lineContent, index) => { + let match: RegExpExecArray | null; + while ((match = linkRegex.exec(lineContent)) !== null) { + links.push({ link: match[2], line: index + 1 }); // Add 1 to make line numbers 1-based + } + }); + + return links; +} + +// Function to resolve relative links using slugs +function resolveLink(link: string, currentSlug: string): string { + if (link.startsWith('#')) { + // If the link is a fragment, resolve it to the current slug + return `${currentSlug}${link}`; + } + + if (link.startsWith('/')) { + // Absolute links are returned as-is + return link; + } + + // Resolve relative links based on slug + const currentSlugDir = path.dirname(currentSlug); + const resolvedSlug = path.normalize(path.join(currentSlugDir, link)).replace(/\\/g, '/'); + return resolvedSlug.startsWith('/docs') ? resolvedSlug : `/docs${resolvedSlug}`; +} + +// Function to extract headings from a markdown file +function extractHeadingsFromMarkdown(filePath: string): string[] { + if (!fs.existsSync(filePath) || !fs.lstatSync(filePath).isFile()) { + return []; // Return an empty list if the file does not exist or is not a file + } + + const fileContent = fs.readFileSync(filePath, 'utf-8'); + const headingRegex = /^(#{1,6})\s+(.*)$/gm; // Match markdown headings like # Heading + const headings: string[] = []; + let match: RegExpExecArray | null; + + while ((match = headingRegex.exec(fileContent)) !== null) { + const heading = match[2].trim(); // Extract the heading text + const slug = heading + .toLowerCase() + .replace(/[^\w\- ]+/g, '') // Remove special characters + .replace(/\s+/g, '-'); // Replace spaces with hyphens + headings.push(slug); + } + + return headings; +} + +// Function to check if the links in .md files match the slugs in nav.ts and validate fragments +function checkLinks(): void { + const brokenLinks: { file: string; link: string; line: number }[] = []; + let totalFiles = 0; + let totalLinks = 0; + let validLinks = 0; + let invalidLinks = 0; + let totalFragments = 0; + let validFragments = 0; + let invalidFragments = 0; + let currentFileFragments = 0; + + // Extract the slug-to-path mapping from nav.ts + const slugToPath = extractSlugToPathMap(nav); + + // Validate that all paths in slugToPath exist + validatePathsExist(slugToPath); + + console.log(`Validated ${slugToPath.size} paths from nav.ts`); + + // Extract valid slugs + const validSlugs = Array.from(slugToPath.keys()); + + // Reverse map from file path to slug for current file resolution + const pathToSlug = new Map(); + slugToPath.forEach((filePath, slug) => { + pathToSlug.set(filePath, slug); + }); + + // Get all .md files to check + const mdFiles = getMarkdownFiles(path.resolve(__dirname, '../docs')); + + totalFiles = mdFiles.length; + + mdFiles.forEach((file) => { + const links = extractLinksFromMarkdown(file); + totalLinks += links.length; + + const currentSlug = pathToSlug.get(file) || ''; + + links.forEach(({ link, line }) => { + // Exclude external links (starting with http://, https://, mailto:, etc.) + if (/^([a-z][a-z0-9+.-]*):/.test(link)) { + return; // Skip external links + } + + const siteLinks = ['/install', '/images']; + for (const siteLink of siteLinks) { + if (link.startsWith(siteLink)) { + return; // Skip site links + } + } + + + // Resolve the link + const resolvedLink = resolveLink(link, currentSlug); + + // Split the resolved link into base and fragment + const [baseLink, fragmentRaw] = resolvedLink.split('#'); + const fragment: string | null = fragmentRaw || null; + + if (fragment) { + totalFragments += 1; + } + + // Check if the base link matches a valid slug + if (!validSlugs.includes(baseLink)) { + brokenLinks.push({ file, link: resolvedLink, line }); + invalidLinks += 1; + return; + } else { + validLinks += 1; + } + + // Validate the fragment, if present + if (fragment) { + const targetFile = slugToPath.get(baseLink); + if (targetFile) { + const targetHeadings = extractHeadingsFromMarkdown(targetFile); + + if (!targetHeadings.includes(fragment)) { + brokenLinks.push({ file, link: resolvedLink, line }); + invalidFragments += 1; + invalidLinks += 1; + } else { + validFragments += 1; + if (baseLink === currentSlug) { + currentFileFragments += 1; + } + } + } + } + }); + }); + + if (brokenLinks.length > 0) { + console.error(`\nFound ${brokenLinks.length} broken links:`); + brokenLinks.forEach(({ file, link, line }) => { + console.error(`File: ${file}:${line}, Link: ${link}`); + }); + } else { + console.log('All links are valid!'); + } + + // Print statistics + console.log('\n=== Link Validation Statistics ==='); + console.log(`Total markdown files processed: ${totalFiles}`); + console.log(`Total links processed: ${totalLinks}`); + console.log(` Valid links: ${validLinks}`); + console.log(` Invalid links: ${invalidLinks}`); + console.log(`Total links with fragments processed: ${totalFragments}`); + console.log(` Valid links with fragments: ${validFragments}`); + console.log(` Invalid links with fragments: ${invalidFragments}`); + console.log(`Fragments referring to the current file: ${currentFileFragments}`); + console.log('================================='); + + if (brokenLinks.length > 0) { + process.exit(1); // Exit with an error code if there are broken links + } +} + +// Function to get all markdown files recursively +function getMarkdownFiles(dir: string): string[] { + let files: string[] = []; + const items = fs.readdirSync(dir); + + items.forEach((item) => { + const fullPath = path.join(dir, item); + const stat = fs.lstatSync(fullPath); + + if (stat.isDirectory()) { + files = files.concat(getMarkdownFiles(fullPath)); // Recurse into directories + } else if (fullPath.endsWith('.md')) { + files.push(fullPath); + } + }); + + return files; +} + +checkLinks(); diff --git a/tsconfig.json b/tsconfig.json index 2a5ee7d2..efe136bd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,8 @@ "target": "ESNext", "module": "commonjs", "outDir": "./docs", + "esModuleInterop": true, + "strict": true, "skipLibCheck": true } } diff --git a/yarn.lock b/yarn.lock index fce89544..d923eebd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,7 +2,196 @@ # yarn lockfile v1 +"@esbuild/aix-ppc64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz#51299374de171dbd80bb7d838e1cfce9af36f353" + integrity sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ== + +"@esbuild/android-arm64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz#58565291a1fe548638adb9c584237449e5e14018" + integrity sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw== + +"@esbuild/android-arm@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.23.1.tgz#5eb8c652d4c82a2421e3395b808e6d9c42c862ee" + integrity sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ== + +"@esbuild/android-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.23.1.tgz#ae19d665d2f06f0f48a6ac9a224b3f672e65d517" + integrity sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg== + +"@esbuild/darwin-arm64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz#05b17f91a87e557b468a9c75e9d85ab10c121b16" + integrity sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q== + +"@esbuild/darwin-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz#c58353b982f4e04f0d022284b8ba2733f5ff0931" + integrity sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw== + +"@esbuild/freebsd-arm64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz#f9220dc65f80f03635e1ef96cfad5da1f446f3bc" + integrity sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA== + +"@esbuild/freebsd-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz#69bd8511fa013b59f0226d1609ac43f7ce489730" + integrity sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g== + +"@esbuild/linux-arm64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz#8050af6d51ddb388c75653ef9871f5ccd8f12383" + integrity sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g== + +"@esbuild/linux-arm@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz#ecaabd1c23b701070484990db9a82f382f99e771" + integrity sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ== + +"@esbuild/linux-ia32@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz#3ed2273214178109741c09bd0687098a0243b333" + integrity sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ== + +"@esbuild/linux-loong64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz#a0fdf440b5485c81b0fbb316b08933d217f5d3ac" + integrity sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw== + +"@esbuild/linux-mips64el@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz#e11a2806346db8375b18f5e104c5a9d4e81807f6" + integrity sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q== + +"@esbuild/linux-ppc64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz#06a2744c5eaf562b1a90937855b4d6cf7c75ec96" + integrity sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw== + +"@esbuild/linux-riscv64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz#65b46a2892fc0d1af4ba342af3fe0fa4a8fe08e7" + integrity sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA== + +"@esbuild/linux-s390x@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz#e71ea18c70c3f604e241d16e4e5ab193a9785d6f" + integrity sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw== + +"@esbuild/linux-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz#d47f97391e80690d4dfe811a2e7d6927ad9eed24" + integrity sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ== + +"@esbuild/netbsd-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz#44e743c9778d57a8ace4b72f3c6b839a3b74a653" + integrity sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA== + +"@esbuild/openbsd-arm64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz#05c5a1faf67b9881834758c69f3e51b7dee015d7" + integrity sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q== + +"@esbuild/openbsd-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz#2e58ae511bacf67d19f9f2dcd9e8c5a93f00c273" + integrity sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA== + +"@esbuild/sunos-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz#adb022b959d18d3389ac70769cef5a03d3abd403" + integrity sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA== + +"@esbuild/win32-arm64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz#84906f50c212b72ec360f48461d43202f4c8b9a2" + integrity sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A== + +"@esbuild/win32-ia32@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz#5e3eacc515820ff729e90d0cb463183128e82fac" + integrity sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ== + +"@esbuild/win32-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz#81fd50d11e2c32b2d6241470e3185b70c7b30699" + integrity sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg== + +"@types/node@^22.10.2": + version "22.10.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.2.tgz#a485426e6d1fdafc7b0d4c7b24e2c78182ddabb9" + integrity sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ== + dependencies: + undici-types "~6.20.0" + +esbuild@~0.23.0: + version "0.23.1" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.23.1.tgz#40fdc3f9265ec0beae6f59824ade1bd3d3d2dab8" + integrity sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg== + optionalDependencies: + "@esbuild/aix-ppc64" "0.23.1" + "@esbuild/android-arm" "0.23.1" + "@esbuild/android-arm64" "0.23.1" + "@esbuild/android-x64" "0.23.1" + "@esbuild/darwin-arm64" "0.23.1" + "@esbuild/darwin-x64" "0.23.1" + "@esbuild/freebsd-arm64" "0.23.1" + "@esbuild/freebsd-x64" "0.23.1" + "@esbuild/linux-arm" "0.23.1" + "@esbuild/linux-arm64" "0.23.1" + "@esbuild/linux-ia32" "0.23.1" + "@esbuild/linux-loong64" "0.23.1" + "@esbuild/linux-mips64el" "0.23.1" + "@esbuild/linux-ppc64" "0.23.1" + "@esbuild/linux-riscv64" "0.23.1" + "@esbuild/linux-s390x" "0.23.1" + "@esbuild/linux-x64" "0.23.1" + "@esbuild/netbsd-x64" "0.23.1" + "@esbuild/openbsd-arm64" "0.23.1" + "@esbuild/openbsd-x64" "0.23.1" + "@esbuild/sunos-x64" "0.23.1" + "@esbuild/win32-arm64" "0.23.1" + "@esbuild/win32-ia32" "0.23.1" + "@esbuild/win32-x64" "0.23.1" + +fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +get-tsconfig@^4.7.5: + version "4.8.1" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.8.1.tgz#8995eb391ae6e1638d251118c7b56de7eb425471" + integrity sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg== + dependencies: + resolve-pkg-maps "^1.0.0" + +resolve-pkg-maps@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" + integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== + +tsx@^4.19.2: + version "4.19.2" + resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.19.2.tgz#2d7814783440e0ae42354d0417d9c2989a2ae92c" + integrity sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g== + dependencies: + esbuild "~0.23.0" + get-tsconfig "^4.7.5" + optionalDependencies: + fsevents "~2.3.3" + typescript@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.2.tgz#00d1c7c1c46928c5845c1ee8d0cc2791031d4c43" integrity sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ== + +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==