Added a script to check the validity of docs links and a .github action (#122)

* Added a script to check the validity of docs links and a .github action

* Removed erroneous thing

* Switched the action trigger

* Added workflow to ensure that the nav.ts has been built to nav.js

* typo

* Build nav.ts

* typo thing

* Fixed script issue

* Fix

* Fixed a few links

* Added relative link resolution and fixed the broken links

* now checking fragments

* Now checking fragments properly and publishing some stats

* Forgot exit code

* Fix broken links

Well, in at least some cases, just remove broken links.

- The BSATN ref contained links to type defns, but didn't have type defns.
  Replace the links with plain text.
- HTTP database links for recovery-code related routes were getting mangled
  in some way I couldn't figure out, so the links weren't working
  despite their targets clearly existing.
  Conveniently, those routes have been removed,
  so remove the links and the corresponding sections.
- The JSON doc (erroneously called "SATN") contained typos,
  spelling "producttype" as "productype".
- C# SDK ref had links to a section on the `Address` type, but no such section.
  Replace the links with plain text.
- Rust SDK ref had a link getting mangled in a way I couldn't figure out.
  Simplify the section title so that the anchor name is predictable.
- TypeSciprt SDK ref used camelCase names in anchor links,
  but we downcase all section titles to create anchor names.

Also slap a section in README.md which says how to run the checker locally.

---------

Co-authored-by: Phoebe Goldman <phoebe@goldman-tribe.org>
This commit is contained in:
Tyler Cloutier
2025-01-03 00:44:00 -05:00
committed by GitHub
parent 85c137ca90
commit 48bab90a41
17 changed files with 539 additions and 80 deletions
+26
View File
@@ -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
+40
View File
@@ -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
+4
View File
@@ -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.
+13 -19
View File
@@ -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_.
-39
View File
@@ -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 <email> <identity>`.
#### 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 <email> <identity>`.
#### 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.
+1 -1
View File
@@ -312,6 +312,6 @@ spacetime sql <module-name> "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).
+14 -3
View File
@@ -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'?
+2 -2
View File
@@ -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<AlgebraicValue>
@@ -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. |
+4 -5
View File
@@ -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.
+2 -2
View File
@@ -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`
+4 -5
View File
@@ -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
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -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.
+4 -1
View File
@@ -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"
+231
View File
@@ -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<string, string> {
const slugToPath = new Map<string, string>();
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<string, string>): 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<string, string>();
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();
+2
View File
@@ -3,6 +3,8 @@
"target": "ESNext",
"module": "commonjs",
"outDir": "./docs",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true
}
}
+189
View File
@@ -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==