From ae1455c83ef551fa4566839f0c70fa3c64599ed5 Mon Sep 17 00:00:00 2001 From: azu Date: Sat, 16 Aug 2025 13:52:30 +0900 Subject: [PATCH 1/2] refactor: remove get-url-origin dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace get-url-origin package with native URL object for getting URL origins. This reduces external dependencies and uses built-in Node.js functionality. Fixes #160 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- package.json | 1 - pnpm-lock.yaml | 8 -------- src/no-dead-link.ts | 47 ++++++++++++++++++++++++++++++++++----------- 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 37be954..67612d2 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "trailingComma": "none" }, "dependencies": { - "get-url-origin": "^1.0.1", "minimatch": "^3.0.4", "p-memoize": "^3.1.0", "p-queue": "^6.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b90afd6..a8483bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,9 +12,6 @@ importers: .: dependencies: - get-url-origin: - specifier: ^1.0.1 - version: 1.0.1 minimatch: specifier: ^3.0.4 version: 3.1.2 @@ -1250,9 +1247,6 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} - get-url-origin@1.0.1: - resolution: {integrity: sha512-MMSKo16gB2+6CjWy55jNdIAqUEaKgw3LzZCb8wVVtFrhoQ78EXyuYXxDdn3COI3A4Xr4ZfM3fZa9RTjO6DOTxw==} - glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -4073,8 +4067,6 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 - get-url-origin@1.0.1: {} - glob-parent@5.1.2: dependencies: is-glob: 4.0.3 diff --git a/src/no-dead-link.ts b/src/no-dead-link.ts index 568b099..8610dd1 100644 --- a/src/no-dead-link.ts +++ b/src/no-dead-link.ts @@ -1,9 +1,7 @@ import { RuleHelper } from "textlint-rule-helper"; -import URL from "url"; import fs from "fs/promises"; import minimatch from "minimatch"; import { isAbsolute } from "path"; -import { getURLOrigin } from "get-url-origin"; import pMemoize from "p-memoize"; import PQueue from "p-queue"; import type { TextlintRuleReporter } from "@textlint/types"; @@ -50,8 +48,8 @@ const URI_REGEXP = * @return {boolean} */ function isHttp(uri: string) { - const { protocol } = URL.parse(uri); - return protocol === "http:" || protocol === "https:"; + const url = URL.parse(uri); + return url ? url.protocol === "http:" || url.protocol === "https:" : false; } /** @@ -61,8 +59,18 @@ function isHttp(uri: string) { * @see https://github.com/panosoft/is-local-path */ function isRelative(uri: string) { - const { host } = URL.parse(uri); - return host === null || host === ""; + const url = URL.parse(uri); + // If URL.parse returns null and it's not an absolute path, it's relative + if (!url) { + return !isAbsolute(uri); + } + // If it has a protocol but no host (except for file://), it's not relative + // URLs like mailto:, ftp:, ws: etc. have protocol but no host + if (url.protocol && url.protocol !== "file:") { + return false; + } + // file:// URLs or URLs without protocol but with no host are relative + return !url.host && !isAbsolute(uri); } /** @@ -105,7 +113,8 @@ function waitTimeMs(ms: number) { const createFetchWithRuleDefaults = (ruleOptions: Options) => { return (uri: string, fetchOptions: RequestInit) => { - const { host } = URL.parse(uri); + const url = URL.parse(uri); + const host = url?.host; return fetch(uri, { ...fetchOptions, // Some website require UserAgent and Accept header @@ -184,7 +193,8 @@ const createCheckAliveURL = (ruleOptions: Options) => { }; } const finalRes = await fetchWithDefaults(redirectedUrl, { ...opts, redirect: "follow" }); - const { hash } = URL.parse(uri); + const url = URL.parse(uri); + const hash = url?.hash || null; return { ok: finalRes.ok, redirected: true, @@ -244,7 +254,13 @@ const createCheckAliveURL = (ruleOptions: Options) => { */ async function isAliveLocalFile(filePath: string): Promise { try { - await fs.access(filePath.replace(/[?#].*?$/, "")); + // Convert file:// URL back to path if needed + let pathToCheck = filePath; + if (filePath.startsWith("file://")) { + const url = URL.parse(filePath); + pathToCheck = url ? url.pathname : filePath; + } + await fs.access(pathToCheck.replace(/[?#].*?$/, "")); return { ok: true, message: "OK" @@ -294,7 +310,12 @@ const reporter: TextlintRuleReporter = (context, options) => { } // eslint-disable-next-line no-param-reassign - uri = URL.resolve(base, uri); + // Convert file path to file:// URL if needed + const baseURL = base.startsWith("http") || base.startsWith("file://") ? base : `file://${base}`; + const resolved = URL.parse(uri, baseURL); + if (resolved) { + uri = resolved.href; + } } // Ignore non http external link @@ -304,7 +325,11 @@ const reporter: TextlintRuleReporter = (context, options) => { } const method = - ruleOptions.preferGET.filter((origin) => getURLOrigin(uri) === getURLOrigin(origin)).length > 0 + ruleOptions.preferGET.filter((origin) => { + const uriURL = URL.parse(uri); + const originURL = URL.parse(origin); + return uriURL && originURL && uriURL.origin === originURL.origin; + }).length > 0 ? "GET" : "HEAD"; From 0996208a9c7eb94cf121db3574c18399933cbe96 Mon Sep 17 00:00:00 2001 From: azu Date: Sat, 16 Aug 2025 14:12:08 +0900 Subject: [PATCH 2/2] fix: use fileURLToPath for proper cross-platform file:// URL handling Use Node.js built-in fileURLToPath function instead of manual string manipulation for converting file:// URLs to file paths. This ensures proper handling across all platforms including Windows. --- src/no-dead-link.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/no-dead-link.ts b/src/no-dead-link.ts index 8610dd1..a835820 100644 --- a/src/no-dead-link.ts +++ b/src/no-dead-link.ts @@ -2,6 +2,7 @@ import { RuleHelper } from "textlint-rule-helper"; import fs from "fs/promises"; import minimatch from "minimatch"; import { isAbsolute } from "path"; +import { fileURLToPath } from "url"; import pMemoize from "p-memoize"; import PQueue from "p-queue"; import type { TextlintRuleReporter } from "@textlint/types"; @@ -254,12 +255,9 @@ const createCheckAliveURL = (ruleOptions: Options) => { */ async function isAliveLocalFile(filePath: string): Promise { try { - // Convert file:// URL back to path if needed - let pathToCheck = filePath; - if (filePath.startsWith("file://")) { - const url = URL.parse(filePath); - pathToCheck = url ? url.pathname : filePath; - } + // Convert file:// URL to path if needed, otherwise use as-is + const pathToCheck = filePath.startsWith("file://") ? fileURLToPath(filePath) : filePath; + await fs.access(pathToCheck.replace(/[?#].*?$/, "")); return { ok: true,