jops-core 1.0.14 → 1.1.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 (30) hide show
  1. package/README.md +346 -28
  2. package/bin/init.js +214 -21
  3. package/lib/jops-core.d.ts +251 -15
  4. package/lib/jops-core.min.js +1 -1
  5. package/lib/nextjs/JopsApp.d.ts +35 -0
  6. package/lib/nextjs/JopsApp.js +50 -0
  7. package/package.json +33 -5
  8. package/templates/nextjs-jops-spa/README.md +34 -0
  9. package/templates/nextjs-jops-spa/data/quotes.json +62 -0
  10. package/templates/nextjs-jops-spa/gitignore +21 -0
  11. package/templates/nextjs-jops-spa/next.config.ts +15 -0
  12. package/templates/nextjs-jops-spa/package.json +25 -0
  13. package/templates/nextjs-jops-spa/public/jops-spa/App.js +10 -0
  14. package/templates/nextjs-jops-spa/public/jops-spa/Login.js +26 -0
  15. package/templates/nextjs-jops-spa/public/jops-spa/Quotes.js +102 -0
  16. package/templates/nextjs-jops-spa/public/jops-spa/api.js +15 -0
  17. package/templates/nextjs-jops-spa/public/jops-spa/badge.js +8 -0
  18. package/templates/nextjs-jops-spa/public/jops-spa/format.js +5 -0
  19. package/templates/nextjs-jops-spa/public/jops-spa/layout/login.html +34 -0
  20. package/templates/nextjs-jops-spa/public/jops-spa/layout/main.html +17 -0
  21. package/templates/nextjs-jops-spa/public/jops-spa/layout/quotes.html +50 -0
  22. package/templates/nextjs-jops-spa/public/jops-spa/layout/quotes_row.html +8 -0
  23. package/templates/nextjs-jops-spa/public/jops-spa/skeleton.js +7 -0
  24. package/templates/nextjs-jops-spa/src/app/about/page.tsx +59 -0
  25. package/templates/nextjs-jops-spa/src/app/api/quotes/route.ts +11 -0
  26. package/templates/nextjs-jops-spa/src/app/globals.css +6 -0
  27. package/templates/nextjs-jops-spa/src/app/jops-spa/page.tsx +11 -0
  28. package/templates/nextjs-jops-spa/src/app/layout.tsx +27 -0
  29. package/templates/nextjs-jops-spa/src/app/page.tsx +5 -0
  30. package/templates/nextjs-jops-spa/tsconfig.json +21 -0
package/bin/init.js CHANGED
@@ -1,23 +1,79 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "fs";
3
+ import https from "https";
3
4
  import path from "path";
4
5
  import readline from "readline";
6
+ import { spawnSync } from "child_process";
5
7
  import { fileURLToPath } from "url";
6
8
 
7
9
  const [,, command, ...flags] = process.argv;
8
10
 
9
11
  if (command !== "init" && command !== "build" && command !== "update") {
10
12
  console.log("Usage:");
11
- console.log(" npx jops-core init — vendor library and configure index.html");
12
- console.log(" npx jops-core update — copy the latest jops-core.min.js to lib/");
13
+ console.log(" npx jops-core init — vendor library and configure index.html, or create a template project");
14
+ console.log(" npx jops-core update — copy the latest jops-core.min.js to lib/ (public/lib/ in a Next.js project)");
13
15
  console.log(" npx jops-core build — generate production-ready dist/ folder");
14
16
  console.log("");
15
17
  console.log("Options (init, update):");
16
18
  console.log(" --cordova | --capacitor | --web override the detected project type");
19
+ console.log("Options (all commands):");
20
+ console.log(" --no-update-check skip the check for a newer jops-core version");
17
21
  process.exit(0);
18
22
  }
19
23
 
