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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-mercato-app",
3
- "version": "0.5.1-develop.2965.38737e655d",
3
+ "version": "0.5.1-develop.2972.6c5cd4a1c3",
4
4
  "type": "module",
5
5
  "description": "Create a new Open Mercato application",
6
6
  "main": "./dist/index.js",
@@ -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
+ }
@@ -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 normalizePublicBaseUrl(process.env.APP_URL)
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 = `http://localhost:${address.port}`
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
  }