@nevios/cli 0.1.0 → 0.2.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/bin/nevios.mjs +61 -3
- package/package.json +2 -2
- package/src/index.mjs +83 -0
package/bin/nevios.mjs
CHANGED
|
@@ -19,7 +19,15 @@ import { execSync, spawn } from "node:child_process";
|
|
|
19
19
|
import { readdir, readFile } from "node:fs/promises";
|
|
20
20
|
import { resolve } from "node:path";
|
|
21
21
|
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
collectAssets,
|
|
24
|
+
collectModules,
|
|
25
|
+
deployStore,
|
|
26
|
+
deriveRoutesFromFiles,
|
|
27
|
+
detectGit,
|
|
28
|
+
syncRoutes,
|
|
29
|
+
undeployStore,
|
|
30
|
+
} from "../src/index.mjs";
|
|
23
31
|
|
|
24
32
|
function parseArgs(argv) {
|
|
25
33
|
const a = { _: [] };
|
|
@@ -76,8 +84,10 @@ const fail = (m) => {
|
|
|
76
84
|
async function main() {
|
|
77
85
|
const args = parseArgs(process.argv.slice(2));
|
|
78
86
|
const cmd = args._[0];
|
|
79
|
-
if (!cmd || !["start", "push", "preview", "undeploy"].includes(cmd)) {
|
|
80
|
-
fail(
|
|
87
|
+
if (!cmd || !["start", "push", "preview", "undeploy", "routes"].includes(cmd)) {
|
|
88
|
+
fail(
|
|
89
|
+
`usage: nevios <start|push|preview|undeploy|routes sync> [--build] [--dir <path>] [--store h] [--account h]`,
|
|
90
|
+
);
|
|
81
91
|
}
|
|
82
92
|
const cwd = process.cwd();
|
|
83
93
|
await loadDotenv(cwd);
|
|
@@ -107,6 +117,54 @@ async function main() {
|
|
|
107
117
|
if (!store) fail("store is required (--store / NEVIOS_STORE / nevios.json)");
|
|
108
118
|
if (!token) fail("token is required (--token / NEVIOS_PAT)");
|
|
109
119
|
|
|
120
|
+
if (cmd === "routes") {
|
|
121
|
+
// `nevios routes sync` — register the store's static pages in the Nevios
|
|
122
|
+
// route registry (feeds sitemap/SEO/dashboard "Live pages"). Reads the
|
|
123
|
+
// manifest `nevios.routes.json`: { app, patterns, exclude, routes: {"/path": {title, seo}} }.
|
|
124
|
+
if (args._[1] !== "sync") fail(`usage: nevios routes sync [--app ./app]`);
|
|
125
|
+
let manifest = {};
|
|
126
|
+
try {
|
|
127
|
+
manifest = JSON.parse(await readFile(resolve(cwd, "nevios.routes.json"), "utf8"));
|
|
128
|
+
} catch {
|
|
129
|
+
/* optional — defaults below */
|
|
130
|
+
}
|
|
131
|
+
const appDir = resolve(cwd, args.app ?? manifest.app ?? "./app");
|
|
132
|
+
const exclude = manifest.exclude ?? ["/checkout", "/account", "/cart", "/search", "/design", "/order"];
|
|
133
|
+
let files = [];
|
|
134
|
+
try {
|
|
135
|
+
files = (await readdir(resolve(appDir, "routes"), { withFileTypes: true }))
|
|
136
|
+
.filter((e) => e.isFile())
|
|
137
|
+
.map((e) => e.name);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
fail(`could not read ${appDir}/routes: ${e.message}`);
|
|
140
|
+
}
|
|
141
|
+
const meta = manifest.routes ?? {};
|
|
142
|
+
const routes = deriveRoutesFromFiles(files, { exclude }).map((path) => ({
|
|
143
|
+
path,
|
|
144
|
+
title: meta[path]?.title ?? null,
|
|
145
|
+
seo: meta[path]?.seo ?? undefined,
|
|
146
|
+
}));
|
|
147
|
+
console.log(`\nnevios routes sync — ${store} @ ${account} → ${apiUrl}`);
|
|
148
|
+
for (const r of routes) console.log(` ${r.path}${r.title ? ` — ${r.title}` : ""}`);
|
|
149
|
+
if (args["dry-run"]) {
|
|
150
|
+
console.log(`\n(dry run — nothing synced)\n`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const result = await syncRoutes({
|
|
154
|
+
apiUrl,
|
|
155
|
+
token,
|
|
156
|
+
account,
|
|
157
|
+
store,
|
|
158
|
+
source: manifest.source ?? "storefront",
|
|
159
|
+
patterns: manifest.patterns,
|
|
160
|
+
routes,
|
|
161
|
+
}).catch((e) => fail(e.message));
|
|
162
|
+
console.log(
|
|
163
|
+
`\n✓ routes synced — +${result.added ?? 0} added · ~${result.updated ?? 0} updated · −${result.deactivated ?? 0} deactivated (${result.active_total ?? routes.length} active)\n`,
|
|
164
|
+
);
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
|
|
110
168
|
if (cmd === "undeploy") {
|
|
111
169
|
console.log(`\nnevios undeploy — ${store} @ ${account}`);
|
|
112
170
|
const sf = await undeployStore({ apiUrl, token, account, store }).catch((e) => fail(e.message));
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nevios/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "nevios
|
|
5
|
+
"description": "nevios — the storefront developer CLI: push a React store to Nevios hosting (Cloudflare), preview, and scaffold.",
|
|
6
6
|
"bin": {
|
|
7
7
|
"nevios": "bin/nevios.mjs"
|
|
8
8
|
},
|
package/src/index.mjs
CHANGED
|
@@ -145,6 +145,89 @@ export async function deployStore({
|
|
|
145
145
|
return json?.storefront ?? json;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Derive the store's custom-route paths from its flat-route filenames
|
|
150
|
+
* (React Router / Remix flat-file convention). Only STATIC routes come out —
|
|
151
|
+
* dynamic segments ($handle) are entity routes, represented by `patterns`
|
|
152
|
+
* server-side, never registered one-by-one.
|
|
153
|
+
*
|
|
154
|
+
* _index.tsx → /
|
|
155
|
+
* o-nas.tsx → /o-nas
|
|
156
|
+
* account_.verify.tsx → /account/verify
|
|
157
|
+
* collections._index.tsx→ /collections
|
|
158
|
+
* products.$handle.tsx → (skipped — dynamic)
|
|
159
|
+
* design.* → (drop via manifest `exclude`)
|
|
160
|
+
*/
|
|
161
|
+
export function deriveRoutesFromFiles(fileNames, { exclude = [] } = {}) {
|
|
162
|
+
const paths = new Set();
|
|
163
|
+
for (const name of fileNames) {
|
|
164
|
+
const m = /^(.*)\.(tsx|ts|jsx|js|mdx?)$/.exec(name);
|
|
165
|
+
if (!m) continue; // not a route module (css, maps, …)
|
|
166
|
+
const base = m[1];
|
|
167
|
+
if (!base || base.includes("$")) continue; // dynamic → covered by patterns
|
|
168
|
+
const segments = [];
|
|
169
|
+
// Split on `.` only outside [x] escapes, so sitemap[.]xml stays one segment.
|
|
170
|
+
for (const raw of base.split(/\.(?![^[\]]*\])/)) {
|
|
171
|
+
if (raw === "_index") continue; // index of its parent path
|
|
172
|
+
if (raw.startsWith("_")) continue; // pathless layout segment
|
|
173
|
+
if (raw.startsWith("(") && raw.endsWith(")")) continue; // optional segment — default URL omits it
|
|
174
|
+
// trailing `_` (account_) opts out of layout nesting; URL segment unchanged.
|
|
175
|
+
const seg = raw.endsWith("_") ? raw.slice(0, -1) : raw;
|
|
176
|
+
if (!seg) continue;
|
|
177
|
+
// [x] escapes a literal char: sitemap[.]xml → sitemap.xml
|
|
178
|
+
segments.push(seg.replace(/\[(.*?)\]/g, "$1"));
|
|
179
|
+
}
|
|
180
|
+
const path = "/" + segments.join("/");
|
|
181
|
+
if (exclude.some((e) => path === e || path.startsWith(e.endsWith("/") ? e : e + "/"))) continue;
|
|
182
|
+
paths.add(path);
|
|
183
|
+
}
|
|
184
|
+
return [...paths].sort();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* POST the store's full custom-route set to Nevios (admin twin of the
|
|
189
|
+
* storefront routes/sync). Authorized by the same NEVIOS_PAT deploy token as
|
|
190
|
+
* `push` — new pages register themselves on every deploy.
|
|
191
|
+
*/
|
|
192
|
+
export async function syncRoutes({
|
|
193
|
+
apiUrl,
|
|
194
|
+
token,
|
|
195
|
+
account,
|
|
196
|
+
store,
|
|
197
|
+
source = "storefront",
|
|
198
|
+
patterns,
|
|
199
|
+
routes,
|
|
200
|
+
apiVersion = DEFAULT_VERSION,
|
|
201
|
+
fetch = globalThis.fetch,
|
|
202
|
+
}) {
|
|
203
|
+
if (!apiUrl) throw new Error("routes sync: apiUrl is required");
|
|
204
|
+
if (!token) throw new Error("routes sync: token (NEVIOS_PAT) is required");
|
|
205
|
+
if (!account) throw new Error("routes sync: account handle is required");
|
|
206
|
+
if (!store) throw new Error("routes sync: store handle is required");
|
|
207
|
+
const base = apiUrl.replace(/\/+$/, "");
|
|
208
|
+
const url = `${base}/admin/${apiVersion}/${encodeURIComponent(account)}/storefronts/${encodeURIComponent(store)}/routes/sync.json`;
|
|
209
|
+
const body = { source, routes };
|
|
210
|
+
if (patterns && Object.keys(patterns).length) body.patterns = patterns;
|
|
211
|
+
const res = await fetch(url, {
|
|
212
|
+
method: "POST",
|
|
213
|
+
headers: {
|
|
214
|
+
Authorization: `Bearer ${token}`,
|
|
215
|
+
"Content-Type": "application/json",
|
|
216
|
+
Accept: "application/json",
|
|
217
|
+
},
|
|
218
|
+
body: JSON.stringify(body),
|
|
219
|
+
});
|
|
220
|
+
const text = await res.text();
|
|
221
|
+
let json = null;
|
|
222
|
+
try {
|
|
223
|
+
json = text ? JSON.parse(text) : null;
|
|
224
|
+
} catch {
|
|
225
|
+
json = null;
|
|
226
|
+
}
|
|
227
|
+
if (!res.ok) throw new Error(json?.error?.message || `routes sync failed (HTTP ${res.status})`);
|
|
228
|
+
return json?.routes_sync ?? json;
|
|
229
|
+
}
|
|
230
|
+
|
|
148
231
|
/** Undeploy a store's isolate → back to the shared default. */
|
|
149
232
|
export async function undeployStore({
|
|
150
233
|
apiUrl,
|