20
24
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "../package.json"), "utf8"));
26
+
27
+ // ─── update check ────────────────────────────────────────────────────────────
28
+ // Started now, awaited at the end, so it runs in parallel with the command.
29
+ // Any failure (offline, timeout, Node without fetch) prints nothing.
30
+
31
+ const skipUpdateCheck =
32
+ process.env.CI ||
33
+ process.env.JOPS_NO_UPDATE_CHECK ||
34
+ process.env.NO_UPDATE_NOTIFIER ||
35
+ flags.includes("--no-update-check");
36
+
37
+ // https.get rather than fetch: destroying the request closes the socket at once,
38
+ // whereas an aborted fetch keeps the process alive until its 10 s connect timeout.
39
+ function fetchLatestVersion() {
40
+ return new Promise((resolve) => {
41
+ const req = https.get("https://registry.npmjs.org/jops-core/latest", (res) => {
42
+ if (res.statusCode !== 200) {
43
+ clearTimeout(timer);
44
+ res.resume();
45
+ return resolve(null);
46
+ }
47
+ let body = "";
48
+ res.setEncoding("utf8");
49
+ res.on("data", (chunk) => (body += chunk));
50
+ res.on("end", () => {
51
+ clearTimeout(timer);
52
+ try {
53
+ resolve(JSON.parse(body).version ?? null);
54
+ } catch {
55
+ resolve(null);
56
+ }
57
+ });
58
+ });
59
+ const timer = setTimeout(() => req.destroy(), 1500);
60
+ req.on("error", () => {
61
+ clearTimeout(timer);
62
+ resolve(null);
63
+ });
64
+ });
65
+ }
66
+
67
+ const latestVersion = skipUpdateCheck ? Promise.resolve(null) : fetchLatestVersion();
68
+
69
+ function isNewer(a, b) {
70
+ const pa = a.split(".").map(Number);
71
+ const pb = b.split(".").map(Number);
72
+ for (let i = 0; i < 3; i++) {
73
+ if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) > (pb[i] || 0);
74
+ }
75
+ return false;
76
+ }
21
77
 
22
78
  // ─── project detection ───────────────────────────────────────────────────────
23
79
  // projectRoot: package.json, .vscode/ webRoot: index.html, lib/, src/
@@ -88,6 +144,133 @@ function copyDir(src, dest) {
88
144
  }
89
145
  }
90
146
 
