create-mercato-app 0.5.1-develop.2965.38737e655d → 0.5.1-develop.2972.6c5cd4a1c3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Reusable URL helpers for dev splash variants (monorepo dev orchestrator,
|
|
2
|
+
// ephemeral dev runtime, and the standalone create-app template). Pure ESM,
|
|
3
|
+
// dependency-free so the dev scripts can use it before `yarn install` runs.
|
|
4
|
+
//
|
|
5
|
+
// All splash variants share the same problem: they used to hardcode
|
|
6
|
+
// `http://localhost:<port>` / `http://127.0.0.1:<port>` for both the
|
|
7
|
+
// splash URL and the printed app URL. When a developer runs the dev
|
|
8
|
+
// runtime behind a reverse proxy (for example on
|
|
9
|
+
// `https://devsandbox.openmercato.com`), the printed URLs and any
|
|
10
|
+
// redirects must follow the configured public base URL, drop standard
|
|
11
|
+
// ports for the scheme (80 for `http:`, 443 for `https:`), and only
|
|
12
|
+
// fall back to the actually-bound port when the configured port was
|
|
13
|
+
// taken and the runtime had to randomize.
|
|
14
|
+
|
|
15
|
+
const STANDARD_PORT_BY_SCHEME = Object.freeze({
|
|
16
|
+
'http:': 80,
|
|
17
|
+
'https:': 443,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '0.0.0.0'])
|
|
21
|
+
|
|
22
|
+
export function parsePortNumber(value) {
|
|
23
|
+
if (typeof value !== 'string' && typeof value !== 'number') return null
|
|
24
|
+
const parsed = Number.parseInt(String(value).trim(), 10)
|
|
25
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
return parsed
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isStandardPort(scheme, port) {
|
|
32
|
+
if (port === null || port === undefined) return false
|
|
33
|
+
const normalized = typeof scheme === 'string' && !scheme.endsWith(':') ? `${scheme}:` : scheme
|
|
34
|
+
const expected = STANDARD_PORT_BY_SCHEME[normalized]
|
|
35
|
+
return expected !== undefined && Number(port) === expected
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function parseConfiguredBaseUrl(value) {
|
|
39
|
+
if (typeof value !== 'string') return null
|
|
40
|
+
const trimmed = value.trim()
|
|
41
|
+
if (!trimmed) return null
|
|
42
|
+
let parsed
|
|
43
|
+
try {
|
|
44
|
+
parsed = new URL(trimmed)
|
|
45
|
+
} catch {
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return null
|
|
49
|
+
const explicitPort = parsed.port ? Number.parseInt(parsed.port, 10) : null
|
|
50
|
+
return {
|
|
51
|
+
protocol: parsed.protocol,
|
|
52
|
+
hostname: parsed.hostname,
|
|
53
|
+
port: Number.isInteger(explicitPort) ? explicitPort : null,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function formatBaseUrl({ protocol, hostname, port } = {}, options = {}) {
|
|
58
|
+
if (typeof protocol !== 'string' || !protocol || typeof hostname !== 'string' || !hostname) {
|
|
59
|
+
throw new TypeError('formatBaseUrl requires a protocol and hostname')
|
|
60
|
+
}
|
|
61
|
+
const normalizedScheme = protocol.endsWith(':') ? protocol : `${protocol}:`
|
|
62
|
+
const includeStandardPort = options.includeStandardPort === true
|
|
63
|
+
const formattedHost = hostname.includes(':') && !hostname.startsWith('[')
|
|
64
|
+
? `[${hostname}]`
|
|
65
|
+
: hostname
|
|
66
|
+
if (port === null || port === undefined) {
|
|
67
|
+
return `${normalizedScheme}//${formattedHost}`
|
|
68
|
+
}
|
|
69
|
+
if (!includeStandardPort && isStandardPort(normalizedScheme, port)) {
|
|
70
|
+
return `${normalizedScheme}//${formattedHost}`
|
|
71
|
+
}
|
|
72
|
+
return `${normalizedScheme}//${formattedHost}:${port}`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function pickConfiguredFromEnv(env) {
|
|
76
|
+
return parseConfiguredBaseUrl(env?.APP_URL) ?? parseConfiguredBaseUrl(env?.NEXT_PUBLIC_APP_URL)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Resolve the developer-facing app base URL.
|
|
80
|
+
//
|
|
81
|
+
// Inputs:
|
|
82
|
+
// env: process.env-like object. Reads APP_URL, NEXT_PUBLIC_APP_URL, PORT.
|
|
83
|
+
// options.actualPort: the port the dev server actually bound to. When the
|
|
84
|
+
// configured port was already in use, this differs from the configured
|
|
85
|
+
// value -- in that case we treat the run as randomized and surface the
|
|
86
|
+
// actually-bound port so the printed URL is reachable.
|
|
87
|
+
// options.defaultPort: the port to assume when nothing else is configured
|
|
88
|
+
// (defaults to 3000, matching Next.js dev defaults).
|
|
89
|
+
// options.defaultHostname: fallback hostname when no APP_URL is configured
|
|
90
|
+
// (defaults to 'localhost').
|
|
91
|
+
//
|
|
92
|
+
// Returns:
|
|
93
|
+
// { url, protocol, hostname, port, hasConfiguredBaseUrl, portWasRandomized }
|
|
94
|
+
// `port` is null when the URL omits an explicit port.
|
|
95
|
+
export function resolveDevBaseUrl(env = {}, options = {}) {
|
|
96
|
+
const actualPort = Number.isInteger(options.actualPort) ? options.actualPort : null
|
|
97
|
+
const defaultPort = Number.isInteger(options.defaultPort) ? options.defaultPort : 3000
|
|
98
|
+
const defaultHostname = typeof options.defaultHostname === 'string' && options.defaultHostname
|
|
99
|
+
? options.defaultHostname
|
|
100
|
+
: 'localhost'
|
|
101
|
+
|
|
102
|
+
const configured = pickConfiguredFromEnv(env)
|
|
103
|
+
const envPort = parsePortNumber(env?.PORT)
|
|
104
|
+
|
|
105
|
+
let protocol
|
|
106
|
+
let hostname
|
|
107
|
+
let configuredPort
|
|
108
|
+
|
|
109
|
+
if (configured) {
|
|
110
|
+
protocol = configured.protocol
|
|
111
|
+
hostname = configured.hostname
|
|
112
|
+
configuredPort = configured.port ?? envPort
|
|
113
|
+
} else {
|
|
114
|
+
protocol = 'http:'
|
|
115
|
+
hostname = defaultHostname
|
|
116
|
+
configuredPort = envPort
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let resolvedPort
|
|
120
|
+
let portWasRandomized = false
|
|
121
|
+
const isLoopbackHost = LOOPBACK_HOSTS.has(hostname.toLowerCase())
|
|
122
|
+
|
|
123
|
+
if (actualPort !== null) {
|
|
124
|
+
if (configured && !isLoopbackHost) {
|
|
125
|
+
// Proxy-fronted dev: the developer reaches the app via the configured
|
|
126
|
+
// public URL. `actualPort` is the internal port the local dev server
|
|
127
|
+
// bound to, which is hidden behind the proxy and must never leak into
|
|
128
|
+
// the printed URL -- regardless of whether the configured URL declared
|
|
129
|
+
// an explicit port or not.
|
|
130
|
+
resolvedPort = configuredPort
|
|
131
|
+
} else if (configuredPort !== null && configuredPort !== actualPort) {
|
|
132
|
+
// Loopback dev: the configured port was taken and the runtime fell back
|
|
133
|
+
// to a free one. The printed URL must reflect what the developer can
|
|
134
|
+
// actually open.
|
|
135
|
+
resolvedPort = actualPort
|
|
136
|
+
portWasRandomized = true
|
|
137
|
+
} else if (configuredPort !== null) {
|
|
138
|
+
resolvedPort = configuredPort
|
|
139
|
+
} else {
|
|
140
|
+
resolvedPort = actualPort
|
|
141
|
+
}
|
|
142
|
+
} else if (configuredPort !== null) {
|
|
143
|
+
resolvedPort = configuredPort
|
|
144
|
+
} else if (configured) {
|
|
145
|
+
resolvedPort = null
|
|
146
|
+
} else {
|
|
147
|
+
resolvedPort = defaultPort
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (resolvedPort !== null && isStandardPort(protocol, resolvedPort)) {
|
|
151
|
+
resolvedPort = null
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const url = formatBaseUrl({ protocol, hostname, port: resolvedPort })
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
url,
|
|
158
|
+
protocol,
|
|
159
|
+
hostname,
|
|
160
|
+
port: resolvedPort,
|
|
161
|
+
hasConfiguredBaseUrl: configured !== null,
|
|
162
|
+
portWasRandomized,
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Resolve the URL where the dev splash itself can be reached. The splash
|
|
167
|
+
// process always binds locally, but the developer's browser may live on
|
|
168
|
+
// the configured public host (proxy-fronted dev sandboxes). We keep the
|
|
169
|
+
// scheme + hostname from the configured public URL when it exists so the
|
|
170
|
+
// splash link the developer sees uses the same origin as the rest of the
|
|
171
|
+
// app, and we always attach the actually-bound splash port so the link
|
|
172
|
+
// resolves regardless of what the configured port was.
|
|
173
|
+
export function resolveSplashUrl(env = {}, splashPort, options = {}) {
|
|
174
|
+
const port = Number.isInteger(splashPort) ? splashPort : null
|
|
175
|
+
const configured = pickConfiguredFromEnv(env)
|
|
176
|
+
const defaultHostname = typeof options.defaultHostname === 'string' && options.defaultHostname
|
|
177
|
+
? options.defaultHostname
|
|
178
|
+
: 'localhost'
|
|
179
|
+
|
|
180
|
+
if (!configured) {
|
|
181
|
+
if (port === null) {
|
|
182
|
+
return formatBaseUrl({ protocol: 'http:', hostname: defaultHostname, port: null })
|
|
183
|
+
}
|
|
184
|
+
return formatBaseUrl({ protocol: 'http:', hostname: defaultHostname, port })
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (port === null) {
|
|
188
|
+
return formatBaseUrl({ protocol: configured.protocol, hostname: configured.hostname, port: null })
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return formatBaseUrl({ protocol: configured.protocol, hostname: configured.hostname, port })
|
|
192
|
+
}
|
package/template/scripts/dev.mjs
CHANGED
|
@@ -26,6 +26,10 @@ import { resolveSpawnCommand } from './dev-spawn-utils.mjs'
|
|
|
26
26
|
import { createDevSplashCodingFlow } from './dev-splash-coding-flow.mjs'
|
|
27
27
|
import { createDevSplashGitRepoFlow } from './dev-splash-git-repo-flow.mjs'
|
|
28
28
|
import { normalizeSplashDisplayState } from './dev-splash-state.mjs'
|
|
29
|
+
import {
|
|
30
|
+
resolveDevBaseUrl,
|
|
31
|
+
resolveSplashUrl as resolveSplashAccessUrl,
|
|
32
|
+
} from './dev-splash-url.mjs'
|
|
29
33
|
|
|
30
34
|
function detectDevRuntimeMode() {
|
|
31
35
|
const cwd = process.cwd()
|
|
@@ -155,20 +159,6 @@ function shouldRetrySplashServerWithRandomPort(error) {
|
|
|
155
159
|
return error.code === 'EADDRINUSE'
|
|
156
160
|
}
|
|
157
161
|
|
|
158
|
-
function normalizePublicBaseUrl(value) {
|
|
159
|
-
if (typeof value !== 'string' || value.trim().length === 0) return null
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
const parsed = new URL(value)
|
|
163
|
-
parsed.pathname = ''
|
|
164
|
-
parsed.search = ''
|
|
165
|
-
parsed.hash = ''
|
|
166
|
-
return parsed.toString().replace(/\/$/, '')
|
|
167
|
-
} catch {
|
|
168
|
-
return null
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
162
|
function isEnabledEnvFlag(value) {
|
|
173
163
|
if (typeof value !== 'string') return false
|
|
174
164
|
return ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase())
|
|
@@ -303,9 +293,7 @@ function formatProgressLine(label, current, total, percent) {
|
|
|
303
293
|
}
|
|
304
294
|
|
|
305
295
|
function resolveExpectedAppBaseUrl() {
|
|
306
|
-
return
|
|
307
|
-
?? normalizePublicBaseUrl(process.env.NEXT_PUBLIC_APP_URL)
|
|
308
|
-
?? `http://localhost:${parsePortNumber(process.env.PORT) ?? 3000}`
|
|
296
|
+
return resolveDevBaseUrl(process.env).url
|
|
309
297
|
}
|
|
310
298
|
|
|
311
299
|
function resolveExpectedBackendUrl() {
|
|
@@ -924,7 +912,7 @@ async function startSplashServer() {
|
|
|
924
912
|
|
|
925
913
|
const address = splashServer.address()
|
|
926
914
|
if (!address || typeof address === 'string') return
|
|
927
|
-
splashUrl =
|
|
915
|
+
splashUrl = resolveSplashAccessUrl(process.env, address.port)
|
|
928
916
|
if (splashPortConfig.port !== 0 && address.port !== splashPortConfig.port) {
|
|
929
917
|
console.log(`🪟 Dev splash moved to ${splashUrl}`)
|
|
930
918
|
}
|