@ttmg/cli 0.4.1 → 0.4.3
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/index.js +146 -19
- package/dist/index.js.map +1 -1
- package/dist/package.json +1 -1
- package/dist/public/assets/Detail-1nSjYvRw.js +1 -0
- package/dist/public/assets/Detail-1nSjYvRw.js.br +0 -0
- package/dist/public/assets/MonetizationMode-hwWN5zUT.js +1 -0
- package/dist/public/assets/MonetizationModeSummary-BmvxAKbI.js +1 -0
- package/dist/public/assets/SectionHeader-BL_bJf61.js +1 -0
- package/dist/public/assets/Tag-B4bSl-_E.js +1 -0
- package/dist/public/assets/baseForm-BAvk37jF.css +1 -0
- package/dist/public/assets/baseForm-BAvk37jF.css.br +0 -0
- package/dist/public/assets/baseForm-zW5Gb1OH.js +10 -0
- package/dist/public/assets/baseForm-zW5Gb1OH.js.br +0 -0
- package/dist/public/assets/col-BXewy14r.css +1 -0
- package/dist/public/assets/col-BXewy14r.css.br +0 -0
- package/dist/public/assets/col-vAurEPaC.js +1 -0
- package/dist/public/assets/{compass-VJNhHwqW.js → compass-DEadzu-v.js} +1 -1
- package/dist/public/assets/index-3FxbJxwu.css +1 -0
- package/dist/public/assets/index-BCdOYP-_.js +1 -0
- package/dist/public/assets/{index-BO5MBsdm.js → index-BJmPZor5.js} +1 -1
- package/dist/public/assets/{index-BJZ4gOGZ.css → index-BRJFt3oz.css} +1 -1
- package/dist/public/assets/index-BRJFt3oz.css.br +0 -0
- package/dist/public/assets/{index-BBR3CPkN.js → index-BXllV1mP.js} +1 -1
- package/dist/public/assets/index-BXllV1mP.js.br +0 -0
- package/dist/public/assets/index-BdixrK5I.css +1 -0
- package/dist/public/assets/index-BdixrK5I.css.br +0 -0
- package/dist/public/assets/index-BlG-Rcwd.css +1 -0
- package/dist/public/assets/{index-s_30KWiA.js → index-Bpx0cjfq.js} +1 -1
- package/dist/public/assets/index-BuiCnshQ.js +1 -0
- package/dist/public/assets/{index-bQUQ2Wmz.js → index-C2jjb4tf.js} +1 -1
- package/dist/public/assets/index-C5k1IRcX.js +1 -0
- package/dist/public/assets/index-C5k1IRcX.js.br +0 -0
- package/dist/public/assets/{index-DMsjcIN8.js → index-CHODjAYV.js} +1 -1
- package/dist/public/assets/index-CYnulX57.css +1 -0
- package/dist/public/assets/index-CYnulX57.css.br +0 -0
- package/dist/public/assets/index-CaArjQkD.js +1 -0
- package/dist/public/assets/index-CaArjQkD.js.br +0 -0
- package/dist/public/assets/index-ChtjciM4.js +1 -0
- package/dist/public/assets/index-ChtjciM4.js.br +0 -0
- package/dist/public/assets/index-CozQgXOx.js +1 -0
- package/dist/public/assets/index-CozQgXOx.js.br +0 -0
- package/dist/public/assets/index-Cw4SLGVh.js +1 -0
- package/dist/public/assets/index-Czg0RYGI.css +1 -0
- package/dist/public/assets/index-Czg0RYGI.css.br +0 -0
- package/dist/public/assets/index-DBDUeSB8.css +1 -0
- package/dist/public/assets/index-DBDUeSB8.css.br +0 -0
- package/dist/public/assets/index-DKxf26_O.css +1 -0
- package/dist/public/assets/{index-RYY-l6Oq.css → index-DM7Cg9s9.css} +1 -1
- package/dist/public/assets/index-DVDX92Zz.js +1 -0
- package/dist/public/assets/{index-DbxY65Om.js → index-Db3F1FgQ.js} +1 -1
- package/dist/public/assets/{index-C1C34gtn.js → index-Dd2Oev0p.js} +1 -1
- package/dist/public/assets/index-DfVst8q1.css +1 -0
- package/dist/public/assets/{index-BH099q_U.js → index-DhK1XTMu.js} +1 -1
- package/dist/public/assets/index-Dj0vBPuW.js +1 -0
- package/dist/public/assets/{index-C_npSvtQ.js → index-Do7tPKef.js} +1 -1
- package/dist/public/assets/index-DpnThngH.js +1 -0
- package/dist/public/assets/index-DyHfBQPm.js +1 -0
- package/dist/public/assets/index-HqjZ5P-l.css +1 -0
- package/dist/public/assets/index-P3QtE2VP.js +1 -0
- package/dist/public/assets/index-P3QtE2VP.js.br +0 -0
- package/dist/public/assets/{index-Dn5HHf-0.js → index-QtH0wQu1.js} +1 -1
- package/dist/public/assets/index-qycdeV7S.js +1 -0
- package/dist/public/assets/index-qycdeV7S.js.br +0 -0
- package/dist/public/assets/index-vlN_-PRJ.css +1 -0
- package/dist/public/assets/index-vuGJOmRn.js +1 -0
- package/dist/public/assets/index-wqj8y8SZ.js +21 -0
- package/dist/public/assets/index-wqj8y8SZ.js.br +0 -0
- package/dist/public/assets/layers-DDrUeTNf.js +1 -0
- package/dist/public/assets/list-checks-C4ci4EKo.js +1 -0
- package/dist/public/assets/{sparkles-Bv2GxrFr.js → sparkles-PwlPP_sT.js} +1 -1
- package/dist/public/assets/triangle-alert-BAadCxAv.js +1 -0
- package/dist/public/assets/{zap-CjkC_qWZ.js → zap-CgIIDLY4.js} +1 -1
- package/dist/public/index.html +2 -2
- package/dist/scripts/docs/README.md +80 -0
- package/dist/scripts/docs/capture.js +91 -7
- package/dist/scripts/docs/check.js +184 -0
- package/dist/scripts/docs/render.js +372 -12
- package/dist/scripts/docs/scenarios/devtool-guide.mjs +921 -0
- package/package.json +1 -1
- package/CHANGELOG.md +0 -267
- package/dist/public/assets/Card-ASGGlJ8S.js +0 -1
- package/dist/public/assets/Detail-DY__Zw0r.js +0 -1
- package/dist/public/assets/Detail-DY__Zw0r.js.br +0 -0
- package/dist/public/assets/MonetizationMode-sHN_4ybQ.js +0 -1
- package/dist/public/assets/MonetizationModeSummary-DLvyriLM.js +0 -1
- package/dist/public/assets/SectionHeader-B7dueBCf.js +0 -1
- package/dist/public/assets/Tag-BsX0d6UL.js +0 -1
- package/dist/public/assets/arrow-left-uixm7Sd1.js +0 -1
- package/dist/public/assets/baseForm-C8XfTFyG.js +0 -10
- package/dist/public/assets/baseForm-C8XfTFyG.js.br +0 -0
- package/dist/public/assets/baseForm-Dl4zA6hU.css +0 -1
- package/dist/public/assets/baseForm-Dl4zA6hU.css.br +0 -0
- package/dist/public/assets/boxes-C7RoIbId.js +0 -1
- package/dist/public/assets/chevron-right-B1WBZrrD.js +0 -1
- package/dist/public/assets/circle-check-wUxXeINd.js +0 -1
- package/dist/public/assets/index-BAud2cRu.css +0 -1
- package/dist/public/assets/index-BBR3CPkN.js.br +0 -0
- package/dist/public/assets/index-BJZ4gOGZ.css.br +0 -0
- package/dist/public/assets/index-BNc8JtET.js +0 -1
- package/dist/public/assets/index-Bd-f8ERX.js +0 -1
- package/dist/public/assets/index-ByX5RuFA.css +0 -1
- package/dist/public/assets/index-C7Tmy9g3.js +0 -1
- package/dist/public/assets/index-CCKb9Y-j.js +0 -14
- package/dist/public/assets/index-CCKb9Y-j.js.br +0 -0
- package/dist/public/assets/index-CFd8iglC.css +0 -1
- package/dist/public/assets/index-CFd8iglC.css.br +0 -0
- package/dist/public/assets/index-CKhfKyA3.js +0 -1
- package/dist/public/assets/index-CKhfKyA3.js.br +0 -0
- package/dist/public/assets/index-Cc1ilXmc.css +0 -1
- package/dist/public/assets/index-DHXlsTU6.css +0 -1
- package/dist/public/assets/index-DHXlsTU6.css.br +0 -0
- package/dist/public/assets/index-DNd8ZTok.js +0 -1
- package/dist/public/assets/index-DNd8ZTok.js.br +0 -0
- package/dist/public/assets/index-DpkktYot.css +0 -1
- package/dist/public/assets/index-DtoDrber.js +0 -1
- package/dist/public/assets/index-DtoDrber.js.br +0 -0
- package/dist/public/assets/index-Dx5VGEQm.css +0 -1
- package/dist/public/assets/index-ZHPtAdJM.js +0 -1
- package/dist/public/assets/index-ZHPtAdJM.js.br +0 -0
- package/dist/public/assets/index-ZtlCPCV9.js +0 -1
- package/dist/public/assets/index-_6n0s04V.css +0 -1
- package/dist/public/assets/index-_6n0s04V.css.br +0 -0
- package/dist/public/assets/index-a-rMUS2V.js +0 -1
- package/dist/public/assets/index-qqnHwFjO.js +0 -1
- package/dist/public/assets/index-r3aSeNL1.js +0 -1
- package/dist/public/assets/index-t4JeqZ4a.js +0 -1
- package/dist/public/assets/layers-Biv61qtE.js +0 -1
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{c as a}from"./index-wqj8y8SZ.js";const o=[["path",{d:"m9 18 6-6-6-6",key:"mthhwq"}]],c=a("chevron-right",o);const e=[["path",{d:"M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z",key:"zw3jo"}],["path",{d:"M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12",key:"1wduqc"}],["path",{d:"M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17",key:"kqbvx6"}]],h=a("layers",e);export{c as C,h as L};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{c}from"./index-wqj8y8SZ.js";const e=[["path",{d:"M13 5h8",key:"a7qcls"}],["path",{d:"M13 12h8",key:"h98zly"}],["path",{d:"M13 19h8",key:"c3s6r1"}],["path",{d:"m3 17 2 2 4-4",key:"1jhpwq"}],["path",{d:"m3 7 2 2 4-4",key:"1obspn"}]],t=c("list-checks",e);export{t as L};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{c as a}from"./index-
|
|
1
|
+
import{c as a}from"./index-wqj8y8SZ.js";const e=[["path",{d:"M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z",key:"1s2grr"}],["path",{d:"M20 2v4",key:"1rf3ol"}],["path",{d:"M22 4h-4",key:"gwowj6"}],["circle",{cx:"4",cy:"20",r:"2",key:"6kqj1y"}]],l=a("sparkles",e);export{l as S};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{c as e}from"./index-wqj8y8SZ.js";const a=[["path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3",key:"wmoenq"}],["path",{d:"M12 9v4",key:"juzpu7"}],["path",{d:"M12 17h.01",key:"p32p05"}]],o=e("triangle-alert",a);export{o as T};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{c as a}from"./index-
|
|
1
|
+
import{c as a}from"./index-wqj8y8SZ.js";const o=[["path",{d:"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z",key:"1xq2db"}]],e=a("zap",o);export{e as Z};
|
package/dist/public/index.html
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
rel="stylesheet"
|
|
13
13
|
/>
|
|
14
14
|
<title>TikTok Mini Games DevTool</title>
|
|
15
|
-
<script type="module" crossorigin src="/assets/index-
|
|
16
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
15
|
+
<script type="module" crossorigin src="/assets/index-wqj8y8SZ.js"></script>
|
|
16
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BRJFt3oz.css">
|
|
17
17
|
</head>
|
|
18
18
|
<!--
|
|
19
19
|
Theme attributes (`theme-mode` for Semi UI + `data-theme` for our
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# DevTool documentation screenshots
|
|
2
|
+
|
|
3
|
+
This directory keeps the local pipeline for DevTool usage-guide screenshots and generated guide drafts.
|
|
4
|
+
|
|
5
|
+
## Main scenario file
|
|
6
|
+
|
|
7
|
+
`scenarios/devtool-guide.mjs` is the first stable scenario manifest. Each recipe describes:
|
|
8
|
+
|
|
9
|
+
- the current Lark source document for the usage guide
|
|
10
|
+
- the DevTool route to open
|
|
11
|
+
- the page state or fixture expected by the screenshot
|
|
12
|
+
- the capture selector and viewport
|
|
13
|
+
- the usage-guide section that consumes the screenshot
|
|
14
|
+
- bilingual notes used by `render.js`
|
|
15
|
+
- acceptance checks that should stay true when the page changes
|
|
16
|
+
- the documentation contract, screenshot inventory, and release checks used for guide review
|
|
17
|
+
|
|
18
|
+
## Commands
|
|
19
|
+
|
|
20
|
+
Render the guide draft and indexes without opening the browser:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm run docs:devtool:render
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Capture screenshots from a running local DevTool page:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm run docs:devtool:capture
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Check that screenshots are fresh enough for publishing:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm run docs:devtool:check
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`docs:devtool:capture` assumes the IDE is available at `http://localhost:5173`. Start or change the local DevTool URL before running it if your port is different.
|
|
39
|
+
|
|
40
|
+
## Doc mode fixture
|
|
41
|
+
|
|
42
|
+
Screenshot routes append `docMode=1&docScenario=<scenario id>`. In this mode the IDE uses stable fixture data for platform and project requests, so usage-guide screenshots do not depend on the current login account, local Node connection, platform availability, or a connected device.
|
|
43
|
+
|
|
44
|
+
Doc mode is only enabled by the URL parameter above or `localStorage.TTMG_DOCS_CAPTURE=1`. Normal DevTool usage still goes through the real local service and platform APIs.
|
|
45
|
+
|
|
46
|
+
Pages that enter the guide should expose stable screenshot anchors such as:
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
<main data-doc-shot="start-debug-qr-idle">
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The scenario manifest should target these anchors through `capture.selector` instead of relying on layout CSS classes.
|
|
53
|
+
|
|
54
|
+
## Outputs
|
|
55
|
+
|
|
56
|
+
The default output directory is `artifacts/devtool-guide/`.
|
|
57
|
+
|
|
58
|
+
- `manifest.json`: scenario snapshot used for this run
|
|
59
|
+
- `screenshot-index.json`: screenshot-to-route/section mapping for later doc updates
|
|
60
|
+
- `doc-version.json`: version snapshot for the guide source, scenario fingerprint, CLI version, ttmg-pack version, and generated artifacts
|
|
61
|
+
- `quality-gate.json`: review contract, release checks, and screenshot inventory for the current guide refresh
|
|
62
|
+
- `capture-report.json`: screenshot freshness evidence from the latest capture run
|
|
63
|
+
- `index.md`: bilingual usage-guide draft referencing screenshots in `assets/`
|
|
64
|
+
|
|
65
|
+
`manifest.json`, `screenshot-index.json`, `doc-version.json`, and `quality-gate.json` include `guideSource` so each generated draft can be traced back to the current Lark guide instead of relying on a one-off screenshot note.
|
|
66
|
+
|
|
67
|
+
The render step is safe to run without screenshots. The capture step should be run only when the local DevTool page is already available.
|
|
68
|
+
|
|
69
|
+
## Freshness contract
|
|
70
|
+
|
|
71
|
+
DevTool screenshots are considered publishable only when:
|
|
72
|
+
|
|
73
|
+
- `capture-report.json` exists in the output directory
|
|
74
|
+
- its scenario fingerprint matches the current `scenarios/devtool-guide.mjs`
|
|
75
|
+
- every expected `recipe + locale` screenshot exists under `assets/`
|
|
76
|
+
- screenshot files are newer than the scenario manifest
|
|
77
|
+
- `capture-report.json` and screenshot files are newer than the latest `manifest.json` / `screenshot-index.json` / `doc-version.json` / `quality-gate.json`
|
|
78
|
+
- the capture report is within the freshness window used by `docs:devtool:check`
|
|
79
|
+
|
|
80
|
+
If any of these checks fail, rerun `npm run docs:devtool:capture` from the current DevTool build before updating the usage guide.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
3
4
|
const { pathToFileURL } = require('url');
|
|
4
5
|
const chromeLauncher = require('chrome-launcher');
|
|
5
6
|
const CDP = require('chrome-remote-interface');
|
|
@@ -42,6 +43,27 @@ function localizeValue(value, lang) {
|
|
|
42
43
|
);
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
function stableStringify(value) {
|
|
47
|
+
if (Array.isArray(value)) {
|
|
48
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
49
|
+
}
|
|
50
|
+
if (!value || typeof value !== 'object') {
|
|
51
|
+
return JSON.stringify(value);
|
|
52
|
+
}
|
|
53
|
+
return `{${Object.keys(value)
|
|
54
|
+
.sort()
|
|
55
|
+
.map(key => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
|
|
56
|
+
.join(',')}}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createScenarioFingerprint({ schemaVersion, locales, guideTitle, guideSource, recipes }) {
|
|
60
|
+
return crypto
|
|
61
|
+
.createHash('sha256')
|
|
62
|
+
.update(stableStringify({ schemaVersion, locales, guideTitle, guideSource, recipes }))
|
|
63
|
+
.digest('hex')
|
|
64
|
+
.slice(0, 16);
|
|
65
|
+
}
|
|
66
|
+
|
|
45
67
|
function escapeForEval(value) {
|
|
46
68
|
return JSON.stringify(value).replace(/<\//g, '<\\/');
|
|
47
69
|
}
|
|
@@ -229,17 +251,35 @@ async function captureFullPage(Page, Runtime, Emulation, outputPath, viewport) {
|
|
|
229
251
|
async function loadRecipes(recipesPath) {
|
|
230
252
|
const module = await import(pathToFileURL(recipesPath).href);
|
|
231
253
|
return {
|
|
254
|
+
schemaVersion: module.schemaVersion || 1,
|
|
232
255
|
recipes: module.recipes || module.default || [],
|
|
233
256
|
locales: module.locales || ['zh-CN', 'en-US'],
|
|
257
|
+
guideTitle: module.guideTitle || 'TikTok 小游戏调试工具使用指南',
|
|
258
|
+
guideSource: module.guideSource || null,
|
|
234
259
|
defaultLocalStorage: module.defaultLocalStorage || {},
|
|
235
260
|
};
|
|
236
261
|
}
|
|
237
262
|
|
|
263
|
+
function getRecipeViewport(recipe) {
|
|
264
|
+
return recipe.viewport || recipe.capture?.viewport || { width: 1440, height: 1400 };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function getRecipeWaitFor(recipe) {
|
|
268
|
+
return recipe.waitFor || {
|
|
269
|
+
selector: recipe.capture?.selector || 'body',
|
|
270
|
+
timeoutMs: 20000,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function createLocalizedRecipeUrl(baseUrl, route, locale) {
|
|
275
|
+
const url = new URL(route, baseUrl);
|
|
276
|
+
url.searchParams.set('docLang', locale);
|
|
277
|
+
return url.toString();
|
|
278
|
+
}
|
|
279
|
+
|
|
238
280
|
async function main() {
|
|
239
281
|
const args = parseArgs(process.argv.slice(2));
|
|
240
282
|
const baseUrl = args['base-url'];
|
|
241
|
-
const recipesPath = path.resolve(args.recipes);
|
|
242
|
-
const outputDir = path.resolve(args['output-dir']);
|
|
243
283
|
|
|
244
284
|
if (!baseUrl) {
|
|
245
285
|
throw new Error('Missing required argument: --base-url');
|
|
@@ -251,9 +291,34 @@ async function main() {
|
|
|
251
291
|
throw new Error('Missing required argument: --output-dir');
|
|
252
292
|
}
|
|
253
293
|
|
|
254
|
-
const
|
|
294
|
+
const recipesPath = path.resolve(args.recipes);
|
|
295
|
+
const outputDir = path.resolve(args['output-dir']);
|
|
296
|
+
const { schemaVersion, recipes, locales, guideTitle, guideSource, defaultLocalStorage } = await loadRecipes(recipesPath);
|
|
297
|
+
const normalizedLocales = locales.map(normalizeLang);
|
|
298
|
+
const scenarioFingerprint = createScenarioFingerprint({
|
|
299
|
+
schemaVersion,
|
|
300
|
+
recipes,
|
|
301
|
+
locales: normalizedLocales,
|
|
302
|
+
guideTitle,
|
|
303
|
+
guideSource,
|
|
304
|
+
});
|
|
255
305
|
const assetsDir = path.join(outputDir, 'assets');
|
|
256
306
|
fs.mkdirSync(assetsDir, { recursive: true });
|
|
307
|
+
const reportPath = path.join(outputDir, 'capture-report.json');
|
|
308
|
+
if (fs.existsSync(reportPath)) {
|
|
309
|
+
fs.rmSync(reportPath);
|
|
310
|
+
}
|
|
311
|
+
const captureReport = {
|
|
312
|
+
schemaVersion: 1,
|
|
313
|
+
title: guideTitle,
|
|
314
|
+
guideSource,
|
|
315
|
+
scenarioFingerprint,
|
|
316
|
+
baseUrl,
|
|
317
|
+
recipesPath,
|
|
318
|
+
generatedAt: new Date().toISOString(),
|
|
319
|
+
outputDir,
|
|
320
|
+
screenshots: [],
|
|
321
|
+
};
|
|
257
322
|
|
|
258
323
|
const chrome = await chromeLauncher.launch({
|
|
259
324
|
chromeFlags: [
|
|
@@ -280,7 +345,7 @@ async function main() {
|
|
|
280
345
|
}),
|
|
281
346
|
]);
|
|
282
347
|
|
|
283
|
-
for (const locale of
|
|
348
|
+
for (const locale of normalizedLocales) {
|
|
284
349
|
const baseHeaders = {
|
|
285
350
|
'Accept-Language': locale,
|
|
286
351
|
'X-TTMG-Docs-Lang': locale,
|
|
@@ -296,7 +361,8 @@ async function main() {
|
|
|
296
361
|
await seedLocalStorage(Runtime, localizedStorage);
|
|
297
362
|
|
|
298
363
|
for (const recipe of recipes) {
|
|
299
|
-
const viewport = recipe
|
|
364
|
+
const viewport = getRecipeViewport(recipe);
|
|
365
|
+
const waitFor = getRecipeWaitFor(recipe);
|
|
300
366
|
const requestHeaders = {
|
|
301
367
|
...baseHeaders,
|
|
302
368
|
...(recipe.requestHeaders || {}),
|
|
@@ -305,17 +371,35 @@ async function main() {
|
|
|
305
371
|
headers: requestHeaders,
|
|
306
372
|
});
|
|
307
373
|
await setViewport(Emulation, viewport.width, viewport.height);
|
|
308
|
-
await navigateAndWait(client, Page,
|
|
309
|
-
await waitForSelector(Runtime,
|
|
374
|
+
await navigateAndWait(client, Page, createLocalizedRecipeUrl(baseUrl, recipe.route, locale));
|
|
375
|
+
await waitForSelector(Runtime, waitFor.selector, waitFor.timeoutMs || 20000);
|
|
310
376
|
await stabilizePage(Runtime);
|
|
311
377
|
await wait(500);
|
|
312
378
|
|
|
313
379
|
const fileName = `${recipe.id}.${locale}.png`;
|
|
314
380
|
const outputPath = path.join(assetsDir, fileName);
|
|
315
381
|
await captureFullPage(Page, Runtime, Emulation, outputPath, viewport);
|
|
382
|
+
const stats = fs.statSync(outputPath);
|
|
383
|
+
captureReport.screenshots.push({
|
|
384
|
+
id: `${recipe.id}.${locale}`,
|
|
385
|
+
recipeId: recipe.id,
|
|
386
|
+
locale,
|
|
387
|
+
route: recipe.route,
|
|
388
|
+
selector: recipe.capture?.selector || recipe.selector || 'body',
|
|
389
|
+
viewport,
|
|
390
|
+
path: `assets/${fileName}`,
|
|
391
|
+
bytes: stats.size,
|
|
392
|
+
capturedAt: new Date().toISOString(),
|
|
393
|
+
});
|
|
316
394
|
console.log(`[ttmg-docs] captured ${fileName}`);
|
|
317
395
|
}
|
|
318
396
|
}
|
|
397
|
+
fs.writeFileSync(
|
|
398
|
+
reportPath,
|
|
399
|
+
`${JSON.stringify(captureReport, null, 2)}\n`,
|
|
400
|
+
'utf8',
|
|
401
|
+
);
|
|
402
|
+
console.log(`[ttmg-docs] wrote capture report into ${outputDir}`);
|
|
319
403
|
} finally {
|
|
320
404
|
if (client) {
|
|
321
405
|
await client.close();
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { pathToFileURL } = require('url');
|
|
5
|
+
|
|
6
|
+
function parseArgs(argv) {
|
|
7
|
+
return argv.reduce((acc, arg) => {
|
|
8
|
+
if (!arg.startsWith('--')) {
|
|
9
|
+
return acc;
|
|
10
|
+
}
|
|
11
|
+
const [key, value] = arg.slice(2).split('=');
|
|
12
|
+
acc[key] = value === undefined ? true : value;
|
|
13
|
+
return acc;
|
|
14
|
+
}, {});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeLang(value) {
|
|
18
|
+
return String(value || '').toLowerCase().includes('en') ? 'en-US' : 'zh-CN';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function stableStringify(value) {
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
24
|
+
}
|
|
25
|
+
if (!value || typeof value !== 'object') {
|
|
26
|
+
return JSON.stringify(value);
|
|
27
|
+
}
|
|
28
|
+
return `{${Object.keys(value)
|
|
29
|
+
.sort()
|
|
30
|
+
.map(key => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
|
|
31
|
+
.join(',')}}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createScenarioFingerprint({ schemaVersion, locales, guideTitle, guideSource, recipes }) {
|
|
35
|
+
return crypto
|
|
36
|
+
.createHash('sha256')
|
|
37
|
+
.update(stableStringify({ schemaVersion, locales, guideTitle, guideSource, recipes }))
|
|
38
|
+
.digest('hex')
|
|
39
|
+
.slice(0, 16);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function loadRecipes(recipesPath) {
|
|
43
|
+
const module = await import(pathToFileURL(recipesPath).href);
|
|
44
|
+
return {
|
|
45
|
+
schemaVersion: module.schemaVersion || 1,
|
|
46
|
+
recipes: module.recipes || module.default || [],
|
|
47
|
+
locales: (module.locales || ['zh-CN', 'en-US']).map(normalizeLang),
|
|
48
|
+
guideTitle: module.guideTitle || 'TikTok 小游戏调试工具使用指南',
|
|
49
|
+
guideSource: module.guideSource || null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readJson(filePath) {
|
|
54
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function fail(message) {
|
|
58
|
+
throw new Error(message);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function assertFreshEnough(report, maxAgeHours) {
|
|
62
|
+
if (!maxAgeHours) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const maxAgeMs = Number(maxAgeHours) * 60 * 60 * 1000;
|
|
66
|
+
const generatedAt = new Date(report.generatedAt).getTime();
|
|
67
|
+
if (!Number.isFinite(generatedAt)) {
|
|
68
|
+
fail('capture-report.json has invalid generatedAt');
|
|
69
|
+
}
|
|
70
|
+
const ageMs = Date.now() - generatedAt;
|
|
71
|
+
if (ageMs > maxAgeMs) {
|
|
72
|
+
fail(`DevTool screenshots are stale: capture-report is older than ${maxAgeHours} hours`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function assertFileFreshAgainstRecipe(filePath, recipesPath) {
|
|
77
|
+
const screenshotStat = fs.statSync(filePath);
|
|
78
|
+
const recipeStat = fs.statSync(recipesPath);
|
|
79
|
+
if (screenshotStat.mtimeMs + 1000 < recipeStat.mtimeMs) {
|
|
80
|
+
fail(`Screenshot is older than scenario manifest: ${filePath}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function assertReportFreshAgainstArtifact(report, artifactPath) {
|
|
85
|
+
if (!fs.existsSync(artifactPath)) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const reportGeneratedAt = new Date(report.generatedAt).getTime();
|
|
89
|
+
const artifactStat = fs.statSync(artifactPath);
|
|
90
|
+
if (reportGeneratedAt + 1000 < artifactStat.mtimeMs) {
|
|
91
|
+
fail(`capture-report.json is older than ${path.basename(artifactPath)}. Re-run docs:devtool:capture.`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function assertScreenshotFreshAgainstArtifact(filePath, artifactPath) {
|
|
96
|
+
if (!fs.existsSync(artifactPath)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const screenshotStat = fs.statSync(filePath);
|
|
100
|
+
const artifactStat = fs.statSync(artifactPath);
|
|
101
|
+
if (screenshotStat.mtimeMs + 1000 < artifactStat.mtimeMs) {
|
|
102
|
+
fail(`Screenshot is older than ${path.basename(artifactPath)}: ${filePath}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function main() {
|
|
107
|
+
const args = parseArgs(process.argv.slice(2));
|
|
108
|
+
if (!args.recipes) {
|
|
109
|
+
fail('Missing required argument: --recipes');
|
|
110
|
+
}
|
|
111
|
+
if (!args['output-dir']) {
|
|
112
|
+
fail('Missing required argument: --output-dir');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const recipesPath = path.resolve(args.recipes);
|
|
116
|
+
const outputDir = path.resolve(args['output-dir']);
|
|
117
|
+
const reportPath = path.join(outputDir, 'capture-report.json');
|
|
118
|
+
const manifestPath = path.join(outputDir, 'manifest.json');
|
|
119
|
+
const screenshotIndexPath = path.join(outputDir, 'screenshot-index.json');
|
|
120
|
+
const docVersionPath = path.join(outputDir, 'doc-version.json');
|
|
121
|
+
const qualityGatePath = path.join(outputDir, 'quality-gate.json');
|
|
122
|
+
|
|
123
|
+
if (!fs.existsSync(reportPath)) {
|
|
124
|
+
fail('Missing capture-report.json. Run docs:devtool:capture before publishing screenshots.');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const { schemaVersion, recipes, locales, guideTitle, guideSource } = await loadRecipes(recipesPath);
|
|
128
|
+
const expectedFingerprint = createScenarioFingerprint({ schemaVersion, recipes, locales, guideTitle, guideSource });
|
|
129
|
+
const report = readJson(reportPath);
|
|
130
|
+
|
|
131
|
+
if (report.scenarioFingerprint !== expectedFingerprint) {
|
|
132
|
+
fail('DevTool screenshots are stale: capture-report does not match the current scenario manifest.');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
assertFreshEnough(report, args['max-age-hours']);
|
|
136
|
+
assertReportFreshAgainstArtifact(report, manifestPath);
|
|
137
|
+
assertReportFreshAgainstArtifact(report, screenshotIndexPath);
|
|
138
|
+
assertReportFreshAgainstArtifact(report, docVersionPath);
|
|
139
|
+
assertReportFreshAgainstArtifact(report, qualityGatePath);
|
|
140
|
+
|
|
141
|
+
for (const optionalJson of [manifestPath, screenshotIndexPath, docVersionPath, qualityGatePath]) {
|
|
142
|
+
if (!fs.existsSync(optionalJson)) continue;
|
|
143
|
+
const data = readJson(optionalJson);
|
|
144
|
+
if (data.scenarioFingerprint && data.scenarioFingerprint !== expectedFingerprint) {
|
|
145
|
+
fail(`${path.basename(optionalJson)} does not match the current scenario manifest.`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const capturedById = new Map((report.screenshots || []).map(item => [item.id, item]));
|
|
150
|
+
const expectedIds = [];
|
|
151
|
+
for (const recipe of recipes) {
|
|
152
|
+
for (const locale of locales) {
|
|
153
|
+
expectedIds.push(`${recipe.id}.${locale}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const id of expectedIds) {
|
|
158
|
+
const item = capturedById.get(id);
|
|
159
|
+
if (!item) {
|
|
160
|
+
fail(`Missing screenshot capture record: ${id}`);
|
|
161
|
+
}
|
|
162
|
+
const screenshotPath = path.join(outputDir, item.path);
|
|
163
|
+
if (!fs.existsSync(screenshotPath)) {
|
|
164
|
+
fail(`Missing screenshot file: ${item.path}`);
|
|
165
|
+
}
|
|
166
|
+
const stats = fs.statSync(screenshotPath);
|
|
167
|
+
if (stats.size < 1024) {
|
|
168
|
+
fail(`Screenshot file looks invalid or empty: ${item.path}`);
|
|
169
|
+
}
|
|
170
|
+
assertFileFreshAgainstRecipe(screenshotPath, recipesPath);
|
|
171
|
+
assertScreenshotFreshAgainstArtifact(screenshotPath, manifestPath);
|
|
172
|
+
assertScreenshotFreshAgainstArtifact(screenshotPath, screenshotIndexPath);
|
|
173
|
+
assertScreenshotFreshAgainstArtifact(screenshotPath, docVersionPath);
|
|
174
|
+
assertScreenshotFreshAgainstArtifact(screenshotPath, qualityGatePath);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log(`[ttmg-docs] screenshot freshness check passed: ${expectedIds.length} screenshots`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
main().catch(error => {
|
|
181
|
+
console.error('[ttmg-docs] screenshot freshness check failed');
|
|
182
|
+
console.error(error);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
});
|