@octane-xplat/cli 0.3.0 → 0.5.0
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/README.md +35 -0
- package/package.json +11 -5
- package/src/cli.mjs +10 -9
- package/src/commands/build.mjs +47 -25
- package/src/commands/clean.mjs +22 -12
- package/src/commands/dev.mjs +47 -22
- package/src/commands/doctor.mjs +265 -33
- package/src/commands/routes.mjs +203 -0
- package/src/commands/typecheck.mjs +22 -11
- package/src/procs.mjs +22 -16
- package/src/targets.mjs +62 -30
- package/src/vite.d.ts +20 -0
- package/src/vite.mjs +286 -0
package/src/vite.mjs
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// @octane-xplat/cli/vite — the shared native Vite preset.
|
|
2
|
+
//
|
|
3
|
+
// Everything in here was previously per-app boilerplate (copied between
|
|
4
|
+
// vite.config.native.mts files) or an app-owned workaround: the nativescript
|
|
5
|
+
// renderer rules, the octane→universal/native alias, the platform-suffix
|
|
6
|
+
// extension chain, the deps-bundle plugin exclusions, the HMR watchdog, and
|
|
7
|
+
// the px→dip CSS rewrite. Apps now write:
|
|
8
|
+
//
|
|
9
|
+
// import { defineConfig } from 'vite'
|
|
10
|
+
// import { xplatNative } from '@octane-xplat/cli/vite'
|
|
11
|
+
// export default defineConfig(({ mode }) => xplatNative(mode))
|
|
12
|
+
//
|
|
13
|
+
// The preset calls octaneConfig itself — consumers only merge in
|
|
14
|
+
// app-specific extras via the `extra` option or a vite mergeConfig wrapper.
|
|
15
|
+
|
|
16
|
+
import { createRequire } from 'node:module'
|
|
17
|
+
import { realpathSync } from 'node:fs'
|
|
18
|
+
import { join } from 'node:path'
|
|
19
|
+
import { pathToFileURL } from 'node:url'
|
|
20
|
+
|
|
21
|
+
// The toolchain modules (vite, vite-octane, the nativescript renderer)
|
|
22
|
+
// belong to the CONSUMING app, not to this package — under pnpm's isolated
|
|
23
|
+
// linker a bare import here would miss them entirely. createRequire from
|
|
24
|
+
// the app's cwd makes the preset use the app's own pinned versions.
|
|
25
|
+
// realpathSync matters: resolve() returns the symlinked node_modules path
|
|
26
|
+
// and ESM import() does not realpath — loading vite through the symlink
|
|
27
|
+
// leaves its internal 'rolldown' bare-import stranded outside its .pnpm
|
|
28
|
+
// peer dir.
|
|
29
|
+
const req = createRequire(join(process.cwd(), 'package.json'))
|
|
30
|
+
const importApp = (spec) => import(pathToFileURL(realpathSync(req.resolve(spec))).href)
|
|
31
|
+
|
|
32
|
+
/** CSS that NS parses but silently ignores or misreads — the app looks
|
|
33
|
+
* identical in source but diverges at runtime. Warned at build time so the
|
|
34
|
+
* divergence is loud instead of invisible. Each entry: pattern + the
|
|
35
|
+
* portable alternative. */
|
|
36
|
+
const CSS_DIVERGENCES = [
|
|
37
|
+
[
|
|
38
|
+
/margin-(?:left|right|top|bottom)\s*:\s*auto|margin\s*:[^;{}]*\bauto\b/,
|
|
39
|
+
'auto margins are ignored on native — use justify-content, alignSelf, or a <Spacer/>',
|
|
40
|
+
],
|
|
41
|
+
[
|
|
42
|
+
/position\s*:\s*(fixed|sticky)\b/,
|
|
43
|
+
'position: fixed/sticky does not exist on native — overlays go through Overlay/Modal services, not positioning',
|
|
44
|
+
],
|
|
45
|
+
[/\bz-index\s*:/, 'z-index is inert on native — paint order follows document order'],
|
|
46
|
+
[/\bfloat\s*:/, 'float is unsupported on native — use flex rows'],
|
|
47
|
+
[
|
|
48
|
+
/\bbox-shadow\s*:/,
|
|
49
|
+
'box-shadow is inert on native — Android elevation and iOS shadows do not map to it (framework mapping is a TODO)',
|
|
50
|
+
],
|
|
51
|
+
[
|
|
52
|
+
/white-space\s*:\s*pre-wrap\b/,
|
|
53
|
+
'Label rejects white-space:pre-wrap — "wrap" is the native wrap value (no space/newline preservation)',
|
|
54
|
+
],
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Shared stylesheets are authored in web units and web semantics. For the
|
|
59
|
+
* native bundle this transform (a) rewrites `px` lengths to `dip` — NS CSS
|
|
60
|
+
* reads `px` as _device_ pixels, not dips (`width:88px` measures 29 dips on
|
|
61
|
+
* a 3x device); inline `style` props are already dips and untouched — and
|
|
62
|
+
* (b) warns once per file on declarations NS silently ignores, so the
|
|
63
|
+
* divergence is loud at build time.
|
|
64
|
+
*/
|
|
65
|
+
function pxToDip() {
|
|
66
|
+
const warned = new Set()
|
|
67
|
+
const process = (code, id, warn) => {
|
|
68
|
+
// Framework authors mark web-only rule blocks — overlays, popovers,
|
|
69
|
+
// dialog modals render through RootLayout/showModal natively, so
|
|
70
|
+
// their CSS is dead weight (and would trip the divergence warnings).
|
|
71
|
+
// Stripped before the warn pass.
|
|
72
|
+
code = code.replace(
|
|
73
|
+
/\/\*\s*xplat-web-only:start[\s\S]*?\*\/[\s\S]*?\/\*\s*xplat-web-only:end[\s\S]*?\*\//g,
|
|
74
|
+
'',
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
for (const [re, hint] of CSS_DIVERGENCES) {
|
|
78
|
+
const key = id + '|' + hint
|
|
79
|
+
if (re.test(code) && !warned.has(key)) {
|
|
80
|
+
warned.add(key)
|
|
81
|
+
warn(`${id}: ${hint}`)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return code.replace(/(-?\d+(?:\.\d+)?)px\b/g, '$1dip')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
name: 'xplat-native-css',
|
|
90
|
+
enforce: 'pre',
|
|
91
|
+
// Per-file pass — covers dev serving where css is transformed
|
|
92
|
+
// per module (the /ns/m bridge path).
|
|
93
|
+
transform(code, id) {
|
|
94
|
+
if (!id.split('?')[0].endsWith('.css')) {
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return process(code, id, (m) => this.warn(m))
|
|
99
|
+
},
|
|
100
|
+
// Build pass — @nativescript/vite collects emitted .css assets in
|
|
101
|
+
// generateBundle and serializes them via addTaggedAdditionalCSS;
|
|
102
|
+
// @import inlining bypasses the transform hook, so the asset text
|
|
103
|
+
// must be rewritten here. 'pre' ordering lands us before it.
|
|
104
|
+
generateBundle(_opts, bundle) {
|
|
105
|
+
for (const file of Object.values(bundle)) {
|
|
106
|
+
if (file.type === 'asset' && file.fileName.endsWith('.css')) {
|
|
107
|
+
const src =
|
|
108
|
+
typeof file.source === 'string' ? file.source : new TextDecoder().decode(file.source)
|
|
109
|
+
|
|
110
|
+
file.source = process(src, file.fileName, (m) => this.warn(m))
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* On-device HMR needs the app's websocket client to attach to /ns-hmr after
|
|
119
|
+
* the HTTP boot. When it never does — the websockets polyfill missing from
|
|
120
|
+
* the bundle, `adb reverse` not covering the vite port, or a boot error
|
|
121
|
+
* before the client import — every save logs `recipients=0` and the device
|
|
122
|
+
* silently stays stale. Warn once when a dev session was fetched but no
|
|
123
|
+
* client ever attached.
|
|
124
|
+
*/
|
|
125
|
+
function nsHmrClientWatchdog() {
|
|
126
|
+
return {
|
|
127
|
+
name: 'xplat-ns-hmr-client-watchdog',
|
|
128
|
+
configureServer(server) {
|
|
129
|
+
let everConnected = false
|
|
130
|
+
let timer
|
|
131
|
+
// Hook the raw 'request' event — middlewares.use() appends after
|
|
132
|
+
// the ns plugin's session handler, which ends the response without
|
|
133
|
+
// next(), so a connect middleware never observes /__ns_dev__/session.
|
|
134
|
+
server.httpServer?.on('request', (req) => {
|
|
135
|
+
if (!everConnected && req.url?.startsWith('/__ns_dev__/session')) {
|
|
136
|
+
clearTimeout(timer)
|
|
137
|
+
timer = setTimeout(() => {
|
|
138
|
+
if (!everConnected) {
|
|
139
|
+
console.warn(
|
|
140
|
+
'[xplat] the app fetched its dev session but no /ns-hmr ' +
|
|
141
|
+
'websocket client connected — edits will not reach the ' +
|
|
142
|
+
'device. Check that @valor/nativescript-websockets is ' +
|
|
143
|
+
'installed, `adb reverse tcp:<port>` covers this vite ' +
|
|
144
|
+
'port (physical Android), and the device log for ' +
|
|
145
|
+
'hmr-client errors.',
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
}, 15_000)
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
server.httpServer?.on('upgrade', (req) => {
|
|
153
|
+
if (req.url?.startsWith('/ns-hmr')) {
|
|
154
|
+
everConnected = true
|
|
155
|
+
clearTimeout(timer)
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The full extension chain, most-specific first: .ios/.android → .native →
|
|
163
|
+
* shared. NS's own file qualifiers (.land, .minWH600…) still apply to
|
|
164
|
+
* assets on top of this. */
|
|
165
|
+
export const nativeExtensions = [
|
|
166
|
+
'.ios.tsrx',
|
|
167
|
+
'.android.tsrx',
|
|
168
|
+
'.native.tsrx',
|
|
169
|
+
'.tsrx',
|
|
170
|
+
'.ios.tsx',
|
|
171
|
+
'.android.tsx',
|
|
172
|
+
'.native.tsx',
|
|
173
|
+
'.tsx',
|
|
174
|
+
'.ios.ts',
|
|
175
|
+
'.android.ts',
|
|
176
|
+
'.native.ts',
|
|
177
|
+
'.mjs',
|
|
178
|
+
'.mts',
|
|
179
|
+
'.ts',
|
|
180
|
+
'.jsx',
|
|
181
|
+
'.js',
|
|
182
|
+
'.json',
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
/** Default renderer rules: every component file the native graph can reach —
|
|
186
|
+
* src plus linked package source — compiles under the nativescript
|
|
187
|
+
* renderer. `.web.*` leaves legitimately use DOM globals; they're
|
|
188
|
+
* unreachable from the native entry but must not fail validation, so each
|
|
189
|
+
* rule excludes them. */
|
|
190
|
+
const nativeRules = [
|
|
191
|
+
{
|
|
192
|
+
include: 'src/**/*.{ts,tsx,tsrx}',
|
|
193
|
+
exclude: 'src/**/*.web.*',
|
|
194
|
+
renderer: 'nativescript',
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
include: '**/packages/**/*.{ts,tsx,tsrx}',
|
|
198
|
+
exclude: '**/*.web.*',
|
|
199
|
+
renderer: 'nativescript',
|
|
200
|
+
},
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Native (iOS/Android) Vite config. `env` is defineConfig's { mode }; `extra`
|
|
205
|
+
* is merged in last for app-specific additions (own plugins, extra
|
|
206
|
+
* optimizeDeps, server options).
|
|
207
|
+
*
|
|
208
|
+
* opts:
|
|
209
|
+
* - deps: extra optimizeDeps.exclude entries (app-shipped NS plugins)
|
|
210
|
+
* - rules: renderer rules override (defaults cover src + packages source)
|
|
211
|
+
*/
|
|
212
|
+
export async function xplatNative(env, opts = {}) {
|
|
213
|
+
const mode = typeof env === 'string' ? env : env.mode
|
|
214
|
+
const [{ mergeConfig }, { octaneConfig }, { nativeScriptRenderer }] = await Promise.all([
|
|
215
|
+
importApp('vite'),
|
|
216
|
+
importApp('@nativescript-community/vite-octane'),
|
|
217
|
+
importApp('@nativescript-community/octane/config'),
|
|
218
|
+
])
|
|
219
|
+
|
|
220
|
+
return mergeConfig(
|
|
221
|
+
octaneConfig(
|
|
222
|
+
{ mode },
|
|
223
|
+
{
|
|
224
|
+
octane: {
|
|
225
|
+
renderers: {
|
|
226
|
+
// The stock renderer ships validation.forbiddenGlobals/Imports
|
|
227
|
+
// by default since 0.2.1 (upstream #6).
|
|
228
|
+
registry: { nativescript: nativeScriptRenderer },
|
|
229
|
+
rules: opts.rules ?? nativeRules,
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
),
|
|
234
|
+
{
|
|
235
|
+
plugins: [pxToDip(), nsHmrClientWatchdog()],
|
|
236
|
+
build: {
|
|
237
|
+
rolldownOptions: {
|
|
238
|
+
// Dev/HMR universal emit retains JSX in expression props
|
|
239
|
+
// (e.g. `renderItem={(item) => <gridlayout>…}`) for the file's
|
|
240
|
+
// own @jsxImportSource pragma to lower. Rolldown only parses
|
|
241
|
+
// JSX in script-lang modules, so mark .tsrx transform
|
|
242
|
+
// output tsx.
|
|
243
|
+
moduleTypes: { '.tsrx': 'tsx' },
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
optimizeDeps: {
|
|
247
|
+
// vite-octane excludes `octane` from the deps bundle, so the
|
|
248
|
+
// optimizeDeps graph walk never descends into it — alien-signals
|
|
249
|
+
// (the signal impl imported by octane/universal/native) is missed
|
|
250
|
+
// and the device fetch 504s. Seed it directly.
|
|
251
|
+
include: ['alien-signals', 'alien-signals/system'],
|
|
252
|
+
// Flattened optimizeDeps chunks get mangled by the /ns/m device
|
|
253
|
+
// transform (`import import "/ns/core/utils"`) and miss the vendor
|
|
254
|
+
// manifest — serve @nativescript plugins per-module instead.
|
|
255
|
+
exclude: [
|
|
256
|
+
'@nativescript/biometrics',
|
|
257
|
+
'@nativescript/geolocation',
|
|
258
|
+
'@nativescript/haptics',
|
|
259
|
+
'@nativescript/imagepicker',
|
|
260
|
+
'@nativescript/local-notifications',
|
|
261
|
+
'@nativescript-community/ui-document-picker',
|
|
262
|
+
'@nativescript/secure-storage',
|
|
263
|
+
'@nativescript/social-share',
|
|
264
|
+
'@nativescript-community/ui-svg',
|
|
265
|
+
'nativescript-clipboard',
|
|
266
|
+
...(opts.deps ?? []),
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
resolve: {
|
|
270
|
+
conditions: ['native'],
|
|
271
|
+
// The compiler retargets hook imports to @nativescript-community/
|
|
272
|
+
// octane, but the deps-bundle scanner sees source-level 'octane'
|
|
273
|
+
// first — without this it vendors octane/dist/index.js (the full
|
|
274
|
+
// DOM runtime). Exact-match only: 'octane/universal/native' itself
|
|
275
|
+
// must not be rewritten.
|
|
276
|
+
alias: [{ find: /^octane$/, replacement: 'octane/universal/native' }],
|
|
277
|
+
// ns-vite sets preserveSymlinks:true; under pnpm's isolated layout
|
|
278
|
+
// that resolves a dep's imports from the symlink path instead of
|
|
279
|
+
// its real .pnpm dir, so declared transitive deps can't be found.
|
|
280
|
+
preserveSymlinks: false,
|
|
281
|
+
extensions: nativeExtensions,
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
opts.extra ?? {},
|
|
285
|
+
)
|
|
286
|
+
}
|