blob: 9d88e8e3c7cb4cc6448c46439ca1b3fce72f5979 [file]
/*!
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/** Shared helpers and paths behind the docs postbuild steps. See tests/deployment-checks.test.mjs. */
import { readdir } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
// One `<link>` shape covers the preconnect hints and the stylesheet alike: the theme emits
// all three with the font host in `href`.
const REMOTE_FONT_TAG =
/<link\b[^>]*href="https:\/\/fonts\.(?:googleapis|gstatic)\.com[^"]*"[^>]*>/g;
const ROOT_RELATIVE = /(?:href|src)="\/(?:_assets|_islands)\//;
const REMOTE_FONTS = /fonts\.(?:googleapis|gstatic)\.com/;
/** The rendered site, resolved from this file so every entry point agrees on it. */
export const HTML_ROOT = fileURLToPath(new URL("../_build/html", import.meta.url));
/** The SDK's package.json, not the docs toolchain's: its version is the published prefix. */
export const SDK_PACKAGE_JSON = new URL("../../package.json", import.meta.url);
/** Empty string means "served from the root", where root-relative URLs are correct. */
export function normalizeBasePath(rawBasePath) {
return rawBasePath.replace(/\/+$/, "");
}
/**
* A blank ``TS_SDK_DOCS_BASE_PATH`` counts as unset rather than as the root, so an empty
* env var cannot quietly publish a root-relative build past checks that then skip themselves.
*/
export function resolveBasePath(version, override = process.env.TS_SDK_DOCS_BASE_PATH) {
return normalizeBasePath(override?.trim() || `/docs/ts-sdk/${version}`);
}
export function stripRemoteFontTags(html) {
return html.replace(REMOTE_FONT_TAG, "");
}
export function hasRemoteFonts(text) {
return REMOTE_FONTS.test(text);
}
export function inspectPage(html, basePath) {
return {
rootRelative: ROOT_RELATIVE.test(html),
remoteFonts: hasRemoteFonts(html),
usesBasePath: basePath !== "" && html.includes(`${basePath}/_assets/`),
};
}
const preview = (files) => `${files.slice(0, 5).join(", ")}${files.length > 5 ? ", ..." : ""}`;
/** @returns {string[]} human-readable failures; empty means the build is publishable. */
export function collectErrors({ basePath, scanned, rootRelative, remoteFonts, usingBasePath }) {
const errors = [];
const servedFromRoot = basePath === "";
if (scanned === 0) return ["no HTML found -- did the TypeDoc build run?"];
if (!servedFromRoot && rootRelative.length > 0) {
errors.push(
`${rootRelative.length} page(s) use root-relative /_assets or /_islands URLs, ` +
`which resolve against the domain root once published: ${preview(rootRelative)}`,
);
}
if (remoteFonts.length > 0) {
errors.push(
`${remoteFonts.length} file(s) load fonts from Google, which ASF privacy guidance ` +
`does not allow: ${preview(remoteFonts)}`,
);
}
if (!servedFromRoot && usingBasePath === 0) {
errors.push(`no page references "${basePath}/_assets/" -- basePath is not the published path.`);
}
return errors;
}
export async function* filesUnder(dir, extensions) {
for (const entry of await readdir(dir, { withFileTypes: true })) {
const path = join(dir, entry.name);
if (entry.isDirectory()) yield* filesUnder(path, extensions);
else if (extensions.some((extension) => entry.name.endsWith(extension))) yield path;
}
}