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.
- package/README.md +57 -0
- package/compatibility.json +199 -0
- package/compatibility.ts +109 -0
- package/dist/compatibility.js +66 -0
- package/dist/index.js +132 -0
- package/dist/scaffold.js +439 -0
- package/{index.mjs → index.ts} +3 -4
- package/library-template/AGENTS.md +3 -1
- package/library-template/README.md +20 -0
- package/library-template/package.json +22 -19
- package/library-template/pnpm-workspace.yaml +150 -11
- package/library-template/src/Badge.tsx +1 -1
- package/library-template/src/Badge.web.tsx +1 -1
- package/library-template/tests/packed-consumer.mjs +2 -1
- package/library-template/tsconfig.envs.json +3 -0
- package/library-template/tsconfig.json +1 -1
- package/library-template/tsrx-typegen.json +1 -1
- package/package.json +23 -6
- package/{scaffold.mjs → scaffold.ts} +51 -40
- package/template/.agents/skills/xplat/SKILL.md +10 -2
- package/template/.agents/skills/xplat/references/components.md +2 -2
- package/template/.agents/skills/xplat/references/platform.md +2 -2
- package/template/AGENTS.md +47 -0
- package/template/App_Resources/Android/app.gradle +1 -1
- package/template/package.json +33 -31
- package/template/pnpm-workspace.yaml +64 -29
- package/template/src/app.web.test.tsx +20 -0
- package/template/src/main.web.tsrx +1 -0
- package/template/src/test-setup.web.ts +17 -0
- package/template/tsconfig.json +1 -1
- package/template/vite.config.ts +2 -2
- package/template/vitest.web.config.mts +13 -0
- package/library-template/.oxlintrc.json +0 -24
- package/template/.oxlintrc.json +0 -24
package/dist/scaffold.js
ADDED
|
@@ -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 };
|
package/{index.mjs → index.ts}
RENAMED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
resolveTargets,
|
|
10
10
|
selectableTargets,
|
|
11
11
|
targets,
|
|
12
|
-
} from './scaffold.
|
|
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
|
-
//
|
|
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 @
|
|
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
|
|
50
|
-
"lint:fix": "xplat
|
|
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": "
|
|
62
|
-
"@octane-xplat/
|
|
63
|
-
"@octane-xplat/ui": "
|
|
64
|
-
"@octanejs/vite-plugin": "catalog:",
|
|
65
|
-
"@tsrx/oxc": "
|
|
66
|
-
"@tsrx/typescript-plugin": "catalog:",
|
|
67
|
-
"octane": "catalog:",
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
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/
|
|
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
|
}
|