@zlooks.cn/cli 1.0.5 → 1.0.6
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/command/index.js +57 -0
- package/dist/command/index.js.map +1 -1
- package/dist/theme-authoring.d.ts +36 -0
- package/dist/theme-authoring.d.ts.map +1 -0
- package/dist/theme-authoring.js +491 -0
- package/dist/theme-authoring.js.map +1 -0
- package/package.json +9 -3
- package/templates/blog-theme/README.md +26 -0
- package/templates/blog-theme/_gitignore +5 -0
- package/templates/blog-theme/hile-rsc.json +22 -0
- package/templates/blog-theme/package.json +64 -0
- package/templates/blog-theme/pnpm-workspace.yaml +5 -0
- package/templates/blog-theme/src/command/index.ts +11 -0
- package/templates/blog-theme/src/identity.ts +7 -0
- package/templates/blog-theme/src/plugin/archive-filter.tsx +62 -0
- package/templates/blog-theme/src/plugin/blog-frame.tsx +91 -0
- package/templates/blog-theme/src/plugin/blog-interactions.tsx +236 -0
- package/templates/blog-theme/src/plugin/page.tsx +174 -0
- package/templates/blog-theme/src/plugin/styles.d.ts +1 -0
- package/templates/blog-theme/src/plugin/theme.css +59 -0
- package/templates/blog-theme/src/services/blog-theme.boot.ts +17 -0
- package/templates/blog-theme/test/interactions.test.tsx.template +177 -0
- package/templates/blog-theme/test/pages.test.tsx.template +125 -0
- package/templates/blog-theme/test/shell.test.tsx.template +102 -0
- package/templates/blog-theme/test/theme-contract.test.ts.template +43 -0
- package/templates/blog-theme/tsconfig.json +16 -0
- package/templates/blog-theme/tsconfig.runtime.json +17 -0
- package/templates/blog-theme/vitest.config.ts +8 -0
package/dist/command/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
3
4
|
import { homedir } from 'node:os';
|
|
4
5
|
import { resolve } from 'node:path';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
5
7
|
import { Command } from 'commander';
|
|
6
8
|
import { completeDeployment, getDeploymentPath, rollbackDeployment, startDeployment, statusDeployment, } from '../deploy.js';
|
|
7
9
|
import { inspectDeployment } from '../doctor.js';
|
|
@@ -11,6 +13,8 @@ import { installOperation, databaseOperation, restartOperation, serviceListOpera
|
|
|
11
13
|
import { confirm, isPromptCancellation, selectMany } from '../prompt.js';
|
|
12
14
|
import { getSetupPaths, setupOperation } from '../setup.js';
|
|
13
15
|
import { executeFile } from '../system.js';
|
|
16
|
+
import { createBlogThemeScaffold, verifyBlogThemeProject } from '../theme-authoring.js';
|
|
17
|
+
const execFileAsync = promisify(execFile);
|
|
14
18
|
const manifest = readManifest();
|
|
15
19
|
const defaultEnvironment = getSetupPaths(homedir()).environment;
|
|
16
20
|
const deploymentPath = getDeploymentPath(homedir());
|
|
@@ -117,6 +121,59 @@ program.command('doctor')
|
|
|
117
121
|
if (!result.ok)
|
|
118
122
|
process.exitCode = 1;
|
|
119
123
|
});
|
|
124
|
+
const theme = program.command('theme').description('Create and verify third-party Blog themes');
|
|
125
|
+
theme.command('create')
|
|
126
|
+
.description('Create a complete runnable Blog theme scaffold')
|
|
127
|
+
.argument('<theme-id>', 'Lowercase kebab-case theme identity')
|
|
128
|
+
.argument('[destination]', 'New project directory')
|
|
129
|
+
.requiredOption('--display-name <name>', 'Human-readable theme name')
|
|
130
|
+
.option('--description <text>', 'Theme description')
|
|
131
|
+
.action(async (themeId, destination, options) => {
|
|
132
|
+
const result = await createBlogThemeScaffold({
|
|
133
|
+
themeId,
|
|
134
|
+
displayName: options.displayName,
|
|
135
|
+
...(options.description ? { description: options.description } : {}),
|
|
136
|
+
destination: resolve(process.cwd(), destination ?? `zlooks-theme-${themeId}-server`),
|
|
137
|
+
});
|
|
138
|
+
runtime.write(`${JSON.stringify(result)}\n`);
|
|
139
|
+
});
|
|
140
|
+
theme.command('verify')
|
|
141
|
+
.description('Run platform-owned structure, test, typecheck, build, and RSC verification gates')
|
|
142
|
+
.argument('[root]', 'Theme project directory', '.')
|
|
143
|
+
.option('--json', 'Write one machine-readable JSON result')
|
|
144
|
+
.action(async (root, options) => {
|
|
145
|
+
const result = await verifyBlogThemeProject({ root: resolve(process.cwd(), root) }, {
|
|
146
|
+
run: async (command, args, runOptions) => {
|
|
147
|
+
try {
|
|
148
|
+
const completed = await execFileAsync(command, [...args], {
|
|
149
|
+
cwd: runOptions.cwd,
|
|
150
|
+
encoding: 'utf8',
|
|
151
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
152
|
+
timeout: 10 * 60 * 1000,
|
|
153
|
+
killSignal: 'SIGKILL',
|
|
154
|
+
});
|
|
155
|
+
return { exitCode: 0, output: completed.stdout };
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (error && typeof error === 'object') {
|
|
159
|
+
const failure = error;
|
|
160
|
+
return {
|
|
161
|
+
exitCode: typeof failure.code === 'number' ? failure.code : 1,
|
|
162
|
+
output: `${typeof failure.stdout === 'string' ? failure.stdout : ''}${typeof failure.stderr === 'string' ? failure.stderr : ''}` || (error instanceof Error ? error.message : String(error)),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return { exitCode: 1, output: String(error) };
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
if (options.json)
|
|
170
|
+
runtime.write(`${JSON.stringify(result)}\n`);
|
|
171
|
+
else
|
|
172
|
+
for (const check of result.checks)
|
|
173
|
+
runtime.write(`${check.ok ? 'PASS' : 'FAIL'}\t${check.id}\t${check.detail}\n`);
|
|
174
|
+
if (!result.ok)
|
|
175
|
+
process.exitCode = 1;
|
|
176
|
+
});
|
|
120
177
|
program.action(() => program.outputHelp());
|
|
121
178
|
try {
|
|
122
179
|
await program.parseAsync(process.argv);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/command/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,gBAAgB,GAEjB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,EACpB,aAAa,EACb,kBAAkB,EAClB,eAAe,GAEhB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,OAAO,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AACzE,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/command/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface CreateBlogThemeScaffoldOptions {
|
|
2
|
+
readonly destination: string;
|
|
3
|
+
readonly themeId: string;
|
|
4
|
+
readonly displayName: string;
|
|
5
|
+
readonly description?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface BlogThemeScaffoldResult {
|
|
8
|
+
readonly destination: string;
|
|
9
|
+
readonly packageName: string;
|
|
10
|
+
readonly themeId: string;
|
|
11
|
+
}
|
|
12
|
+
export interface BlogThemeVerificationCheck {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly ok: boolean;
|
|
15
|
+
readonly detail: string;
|
|
16
|
+
}
|
|
17
|
+
export interface BlogThemeVerificationResult {
|
|
18
|
+
readonly ok: boolean;
|
|
19
|
+
readonly stage: 'PRECHECK' | 'SCAFFOLDED' | 'LOCAL_VERIFIED';
|
|
20
|
+
readonly checks: readonly BlogThemeVerificationCheck[];
|
|
21
|
+
}
|
|
22
|
+
export interface BlogThemeVerificationRuntime {
|
|
23
|
+
readonly run: (command: string, args: readonly string[], options: {
|
|
24
|
+
readonly cwd: string;
|
|
25
|
+
}) => Promise<{
|
|
26
|
+
readonly exitCode: number;
|
|
27
|
+
readonly output: string;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
export declare function createBlogThemeScaffold(options: CreateBlogThemeScaffoldOptions): Promise<BlogThemeScaffoldResult>;
|
|
31
|
+
export declare function verifyBlogThemeProject(options: {
|
|
32
|
+
readonly root: string;
|
|
33
|
+
}, runtime: BlogThemeVerificationRuntime): Promise<BlogThemeVerificationResult>;
|
|
34
|
+
export declare function inspectInstalledThemePeers(root: string): Promise<BlogThemeVerificationCheck>;
|
|
35
|
+
export declare function inspectBlogThemeProject(root: string): Promise<readonly BlogThemeVerificationCheck[]>;
|
|
36
|
+
//# sourceMappingURL=theme-authoring.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme-authoring.d.ts","sourceRoot":"","sources":["../src/theme-authoring.ts"],"names":[],"mappings":"AA2CA,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG,YAAY,GAAG,gBAAgB,CAAC;IAC7D,QAAQ,CAAC,MAAM,EAAE,SAAS,0BAA0B,EAAE,CAAC;CACxD;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,GAAG,EAAE,CACZ,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;KAAE,KAC9B,OAAO,CAAC;QAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACtE;AAED,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,8BAA8B,GACtC,OAAO,CAAC,uBAAuB,CAAC,CA4BlC;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAClC,OAAO,EAAE,4BAA4B,GACpC,OAAO,CAAC,2BAA2B,CAAC,CAsCtC;AAED,wBAAsB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,0BAA0B,CAAC,CA0BlG;AAsDD,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,0BAA0B,EAAE,CAAC,CAuE1G"}
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { readdir, readFile, rename, rm, stat, writeFile, mkdir, mkdtemp } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { satisfies, valid as validSemver } from 'semver';
|
|
7
|
+
import { parse as parseYaml } from 'yaml';
|
|
8
|
+
import { parseZlooksPackageManifest } from './package-manifest.js';
|
|
9
|
+
const THEME_ID_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
10
|
+
const THIRD_PARTY_THEME_PACKAGE_PATTERN = /^zlooks-theme-[a-z][a-z0-9]*(?:-[a-z0-9]+)*-server$/;
|
|
11
|
+
const REQUIRED_ROUTES = [
|
|
12
|
+
'/',
|
|
13
|
+
'/posts/[slug]',
|
|
14
|
+
'/archives',
|
|
15
|
+
'/pages/[slug]',
|
|
16
|
+
'/categories/[slug]',
|
|
17
|
+
'/tags/[slug]',
|
|
18
|
+
'/friends',
|
|
19
|
+
];
|
|
20
|
+
const REQUIRED_FILES = [
|
|
21
|
+
'package.json',
|
|
22
|
+
'pnpm-workspace.yaml',
|
|
23
|
+
'hile-rsc.json',
|
|
24
|
+
'tsconfig.json',
|
|
25
|
+
'tsconfig.runtime.json',
|
|
26
|
+
'src/command/index.ts',
|
|
27
|
+
'src/identity.ts',
|
|
28
|
+
'src/services/blog-theme.boot.ts',
|
|
29
|
+
'src/plugin/page.tsx',
|
|
30
|
+
'src/plugin/blog-frame.tsx',
|
|
31
|
+
'src/plugin/blog-interactions.tsx',
|
|
32
|
+
'src/plugin/theme.css',
|
|
33
|
+
'test/theme-contract.test.ts',
|
|
34
|
+
];
|
|
35
|
+
const LOCAL_GATES = [
|
|
36
|
+
['install', '--frozen-lockfile=false'],
|
|
37
|
+
['run', 'test'],
|
|
38
|
+
['run', 'typecheck'],
|
|
39
|
+
['run', 'build'],
|
|
40
|
+
['run', 'verify:rsc'],
|
|
41
|
+
];
|
|
42
|
+
export async function createBlogThemeScaffold(options) {
|
|
43
|
+
const themeId = parseThemeId(options.themeId);
|
|
44
|
+
const displayName = parseRequiredText(options.displayName, 'displayName', 100);
|
|
45
|
+
const description = parseRequiredText(options.description ?? `${displayName} Zlooks 博客主题`, 'description', 500);
|
|
46
|
+
const destination = resolve(options.destination);
|
|
47
|
+
const packageName = `zlooks-theme-${themeId}-server`;
|
|
48
|
+
await assertDestinationAbsent(destination);
|
|
49
|
+
const temporary = join(dirname(destination), `.${basename(destination)}.tmp-${randomUUID()}`);
|
|
50
|
+
await mkdir(temporary, { recursive: false });
|
|
51
|
+
try {
|
|
52
|
+
await copyTemplate(new URL('../templates/blog-theme/', import.meta.url), temporary, {
|
|
53
|
+
'__THEME_ID__': themeId,
|
|
54
|
+
'__PACKAGE_NAME__': packageName,
|
|
55
|
+
'__DISPLAY_NAME_JSON__': JSON.stringify(displayName),
|
|
56
|
+
'__DESCRIPTION_JSON__': JSON.stringify(description),
|
|
57
|
+
...await themeSdkVersions(),
|
|
58
|
+
});
|
|
59
|
+
await rename(temporary, destination);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
await rm(temporary, { recursive: true, force: true });
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
return Object.freeze({ destination, packageName, themeId });
|
|
66
|
+
}
|
|
67
|
+
export async function verifyBlogThemeProject(options, runtime) {
|
|
68
|
+
const root = resolve(options.root);
|
|
69
|
+
const checks = await inspectBlogThemeProject(root);
|
|
70
|
+
if (checks.some((check) => !check.ok)) {
|
|
71
|
+
return Object.freeze({ ok: false, stage: 'PRECHECK', checks });
|
|
72
|
+
}
|
|
73
|
+
const executed = [...checks];
|
|
74
|
+
for (const args of LOCAL_GATES) {
|
|
75
|
+
const id = `command.${args.slice(1).join('.') || args[0]}`;
|
|
76
|
+
const result = await runtime.run('pnpm', args, { cwd: root });
|
|
77
|
+
executed.push(Object.freeze({
|
|
78
|
+
id,
|
|
79
|
+
ok: result.exitCode === 0,
|
|
80
|
+
detail: result.exitCode === 0 ? `pnpm ${args.join(' ')} passed` : boundedOutput(result.output),
|
|
81
|
+
}));
|
|
82
|
+
if (result.exitCode !== 0) {
|
|
83
|
+
return Object.freeze({ ok: false, stage: 'SCAFFOLDED', checks: executed });
|
|
84
|
+
}
|
|
85
|
+
if (args[0] === 'install') {
|
|
86
|
+
const compatible = await inspectInstalledThemePeers(root);
|
|
87
|
+
executed.push(compatible);
|
|
88
|
+
if (!compatible.ok)
|
|
89
|
+
return Object.freeze({ ok: false, stage: 'SCAFFOLDED', checks: executed });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const finalChecks = [
|
|
93
|
+
...await inspectBlogThemeProject(root),
|
|
94
|
+
await inspectInstalledThemePeers(root),
|
|
95
|
+
].map((check) => ({ ...check, id: `final.${check.id}` }));
|
|
96
|
+
executed.push(...finalChecks);
|
|
97
|
+
if (finalChecks.some((check) => !check.ok))
|
|
98
|
+
return Object.freeze({ ok: false, stage: 'SCAFFOLDED', checks: executed });
|
|
99
|
+
const artifact = await inspectProducedArtifact(root, runtime);
|
|
100
|
+
executed.push(artifact);
|
|
101
|
+
if (!artifact.ok)
|
|
102
|
+
return Object.freeze({ ok: false, stage: 'SCAFFOLDED', checks: executed });
|
|
103
|
+
const packed = await inspectPackedTheme(root, runtime);
|
|
104
|
+
executed.push(packed);
|
|
105
|
+
if (!packed.ok)
|
|
106
|
+
return Object.freeze({ ok: false, stage: 'SCAFFOLDED', checks: executed });
|
|
107
|
+
return Object.freeze({ ok: true, stage: 'LOCAL_VERIFIED', checks: executed });
|
|
108
|
+
}
|
|
109
|
+
export async function inspectInstalledThemePeers(root) {
|
|
110
|
+
try {
|
|
111
|
+
const readInstalled = async (name) => JSON.parse(await readFile(join(root, 'node_modules', name, 'package.json'), 'utf8'));
|
|
112
|
+
const ui = await readInstalled('@zlooks.cn/ui');
|
|
113
|
+
const theme = await readInstalled('@zlooks.cn/blog-theme');
|
|
114
|
+
const requirements = {
|
|
115
|
+
antd: ui.peerDependencies?.antd,
|
|
116
|
+
react: ui.peerDependencies?.react,
|
|
117
|
+
'react-dom': ui.peerDependencies?.['react-dom'],
|
|
118
|
+
'react-server-dom-webpack': ui.peerDependencies?.react,
|
|
119
|
+
'@hile/rsc': theme.dependencies?.['@hile/rsc'],
|
|
120
|
+
};
|
|
121
|
+
for (const [name, range] of Object.entries(requirements)) {
|
|
122
|
+
if (!range)
|
|
123
|
+
throw new Error(`Installed SDK is missing its ${name} compatibility contract`);
|
|
124
|
+
const installed = await readInstalled(name);
|
|
125
|
+
if (!satisfies(installed.version, range)) {
|
|
126
|
+
const prefix = name === 'antd' ? '--save-exact' : '--save-prefix=^';
|
|
127
|
+
throw new Error(`${name}@${installed.version} does not satisfy the SDK contract ${range}. Resolve a compatible version with pnpm add ${prefix} ${name}@${range}, preserve pnpm-lock.yaml, and rerun verification.`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return { id: 'installed.compatible', ok: true, detail: 'Installed Ant Design, React and Hile versions match SDK compatibility contracts' };
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
return { id: 'installed.compatible', ok: false, detail: boundedOutput(error instanceof Error ? error.message : String(error)) };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async function inspectProducedArtifact(root, runtime) {
|
|
137
|
+
try {
|
|
138
|
+
const result = await runtime.run('pnpm', ['exec', 'hile-rsc', 'verify'], { cwd: root });
|
|
139
|
+
const verified = JSON.parse(result.output);
|
|
140
|
+
if (result.exitCode !== 0 || verified.valid !== true || verified.pluginId !== 'blog')
|
|
141
|
+
throw new Error('Direct Hile RSC verification did not confirm a valid Blog artifact');
|
|
142
|
+
const builds = await readdir(join(root, '.hile-rsc'), { withFileTypes: true });
|
|
143
|
+
if (builds.length !== 1 || !builds[0].isDirectory() || builds[0].name.startsWith('.'))
|
|
144
|
+
throw new Error('.hile-rsc must contain exactly one production build and no development state');
|
|
145
|
+
const prefix = `.hile-rsc/${builds[0].name}`;
|
|
146
|
+
const manifest = JSON.parse(await readFile(join(root, prefix, 'plugin.json'), 'utf8'));
|
|
147
|
+
if (!REQUIRED_ROUTES.every((path) => manifest.routes?.some((route) => route.path === path)))
|
|
148
|
+
throw new Error('The compiled artifact is missing required Blog routes');
|
|
149
|
+
return { id: 'artifact.verified', ok: true, detail: 'Direct Hile verification confirms one immutable Blog build with all seven routes' };
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
return { id: 'artifact.verified', ok: false, detail: boundedOutput(error instanceof Error ? error.message : String(error)) };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function inspectPackedTheme(root, runtime) {
|
|
156
|
+
const destination = await mkdtemp(join(tmpdir(), 'zlooks-theme-pack-'));
|
|
157
|
+
try {
|
|
158
|
+
const result = await runtime.run('npm', ['pack', '--ignore-scripts', '--json', '--pack-destination', destination], { cwd: root });
|
|
159
|
+
if (result.exitCode !== 0)
|
|
160
|
+
throw new Error(result.output || 'npm pack failed');
|
|
161
|
+
const output = JSON.parse(result.output);
|
|
162
|
+
if (output.length !== 1 || basename(output[0].filename) !== output[0].filename)
|
|
163
|
+
throw new Error('npm pack returned an invalid tarball');
|
|
164
|
+
const tarball = await stat(join(destination, output[0].filename));
|
|
165
|
+
if (!tarball.isFile() || tarball.size === 0)
|
|
166
|
+
throw new Error('npm pack produced no tarball');
|
|
167
|
+
const packed = new Set(output[0].files.map(({ path }) => path));
|
|
168
|
+
const manifest = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
|
|
169
|
+
const expected = ['package.json', ...Object.values(manifest.bin), ...await regularFiles(root, '.hile-rsc'), ...await regularFiles(root, 'dist')];
|
|
170
|
+
if (!expected.some((path) => path.endsWith('.boot.js')))
|
|
171
|
+
throw new Error('The compiled service has no boot file');
|
|
172
|
+
const missing = expected.filter((path) => !packed.has(path.replace(/^\.\//, '')));
|
|
173
|
+
if (missing.length)
|
|
174
|
+
throw new Error(`Tarball is missing build files: ${missing.join(', ')}`);
|
|
175
|
+
const unexpected = [...packed].filter((path) => !path.startsWith('dist/') && !path.startsWith('.hile-rsc/') && !/^(?:package\.json|readme(?:\.[^/]*)?|licen[cs]e(?:\.[^/]*)?)$/i.test(path));
|
|
176
|
+
if (unexpected.length)
|
|
177
|
+
throw new Error(`Tarball contains non-runtime files: ${unexpected.join(', ')}`);
|
|
178
|
+
return { id: 'package.packed', ok: true, detail: `Real npm tarball contains all ${expected.length} required metadata and build files; test archive removed` };
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
return { id: 'package.packed', ok: false, detail: boundedOutput(error instanceof Error ? error.message : String(error)) };
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
await rm(destination, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async function regularFiles(root, directory) {
|
|
188
|
+
const files = [];
|
|
189
|
+
for (const entry of await readdir(join(root, directory), { withFileTypes: true })) {
|
|
190
|
+
const path = `${directory}/${entry.name}`;
|
|
191
|
+
if (entry.isDirectory())
|
|
192
|
+
files.push(...await regularFiles(root, path));
|
|
193
|
+
else if (entry.isFile())
|
|
194
|
+
files.push(path);
|
|
195
|
+
else
|
|
196
|
+
throw new Error(`Build output must contain regular files only: ${path}`);
|
|
197
|
+
}
|
|
198
|
+
return files;
|
|
199
|
+
}
|
|
200
|
+
export async function inspectBlogThemeProject(root) {
|
|
201
|
+
const files = new Map();
|
|
202
|
+
const fileChecks = await Promise.all(REQUIRED_FILES.map(async (path) => {
|
|
203
|
+
try {
|
|
204
|
+
const content = await readFile(join(root, path), 'utf8');
|
|
205
|
+
files.set(path, content);
|
|
206
|
+
return { id: `file.${path}`, ok: content.length > 0, detail: `${path} is present` };
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return { id: `file.${path}`, ok: false, detail: `${path} is missing` };
|
|
210
|
+
}
|
|
211
|
+
}));
|
|
212
|
+
const checks = [...fileChecks];
|
|
213
|
+
checks.push(validateManifest(files.get('package.json')));
|
|
214
|
+
checks.push(validateWorkspace(files.get('pnpm-workspace.yaml')));
|
|
215
|
+
checks.push(validateRoutes(files.get('hile-rsc.json')));
|
|
216
|
+
let source = '';
|
|
217
|
+
try {
|
|
218
|
+
const sourcePaths = (await regularFiles(root, 'src')).filter((path) => /\.[cm]?[jt]sx?$/.test(path));
|
|
219
|
+
source = (await Promise.all(sourcePaths.map((path) => readFile(join(root, path), 'utf8')))).join('\n');
|
|
220
|
+
checks.push({ id: 'source.inspectable', ok: true, detail: `Inspected all ${sourcePaths.length} production source files` });
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
checks.push({ id: 'source.inspectable', ok: false, detail: error instanceof Error ? error.message : String(error) });
|
|
224
|
+
}
|
|
225
|
+
checks.push(markerCheck('runtime.sdk', source, [
|
|
226
|
+
'defineBlogTheme',
|
|
227
|
+
'createBlogThemeBootService',
|
|
228
|
+
'createServiceBackedBlogThemeDataLoader',
|
|
229
|
+
]));
|
|
230
|
+
checks.push(markerCheck('routes.exports', files.get('src/plugin/page.tsx') ?? '', [
|
|
231
|
+
'BlogHomePage',
|
|
232
|
+
'BlogPostPage',
|
|
233
|
+
'BlogArchivePage',
|
|
234
|
+
'BlogSinglePagePage',
|
|
235
|
+
'BlogCategoryPage',
|
|
236
|
+
'BlogTagPage',
|
|
237
|
+
'BlogFriendLinksPage',
|
|
238
|
+
]));
|
|
239
|
+
checks.push(markerCheck('interactions.complete', files.get('src/plugin/blog-interactions.tsx') ?? '', [
|
|
240
|
+
'searchPosts',
|
|
241
|
+
'likePost',
|
|
242
|
+
'unlikePost',
|
|
243
|
+
'listComments',
|
|
244
|
+
'createComment',
|
|
245
|
+
]));
|
|
246
|
+
const unsafeAntdClientImport = /import\s*\{[^}]*\b(?:App|Button)\b[^}]*\}\s*from\s*['"]antd['"]/s.test(source);
|
|
247
|
+
checks.push(markerCheck('ui.shell-complete', source, [
|
|
248
|
+
'ZlooksShellProvider', 'ZlooksSiteConfigProvider', 'ZlooksUserAccess',
|
|
249
|
+
'useZlooksNavigation', 'useZlooksTheme', 'setMode', 'onLogout', 'legal.registrations', 'legal.copyright',
|
|
250
|
+
]));
|
|
251
|
+
checks.push(Object.freeze({
|
|
252
|
+
id: 'ui.rsc-compatible',
|
|
253
|
+
ok: source.includes('@zlooks.cn/ui/button') && !unsafeAntdClientImport,
|
|
254
|
+
detail: source.includes('@zlooks.cn/ui/button') && !unsafeAntdClientImport
|
|
255
|
+
? 'Buttons use the Hile-compatible shared UI boundary'
|
|
256
|
+
: 'Import ZlooksButton from @zlooks.cn/ui/button and use @zlooks.cn/ui/app instead of importing App or Button from the Ant Design root barrel',
|
|
257
|
+
}));
|
|
258
|
+
checks.push(Object.freeze({
|
|
259
|
+
id: 'markdown.safe',
|
|
260
|
+
ok: !source.includes('dangerouslySetInnerHTML') && source.includes('react-markdown'),
|
|
261
|
+
detail: !source.includes('dangerouslySetInnerHTML') && source.includes('react-markdown')
|
|
262
|
+
? 'Markdown uses a structured renderer without raw HTML injection'
|
|
263
|
+
: 'Use react-markdown and remove dangerouslySetInnerHTML',
|
|
264
|
+
}));
|
|
265
|
+
checks.push(Object.freeze({
|
|
266
|
+
id: 'styles.bound',
|
|
267
|
+
ok: (files.get('src/plugin/blog-frame.tsx') ?? '').includes("./theme.css"),
|
|
268
|
+
detail: 'The Client Boundary must import ./theme.css',
|
|
269
|
+
}));
|
|
270
|
+
return Object.freeze(checks);
|
|
271
|
+
}
|
|
272
|
+
function validateManifest(source) {
|
|
273
|
+
try {
|
|
274
|
+
const raw = JSON.parse(source ?? '');
|
|
275
|
+
const parsed = parseZlooksPackageManifest(raw);
|
|
276
|
+
if (!THIRD_PARTY_THEME_PACKAGE_PATTERN.test(parsed.name) || parsed.name.startsWith('@zlooks.cn/')) {
|
|
277
|
+
throw new Error('Third-party themes must use zlooks-theme-<themeId>-server without the @zlooks.cn scope');
|
|
278
|
+
}
|
|
279
|
+
if (parsed.kind !== 'theme')
|
|
280
|
+
throw new Error('zlooks.kind must be theme');
|
|
281
|
+
const expectedThemeId = parsed.name.slice('zlooks-theme-'.length, -'-server'.length);
|
|
282
|
+
if (raw.zlooks?.themeId !== expectedThemeId) {
|
|
283
|
+
throw new Error(`zlooks.themeId must be ${expectedThemeId} so package and runtime identity match`);
|
|
284
|
+
}
|
|
285
|
+
if (raw.private !== false)
|
|
286
|
+
throw new Error('Third-party theme package must set private=false');
|
|
287
|
+
if (!Array.isArray(raw.files) || !raw.files.includes('dist') || !raw.files.includes('.hile-rsc')) {
|
|
288
|
+
throw new Error('files must publish dist and .hile-rsc');
|
|
289
|
+
}
|
|
290
|
+
if (!Array.isArray(raw.zlooks?.nodeConditions) || !raw.zlooks.nodeConditions.includes('react-server')) {
|
|
291
|
+
throw new Error('zlooks.nodeConditions must include react-server');
|
|
292
|
+
}
|
|
293
|
+
const scripts = requireRecord(raw.scripts, 'scripts');
|
|
294
|
+
for (const script of ['test', 'typecheck', 'build', 'verify:rsc']) {
|
|
295
|
+
if (typeof scripts[script] !== 'string' || scripts[script].trim().length === 0) {
|
|
296
|
+
throw new Error(`package.json must define ${script}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const runtimeScripts = {
|
|
300
|
+
dev: 'NODE_OPTIONS=--conditions=react-server hile start --dev --env-file ~/.zlooks.cn/.env',
|
|
301
|
+
start: 'NODE_OPTIONS=--conditions=react-server hile start --env-file ~/.zlooks.cn/.env',
|
|
302
|
+
};
|
|
303
|
+
for (const [script, expected] of Object.entries(runtimeScripts)) {
|
|
304
|
+
if (scripts[script] !== expected) {
|
|
305
|
+
throw new Error(`${script} must load the canonical ~/.zlooks.cn/.env through Hile`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
for (const [section, dependencies] of [
|
|
309
|
+
['dependencies', raw.dependencies],
|
|
310
|
+
['devDependencies', raw.devDependencies],
|
|
311
|
+
['peerDependencies', raw.peerDependencies],
|
|
312
|
+
['optionalDependencies', raw.optionalDependencies],
|
|
313
|
+
]) {
|
|
314
|
+
if (dependencies === undefined)
|
|
315
|
+
continue;
|
|
316
|
+
for (const [name, version] of Object.entries(requireRecord(dependencies, section))) {
|
|
317
|
+
const valid = typeof version === 'string' && (name === 'antd'
|
|
318
|
+
? validSemver(version) !== null
|
|
319
|
+
: version.startsWith('^') && validSemver(version.slice(1)) !== null);
|
|
320
|
+
if (!valid) {
|
|
321
|
+
throw new Error(`${section}.${name} must use a caret npm version such as ^1.0.0, except antd must use an exact version; workspace:, file:, link:, tags, and paths are forbidden`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
assertPortableResolutionConfig(raw.pnpm, 'pnpm');
|
|
326
|
+
assertPortableResolutionConfig(raw.overrides, 'overrides');
|
|
327
|
+
assertPortableResolutionConfig(raw.resolutions, 'resolutions');
|
|
328
|
+
return Object.freeze({ id: 'manifest.deployable', ok: true, detail: `${parsed.name} is a deployable third-party theme` });
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
return Object.freeze({
|
|
332
|
+
id: 'manifest.deployable',
|
|
333
|
+
ok: false,
|
|
334
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function validateWorkspace(source) {
|
|
339
|
+
try {
|
|
340
|
+
const parsed = requireRecord(parseYaml(source ?? ''), 'pnpm-workspace.yaml');
|
|
341
|
+
for (const field of ['overrides', 'catalog', 'catalogs', 'packageExtensions', 'patchedDependencies']) {
|
|
342
|
+
assertPortableResolutionConfig(parsed[field], field);
|
|
343
|
+
}
|
|
344
|
+
const packages = parsed.packages;
|
|
345
|
+
const allowBuilds = requireRecord(parsed.allowBuilds, 'allowBuilds');
|
|
346
|
+
const ok = Array.isArray(packages)
|
|
347
|
+
&& packages.length === 1
|
|
348
|
+
&& packages[0] === '.'
|
|
349
|
+
&& allowBuilds.esbuild === true;
|
|
350
|
+
return Object.freeze({
|
|
351
|
+
id: 'workspace.installable',
|
|
352
|
+
ok,
|
|
353
|
+
detail: ok
|
|
354
|
+
? 'Standalone pnpm workspace permits the required esbuild install script'
|
|
355
|
+
: 'pnpm-workspace.yaml must contain packages: [.] and allowBuilds.esbuild: true',
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
return Object.freeze({
|
|
360
|
+
id: 'workspace.installable',
|
|
361
|
+
ok: false,
|
|
362
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function assertPortableResolutionConfig(value, field) {
|
|
367
|
+
if (typeof value === 'string') {
|
|
368
|
+
const specifier = value.trim();
|
|
369
|
+
if (/^(?:workspace:|file:|link:|portal:|patch:|\.\.?[\\/]|~[\\/]|[a-z]:[\\/])/i.test(specifier) || isAbsolute(specifier)) {
|
|
370
|
+
throw new Error(`${field} contains a non-portable local dependency; use published npm packages, not workspace:, file:, link:, or filesystem paths`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
else if (value && typeof value === 'object') {
|
|
374
|
+
for (const [key, child] of Object.entries(value))
|
|
375
|
+
assertPortableResolutionConfig(child, `${field}.${key}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function validateRoutes(source) {
|
|
379
|
+
try {
|
|
380
|
+
const parsed = JSON.parse(source ?? '');
|
|
381
|
+
const paths = Array.isArray(parsed.routes) ? parsed.routes.map(({ path }) => path) : [];
|
|
382
|
+
const ok = parsed.pluginId === 'blog'
|
|
383
|
+
&& REQUIRED_ROUTES.every((path) => paths.includes(path))
|
|
384
|
+
&& Array.isArray(parsed.styles)
|
|
385
|
+
&& parsed.styles.includes('@zlooks.cn/ui/antd.css');
|
|
386
|
+
return Object.freeze({
|
|
387
|
+
id: 'routes.complete',
|
|
388
|
+
ok,
|
|
389
|
+
detail: ok ? 'All seven Blog routes and the shared Ant Design stylesheet are declared' : 'hile-rsc.json must declare pluginId blog, all seven routes, and @zlooks.cn/ui/antd.css',
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
return Object.freeze({ id: 'routes.complete', ok: false, detail: 'hile-rsc.json is invalid or missing' });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function markerCheck(id, source, markers) {
|
|
397
|
+
const missing = markers.filter((marker) => !source.includes(marker));
|
|
398
|
+
return Object.freeze({
|
|
399
|
+
id,
|
|
400
|
+
ok: missing.length === 0,
|
|
401
|
+
detail: missing.length === 0 ? `${id} is complete` : `Missing: ${missing.join(', ')}`,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
async function assertDestinationAbsent(destination) {
|
|
405
|
+
try {
|
|
406
|
+
await stat(destination);
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
if (isNodeError(error) && error.code === 'ENOENT')
|
|
410
|
+
return;
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
413
|
+
throw new Error(`Destination is not empty or already exists: ${destination}`);
|
|
414
|
+
}
|
|
415
|
+
async function copyTemplate(sourceUrl, destination, replacements) {
|
|
416
|
+
const entries = await readdir(sourceUrl, { withFileTypes: true });
|
|
417
|
+
for (const entry of entries) {
|
|
418
|
+
const source = new URL(entry.name, sourceUrl);
|
|
419
|
+
const scaffoldName = entry.name.endsWith('.template') ? entry.name.slice(0, -'.template'.length) : entry.name;
|
|
420
|
+
const name = scaffoldName === '_gitignore' ? '.gitignore' : scaffoldName;
|
|
421
|
+
const target = join(destination, name);
|
|
422
|
+
if (entry.isDirectory()) {
|
|
423
|
+
await mkdir(target);
|
|
424
|
+
await copyTemplate(new URL(`${entry.name}/`, sourceUrl), target, replacements);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (!entry.isFile())
|
|
428
|
+
throw new Error(`Unsupported scaffold entry: ${entry.name}`);
|
|
429
|
+
const content = (await readFile(source, 'utf8')).replace(/__[A-Z_]+__/g, (placeholder) => replacements[placeholder] ?? placeholder);
|
|
430
|
+
await writeFile(target, content, { encoding: 'utf8', flag: 'wx' });
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
async function themeSdkVersions() {
|
|
434
|
+
const packages = {
|
|
435
|
+
'__BLOG_SCHEMA_VERSION__': '@zlooks.cn/blog-schema',
|
|
436
|
+
'__BLOG_THEME_VERSION__': '@zlooks.cn/blog-theme',
|
|
437
|
+
'__GLOBAL_CONFIG_VERSION__': '@zlooks.cn/global-config-shared',
|
|
438
|
+
'__SERVICE_COMMAND_VERSION__': '@zlooks.cn/service-command',
|
|
439
|
+
'__UI_VERSION__': '@zlooks.cn/ui',
|
|
440
|
+
};
|
|
441
|
+
const versions = await Promise.all(Object.entries(packages).map(async ([placeholder, name]) => {
|
|
442
|
+
// Resolve metadata without evaluating React, Hile services, or SDK runtime code.
|
|
443
|
+
let directory = dirname(fileURLToPath(import.meta.resolve(name)));
|
|
444
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
445
|
+
let manifest = {};
|
|
446
|
+
try {
|
|
447
|
+
manifest = JSON.parse(await readFile(join(directory, 'package.json'), 'utf8'));
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
if (!isNodeError(error) || error.code !== 'ENOENT')
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
if (manifest.name === name && manifest.version && validSemver(manifest.version))
|
|
454
|
+
return [placeholder, manifest.version];
|
|
455
|
+
const parent = dirname(directory);
|
|
456
|
+
if (parent === directory)
|
|
457
|
+
break;
|
|
458
|
+
directory = parent;
|
|
459
|
+
}
|
|
460
|
+
throw new Error(`Cannot resolve the installed theme SDK version: ${name}. Reinstall the matching Zlooks CLI release.`);
|
|
461
|
+
}));
|
|
462
|
+
return Object.fromEntries(versions);
|
|
463
|
+
}
|
|
464
|
+
function parseThemeId(value) {
|
|
465
|
+
const parsed = value.trim();
|
|
466
|
+
if (!THEME_ID_PATTERN.test(parsed) || parsed.length > 50) {
|
|
467
|
+
throw new Error('themeId must be lowercase kebab-case, start with a letter, and contain at most 50 characters');
|
|
468
|
+
}
|
|
469
|
+
return parsed;
|
|
470
|
+
}
|
|
471
|
+
function parseRequiredText(value, field, maxLength) {
|
|
472
|
+
const parsed = value.trim();
|
|
473
|
+
if (parsed.length === 0 || parsed.length > maxLength) {
|
|
474
|
+
throw new Error(`${field} must contain 1-${maxLength} characters`);
|
|
475
|
+
}
|
|
476
|
+
return parsed;
|
|
477
|
+
}
|
|
478
|
+
function boundedOutput(value) {
|
|
479
|
+
const normalized = value.trim();
|
|
480
|
+
return normalized.length <= 2_000 ? normalized : `${normalized.slice(0, 500)}\n… output truncated …\n${normalized.slice(-1_450)}`;
|
|
481
|
+
}
|
|
482
|
+
function isNodeError(error) {
|
|
483
|
+
return error instanceof Error && 'code' in error;
|
|
484
|
+
}
|
|
485
|
+
function requireRecord(value, field) {
|
|
486
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
487
|
+
throw new Error(`${field} must be an object`);
|
|
488
|
+
}
|
|
489
|
+
return value;
|
|
490
|
+
}
|
|
491
|
+
//# sourceMappingURL=theme-authoring.js.map
|