@octane-xplat/cli 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octane-xplat/cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "xplat — dev/build/doctor for octane-xplat apps (web + iOS + Android from one codebase)",
5
5
  "bin": {
6
6
  "xplat": "./src/cli.mjs"
@@ -1,7 +1,178 @@
1
1
  import { command } from '@alloc/cmd-ts'
2
2
  import { execFileSync } from 'node:child_process'
3
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
4
+ import { dirname, join, parse, relative, resolve } from 'node:path'
3
5
  import * as p from '@clack/prompts'
4
6
 
7
+ const frameworkFallbacks = {
8
+ '@octane-xplat/ui': [
9
+ '@nativescript-community/gesturehandler',
10
+ '@nativescript-community/ui-canvas',
11
+ '@nativescript-community/ui-drawer',
12
+ '@nativescript-community/ui-svg',
13
+ ],
14
+ '@octane-xplat/platform': [
15
+ '@nativescript-community/ui-document-picker',
16
+ '@nativescript/biometrics',
17
+ '@nativescript/geolocation',
18
+ '@nativescript/haptics',
19
+ '@nativescript/imagepicker',
20
+ '@nativescript/local-notifications',
21
+ '@nativescript/secure-storage',
22
+ '@nativescript/social-share',
23
+ 'nativescript-clipboard',
24
+ ],
25
+ }
26
+
27
+ const nativePlugin = (name) =>
28
+ (name.startsWith('@nativescript/') ||
29
+ name.startsWith('@nativescript-community/') ||
30
+ name.startsWith('nativescript-')) &&
31
+ !name.endsWith('/octane') &&
32
+ !name.endsWith('/core')
33
+
34
+ const readJson = (file) => {
35
+ try {
36
+ return JSON.parse(readFileSync(file, 'utf8'))
37
+ } catch {
38
+ return null
39
+ }
40
+ }
41
+
42
+ const packageName = (specifier) => {
43
+ if (!specifier.startsWith('@') && !specifier.includes('/')) {return specifier}
44
+ const parts = specifier.split('/')
45
+ return specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]
46
+ }
47
+
48
+ const importSpecifiers = (source) => {
49
+ const specs = new Set()
50
+ for (const pattern of [
51
+ /\bfrom\s*['"]([^'"]+)['"]/g,
52
+ /\bimport\s*['"]([^'"]+)['"]/g,
53
+ /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
54
+ ]) {
55
+ for (const match of source.matchAll(pattern)) {specs.add(match[1])}
56
+ }
57
+
58
+ return specs
59
+ }
60
+
61
+ const sourceFiles = (root) => {
62
+ if (!existsSync(root)) {return []}
63
+ const out = []
64
+ const visit = (dir) => {
65
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
66
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) {continue}
67
+ const file = join(dir, entry.name)
68
+ if (entry.isDirectory()) {visit(file)}
69
+ else if (/\.(?:m?[jt]sx?|tsrx)$/.test(entry.name)) {out.push(file)}
70
+ }
71
+ }
72
+
73
+ visit(root)
74
+ return out
75
+ }
76
+
77
+ const workspacePackages = (cwd) => {
78
+ const found = new Map()
79
+ let dir = resolve(cwd)
80
+ while (true) {
81
+ for (const group of ['apps', 'packages']) {
82
+ const parent = join(dir, group)
83
+ if (!existsSync(parent)) {continue}
84
+ for (const name of readdirSync(parent)) {
85
+ const root = join(parent, name)
86
+ const manifest = readJson(join(root, 'package.json'))
87
+ if (manifest?.name) {found.set(manifest.name, root)}
88
+ }
89
+ }
90
+
91
+ const next = dirname(dir)
92
+ if (next === dir) {break}
93
+ dir = next
94
+ }
95
+
96
+ return found
97
+ }
98
+
99
+ const packageRoot = (cwd, name, workspaces) => {
100
+ const direct = join(cwd, 'node_modules', ...name.split('/'))
101
+ if (existsSync(join(direct, 'package.json'))) {return direct}
102
+ return workspaces.get(name)
103
+ }
104
+
105
+ const frameworkPlugins = (cwd, name, workspaces) => {
106
+ const root = packageRoot(cwd, name, workspaces)
107
+ const manifest = root && readJson(join(root, 'package.json'))
108
+ const declared = [
109
+ ...Object.keys(manifest?.dependencies ?? {}),
110
+ ...Object.keys(manifest?.peerDependencies ?? {}),
111
+ ].filter(nativePlugin)
112
+
113
+ return declared.length ? declared : frameworkFallbacks[name] ?? []
114
+ }
115
+
116
+ /**
117
+ * Find native plugins required by the framework packages reachable from an app
118
+ * source tree, then compare them with the app's own package.json. This is a
119
+ * warning because the web target does not need native plugin declarations and
120
+ * a package may intentionally keep an optional native capability unused.
121
+ */
122
+ export function findMissingPluginDeclarations(cwd) {
123
+ const manifest = readJson(join(cwd, 'package.json')) ?? {}
124
+ const owned = new Set([
125
+ ...Object.keys(manifest.dependencies ?? {}),
126
+ ...Object.keys(manifest.devDependencies ?? {}),
127
+ ...Object.keys(manifest.optionalDependencies ?? {}),
128
+ ...Object.keys(manifest.peerDependencies ?? {}),
129
+ ])
130
+
131
+ const workspaces = workspacePackages(cwd)
132
+ const pending = sourceFiles(join(cwd, 'src'))
133
+ for (const dir of ['app', 'src/app']) {pending.push(...sourceFiles(join(cwd, dir)))}
134
+ const visited = new Set()
135
+ const frameworks = new Map()
136
+
137
+ while (pending.length) {
138
+ const file = pending.pop()
139
+ if (visited.has(file)) {continue}
140
+ visited.add(file)
141
+ let source
142
+ try {
143
+ source = readFileSync(file, 'utf8')
144
+ } catch {
145
+ continue
146
+ }
147
+
148
+ for (const specifier of importSpecifiers(source)) {
149
+ const name = packageName(specifier)
150
+ if (frameworkFallbacks[name]) {
151
+ if (!frameworks.has(name)) {frameworks.set(name, new Set())}
152
+ frameworks.get(name).add(relative(cwd, file) || parse(file).base)
153
+ continue
154
+ }
155
+
156
+ const root = packageRoot(cwd, name, workspaces)
157
+ if (root && workspaces.has(name)) {pending.push(...sourceFiles(root))}
158
+ }
159
+ }
160
+
161
+ const missing = new Map()
162
+ for (const [framework] of frameworks) {
163
+ for (const plugin of frameworkPlugins(cwd, framework, workspaces)) {
164
+ if (!owned.has(plugin)) {
165
+ if (!missing.has(plugin)) {missing.set(plugin, new Set())}
166
+ missing.get(plugin).add(framework)
167
+ }
168
+ }
169
+ }
170
+
171
+ return [...missing.entries()]
172
+ .sort(([a], [b]) => a.localeCompare(b))
173
+ .map(([plugin, frameworks]) => ({ plugin, frameworks: [...frameworks].sort() }))
174
+ }
175
+
5
176
  const check = (cmd, args) => {
6
177
  try {
7
178
  return {
@@ -25,6 +196,7 @@ export const doctor = command({
25
196
  description: 'Check the toolchain for web + native builds',
26
197
  args: {},
27
198
  handler: async () => {
199
+ const cwd = process.cwd()
28
200
  p.intro('xplat doctor')
29
201
  const rows = []
30
202
  const row = (name, ok, detail, hint) => rows.push({ name, ok, detail, hint })
@@ -77,6 +249,13 @@ export const doctor = command({
77
249
  'JDK 17 (JDK 25 breaks the Android toolchain)',
78
250
  )
79
251
 
252
+ const pluginWarnings = findMissingPluginDeclarations(cwd)
253
+ for (const { plugin, frameworks } of pluginWarnings) {
254
+ p.log.warn(
255
+ `native plugin declaration — ${plugin} is required by ${frameworks.join(', ')}; add it to this app's package.json`,
256
+ )
257
+ }
258
+
80
259
  let bad = 0
81
260
  for (const r of rows) {
82
261
  if (r.ok) {
@@ -87,10 +266,15 @@ export const doctor = command({
87
266
  }
88
267
  }
89
268
 
90
- p.outro(
269
+ const summary =
91
270
  bad === 0
92
271
  ? 'All checks pass'
93
- : `${bad} missing — web still works, native targets need the above`,
272
+ : `${bad} missing — web still works, native targets need the above`
273
+
274
+ p.outro(
275
+ pluginWarnings.length
276
+ ? `${summary}; ${pluginWarnings.length} native plugin declaration warning(s)`
277
+ : summary,
94
278
  )
95
279
  },
96
280
  })
package/src/vite.mjs CHANGED
@@ -233,7 +233,22 @@ export async function xplatNative(env, opts = {}) {
233
233
  ),
234
234
  {
235
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
+ },
236
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'],
237
252
  // Flattened optimizeDeps chunks get mangled by the /ns/m device
238
253
  // transform (`import import "/ns/core/utils"`) and miss the vendor
239
254
  // manifest — serve @nativescript plugins per-module instead.