@shop-on/theme-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/dist/audit/command.mjs +2 -18
- package/dist/audit/server.mjs +1 -1
- package/dist/index.mjs +3 -0
- package/dist/theme/build.mjs +26 -0
- package/dist/theme/dev.mjs +1 -1
- package/dist/theme/init.mjs +1 -1
- package/dist/theme/pack.mjs +124 -0
- package/package.json +9 -8
package/dist/audit/command.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { BAR, formatReport } from "./report.mjs";
|
|
|
6
6
|
import { auditRoutes, checkRoutes } from "./routes.mjs";
|
|
7
7
|
import { startBuiltServer } from "./server.mjs";
|
|
8
8
|
import { fetchStoreData, storeProblems } from "./store.mjs";
|
|
9
|
-
import {
|
|
9
|
+
import { runThemeBuild } from "../theme/build.mjs";
|
|
10
10
|
import { existsSync } from "node:fs";
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import { parseArgs, styleText } from "node:util";
|
|
@@ -30,23 +30,7 @@ const processIo = {
|
|
|
30
30
|
};
|
|
31
31
|
const defaultDeps = {
|
|
32
32
|
fetchStoreData,
|
|
33
|
-
build:
|
|
34
|
-
const child = spawn("pnpm", ["run", "build"], {
|
|
35
|
-
cwd: themeDir,
|
|
36
|
-
stdio: [
|
|
37
|
-
"ignore",
|
|
38
|
-
process.stderr,
|
|
39
|
-
process.stderr
|
|
40
|
-
]
|
|
41
|
-
});
|
|
42
|
-
const kill = () => child.kill();
|
|
43
|
-
process.once("exit", kill);
|
|
44
|
-
child.once("error", reject);
|
|
45
|
-
child.once("exit", (code) => {
|
|
46
|
-
process.off("exit", kill);
|
|
47
|
-
resolve(code ?? 1);
|
|
48
|
-
});
|
|
49
|
-
}),
|
|
33
|
+
build: runThemeBuild,
|
|
50
34
|
startServer: startBuiltServer,
|
|
51
35
|
openBrowser
|
|
52
36
|
};
|
package/dist/audit/server.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { redact, secretValues } from "./env.mjs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
4
|
import { closeSync, cpSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
5
|
import path from "node:path";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
7
|
import net from "node:net";
|
|
8
8
|
import os from "node:os";
|
|
9
9
|
//#region src/audit/server.ts
|
package/dist/index.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { runLogin } from "./login.mjs";
|
|
|
4
4
|
import { runCredentialsRevoke } from "./theme/credentials.mjs";
|
|
5
5
|
import { runDev } from "./theme/dev.mjs";
|
|
6
6
|
import { runInit } from "./theme/init.mjs";
|
|
7
|
+
import { runPack } from "./theme/pack.mjs";
|
|
7
8
|
//#region src/index.ts
|
|
8
9
|
const USAGE = `usage: shop-on <command>
|
|
9
10
|
|
|
@@ -12,6 +13,7 @@ const USAGE = `usage: shop-on <command>
|
|
|
12
13
|
theme dev run this theme in the hosted theme editor
|
|
13
14
|
theme credentials revoke <client-id> revoke a dev storefront credential
|
|
14
15
|
theme audit measure this theme against the quality bar
|
|
16
|
+
theme pack pack the built theme into its version tarball
|
|
15
17
|
|
|
16
18
|
Each command takes --help.`;
|
|
17
19
|
const [group, command, ...rest] = process.argv.slice(2);
|
|
@@ -20,6 +22,7 @@ else if (group === "theme" && command === "init") process.exitCode = await runIn
|
|
|
20
22
|
else if (group === "theme" && command === "dev") process.exitCode = await runDev({ argv: rest });
|
|
21
23
|
else if (group === "theme" && command === "credentials" && rest[0] === "revoke") process.exitCode = await runCredentialsRevoke({ argv: rest.slice(1) });
|
|
22
24
|
else if (group === "theme" && command === "audit") process.exitCode = await runAudit({ argv: rest });
|
|
25
|
+
else if (group === "theme" && command === "pack") process.exitCode = await runPack({ argv: rest });
|
|
23
26
|
else {
|
|
24
27
|
const help = group === void 0 || group === "--help" || group === "-h";
|
|
25
28
|
(help ? console.log : console.error)(USAGE);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
//#region src/theme/build.ts
|
|
4
|
+
/** Runs the theme's `pnpm run build` and resolves with its exit code. Build
|
|
5
|
+
* output goes to stderr, so a command's own stdout stays parseable. */
|
|
6
|
+
function runThemeBuild(themeDir) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const child = spawn("pnpm", ["run", "build"], {
|
|
9
|
+
cwd: themeDir,
|
|
10
|
+
stdio: [
|
|
11
|
+
"ignore",
|
|
12
|
+
process.stderr,
|
|
13
|
+
process.stderr
|
|
14
|
+
]
|
|
15
|
+
});
|
|
16
|
+
const kill = () => child.kill();
|
|
17
|
+
process.once("exit", kill);
|
|
18
|
+
child.once("error", reject);
|
|
19
|
+
child.once("exit", (code) => {
|
|
20
|
+
process.off("exit", kill);
|
|
21
|
+
resolve(code ?? 1);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { runThemeBuild };
|
package/dist/theme/dev.mjs
CHANGED
|
@@ -6,10 +6,10 @@ import { adminOrigin, discover, ensureLoggedIn, resolveApiUrl } from "../auth/oa
|
|
|
6
6
|
import { openUrl } from "../open-url.mjs";
|
|
7
7
|
import { graphql } from "../auth/api.mjs";
|
|
8
8
|
import { createRequire } from "node:module";
|
|
9
|
-
import { spawn } from "node:child_process";
|
|
10
9
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
10
|
import path from "node:path";
|
|
12
11
|
import { parseArgs } from "node:util";
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
13
|
//#region src/theme/dev.ts
|
|
14
14
|
const DEV_USAGE = `usage: shop-on theme dev [--api <url>] [--env dev|staging|prod]
|
|
15
15
|
|
package/dist/theme/init.mjs
CHANGED
|
@@ -5,10 +5,10 @@ import { apiFlag, ensureLoggedIn, resolveApiUrl } from "../auth/oauth.mjs";
|
|
|
5
5
|
import { openUrl } from "../open-url.mjs";
|
|
6
6
|
import { graphql } from "../auth/api.mjs";
|
|
7
7
|
import { formatCredentialList } from "./credentials.mjs";
|
|
8
|
-
import { spawn } from "node:child_process";
|
|
9
8
|
import { existsSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
10
9
|
import path from "node:path";
|
|
11
10
|
import { parseArgs } from "node:util";
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
12
|
import os from "node:os";
|
|
13
13
|
import { randomBytes } from "node:crypto";
|
|
14
14
|
import { packageManagerCommands, scaffoldTheme } from "@shop-on/create-theme";
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runThemeBuild } from "./build.mjs";
|
|
3
|
+
import { processIo } from "../audit/command.mjs";
|
|
4
|
+
import { existsSync, readFileSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
import { create } from "tar";
|
|
8
|
+
//#region src/theme/pack.ts
|
|
9
|
+
const USAGE = `usage: shop-on theme pack [--theme <dir>] [--out <path>] [--build]
|
|
10
|
+
|
|
11
|
+
--theme <dir> the theme to pack (default: the current directory)
|
|
12
|
+
--out <path> where to write the tarball, a file or a directory
|
|
13
|
+
(default: <handle>-<version>.tar.gz in the theme directory)
|
|
14
|
+
--build run \`pnpm run build\` first`;
|
|
15
|
+
/** The entries the theme store refuses a version without
|
|
16
|
+
* (`Theme::Versions::Create::REQUIRED_FILES` in the api). */
|
|
17
|
+
const REQUIRED_FILES = [
|
|
18
|
+
"theme.json",
|
|
19
|
+
"release-notes.md",
|
|
20
|
+
"server/index.mjs",
|
|
21
|
+
"config/settings-schema.json"
|
|
22
|
+
];
|
|
23
|
+
const SEMVER = /^\d+\.\d+\.\d+$/;
|
|
24
|
+
/** Packs the theme and returns the exit code. */
|
|
25
|
+
async function runPack({ argv, cwd = process.cwd(), io = processIo, deps: overrides = {} }) {
|
|
26
|
+
const deps = {
|
|
27
|
+
build: runThemeBuild,
|
|
28
|
+
...overrides
|
|
29
|
+
};
|
|
30
|
+
let values;
|
|
31
|
+
try {
|
|
32
|
+
({values} = parseArgs({
|
|
33
|
+
args: argv,
|
|
34
|
+
options: {
|
|
35
|
+
theme: { type: "string" },
|
|
36
|
+
out: { type: "string" },
|
|
37
|
+
build: {
|
|
38
|
+
type: "boolean",
|
|
39
|
+
default: false
|
|
40
|
+
},
|
|
41
|
+
help: {
|
|
42
|
+
type: "boolean",
|
|
43
|
+
short: "h",
|
|
44
|
+
default: false
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}));
|
|
48
|
+
} catch (err) {
|
|
49
|
+
io.err(`${err.message}\n\n${USAGE}`);
|
|
50
|
+
return 2;
|
|
51
|
+
}
|
|
52
|
+
if (values.help) {
|
|
53
|
+
io.out(USAGE);
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
const themeDir = path.resolve(cwd, values.theme ?? ".");
|
|
57
|
+
const fail = (message) => {
|
|
58
|
+
io.err(message);
|
|
59
|
+
return 1;
|
|
60
|
+
};
|
|
61
|
+
const pkgPath = path.join(themeDir, "package.json");
|
|
62
|
+
if (!existsSync(pkgPath)) return fail(`${themeDir} is not a theme: it has no package.json`);
|
|
63
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
64
|
+
if (values.build) {
|
|
65
|
+
const code = await deps.build(themeDir);
|
|
66
|
+
if (code !== 0) return fail(`the build failed (exit ${code})`);
|
|
67
|
+
}
|
|
68
|
+
const outputDir = path.join(themeDir, ".output");
|
|
69
|
+
if (!existsSync(outputDir)) return fail(`${themeDir} has no .output: run \`pnpm build\` first, or pass --build`);
|
|
70
|
+
const missing = REQUIRED_FILES.filter((rel) => !isFile(path.join(outputDir, rel)));
|
|
71
|
+
if (missing.length > 0) return fail(`.output is missing ${missing.join(", ")}; the theme store refuses a version without them`);
|
|
72
|
+
let version;
|
|
73
|
+
try {
|
|
74
|
+
({version} = JSON.parse(readFileSync(path.join(outputDir, "theme.json"), "utf8")));
|
|
75
|
+
} catch (err) {
|
|
76
|
+
return fail(`.output/theme.json is not valid JSON: ${err.message}`);
|
|
77
|
+
}
|
|
78
|
+
if (typeof version !== "string" || !SEMVER.test(version)) return fail(`.output/theme.json#version ${JSON.stringify(version)} is not semver (X.Y.Z)`);
|
|
79
|
+
if (pkg.version !== void 0 && pkg.version !== version) return fail(`.output is version ${version} but package.json says ${pkg.version}: rebuild, or pass --build`);
|
|
80
|
+
const fileName = `${themeHandle(pkg.name, themeDir)}-${version}.tar.gz`;
|
|
81
|
+
let outPath = path.join(themeDir, fileName);
|
|
82
|
+
if (values.out !== void 0) {
|
|
83
|
+
const out = path.resolve(cwd, values.out);
|
|
84
|
+
outPath = isDirectory(out) || values.out.endsWith("/") ? path.join(out, fileName) : out;
|
|
85
|
+
}
|
|
86
|
+
const tmpPath = `${outPath}.partial`;
|
|
87
|
+
let entries = 0;
|
|
88
|
+
try {
|
|
89
|
+
await create({
|
|
90
|
+
gzip: true,
|
|
91
|
+
file: tmpPath,
|
|
92
|
+
cwd: outputDir,
|
|
93
|
+
portable: true,
|
|
94
|
+
filter: (entryPath, stat) => {
|
|
95
|
+
const base = path.basename(entryPath);
|
|
96
|
+
if (base === ".DS_Store" || base.startsWith("._")) return false;
|
|
97
|
+
const keep = "isFile" in stat && (stat.isFile() || stat.isDirectory());
|
|
98
|
+
if (keep) entries += 1;
|
|
99
|
+
return keep;
|
|
100
|
+
}
|
|
101
|
+
}, readdirSync(outputDir).sort());
|
|
102
|
+
renameSync(tmpPath, outPath);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
rmSync(tmpPath, { force: true });
|
|
105
|
+
return fail(`could not write ${outPath}: ${err.message}`);
|
|
106
|
+
}
|
|
107
|
+
const kb = Math.round(statSync(outPath).size / 1024);
|
|
108
|
+
const shown = path.relative(cwd, outPath);
|
|
109
|
+
io.out(`${shown.startsWith("..") ? outPath : shown} (${entries} entries, ${kb} KB)`);
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
/** `@shop-on/themes/kito` → `kito`; the directory name when there's no name. */
|
|
113
|
+
function themeHandle(name, themeDir) {
|
|
114
|
+
const last = name?.split("/").pop();
|
|
115
|
+
return last && last.length > 0 ? last : path.basename(themeDir);
|
|
116
|
+
}
|
|
117
|
+
function isFile(p) {
|
|
118
|
+
return existsSync(p) && statSync(p).isFile();
|
|
119
|
+
}
|
|
120
|
+
function isDirectory(p) {
|
|
121
|
+
return existsSync(p) && statSync(p).isDirectory();
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
export { REQUIRED_FILES, USAGE, runPack };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shop-on/theme-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "The Shop-On theme developer CLI: `shop-on login`, `shop-on theme init`, `shop-on theme dev` and `shop-on theme
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "The Shop-On theme developer CLI: `shop-on login`, `shop-on theme init`, `shop-on theme dev`, `shop-on theme audit` and `shop-on theme pack`.",
|
|
5
5
|
"license": "SEE LICENSE IN dist/LICENSE.md",
|
|
6
6
|
"bin": {
|
|
7
7
|
"shop-on": "./dist/index.mjs"
|
|
@@ -13,21 +13,22 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"chrome-launcher": "^1.2.2",
|
|
15
15
|
"lighthouse": "^13.5.0",
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"@shop-on/theme
|
|
16
|
+
"tar": "^7.5.22",
|
|
17
|
+
"wrangler": "4.151.0",
|
|
18
|
+
"@shop-on/create-theme": "0.2.0",
|
|
19
|
+
"@shop-on/theme-dev-host": "0.2.0"
|
|
19
20
|
},
|
|
20
21
|
"devDependencies": {
|
|
21
|
-
"@types/node": "26.6.
|
|
22
|
+
"@types/node": "26.6.5",
|
|
22
23
|
"tsx": "^4.23.15",
|
|
23
24
|
"typescript": "7.0.2",
|
|
24
|
-
"vitest": "5.0.
|
|
25
|
+
"vitest": "5.0.5"
|
|
25
26
|
},
|
|
26
27
|
"engines": {
|
|
27
28
|
"node": "^20.19.0 || >=22.12.0"
|
|
28
29
|
},
|
|
29
30
|
"scripts": {
|
|
30
31
|
"check-types": "tsc --noEmit",
|
|
31
|
-
"test": "vitest run"
|
|
32
|
+
"test": "vitest run --maxWorkers=2"
|
|
32
33
|
}
|
|
33
34
|
}
|