create-octane-xplat 0.15.0 → 0.16.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.
Files changed (34) hide show
  1. package/README.md +57 -0
  2. package/compatibility.json +199 -0
  3. package/compatibility.ts +109 -0
  4. package/dist/compatibility.js +66 -0
  5. package/dist/index.js +132 -0
  6. package/dist/scaffold.js +439 -0
  7. package/{index.mjs → index.ts} +3 -4
  8. package/library-template/AGENTS.md +3 -1
  9. package/library-template/README.md +20 -0
  10. package/library-template/package.json +22 -19
  11. package/library-template/pnpm-workspace.yaml +150 -11
  12. package/library-template/src/Badge.tsx +1 -1
  13. package/library-template/src/Badge.web.tsx +1 -1
  14. package/library-template/tests/packed-consumer.mjs +2 -1
  15. package/library-template/tsconfig.envs.json +3 -0
  16. package/library-template/tsconfig.json +1 -1
  17. package/library-template/tsrx-typegen.json +1 -1
  18. package/package.json +23 -6
  19. package/{scaffold.mjs → scaffold.ts} +51 -40
  20. package/template/.agents/skills/xplat/SKILL.md +10 -2
  21. package/template/.agents/skills/xplat/references/components.md +2 -2
  22. package/template/.agents/skills/xplat/references/platform.md +2 -2
  23. package/template/AGENTS.md +47 -0
  24. package/template/App_Resources/Android/app.gradle +1 -1
  25. package/template/package.json +33 -31
  26. package/template/pnpm-workspace.yaml +64 -29
  27. package/template/src/app.web.test.tsx +20 -0
  28. package/template/src/main.web.tsrx +1 -0
  29. package/template/src/test-setup.web.ts +17 -0
  30. package/template/tsconfig.json +1 -1
  31. package/template/vite.config.ts +2 -2
  32. package/template/vitest.web.config.mts +13 -0
  33. package/library-template/.oxlintrc.json +0 -24
  34. package/template/.oxlintrc.json +0 -24
