Fix Bare dockerode failure: polyfill Node url.resolve
Release rolling / release (push) Has been cancelled
Release rolling / release (push) Has been cancelled
docker-modem calls url.resolve/parse/format, which bare-url does not implement. Map bare url imports to build/shims/node-url.cjs so the standalone peardock-server can talk to the Docker socket again.
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
/**
|
||||
* Node.js-compatible `url` for Bare.
|
||||
*
|
||||
* bare-url is WHATWG-only (no url.resolve / url.parse / url.format).
|
||||
* docker-modem (via dockerode) requires the legacy Node API:
|
||||
* url.resolve, url.parse, url.format, url.URL
|
||||
*
|
||||
* Mapped from package.json imports under the "bare" condition.
|
||||
*/
|
||||
'use strict'
|
||||
|
||||
let URLImpl
|
||||
let URLSearchParamsImpl
|
||||
try {
|
||||
const bare = require('bare-url')
|
||||
URLImpl = bare.URL || bare
|
||||
URLSearchParamsImpl =
|
||||
bare.URLSearchParams ||
|
||||
(typeof globalThis !== 'undefined' && globalThis.URLSearchParams) ||
|
||||
null
|
||||
} catch {
|
||||
URLImpl = globalThis.URL
|
||||
URLSearchParamsImpl = globalThis.URLSearchParams
|
||||
}
|
||||
|
||||
const URL = URLImpl
|
||||
|
||||
function isAbsoluteUrl(s) {
|
||||
return /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(s)
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal Node url.parse compatible object.
|
||||
* @param {string} urlStr
|
||||
* @param {boolean} [parseQueryString]
|
||||
*/
|
||||
function parse(urlStr, parseQueryString) {
|
||||
const input = String(urlStr ?? '')
|
||||
try {
|
||||
let u
|
||||
if (isAbsoluteUrl(input)) {
|
||||
u = new URL(input)
|
||||
} else if (input.startsWith('//')) {
|
||||
u = new URL('http:' + input)
|
||||
} else {
|
||||
// Path-only / relative — give a dummy base then re-map
|
||||
u = new URL(input, 'http://peardock.invalid')
|
||||
}
|
||||
|
||||
const dummy = u.hostname === 'peardock.invalid' && !isAbsoluteUrl(input)
|
||||
const auth =
|
||||
u.username || u.password
|
||||
? decodeURIComponent(u.username) +
|
||||
(u.password ? ':' + decodeURIComponent(u.password) : '')
|
||||
: null
|
||||
|
||||
const search = u.search || null
|
||||
let query = null
|
||||
if (search) {
|
||||
if (parseQueryString && URLSearchParamsImpl) {
|
||||
query = Object.fromEntries(new URLSearchParamsImpl(u.searchParams || search))
|
||||
} else {
|
||||
query = search.startsWith('?') ? search.slice(1) : search
|
||||
}
|
||||
}
|
||||
|
||||
const pathname = u.pathname || null
|
||||
const path = (pathname || '') + (search || '') || null
|
||||
|
||||
return {
|
||||
protocol: dummy ? null : u.protocol || null,
|
||||
slashes: dummy ? null : true,
|
||||
auth: dummy ? null : auth,
|
||||
host: dummy ? null : u.host || null,
|
||||
port: dummy ? null : u.port || null,
|
||||
hostname: dummy ? null : u.hostname || null,
|
||||
hash: u.hash || null,
|
||||
search,
|
||||
query,
|
||||
pathname,
|
||||
path,
|
||||
href: dummy ? input : u.href,
|
||||
}
|
||||
} catch {
|
||||
return {
|
||||
protocol: null,
|
||||
slashes: null,
|
||||
auth: null,
|
||||
host: null,
|
||||
port: null,
|
||||
hostname: null,
|
||||
hash: null,
|
||||
search: null,
|
||||
query: null,
|
||||
pathname: null,
|
||||
path: null,
|
||||
href: input,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal Node url.format.
|
||||
* @param {string|object} urlObj
|
||||
*/
|
||||
function format(urlObj) {
|
||||
if (urlObj == null) return ''
|
||||
if (typeof urlObj === 'string') return urlObj
|
||||
if (typeof URLImpl === 'function' && urlObj instanceof URLImpl) return urlObj.href
|
||||
|
||||
let protocol = urlObj.protocol || ''
|
||||
if (protocol && protocol[protocol.length - 1] !== ':') protocol += ':'
|
||||
|
||||
let auth = ''
|
||||
if (urlObj.auth) {
|
||||
auth = String(urlObj.auth).replace(/@/g, '') + '@'
|
||||
}
|
||||
|
||||
let host = ''
|
||||
if (urlObj.host != null && urlObj.host !== '') {
|
||||
host = String(urlObj.host)
|
||||
} else if (urlObj.hostname != null && urlObj.hostname !== '') {
|
||||
host = String(urlObj.hostname)
|
||||
if (urlObj.port != null && urlObj.port !== '') host += ':' + urlObj.port
|
||||
}
|
||||
|
||||
let pathname = urlObj.pathname != null ? String(urlObj.pathname) : ''
|
||||
if (pathname && pathname[0] !== '/' && host) pathname = '/' + pathname
|
||||
|
||||
let search = ''
|
||||
if (urlObj.search != null && urlObj.search !== '') {
|
||||
search = String(urlObj.search)
|
||||
if (search[0] !== '?') search = '?' + search
|
||||
} else if (urlObj.query != null && urlObj.query !== '') {
|
||||
if (typeof urlObj.query === 'string') {
|
||||
search = urlObj.query[0] === '?' ? urlObj.query : '?' + urlObj.query
|
||||
} else if (typeof urlObj.query === 'object' && URLSearchParamsImpl) {
|
||||
const q = new URLSearchParamsImpl(urlObj.query).toString()
|
||||
if (q) search = '?' + q
|
||||
}
|
||||
}
|
||||
|
||||
let hash = urlObj.hash != null ? String(urlObj.hash) : ''
|
||||
if (hash && hash[0] !== '#') hash = '#' + hash
|
||||
|
||||
// Match Node quirks roughly: with host → scheme://host/path; without → scheme:/path
|
||||
if (host) {
|
||||
const slashes = protocol ? '//' : ''
|
||||
return protocol + slashes + auth + host + pathname + search + hash
|
||||
}
|
||||
if (protocol) {
|
||||
// Node: url.format({ protocol: 'http:', pathname: '/x' }) => 'http:/x'
|
||||
return protocol + (pathname.startsWith('/') ? pathname : '/' + pathname) + search + hash
|
||||
}
|
||||
return pathname + search + hash
|
||||
}
|
||||
|
||||
/**
|
||||
* Node url.resolve(from, to)
|
||||
* @param {string} from
|
||||
* @param {string} to
|
||||
*/
|
||||
function resolve(from, to) {
|
||||
const base = String(from ?? '')
|
||||
const rel = String(to ?? '')
|
||||
|
||||
if (!rel) {
|
||||
try {
|
||||
return isAbsoluteUrl(base) ? new URL(base).href : format(parse(base))
|
||||
} catch {
|
||||
return base
|
||||
}
|
||||
}
|
||||
|
||||
if (isAbsoluteUrl(rel)) {
|
||||
try {
|
||||
return new URL(rel).href
|
||||
} catch {
|
||||
return rel
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (isAbsoluteUrl(base)) {
|
||||
return new URL(rel, base).href
|
||||
}
|
||||
// Path-only base (e.g. "/v1.41/" + "containers/json")
|
||||
const origin = 'http://peardock.invalid'
|
||||
const b = base.startsWith('/') ? base : '/' + base
|
||||
const resolved = new URL(rel, origin + b)
|
||||
return resolved.pathname + resolved.search + resolved.hash
|
||||
} catch {
|
||||
if (rel.startsWith('/')) return rel
|
||||
const slash = base.endsWith('/') ? '' : '/'
|
||||
return base + slash + rel
|
||||
}
|
||||
}
|
||||
|
||||
function pathToFileURL(filepath) {
|
||||
if (typeof URLImpl !== 'function') throw new Error('URL not available')
|
||||
const path = require('path')
|
||||
let resolved = path.resolve(String(filepath))
|
||||
// Windows drive letters
|
||||
if (/^[a-zA-Z]:[\\/]/.test(resolved)) {
|
||||
resolved = '/' + resolved.replace(/\\/g, '/')
|
||||
} else if (!resolved.startsWith('/')) {
|
||||
resolved = '/' + resolved
|
||||
}
|
||||
return new URLImpl('file://' + encodeURI(resolved).replace(/#/g, '%23'))
|
||||
}
|
||||
|
||||
function fileURLToPath(url) {
|
||||
const u = typeof url === 'string' ? new URLImpl(url) : url
|
||||
if (u.protocol !== 'file:') {
|
||||
throw new TypeError('Must be a file URL')
|
||||
}
|
||||
let p = decodeURIComponent(u.pathname)
|
||||
// /C:/... → C:\... on windows-like
|
||||
if (/^\/[a-zA-Z]:\//.test(p)) {
|
||||
p = p.slice(1).replace(/\//g, require('path').sep)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
function domainToASCII(domain) {
|
||||
return String(domain ?? '')
|
||||
}
|
||||
|
||||
function domainToUnicode(domain) {
|
||||
return String(domain ?? '')
|
||||
}
|
||||
|
||||
function urlToHttpOptions(url) {
|
||||
const u = typeof url === 'string' ? new URLImpl(url) : url
|
||||
return {
|
||||
protocol: u.protocol,
|
||||
hostname: u.hostname,
|
||||
hash: u.hash,
|
||||
search: u.search,
|
||||
pathname: u.pathname,
|
||||
path: u.pathname + u.search,
|
||||
href: u.href,
|
||||
port: u.port,
|
||||
auth: u.username ? u.username + (u.password ? ':' + u.password : '') : '',
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
URL,
|
||||
Url: URL,
|
||||
URLSearchParams: URLSearchParamsImpl,
|
||||
parse,
|
||||
format,
|
||||
resolve,
|
||||
pathToFileURL,
|
||||
fileURLToPath,
|
||||
domainToASCII,
|
||||
domainToUnicode,
|
||||
urlToHttpOptions,
|
||||
}
|
||||
+2
-2
@@ -408,11 +408,11 @@
|
||||
"default": "tty"
|
||||
},
|
||||
"url": {
|
||||
"bare": "bare-url",
|
||||
"bare": "./build/shims/node-url.cjs",
|
||||
"default": "url"
|
||||
},
|
||||
"node:url": {
|
||||
"bare": "bare-url",
|
||||
"bare": "./build/shims/node-url.cjs",
|
||||
"default": "url"
|
||||
},
|
||||
"util": {
|
||||
|
||||
@@ -82,6 +82,18 @@ function buildImportsMap() {
|
||||
default: typeof map[spec] === 'object' && map[spec]?.default ? map[spec].default : spec,
|
||||
}
|
||||
}
|
||||
|
||||
// bare-pack resolves relative "bare" import targets poorly from nested
|
||||
// node_modules — absolutize repo-local shims (e.g. ./build/shims/node-url.cjs).
|
||||
for (const [spec, target] of Object.entries(map)) {
|
||||
if (!target || typeof target !== 'object') continue
|
||||
if (typeof target.bare === 'string' && target.bare.startsWith('./')) {
|
||||
map[spec] = {
|
||||
...target,
|
||||
bare: fileURL(target.bare.replace(/^\.\//, '')),
|
||||
}
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Ensure Bare Node url shim matches Node for docker-modem call patterns.
|
||||
*/
|
||||
import test from 'brittle'
|
||||
import { createRequire } from 'module'
|
||||
const require = createRequire(import.meta.url)
|
||||
const nodeUrl = require('url')
|
||||
const shim = require('../build/shims/node-url.cjs')
|
||||
|
||||
test('url.resolve matches Node for docker-modem patterns', (t) => {
|
||||
const cases = [
|
||||
['http://localhost:2375/v1.41/', 'containers/json'],
|
||||
['http://localhost:2375/v1.41', 'containers/json'],
|
||||
['http://localhost/v1.41/', '/v1.41/containers/json'],
|
||||
['/v1.41/', 'containers/json'],
|
||||
]
|
||||
for (const [a, b] of cases) {
|
||||
t.is(shim.resolve(a, b), nodeUrl.resolve(a, b), `${a} + ${b}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('url.parse provides host/path/protocol', (t) => {
|
||||
const p = shim.parse('http://localhost:2375/v1.41/containers/json')
|
||||
t.is(p.protocol, 'http:')
|
||||
t.is(p.host, 'localhost:2375')
|
||||
t.is(p.path, '/v1.41/containers/json')
|
||||
})
|
||||
|
||||
test('url.format builds request URLs', (t) => {
|
||||
t.is(
|
||||
shim.format({
|
||||
protocol: 'http:',
|
||||
hostname: 'localhost',
|
||||
port: '2375',
|
||||
pathname: '/v1.41/x',
|
||||
}),
|
||||
'http://localhost:2375/v1.41/x'
|
||||
)
|
||||
t.ok(typeof shim.URL === 'function')
|
||||
})
|
||||
Reference in New Issue
Block a user