147
+ // A Next.js project: the library is served from public/lib/.
148
+ function isNextProject(dir) {
149
+ return ["next.config.ts", "next.config.js", "next.config.mjs"].some((f) => fs.existsSync(path.join(dir, f)));
150
+ }
151
+
152
+ // Copies a template project into dest (README.md included, unlike copyDir()).
153
+ // package.json is merged separately; existing files are never overwritten.
154
+ // npm drops .gitignore files from published packages, so the template stores
155
+ // it as "gitignore".
156
+ function copyTemplateProject(src, dest, root = dest) {
157
+ const SKIP = new Set(["node_modules", ".next", ".DS_Store", "package.json"]);
158
+ fs.mkdirSync(dest, { recursive: true });
159
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
160
+ if (dest === root && SKIP.has(entry.name)) continue;
161
+ if (entry.name === ".DS_Store") continue;
162
+ const srcPath = path.join(src, entry.name);
163
+ const destPath = path.join(dest, entry.name === "gitignore" ? ".gitignore" : entry.name);
164
+ if (entry.isDirectory()) copyTemplateProject(srcPath, destPath, root);
165
+ else if (fs.existsSync(destPath)) console.log(`[jops-core] Kept the existing ${path.relative(root, destPath)}`);
166
+ else fs.copyFileSync(srcPath, destPath);
167
+ }
168
+ }
169
+
170
+ // Creates the template's package.json, or merges its dependencies and scripts
171
+ // into an existing one: existing entries (e.g. an installed jops-core) are kept.
172
+ function mergeTemplatePackageJson(templatePath, targetPath) {
173
+ const template = JSON.parse(fs.readFileSync(templatePath, "utf8"));
174
+ if (!fs.existsSync(targetPath)) {
175
+ fs.writeFileSync(targetPath, JSON.stringify(template, null, 2) + "\n", "utf8");
176
+ console.log("[jops-core] Created package.json");
177
+ return;
178
+ }
179
+ const pkg = JSON.parse(fs.readFileSync(targetPath, "utf8"));
180
+ pkg.private ??= true;
181
+ // `npm init -y` writes "type": "commonjs", which makes Next.js (Turbopack)
182
+ // reject the project's ES module sources; Next.js projects have no "type".
183
+ if (pkg.type === "commonjs") {
184
+ delete pkg.type;
185
+ console.log('[jops-core] Removed "type": "commonjs" from package.json (Next.js uses ES modules)');
186
+ }
187
+ for (const section of ["dependencies", "devDependencies"]) {
188
+ for (const [name, spec] of Object.entries(template[section] ?? {})) {
189
+ const existing = pkg.dependencies?.[name] ?? pkg.devDependencies?.[name];
190
+ if (existing) {
191
+ if (name === "jops-core") console.log(`[jops-core] Kept the installed jops-core (${existing})`);
192
+ continue;
193
+ }
194
+ pkg[section] = { ...pkg[section], [name]: spec };
195
+ }
196
+ }
197
+ for (const [name, command] of Object.entries(template.scripts ?? {})) {
198
+ const existing = pkg.scripts?.[name];
199
+ if (existing && existing !== command) {
200
+ console.log(`[jops-core] Kept the existing "${name}" script (${existing}); the template uses "${command}"`);
201
+ continue;
202
+ }
203
+ pkg.scripts = { ...pkg.scripts, [name]: command };
204
+ }
205
+ fs.writeFileSync(targetPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
206
+ console.log("[jops-core] Added the Next.js dependencies and scripts to package.json");
207
+ }
208
+
209
+ // ─── template selection ──────────────────────────────────────────────────────
210
+ // Asked before init writes anything, because the Next.js template creates its
211
+ // own project folder instead of configuring the current one.
212
+
213
+ async function askTemplate() {
214
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
215
+ const answer = await new Promise((resolve) => rl.question(
216
+ "\nCreate JOPS project from existing templates:\n" +
217
+ " 1. Empty JOPS project (basic skeleton for JOPS project architecture)\n" +
218
+ " 2. Hello World JOPS project (an example project utilizing all features)\n" +
219
+ " 3. Next.js JOPS Embedded SPA (a Next.js project hosting a JOPS SPA)\n" +
220
+ " 4. None (I'll create my own architecture)\n" +
221
+ "Enter choice (1-4): ",
222
+ resolve,
223
+ ));
224
+ rl.close();
225
+ return answer.trim();
226
+ }
227
+
228
+ const choice = command === "init" ? await askTemplate() : null;
229
+
230
+ // ─── init: Next.js JOPS Embedded SPA (template 3) ────────────────────────────
231
+
232
+ if (command === "init" && choice === "3") {
233
+ const cwd = process.cwd();
234
+ const templateDir = path.join(__dirname, "../templates/nextjs-jops-spa");
235
+ // Paths that would clash with an existing project: nothing is written then.
236
+ const conflicts = [
237
+ "next.config.ts", "next.config.js", "next.config.mjs",
238
+ "tsconfig.json", "src/app", "public/jops-spa", "data/quotes.json",
239
+ ].filter((p) => fs.existsSync(path.join(cwd, p)));
240
+
241
+ if (isHybrid) {
242
+ console.error(`[jops-core] This is a ${type} project; create the Next.js project in another folder.`);
243
+ process.exitCode = 1;
244
+ } else if (conflicts.length) {
245
+ console.error(`[jops-core] This folder already has ${conflicts.join(", ")}. Nothing was changed.`);
246
+ console.error(" Run init in an empty folder (or a fresh npm project) to create the Next.js project.");
247
+ process.exitCode = 1;
248
+ } else {
249
+ copyTemplateProject(templateDir, cwd);
250
+ mergeTemplatePackageJson(path.join(templateDir, "package.json"), path.join(cwd, "package.json"));
251
+ console.log("[jops-core] Created the Next.js JOPS Embedded SPA project in this folder.");
252
+
253
+ const libDir = path.join(cwd, "public", "lib");
254
+ fs.mkdirSync(libDir, { recursive: true });
255
+ fs.copyFileSync(path.join(__dirname, "../lib/jops-core.min.js"), path.join(libDir, "jops-core.min.js"));
256
+ console.log("[jops-core] Vendored jops-core.min.js → public/lib/jops-core.min.js");
257
+
258
+ console.log("[jops-core] Installing dependencies (npm install)…");
259
+ const install = spawnSync("npm", ["install"], {
260
+ cwd,
261
+ stdio: "inherit",
262
+ shell: process.platform === "win32",
263
+ });
264
+ if (install.status !== 0) {
265
+ console.warn("[jops-core] npm install did not complete. Run it yourself: npm install");
266
+ }
267
+ console.log("");
268
+ console.log("[jops-core] Next steps:");
269
+ console.log(" npm run dev");
270
+ console.log(" open http://localhost:3000/about and http://localhost:3000/jops-spa");
271
+ }
272
+ }
273
+
91
274
  // ─── index.html helpers (hybrid projects) ────────────────────────────────────
92
275
 
93
276
  // Same policy as Cordova's default, plus 'unsafe-inline' for the inline import map
@@ -187,7 +370,7 @@ function schemeWarnings(xml) {
187
370
 
188
371
  // ─── init ────────────────────────────────────────────────────────────────────
189
372
 
190
- if (command === "init") {
373
+ if (command === "init" && choice !== "3") {
191
374
  fs.mkdirSync(webRoot, { recursive: true });
192
375
  if (isHybrid) {
193
376
  console.log(`[jops-core] ${forced ? "Using" : "Detected"} ${type} project → web folder: ${rel(webRoot)}/`);
@@ -350,22 +533,10 @@ ${cordovaBody}${MODULE_SCRIPT}
350
533
  if (changed) fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
351
534
  }
352
535
 
353
- // Template selection
354
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
355
- const ask = (q) => new Promise(resolve => rl.question(q, resolve));
356
-
357
- const choice = await ask(
358
- "\nCreate JOPS project from existing templates:\n" +
359
- " 1. Empty JOPS project (basic skeleton for JOPS project architecture)\n" +
360
- " 2. Hello World JOPS project (an example project utilizing all features)\n" +
361
- " 3. None (I'll create my own architecture)\n" +
362
- "Enter choice (1-3): "
363
- );
364
- rl.close();
365
-
536
+ // Template choice: asked at the start (see askTemplate()).
366
537
  // Cordova's sample page (css/index.css, js/index.js, #deviceready) conflicts with a JOPS
367
538
  // template: back it up and start from the JOPS Cordova index.html instead.
368
- if (type === "cordova" && ["1", "2"].includes(choice.trim()) &&
539
+ if (type === "cordova" && ["1", "2"].includes(choice) &&
369
540
  fs.readFileSync(indexPath, "utf8").includes('id="deviceready"')) {
370
541
  let backup = path.join(webRoot, "index.cordova.html");
371
542
  for (let n = 1; fs.existsSync(backup); n++) backup = path.join(webRoot, `index.cordova-${n}.html`);
@@ -375,7 +546,7 @@ ${cordovaBody}${MODULE_SCRIPT}
375
546
  }
376
547
 
377
548
  const templateSrc = path.join(__dirname, "../templates");
378
- if (choice.trim() === "1") {
549
+ if (choice === "1") {
379
550
  copyDir(path.join(templateSrc, "empty"), webRoot);
380
551
  // Add data-app to the module script in index.html
381
552
  let html = fs.readFileSync(indexPath, "utf8");
@@ -385,7 +556,7 @@ ${cordovaBody}${MODULE_SCRIPT}
385
556
  );
386
557
  fs.writeFileSync(indexPath, html, "utf8");
387
558
  console.log("[jops-core] Created empty JOPS project skeleton.");
388
- } else if (choice.trim() === "2") {
559
+ } else if (choice === "2") {
389
560
  copyDir(path.join(templateSrc, "hello-world"), webRoot);
390
561
  let html = fs.readFileSync(indexPath, "utf8");
391
562
  html = html.replace(
@@ -402,7 +573,9 @@ ${cordovaBody}${MODULE_SCRIPT}
402
573
  // ─── update ──────────────────────────────────────────────────────────────────
403
574
 
404
575
  if (command === "update") {
405
- const libDir = path.join(webRoot, "lib");
576
+ // A Next.js project serves the library from public/lib/.
577
+ const libRoot = type === "web" && isNextProject(projectRoot) ? path.join(projectRoot, "public") : webRoot;
578
+ const libDir = path.join(libRoot, "lib");
406
579
  const vendorDest = path.join(libDir, "jops-core.min.js");
407
580
  const vendorSrc = path.join(__dirname, "../lib/jops-core.min.js");
408
581
 
@@ -413,7 +586,6 @@ if (command === "update") {
413
586
 
414
587
  fs.copyFileSync(vendorSrc, vendorDest);
415
588
 
416
- const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "../package.json"), "utf8"));
417
589
  console.log(`[jops-core] Updated ${rel(vendorDest)} to v${pkg.version}`);
418
590
  }
419
591
 
@@ -446,8 +618,29 @@ if (command === "build") {
446
618
  }
447
619
  }
448
620
 
621
+ const vendored = path.join(webRoot, "lib", "jops-core.min.js");
622
+ const installed = path.join(__dirname, "../lib/jops-core.min.js");
623
+ if (fs.existsSync(vendored) && !fs.readFileSync(vendored).equals(fs.readFileSync(installed))) {
624
+ console.warn(`[jops-core] ${rel(vendored)} differs from the installed v${pkg.version} — run \`npx jops-core update\` before building.`);
625
+ }
626
+
449
627
  const distDir = path.join(webRoot, "dist");
450
628
  if (fs.existsSync(distDir)) fs.rmSync(distDir, { recursive: true, force: true });
451
629
  buildCopyDir(webRoot, distDir);
452
630
  console.log("[jops-core] Built → dist/");
453
631
  }
632
+
633
+ // ─── update notice ───────────────────────────────────────────────────────────
634
+
635
+ const latest = await latestVersion;
636
+ if (latest && isNewer(latest, pkg.version)) {
637
+ console.log("");
638
+ if (command === "update") {
639
+ console.log(`[jops-core] Copied v${pkg.version} from node_modules, but v${latest} is available.`);
640
+ console.log(" Run `npm i jops-core@latest` first, then `npx jops-core update` again.");
641
+ } else {
642
+ console.log(`[jops-core] Update available: ${pkg.version} → ${latest}`);
643
+ console.log(" npm i jops-core@latest && npx jops-core update");
644
+ }
645
+ console.log(" What's new: https://github.com/uidb-dev/jops-core/blob/main/CHANGELOG.md");
646
+ }