@@ -0,0 +1,439 @@
1
+ import { ensureCompatibilityCatalog } from "./compatibility.js";
2
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { parseDocument } from "yaml";
6
+ //#region scaffold.ts
7
+ var moduleDir = dirname(fileURLToPath(import.meta.url));
8
+ var pkgRoot = basename(moduleDir) === "dist" ? join(moduleDir, "..") : moduleDir;
9
+ var TEMPLATE_DIR = join(pkgRoot, "template");
10
+ var LIBRARY_TEMPLATE_DIR = join(pkgRoot, "library-template");
11
+ /** Scaffoldable targets. `files` entries are template-relative paths —
12
+ * directories copy recursively. `deps`/`devDeps`/`scripts` name keys whose
13
+ * values come from template/package.json. `typecheck` contributes one clause
14
+ * to the generated typecheck script. */
15
+ var targets = {
16
+ web: {
17
+ label: "Web (Vite)",
18
+ files: [
19
+ "index.html",
20
+ "vite.config.ts",
21
+ "src/main.web.tsrx",
22
+ "vitest.web.config.mts",
23
+ "src/app.web.test.tsx",
24
+ "src/test-setup.web.ts"
25
+ ],
26
+ scripts: [
27
+ "dev",
28
+ "build",
29
+ "test",
30
+ "test:web"
31
+ ],
32
+ devDeps: ["@octanejs/vite-plugin", "jsdom"]
33
+ },
34
+ ios: {
35
+ label: "iOS (NativeScript)",
36
+ requires: ["native-shared"],
37
+ files: ["App_Resources/iOS"],
38
+ scripts: [
39
+ "dev:ios",
40
+ "build:ios",
41
+ "test:ios"
42
+ ],
43
+ devDeps: ["@nativescript/ios"],
44
+ note: "needs macOS + Xcode — `pnpm xplat doctor` checks the toolchain"
45
+ },
46
+ android: {
47
+ label: "Android (NativeScript)",
48
+ requires: ["native-shared"],
49
+ files: ["App_Resources/Android"],
50
+ scripts: [
51
+ "dev:android",
52
+ "build:android",
53
+ "test:android"
54
+ ],
55
+ devDeps: ["@nativescript/android"],
56
+ note: "needs Android SDK + a compatible JDK — `pnpm xplat doctor` checks the toolchain"
57
+ },
58
+ "native-shared": {
59
+ hidden: true,
60
+ patches: [
61
+ "@nativescript/core",
62
+ "@nativescript/types",
63
+ "css",
64
+ "@nativescript/vite",
65
+ "@nativescript-community/octane",
66
+ "@nativescript-community/vite-octane"
67
+ ],
68
+ files: [
69
+ "nativescript.config.ts",
70
+ "vite.config.native.mts",
71
+ "references.d.ts",
72
+ "src/main.ts",
73
+ "vitest.config.mts",
74
+ "src/test.ts",
75
+ "src/app.spec.tsrx",
76
+ "src/native-runtime.spec.ts",
77
+ "src/failure-reporting.spec.ts"
78
+ ],
79
+ deps: [
80
+ "@nativescript-community/octane",
81
+ "@nativescript/core",
82
+ "@valor/nativescript-websockets"
83
+ ],
84
+ devDeps: [
85
+ "@nativescript-community/vite-octane",
86
+ "@nativescript/types",
87
+ "@nativescript/unit-test-runner",
88
+ "@nativescript/vite",
89
+ "nativescript"
90
+ ],
91
+ typecheck: "xplat typecheck"
92
+ }
93
+ };
94
+ var selectableTargets = Object.keys(targets).filter((id) => !targets[id].hidden);
95
+ /** `pnpm create` without --targets and `xplat add`'s prompt default. */
96
+ var defaultTargets = [
97
+ "web",
98
+ "ios",
99
+ "android"
100
+ ];
101
+ var BASE_TYPECHECK = "xplat typecheck";
102
+ var BASE_SCRIPTS = [
103
+ "xplat",
104
+ "lint",
105
+ "lint:fix",
106
+ "format"
107
+ ];
108
+ /** Expand `requires` edges, dependencies first. Throws on unknown ids. */
109
+ function resolveTargets(ids) {
110
+ const resolved = [];
111
+ const visit = (id) => {
112
+ const t = targets[id];
113
+ if (!t) throw new Error(`unknown target "${id}" — expected one of: ${selectableTargets.join(", ")}`);
114
+ for (const req of t.requires ?? []) visit(req);
115
+ if (!resolved.includes(id)) resolved.push(id);
116
+ };
117
+ for (const id of ids) visit(id);
118
+ return resolved;
119
+ }
120
+ var templateManifest = () => JSON.parse(readFileSync(join(TEMPLATE_DIR, "package.json"), "utf8"));
121
+ /** Which target claims a template-relative path (entries may be dirs). */
122
+ function ownerOf(path) {
123
+ for (const [id, t] of Object.entries(targets)) for (const entry of t.files) if (path === entry || path.startsWith(entry + "/")) return id;
124
+ return null;
125
+ }
126
+ function walk(dir, prefix = "") {
127
+ const out = [];
128
+ for (const name of readdirSync(dir)) {
129
+ if (name === "node_modules") continue;
130
+ const rel = prefix ? `${prefix}/${name}` : name;
131
+ if (statSync(join(dir, name)).isDirectory()) out.push(...walk(join(dir, name), rel));
132
+ else out.push(rel);
133
+ }
134
+ return out;
135
+ }
136
+ function pickKeys(source, names, label) {
137
+ const out = {};
138
+ for (const name of names) {
139
+ if (source[name] === void 0) throw new Error(`template/package.json has no ${label} entry "${name}"`);
140
+ out[name] = source[name];
141
+ }
142
+ return out;
143
+ }
144
+ /**
145
+ * The package.json a given target set produces. With every target selected
146
+ * this is identical to template/package.json.
147
+ */
148
+ function composeManifest(ids) {
149
+ const resolved = resolveTargets(ids);
150
+ const tpl = templateManifest();
151
+ const manifest = { ...tpl };
152
+ for (const section of ["dependencies", "devDependencies"]) {
153
+ const key = section === "dependencies" ? "deps" : "devDeps";
154
+ const wanted = /* @__PURE__ */ new Set();
155
+ for (const id of resolved) for (const name of targets[id][key] ?? []) wanted.add(name);
156
+ const next = {};
157
+ for (const [name, spec] of Object.entries(tpl[section] ?? {})) if (!Object.keys(targets).some((id) => (targets[id][key] ?? []).includes(name)) || wanted.has(name)) next[name] = spec;
158
+ manifest[section] = next;
159
+ }
160
+ const wantedScripts = new Set(BASE_SCRIPTS);
161
+ for (const id of resolved) for (const name of targets[id].scripts ?? []) wantedScripts.add(name);
162
+ const scripts = pickKeys(tpl.scripts ?? {}, [...wantedScripts].sort(), "script");
163
+ const typecheck = [BASE_TYPECHECK];
164
+ for (const id of resolved) if (targets[id].typecheck) typecheck.push(targets[id].typecheck);
165
+ scripts.typecheck = [...new Set(typecheck)].join(" && ");
166
+ manifest.scripts = {};
167
+ for (const name of Object.keys(tpl.scripts ?? {})) if (name === "typecheck" || wantedScripts.has(name)) manifest.scripts[name] = scripts[name];
168
+ if (!resolved.includes("native-shared")) delete manifest.main;
169
+ return manifest;
170
+ }
171
+ var PATCH_ENTRY = /^\s+"?(@?[\w./-]+@[\w.-]+)"?:\s+\S+\s*$/;
172
+ var patchPkg = (key) => key.replace(/@[^@]+$/, "");
173
+ function parsePatchEntries(lines) {
174
+ const entries = [];
175
+ let inPatches = false;
176
+ let pending = [];
177
+ for (const line of lines) {
178
+ if (/^\S/.test(line)) {
179
+ inPatches = line.startsWith("patchedDependencies");
180
+ pending = [];
181
+ continue;
182
+ }
183
+ if (!inPatches) continue;
184
+ const m = line.match(PATCH_ENTRY);
185
+ if (m) {
186
+ entries.push({
187
+ name: patchPkg(m[1]),
188
+ comments: pending,
189
+ line
190
+ });
191
+ pending = [];
192
+ } else pending.push(line);
193
+ }
194
+ return entries;
195
+ }
196
+ /** The pnpm-workspace.yaml a given target set produces. */
197
+ function composeWorkspaceYaml(ids) {
198
+ const resolved = new Set(resolveTargets(ids));
199
+ const drop = /* @__PURE__ */ new Set();
200
+ for (const [id, t] of Object.entries(targets)) if (!resolved.has(id)) for (const name of t.patches ?? []) drop.add(name);
201
+ const lines = readFileSync(join(TEMPLATE_DIR, "pnpm-workspace.yaml"), "utf8").split("\n");
202
+ const out = [];
203
+ let inPatches = false;
204
+ let pending = [];
205
+ for (const line of lines) {
206
+ if (/^\S/.test(line)) {
207
+ out.push(...pending, line);
208
+ pending = [];
209
+ inPatches = line.startsWith("patchedDependencies");
210
+ continue;
211
+ }
212
+ if (!inPatches) {
213
+ out.push(line);
214
+ continue;
215
+ }
216
+ const m = line.match(PATCH_ENTRY);
217
+ if (!m) {
218
+ pending.push(line);
219
+ continue;
220
+ }
221
+ if (drop.has(patchPkg(m[1]))) {
222
+ pending = [];
223
+ continue;
224
+ }
225
+ out.push(...pending, line);
226
+ pending = [];
227
+ }
228
+ out.push(...pending);
229
+ const doc = parseDocument(out.join("\n"));
230
+ const manifest = composeManifest(ids);
231
+ const used = /* @__PURE__ */ new Set([...Object.keys(manifest.dependencies), ...Object.keys(manifest.devDependencies)]);
232
+ for (const name of Object.keys(doc.toJS().catalogs?.xplat ?? {})) if (!used.has(name)) doc.deleteIn([
233
+ "catalogs",
234
+ "xplat",
235
+ name
236
+ ]);
237
+ return String(doc);
238
+ }
239
+ /**
240
+ * Add the patch entries that `enabledIds` owns to the app's
241
+ * pnpm-workspace.yaml. Entries already present (by package name, any
242
+ * version) are kept. Returns true if the file changed.
243
+ */
244
+ function ensurePatchEntries(appRoot, enabledIds) {
245
+ const needed = /* @__PURE__ */ new Set();
246
+ for (const id of enabledIds) for (const name of targets[id].patches ?? []) needed.add(name);
247
+ if (!needed.size) return false;
248
+ const yamlPath = join(appRoot, "pnpm-workspace.yaml");
249
+ const appText = existsSync(yamlPath) ? readFileSync(yamlPath, "utf8") : "";
250
+ const present = new Set(parsePatchEntries(appText.split("\n")).map((e) => e.name));
251
+ const missing = [...needed].filter((name) => !present.has(name));
252
+ if (!missing.length) return false;
253
+ const blocks = parsePatchEntries(readFileSync(join(TEMPLATE_DIR, "pnpm-workspace.yaml"), "utf8").split("\n")).filter((e) => missing.includes(e.name)).map((e) => [...e.comments, e.line].join("\n"));
254
+ if (!appText.trim()) {
255
+ writeFileSync(yamlPath, composeWorkspaceYaml(enabledIds));
256
+ return true;
257
+ }
258
+ let next = appText;
259
+ const lines = next.split("\n");
260
+ const sectionStart = lines.findIndex((l) => l.startsWith("patchedDependencies"));
261
+ if (sectionStart === -1) next = `${appText.trimEnd()}\npatchedDependencies:\n${blocks.join("\n")}\n`;
262
+ else {
263
+ let insertAt = lines.length;
264
+ for (let i = sectionStart + 1; i < lines.length; i++) if (/^\S/.test(lines[i])) {
265
+ insertAt = i;
266
+ break;
267
+ }
268
+ lines.splice(insertAt, 0, ...blocks.join("\n").split("\n"));
269
+ next = lines.join("\n");
270
+ }
271
+ writeFileSync(yamlPath, next.endsWith("\n") ? next : next + "\n");
272
+ return true;
273
+ }
274
+ /**
275
+ * The tsconfig.json a given target set produces. One program covers every
276
+ * platform suffix (moduleSuffixes is the fixed precedence order); a web-only
277
+ * scaffold just drops the native ambient root and entrypoint.
278
+ */
279
+ function composeTsconfig(ids) {
280
+ const resolved = new Set(resolveTargets(ids));
281
+ const config = JSON.parse(readFileSync(join(TEMPLATE_DIR, "tsconfig.json"), "utf8"));
282
+ if (!resolved.has("native-shared")) {
283
+ delete config.files;
284
+ config.compilerOptions.types = (config.compilerOptions.types ?? []).filter((t) => t !== "@nativescript/types");
285
+ config.exclude = [.../* @__PURE__ */ new Set([...config.exclude ?? [], "src/main.ts"])];
286
+ }
287
+ return JSON.stringify(config, null, " ") + "\n";
288
+ }
289
+ /** Write a scaffold with tests for `ids` into `dir` (must not exist or be empty). */
290
+ function composeTargets(ids, dir) {
291
+ const resolved = new Set(resolveTargets(ids));
292
+ mkdirSync(dir, { recursive: true });
293
+ for (const rel of walk(TEMPLATE_DIR)) {
294
+ if (rel === "package.json" || rel === "pnpm-workspace.yaml" || rel === "tsconfig.json") continue;
295
+ const owner = ownerOf(rel);
296
+ if (owner && !resolved.has(owner)) continue;
297
+ const dest = join(dir, rel === "gitignore" ? ".gitignore" : rel);
298
+ mkdirSync(dirname(dest), { recursive: true });
299
+ cpSync(join(TEMPLATE_DIR, rel), dest, { recursive: true });
300
+ }
301
+ writeFileSync(join(dir, "package.json"), JSON.stringify(composeManifest(ids), null, " ") + "\n");
302
+ writeFileSync(join(dir, "pnpm-workspace.yaml"), composeWorkspaceYaml(ids));
303
+ writeFileSync(join(dir, "tsconfig.json"), composeTsconfig(ids));
304
+ return [...resolved];
305
+ }
306
+ /** Write the cross-platform library starter into `dir`. */
307
+ function composeLibrary(dir, packageName = basename(resolve(dir))) {
308
+ if (packageName.length > 214 || !/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(packageName)) throw new Error(`invalid npm package name "${packageName}" — use lowercase letters, numbers, dots, underscores, or hyphens`);
309
+ mkdirSync(dir, { recursive: true });
310
+ for (const rel of walk(LIBRARY_TEMPLATE_DIR)) {
311
+ if (rel === "package.json") continue;
312
+ const dest = join(dir, rel === "gitignore" ? ".gitignore" : rel);
313
+ mkdirSync(dirname(dest), { recursive: true });
314
+ const source = join(LIBRARY_TEMPLATE_DIR, rel);
315
+ if (/\.(?:md|ts|tsx|mjs|json)$/.test(rel)) {
316
+ const content = readFileSync(source, "utf8").replaceAll("octane-xplat-library", packageName);
317
+ writeFileSync(dest, content);
318
+ } else cpSync(source, dest, { recursive: true });
319
+ }
320
+ const manifest = JSON.parse(readFileSync(join(LIBRARY_TEMPLATE_DIR, "package.json"), "utf8"));
321
+ manifest.name = packageName;
322
+ writeFileSync(join(dir, "package.json"), JSON.stringify(manifest, null, " ") + "\n");
323
+ }
324
+ var readJson = (file) => {
325
+ try {
326
+ return JSON.parse(readFileSync(file, "utf8"));
327
+ } catch {
328
+ return null;
329
+ }
330
+ };
331
+ /** Sentinel-based enablement check, mirroring the CLI's targets.mjs. */
332
+ function targetEnabled(appRoot, id) {
333
+ if (id === "web") return existsSync(join(appRoot, "vite.config.ts")) || existsSync(join(appRoot, "vite.config.mts"));
334
+ if (id === "native-shared") return existsSync(join(appRoot, "nativescript.config.ts"));
335
+ const manifest = readJson(join(appRoot, "package.json")) ?? {};
336
+ const declared = {
337
+ ...manifest.dependencies,
338
+ ...manifest.devDependencies
339
+ };
340
+ const runtime = {
341
+ ios: "@nativescript/ios",
342
+ android: "@nativescript/android"
343
+ }[id];
344
+ return runtime ? typeof declared[runtime] === "string" : false;
345
+ }
346
+ /** Copy a template file/dir into the app unless the destination exists. */
347
+ function copyMissing(rel, appRoot, report) {
348
+ const src = join(TEMPLATE_DIR, rel);
349
+ const dest = join(appRoot, rel === "gitignore" ? ".gitignore" : rel);
350
+ if (existsSync(dest)) {
351
+ report.skipped.push(rel);
352
+ return;
353
+ }
354
+ mkdirSync(dirname(dest), { recursive: true });
355
+ cpSync(src, dest, { recursive: true });
356
+ report.copied.push(rel);
357
+ }
358
+ /**
359
+ * Fold a native-capable app's sources into the one tsconfig program: the
360
+ * manifest (tsconfig.envs.json) is gone — one program covers every platform
361
+ * suffix — so enabling native means wiring the ambient declaration file and
362
+ * the entrypoint into the app's own config.
363
+ */
364
+ function enableNativeInTsconfig(appRoot) {
365
+ const path = join(appRoot, "tsconfig.json");
366
+ const text = existsSync(path) ? readFileSync(path, "utf8") : "";
367
+ const config = readJson(path);
368
+ if (!config || typeof config !== "object") return false;
369
+ const indent = text.match(/\n(\s+)"/)?.[1] ?? " ";
370
+ config.files = [.../* @__PURE__ */ new Set(["references.d.ts", ...config.files ?? []])];
371
+ config.exclude = (config.exclude ?? []).filter((e) => e !== "src/main.ts");
372
+ const types = config.compilerOptions?.types;
373
+ if (config.compilerOptions && Array.isArray(types) && !types.includes("@nativescript/types")) config.compilerOptions.types = [...types, "@nativescript/types"];
374
+ writeFileSync(path, JSON.stringify(config, null, indent) + "\n");
375
+ return true;
376
+ }
377
+ /**
378
+ * Enable `id` (plus unmet `requires`) inside an existing app at `appRoot`.
379
+ * Additive only: existing files, scripts, and dep entries are skipped, never
380
+ * overwritten. Returns a report for the caller to render.
381
+ */
382
+ function applyTarget(appRoot, id) {
383
+ const manifestPath = join(appRoot, "package.json");
384
+ const raw = readFileSync(manifestPath, "utf8");
385
+ const manifest = JSON.parse(raw);
386
+ const tpl = templateManifest();
387
+ const report = {
388
+ enabled: [],
389
+ already: [],
390
+ copied: [],
391
+ skipped: [],
392
+ scriptsAdded: [],
393
+ scriptsSkipped: [],
394
+ depsAdded: []
395
+ };
396
+ let touched = false;
397
+ for (const tid of resolveTargets([id])) {
398
+ if (targetEnabled(appRoot, tid)) {
399
+ report.already.push(tid);
400
+ continue;
401
+ }
402
+ report.enabled.push(tid);
403
+ const t = targets[tid];
404
+ for (const rel of t.files) copyMissing(rel, appRoot, report);
405
+ for (const [section, key] of [["dependencies", "deps"], ["devDependencies", "devDeps"]]) for (const name of t[key] ?? []) {
406
+ manifest[section] ??= {};
407
+ if (manifest[section][name] === void 0) {
408
+ manifest[section][name] = tpl[section][name];
409
+ report.depsAdded.push(name);
410
+ }
411
+ }
412
+ manifest.scripts ??= {};
413
+ for (const name of t.scripts ?? []) if (manifest.scripts[name] === void 0) {
414
+ manifest.scripts[name] = tpl.scripts[name];
415
+ report.scriptsAdded.push(name);
416
+ } else if (manifest.scripts[name] !== tpl.scripts[name]) report.scriptsSkipped.push(name);
417
+ if (t.typecheck) {
418
+ const current = manifest.scripts.typecheck ?? "";
419
+ if (!current.includes(t.typecheck)) {
420
+ manifest.scripts.typecheck = current ? `${current} && ${t.typecheck}` : t.typecheck;
421
+ report.scriptsAdded.push("typecheck");
422
+ }
423
+ }
424
+ if (tid === "native-shared") {
425
+ if (manifest.main === void 0) manifest.main = tpl.main;
426
+ report.tsconfigUpdated = enableNativeInTsconfig(appRoot);
427
+ }
428
+ touched = true;
429
+ }
430
+ if (ensurePatchEntries(appRoot, [...report.enabled, ...report.already])) report.patchesAdded = true;
431
+ ensureCompatibilityCatalog(appRoot, manifest);
432
+ if (touched) {
433
+ const indent = raw.match(/\n(\s+)"/)?.[1] ?? " ";
434
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, indent) + "\n");
435
+ }
436
+ return report;
437
+ }
438
+ //#endregion
439
+ export { LIBRARY_TEMPLATE_DIR, TEMPLATE_DIR, applyTarget, composeLibrary, composeManifest, composeTargets, composeTsconfig, composeWorkspaceYaml, defaultTargets, resolveTargets, selectableTargets, targetEnabled, targets };
@@ -9,7 +9,7 @@ import {
9
9
  resolveTargets,
10
10
  selectableTargets,
11
11
  targets,
12
- } from './scaffold.mjs'
12
+ } from './scaffold.ts'
13
13
 
