From 2d25a106a11248a9acc7cc48e9915a845022919f Mon Sep 17 00:00:00 2001 From: Christian Georgi Date: Fri, 26 Jun 2026 17:17:23 +0200 Subject: [PATCH 1/4] Remove links to removed props from Java 5 --- java/migration.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/java/migration.md b/java/migration.md index ac997e24c..fa21c8ed8 100644 --- a/java/migration.md +++ b/java/migration.md @@ -106,8 +106,8 @@ The following properties have been deprecated and might be removed in a future m | Deprecated Property | Explanation | | --- | --- | | `cds.dashboard.*` | The entire `cds.dashboard` configuration namespace is deprecated and may be removed in a future major version. | -| [cds.outbox.inMemory.emitDuringChangeSetContext](./developing-applications/properties#cds-outbox-inmemory-emitduringchangesetcontext) | The functionality provided by this property is enabled by default and there is no reason to switch it off. | -| [cds.outbox.inMemory.enabled](./developing-applications/properties#cds-outbox-inmemory-enabled) | The functionality provided by this property is enabled by default and there is no reason to switch it off. | +| `cds.outbox.inMemory.emitDuringChangeSetContext` | The functionality provided by this property is enabled by default and there is no reason to switch it off. | +| `cds.outbox.inMemory.enabled` | The functionality provided by this property is enabled by default and there is no reason to switch it off. | ### Removed Properties
@@ -179,7 +179,7 @@ The `cds-services-archetype` is used by the `@sap/cds-dk` to generate initial CA The default JDK version of new CAP Java projects has been changed to JDK **25**. The minimum required JDK version is now **21**. -If your project uses Lombok, you need to explicitly add its annotation processor to your POM when you switch to Java 25. This is a change in Java compiler and affects all other annotation processors. +If your project uses Lombok, you need to explicitly add its annotation processor to your POM when you switch to Java 25. This is a change in Java compiler and affects all other annotation processors. [Learn more about Maven setup with Lombok.](https://projectlombok.org/setup/maven){.learn-more} [Learn more about about the change in the Java compiler.](https://bugs.java.com/bugdatabase/JDK-8321314/description){.learn-more} From 8f0ad209446d8b9ad4d98cfbe05c2bb248bdb856 Mon Sep 17 00:00:00 2001 From: Christian Georgi Date: Fri, 26 Jun 2026 17:50:25 +0200 Subject: [PATCH 2/4] Run broken-link-checker on external content --- .github/etc/blc.js | 331 ++++++++++++++++++++++++++++ cds/compiler/hdbcds-to-hdbtable.md | 2 +- get-started/get-help.md | 4 +- guides/multitenancy/index.md | 14 +- guides/multitenancy/old-mtx-apis.md | 2 +- guides/protocols/asyncapi.md | 2 +- node.js/cds-serve.md | 4 +- node.js/cds-server.md | 1 + package.json | 2 +- tools/apis/cds-build.md | 2 +- 10 files changed, 348 insertions(+), 16 deletions(-) create mode 100755 .github/etc/blc.js diff --git a/.github/etc/blc.js b/.github/etc/blc.js new file mode 100755 index 000000000..af49138a2 --- /dev/null +++ b/.github/etc/blc.js @@ -0,0 +1,331 @@ +#!/usr/bin/env node + +import { parseDocument } from 'htmlparser2' +import { spawn } from 'child_process' +import { readdirSync, existsSync } from 'fs' +import { join } from 'path' +import { parseArgs } from 'node:util' + +const { Bright,Dim,Reset, foreground:{ + Red, Yellow, Green +}} = { + + Reset: "\x1b[0m", + Bright: "\x1b[1m", + Dim: "\x1b[2m", + Underscore: "\x1b[4m", + Blink: "\x1b[5m", + Reverse: "\x1b[7m", + Hidden: "\x1b[8m", + + foreground: { + Black: "\x1b[30m", + Red: "\x1b[31m", + Green: "\x1b[32m", + Yellow: "\x1b[33m", + Blue: "\x1b[34m", + Magenta: "\x1b[35m", + Cyan: "\x1b[36m", + White: "\x1b[37m", + }, + background: { + Black: "\x1b[40m", + Red: "\x1b[41m", + Green: "\x1b[42m", + Yellow: "\x1b[43m", + Blue: "\x1b[44m", + Magenta: "\x1b[45m", + Cyan: "\x1b[46m", + White: "\x1b[47m", + } +} + +const urlExcludesBase = [ + /\/java\/assets\/cds-maven-plugin-site\//, + /\/java\/custom-logic\//, + /\/releases\/changelog\//, + /\/releases\/latest/, + /\/releases\/current/, + /\/tools\/lint/, +] + +// extended set of excludes because public content refers to internal content in some places +const urlExcludesPublicRepo = [ + ...urlExcludesBase, + /\/guides\/security\//, + /\/releases/, + /\/resources/, + /cds\/compiler\/messages/, + /mcp/ +] + +const { values, positionals } = parseArgs({ + args: process.argv.slice(2), + options: { + x: { type: 'boolean', short: 'x', default: false }, + public: { type: 'boolean', default: false }, + }, + allowPositionals: true, +}) + +const urlExcludes = values.public ? urlExcludesPublicRepo : urlExcludesBase + +let [base] = positionals + +// Build a set of URL paths that are directory-backed (have index.html) +// so we know when to add trailing slash for correct relative URL resolution. +const distDir = '.vitepress/dist' +const dirPages = new Set() +function scanDirs(dir, prefix) { + try { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + if (entry.isDirectory()) { + const sub = join(dir, entry.name) + const subPrefix = prefix + entry.name + '/' + if (existsSync(join(sub, 'index.html'))) dirPages.add(subPrefix) + scanDirs(sub, subPrefix) + } + } + } catch {} +} +scanDirs(distDir, '/') + +let server +if (!base) { + // Auto-start a vitepress preview server + const port = 4173 + Math.floor(Math.random() * 1000) + base = `http://localhost:${port}/docs/` + server = spawn('npx', ['vitepress', 'preview', '.', '--port', port], { + stdio: 'ignore', + detached: true, + }) + // Wait for server to be ready + for (let i = 0; i < 30; i++) { + try { + const res = await fetch(base) + if (res.ok) break + } catch {} + await new Promise(r => setTimeout(r, 500)) + } +} + +try { + if (values.x) { console.log (`Checking external links on ${base}...`); await check ({ excludeInternalLinks:true }) } + else { console.log (`Checking internal links on ${base}...`); await check ({ excludeExternalLinks:true }) } +} finally { + if (server) { + process.kill(-server.pid) + await new Promise(r => server.on('close', r)) + } +} + +async function check (options={}) { + + let N=0, all=new Set, broken={}, errors=[], pages={} + const visited = new Set() + const failedUrls = new Set() + const queue = [base] + const incomingLinks = {} // url -> [{ from: page, original: href }] + + function record (link, reason, p) { + ++N + if (broken.page !== p) errors.push (broken = {page:p,links:[]}) + broken.links.push ({ link, reason, toString() { return reason +': '+ link } }) + } + + // Phase 1: Crawl all reachable internal pages + while (queue.length > 0) { + const batch = queue.splice(0, 10) + await Promise.all(batch.map(crawlPage)) + } + + async function crawlPage (url) { + let cleanUrl = url.split('#')[0] + // Normalize /index URLs to directory form (e.g. /releases/index -> /releases/) + if (cleanUrl.endsWith('/index')) cleanUrl = cleanUrl.slice(0, -5) + if (visited.has(cleanUrl)) return + visited.add(cleanUrl) + // Also mark the counterpart (with/without trailing slash) as visited + // to avoid crawling the same page twice with different URL resolution + if (cleanUrl.endsWith('/')) visited.add(cleanUrl.slice(0,-1)) + else visited.add(cleanUrl + '/') + + const path = cleanUrl.replace(base,'/') + if (path.startsWith('/assets')) return + if (urlExcludes.find(l => l.test(path))) return + + let html, finalUrl, resolveBase + try { + const res = await fetch(cleanUrl) + if (!res.ok) { + failedUrls.add(cleanUrl) + return + } + const ct = res.headers.get('content-type') || '' + if (!ct.includes('text/html')) return + finalUrl = res.url // after redirects + // Use filesystem knowledge to determine if this page is directory-backed. + // Directory pages (served from dir/index.html) need trailing slash for + // correct relative URL resolution (e.g. ./foo resolves within the directory). + const urlPath = path.endsWith('/') ? path : path + '/' + resolveBase = dirPages.has(urlPath) ? cleanUrl.replace(/\/?$/, '/') : finalUrl + html = await res.text() + } catch(e) { + failedUrls.add(cleanUrl) + console.error(`Error fetching ${cleanUrl}: ${e.message}`) + return + } + + const doc = parseDocument(html) + const p = { + url: cleanUrl, path, + doc, + anchors: {}, + hashed: [], + } + pages[path] = p + + console.log (Dim+path, Reset) + + for (let hash of fetchLocalIn(doc)) p.hashed.push ({ hash }) + + walkLinks(doc, (href) => { + if (!href || href.startsWith('mailto:') || href.startsWith('javascript:') || href.startsWith('tel:')) return + let resolved + try { resolved = new URL(href, resolveBase).href } catch { return } + + all.add(resolved) + const [resolvedBase] = resolved.split('#') + const [,hash] = href.split('#') + const isInternal = resolvedBase.startsWith(base) + + if (hash && isInternal) { + p.hashed.push ({ url: resolvedBase.replace(base,'/'), hash }) + } + + if (isInternal && !options.excludeInternalLinks) { + if (!incomingLinks[resolvedBase]) incomingLinks[resolvedBase] = [] + incomingLinks[resolvedBase].push({ from: p, original: href }) + if (!visited.has(resolvedBase)) { + queue.push(resolvedBase) + } + } + }) + } + + // Phase 2: Check for broken internal links (pages that failed to load) + if (!options.excludeInternalLinks) { + for (const [url, links] of Object.entries(incomingLinks)) { + if (failedUrls.has(url)) { + const linkRel = url.replace(base,'/') + if (urlExcludes.find(l => l.test(linkRel))) continue + for (const { from, original } of links) { + record(original, 'Not found', from) + } + } + } + } + + // Phase 3: Check external links (if -x mode) + if (!options.excludeExternalLinks) { + const externalLinks = new Map() + for (const p of Object.values(pages)) { + walkLinks(p.doc, href => { + if (!href || href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('javascript:') || href.startsWith('tel:')) return + let resolved + try { resolved = new URL(href, p.url).href } catch { return } + if (!resolved.startsWith(base)) { + if (!externalLinks.has(resolved)) externalLinks.set(resolved, []) + externalLinks.get(resolved).push({ from: p, original: href }) + } + }) + } + + console.log(`\nChecking ${externalLinks.size} external links...`) + const entries = [...externalLinks.entries()] + for (let i = 0; i < entries.length; i += 10) { + await Promise.all(entries.slice(i, i + 10).map(async ([url, links]) => { + try { + const res = await fetch(url, { + method: 'HEAD', + signal: AbortSignal.timeout(10000), + headers: { 'User-Agent': 'Mozilla/5.0 (compatible; LinkChecker)' }, + redirect: 'follow', + }) + if (!res.ok) { + for (const { from, original } of links) record(original, `HTTP ${res.status}`, from) + } + } catch (e) { + for (const { from, original } of links) record(original, e.message || 'Connection error', from) + } + })) + } + } + + // Phase 4: Check hash/anchor links across pages + for (let p of Object.values(pages)) { + for (let {url,hash} of p.hashed) try { + if (url) { + if (urlExcludes.find(l => l.test(url))) continue + const page = pages[url] || pages[url.replace(/\/$/, '')] + if (!page) continue + checkLocal (page.doc,hash) || record (url+' #'+hash, 'Unresolved hash link', p) + } + else if (hash) { + if (urlExcludes.find(l => l.test(p.path))) continue + checkLocal (p.doc,hash) || record ('#'+hash, 'Unresolved local link', p) + } + } catch(e) { record(url+' #'+hash, 'Unresolved hash link', p) } + } + + // Phase 5: Report results + console.log (`\n-----------------------------------------------------------------`) + if (Object.keys(pages).length === 0) { + console.log (Bright+Red+`Could not fetch any pages from ${base}\n`, Reset) + process.exitCode = 1 + } else if (broken.links) { + console.log (Bright+Red+`Found ${N} broken link(s) to internal targets in ${errors.length} source(s):`, Reset) + for (let broken of errors) { + console.log ('in:', broken.page.path) + for (let each of broken.links) console.log (Bright+Red+ each) + console.log (Reset) + } + if (N > 0) process.exitCode = 1 + } else { + console.log (Bright+Green+`It's all fine in ${all.size} links, no broken links found\n`, Reset) + } +} + +function checkLocal (doc, id) { + return doc._anchors?.[id] ?? ((doc._anchors ??= {})[id] = findById(doc, id)) +} + +function findById (node, id) { + for (let each of (node.children || [])) { + if (each.attribs?.id === id) return each + if (each.children) { + const found = findById (each, id) + if (found) return found + } + } +} + +function fetchLocalIn (node, all=new Set) { + for (let each of (node.children || [])) { + if (each.name === 'a') { + const href = each.attribs?.href + if (href && href[0]==='#') all.add (href.slice(1)) + } + if (each.children) fetchLocalIn (each,all) + } + return all +} + +function walkLinks (node, callback) { + for (let each of (node.children || [])) { + if (each.name === 'a' && each.attribs?.href) { + callback(each.attribs.href) + } + if (each.children) walkLinks(each, callback) + } +} diff --git a/cds/compiler/hdbcds-to-hdbtable.md b/cds/compiler/hdbcds-to-hdbtable.md index 4e6673bdd..fe605d3b4 100644 --- a/cds/compiler/hdbcds-to-hdbtable.md +++ b/cds/compiler/hdbcds-to-hdbtable.md @@ -9,7 +9,7 @@ status: released If you are already using SAP HANA Cloud, there is no SAP HANA CDS. ::: -The deployment format `hdbcds` for SAP HANA together with the function [`to.hdbcds`](../../node.js/cds-compile#hdbcds) have been deprecated with `@sap/cds-compiler@5` and `@sap/cds@8`. Users are advised to switch to the default format `hdbtable`. This guide provides step-by-step instructions for making the switch, including potential issues and work-arounds, such as handling annotations `@sql.prepend/append` and dealing with associations. +The deployment format `hdbcds` for SAP HANA together with the function `to.hdbcds` have been deprecated with `@sap/cds-compiler@5` and `@sap/cds@8`. Users are advised to switch to the default format `hdbtable`. This guide provides step-by-step instructions for making the switch, including potential issues and work-arounds, such as handling annotations `@sql.prepend/append` and dealing with associations. New CDS features will not be available for `hdbcds` format, and will be removed in a major release. diff --git a/get-started/get-help.md b/get-started/get-help.md index 9123a3bcb..60cd1b159 100644 --- a/get-started/get-help.md +++ b/get-started/get-help.md @@ -392,7 +392,7 @@ If you don't want to exclude dependencies completely, but make sure that an in-m ### How do I generate an OData response in Node.js for Error 404? -If your application(s) endpoints are served with OData and you want to change the standard HTML response to an OData response, adapt the following snippet to your needs and add it in your [custom _server.js_ file](../node.js/cds-serve#custom-server-js). +If your application(s) endpoints are served with OData and you want to change the standard HTML response to an OData response, adapt the following snippet to your needs and add it in your [custom _server.js_ file](../node.js/cds-server#custom-server-js). ```js let app @@ -717,7 +717,7 @@ When using HANA TMS v2, the message "Subaccount verification failed" indicates t Most probably, you are using the same `hana_tenant_prefix` and `tenant_id` as another application that has been deployed in another subaccount. -See how to [handle HANA tenants with HANA TMS v2](/@external/guides/multitenancy/index.md#handle-sap-hana-tenants) to avoid this situation. +See how to [handle HANA tenants with HANA TMS v2](../guides/multitenancy/index.md#handle-sap-hana-tenants) to avoid this situation. ## BTP diff --git a/guides/multitenancy/index.md b/guides/multitenancy/index.md index 302bbc06f..7714d5f29 100644 --- a/guides/multitenancy/index.md +++ b/guides/multitenancy/index.md @@ -836,9 +836,9 @@ If you start with a multitenant application that's configured to use to SAP HANA > [!danger] Only use the `hana-multitenancy` plan > As the tenant containers are filtered by the SAP HANA Cloud service instance, applications will potentially access data of other applications when using a different plan. -For SAP HANA TMS v2, you also need to specify the database ID of the database that you plan to use for your tenant containers. You can specify this using the [`cds.xt.DeploymentService` configuration](/@external/guides/multitenancy/mtxs#deployment-config). +For SAP HANA TMS v2, you also need to specify the database ID of the database that you plan to use for your tenant containers. You can specify this using the [`cds.xt.DeploymentService` configuration](./mtxs#deployment-config). -With `cds.xt.DeploymentService` you only configure the default database ID. If you want to specify different database IDs for a tenant HDI container, you need to [add the database ID to the payload of the individual subscription using a handler](/@external/guides/multitenancy/mtxs#example-handler-for-saasprovisioningservice). +With `cds.xt.DeploymentService` you only configure the default database ID. If you want to specify different database IDs for a tenant HDI container, you need to [add the database ID to the payload of the individual subscription using a handler](./mtxs#example-handler-for-saasprovisioningservice). To keep the application configuration agnostic, we recommend adding the cds/requires/cds.xt.DeploymentService/hdi/create/database_id configuration as an environment variable to the MTX service in _mta.yaml_: ```yaml{6-17} @@ -863,7 +863,7 @@ To keep the application configuration agnostic, we recommend adding the @@ -965,8 +965,8 @@ When you unsubscribe and the tenant container is deleted, the corresponding SAP There are still some limitations with the current client implementation. - **Database ID is Mandatory** - As mentioned, you need to specify a database ID that's to be used, either for all tenants or per subscription request, see [Deployment configuration](/@external/guides/multitenancy/mtxs#deployment-config). -- [`clusterSize` configuration](/@external/guides/multitenancy/mtxs#saas-provisioning-config) needs to be set to `1` (default is `3`). HANA TMS v2 does not provide a performant way to determine all database IDs, + As mentioned, you need to specify a database ID that's to be used, either for all tenants or per subscription request, see [Deployment configuration](./mtxs#deployment-config). +- [`clusterSize` configuration](./mtxs#saas-provisioning-config) needs to be set to `1` (default is `3`). HANA TMS v2 does not provide a performant way to determine all database IDs, so clustering the upgrade by database does not work properly. diff --git a/guides/multitenancy/old-mtx-apis.md b/guides/multitenancy/old-mtx-apis.md index 97bbee657..634b25fbc 100644 --- a/guides/multitenancy/old-mtx-apis.md +++ b/guides/multitenancy/old-mtx-apis.md @@ -367,7 +367,7 @@ Returns information about a tenant's HDI container. > --- `cds-mtx` APIs are implemented as CDS services. Therefore, service implementations can be overridden using [CDS event handlers](../../node.js/core-services#srv-on-before-after). -For `cds-mtx` APIs, custom handlers have to be registered on the `mtx` event in a [custom `server.js`](../../node.js/cds-serve#custom-server-js): +For `cds-mtx` APIs, custom handlers have to be registered on the `mtx` event in a [custom `server.js`](../../node.js/cds-server#custom-server-js): ```js const cds = require('@sap/cds') diff --git a/guides/protocols/asyncapi.md b/guides/protocols/asyncapi.md index 009eb5943..c12897803 100644 --- a/guides/protocols/asyncapi.md +++ b/guides/protocols/asyncapi.md @@ -34,7 +34,7 @@ If you want to generate one AsyncAPI document for all the services, you can use cds compile srv --service all -o docs --to asyncapi --asyncapi:merged ``` -[Learn how to programmatically convert the CSN file into an AsyncAPI Document](../../node.js/cds-compile#to-asyncapi){.learn-more} +[Learn how to programmatically convert the CSN file into an AsyncAPI Document](../../node.js/cds-compile#asyncapi){.learn-more} ## Presets { #presets} diff --git a/node.js/cds-serve.md b/node.js/cds-serve.md index 6f8f4f6e8..4bc99ed3c 100644 --- a/node.js/cds-serve.md +++ b/node.js/cds-serve.md @@ -97,7 +97,7 @@ This uses these defaults for all options: Alternatively you can construct services individually, also from other models, and also mount them yourself, as document in the subsequent sections on individual fluent API options. -If you just want to add some additional middleware, it's recommended to bootstrap from a [custom `server.js`](#cds-server). +If you just want to add some additional middleware, it's recommended to bootstrap from a [custom `server.js`](cds-server#custom-server-js). @@ -271,7 +271,7 @@ cds.middlewares.add (mw) // to the end ### Custom Middlewares -The configuration of middlewares must be done programmatically before bootstrapping the CDS services, for example, in a [custom server.js](cds-serve#custom-server-js). +The configuration of middlewares must be done programmatically before bootstrapping the CDS services, for example, in a [custom server.js](cds-server#custom-server-js). The framework exports the default middlewares itself and the list of middlewares which run before the protocol adapter starts processing the request. diff --git a/node.js/cds-server.md b/node.js/cds-server.md index 0099b2a24..e0124088e 100644 --- a/node.js/cds-server.md +++ b/node.js/cds-server.md @@ -97,6 +97,7 @@ The express.js `app` constructed by the server implementation. ## Custom `server.js` +
The CLI command `cds serve` optionally bootstraps from project-local `./server.js` or `./srv/server.js`. diff --git a/package.json b/package.json index 3170129ea..2c4f59c45 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "docs:build": "vitepress build .", "docs:preview": "vitepress preview .", "lint": "eslint .", - "test": "echo 'No tests'" + "test": "node .github/etc/blc --public" }, "author": "SAP SE (https://www.sap.com)", "license": "SEE LICENSE IN LICENSE", diff --git a/tools/apis/cds-build.md b/tools/apis/cds-build.md index 7cd7eb4fa..b052b2290 100644 --- a/tools/apis/cds-build.md +++ b/tools/apis/cds-build.md @@ -65,7 +65,7 @@ The CDS build system auto-detects all required build tasks by invoking the stati The compiled CSN model can be accessed using the asynchronous methods `model()` or `basemodel()`. - The method `model()` returns a CSN model for the scope defined by the `options.model` setting. If [feature toggles](../../guides/extensibility/feature-toggles) are enabled, this model also includes any toggled feature enhancements. -- To get a CSN model without features, use the method `baseModel()` instead. The model can be used as input for further [model processing](../../node.js/cds-compile#cds-compile-to-xyz), like `to.edmx`, `to.hdbtable`, `for.odata`, etc. +- To get a CSN model without features, use the method `baseModel()` instead. The model can be used as input for further [model processing](../../node.js/cds-compile#cds-compile-to), like `to.edmx`, `to.hdbtable`, `for.odata`, etc. - Use [`cds.reflect`](../../node.js/cds-reflect) to access advanced query and filter functionality on the CDS model. ## Add build task type to cds schema From a1afd5726e5be9ca8fc23ec3f76c87b05648611f Mon Sep 17 00:00:00 2001 From: Christian Georgi Date: Fri, 26 Jun 2026 18:17:50 +0200 Subject: [PATCH 3/4] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/etc/blc.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/etc/blc.js b/.github/etc/blc.js index af49138a2..40aff135d 100755 --- a/.github/etc/blc.js +++ b/.github/etc/blc.js @@ -190,7 +190,7 @@ async function check (options={}) { for (let hash of fetchLocalIn(doc)) p.hashed.push ({ hash }) walkLinks(doc, (href) => { - if (!href || href.startsWith('mailto:') || href.startsWith('javascript:') || href.startsWith('tel:')) return + if (!href || href.startsWith('mailto:') || href.startsWith('javascript:') || href.startsWith('data:') || href.startsWith('vbscript:') || href.startsWith('tel:')) return let resolved try { resolved = new URL(href, resolveBase).href } catch { return } @@ -231,7 +231,7 @@ async function check (options={}) { const externalLinks = new Map() for (const p of Object.values(pages)) { walkLinks(p.doc, href => { - if (!href || href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('javascript:') || href.startsWith('tel:')) return + if (!href || href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('javascript:') || href.startsWith('data:') || href.startsWith('vbscript:') || href.startsWith('tel:')) return let resolved try { resolved = new URL(href, p.url).href } catch { return } if (!resolved.startsWith(base)) { From 5d41c5cba1caf44cb0afa70a9182d4dd40181510 Mon Sep 17 00:00:00 2001 From: Christian Georgi Date: Fri, 26 Jun 2026 18:22:43 +0200 Subject: [PATCH 4/4] Add dependency to htmlparser2 --- package-lock.json | 121 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 122 insertions(+) diff --git a/package-lock.json b/package-lock.json index aeb619cb9..12d1a5b81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "express": "^5", "fflate": "^0.8.2", "globals": "^17.4.0", + "htmlparser2": "^12", "monaco-editor": "^0.55.1", "sass": "^1.62.1", "vite": "^8", @@ -3451,6 +3452,71 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/dom-serializer": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz", + "integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0", + "entities": "^8.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz", + "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/domhandler": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz", + "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, "node_modules/dompurify": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", @@ -3461,6 +3527,25 @@ "@types/trusted-types": "^2.0.7" } }, + "node_modules/domutils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz", + "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^3.0.0", + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4238,6 +4323,42 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/htmlparser2": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-12.0.0.tgz", + "integrity": "sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0", + "domutils": "^4.0.2", + "entities": "^8.0.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", diff --git a/package.json b/package.json index 2c4f59c45..14cac7427 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "express": "^5", "fflate": "^0.8.2", "globals": "^17.4.0", + "htmlparser2": "^12", "monaco-editor": "^0.55.1", "sass": "^1.62.1", "vite": "^8",