14
14
  const argv = process.argv.slice(2)
15
15
  const noInstall = argv.includes('--no-install')
@@ -111,7 +111,7 @@ const parseTargets = (value) => {
111
111
  return resolveTargets(ids)
112
112
  }
113
113
 
114
- let ids = []
114
+ let ids: string[] = []
115
115
  if (templateArg === 'library') {
116
116
  const packageName = nameArg ?? basename(dir)
117
117
  try {
@@ -131,8 +131,7 @@ if (templateArg === 'library') {
131
131
  process.exit(1)
132
132
  }
133
133
  } else if (process.stdout.isTTY) {
134
- // Lazy: the extracted tarball has no installed deps — only the interactive
135
- // path needs @clack/prompts (verify-consumer runs the bin dep-free).
134
+ // Non-interactive creation does not need the prompt module.
136
135
  const p = await import('@clack/prompts')
137
136
  p.intro('create-octane-xplat')
138
137
  const picked = await p.multiselect({
@@ -1,12 +1,14 @@
1
1
  # Library package notes
2
2
 
3
3
  - Use pnpm for package management.
4
+ - Keep build dependencies in `catalog:xplat`. Use `pnpm dlx @octane-xplat/cli@latest upgrade --check` to preview a workspace-wide version/patch update and `pnpm dlx @octane-xplat/cli@latest upgrade` to apply it. Keep consumer peer ranges under the library's control; run `pnpm xplat doctor` and the package checks afterward.
4
5
  - Keep the public API in `src/index.ts` and `src/index.web.ts`; both barrels
5
6
  must export the same names and types.
6
7
  - Put shared prop contracts in `src/props.ts` and document public exports.
7
8
  - The unsuffixed `.tsx` file is the NativeScript implementation. Add a
8
9
  `.web.tsx` sibling when browser behavior or markup needs to differ. Native
9
- files start with `/** @jsxImportSource @nativescript-community/octane */`.
10
+ files start with `/** @jsxImportSource @octane-xplat/jsx */`; browser files
11
+ start with `/** @jsxImportSource @octane-xplat/jsx/dom */`.
10
12
  - Keep DOM globals out of native files. Use the shared `@octane-xplat/ui`
11
13
  elements when behavior can be shared.
12
14
  - Declare every imported package. Runtime plugins belong in `dependencies`;
@@ -29,11 +29,21 @@ Run these commands from the package directory:
29
29
  ```sh
30
30
  pnpm typecheck
31
31
  pnpm lint
32
+ pnpm lint:fix
32
33
  pnpm test
33
34
  pnpm typegen
34
35
  pnpm test:packed
35
36
  ```
36
37
 
38
+ Linting uses the CLI's bundled framework defaults. Apply safe lint fixes with
39
+ `lint:fix`; use `format` to format source and `fmt --check` to check formatting
40
+ without changing files:
41
+
42
+ ```sh
43
+ pnpm format
44
+ pnpm exec xplat fmt --check
45
+ ```
46
+
37
47
  The package publishes authored source for the app's bundler to compile.
38
48
  `pnpm typegen` generates `.d.ts` files so consumers' type checkers use
39
49
  declarations. `pnpm test:packed` packs the package, places that artifact into
@@ -42,6 +52,16 @@ small web and native TypeScript consumers, and checks both export conditions.
42
52
  The package sets `nodeLinker: hoisted` in `pnpm-workspace.yaml` because the
43
53
  NativeScript bundler expects a flat dependency tree.
44
54
 
55
+ Its `catalog:xplat` build dependencies and patches follow one framework
56
+ compatibility set. Update that set together; consumer `peerDependencies`
57
+ ranges are preserved for you to maintain:
58
+
59
+ ```sh
60
+ pnpm dlx @octane-xplat/cli@latest upgrade --check
61
+ pnpm dlx @octane-xplat/cli@latest upgrade
62
+ pnpm xplat doctor
63
+ ```
64
+
45
65
  ## Platform files
46
66
 
47
67
  `src/Badge.tsx` is the shared iOS and Android implementation. The
@@ -46,8 +46,8 @@
46
46
  "scripts": {
47
47
  "typegen": "tsrx-typegen --project tsconfig.types.json",
48
48
  "typecheck": "xplat typecheck",
49
- "lint": "xplat-lint",
50
- "lint:fix": "xplat-lint --fix",
49
+ "lint": "xplat lint",
50
+ "lint:fix": "xplat lint --fix",
51
51
  "test": "vitest run",
52
52
  "test:packed": "node tests/packed-consumer.mjs",
53
53
  "prepack": "pnpm typegen && tsrx-typegen --pack-check",
@@ -55,26 +55,26 @@
55
55
  "format": "xplat fmt"
56
56
  },
57
57
  "devDependencies": {
58
- "@nativescript-community/octane": "catalog:",
59
- "@nativescript/core": "catalog:",
60
- "@nativescript/types": "catalog:",
61
- "@octane-xplat/cli": "^0.15.0",
62
- "@octane-xplat/lint": "^0.15.0",
63
- "@octane-xplat/ui": "^0.15.0",
64
- "@octanejs/vite-plugin": "catalog:",
65
- "@tsrx/oxc": "0.20.0",
66
- "@tsrx/typescript-plugin": "catalog:",
67
- "octane": "catalog:",
68
- "oxlint": "^1.74.0",
69
- "tsrx-typegen": "^0.2.2",
70
- "typescript": "catalog:",
71
- "vite": "catalog:",
72
- "vitest": "catalog:"
58
+ "@nativescript-community/octane": "catalog:xplat",
59
+ "@nativescript/core": "catalog:xplat",
60
+ "@nativescript/types": "catalog:xplat",
61
+ "@octane-xplat/cli": "catalog:xplat",
62
+ "@octane-xplat/jsx": "catalog:xplat",
63
+ "@octane-xplat/ui": "catalog:xplat",
64
+ "@octanejs/vite-plugin": "catalog:xplat",
65
+ "@tsrx/oxc": "catalog:xplat",
66
+ "@tsrx/typescript-plugin": "catalog:xplat",
67
+ "octane": "catalog:xplat",
68
+ "tsrx-typegen": "catalog:xplat",
69
+ "typescript": "catalog:xplat",
70
+ "vite": "catalog:xplat",
71
+ "vitest": "catalog:xplat"
73
72
  },
74
73
  "peerDependencies": {
75
74
  "@nativescript-community/octane": ">=0.2.1 <1",
76
75
  "@nativescript/core": ">=9.1.0 <10",
77
- "@octane-xplat/ui": "^0.15.0",
76
+ "@octane-xplat/jsx": "^0.16.0",
77
+ "@octane-xplat/ui": "^0.16.0",
78
78
  "octane": ">=0.6.3 <1"
79
79
  },
80
80
  "peerDependenciesMeta": {
@@ -85,5 +85,8 @@
85
85
  "optional": true
86
86
  }
87
87
  },
88
- "packageManager": "pnpm@11.24.0"
88
+ "packageManager": "pnpm@11.24.0",
89
+ "engines": {
90
+ "node": ">=22.18.0"
91
+ }
89
92
  }