@plitzi/cli 0.38.3 → 0.38.4
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/CHANGELOG.md +196 -0
- package/README.md +67 -13
- package/dist/index.js +1424 -413
- package/package.json +3 -3
- package/skills/plitzi-cli/SKILL.md +17 -16
- package/skills/plitzi-cli/reference/from-space.md +10 -1
- package/skills/plitzi-cli/reference/functions.md +14 -1
package/dist/index.js
CHANGED
|
@@ -12,15 +12,27 @@ import { createServer } from "node:http";
|
|
|
12
12
|
import tls from "node:tls";
|
|
13
13
|
import readline from "node:readline/promises";
|
|
14
14
|
import { readFileSync, readdirSync, watch } from "node:fs";
|
|
15
|
-
import { EXPLAIN_KINDS, actionSpecFromEntry, actionToSource, authorSpace, blankSpaceSource, catalogTemplateFiles, comparePictures, darkScheme, elementTriggers, elementTypeNames, emptySpaceSource, explain, explainKindOf, explainList, explanationText, failedFlowText, importProbe, importedFiles, inspectDocument, inspectPage, pageRegions, planFixes, readDevTools, refusalOf } from "@plitzi/sdk-authoring";
|
|
15
|
+
import { EXPLAIN_KINDS, PAGE_TEXT_LIMIT, actionSpecFromEntry, actionToSource, authorSpace, blankSpaceSource, catalogTemplateFiles, comparePictures, compareTexts, darkScheme, dataIssues, elementTriggers, elementTypeNames, emptySpaceSource, explain, explainKindOf, explainList, explanationText, failedFlowText, importProbe, importedFiles, inspectDocument, inspectPage, loadImages, pageRegions, pageTexts, planFixes, readDevTools, refusalOf, unrollPage } from "@plitzi/sdk-authoring";
|
|
16
16
|
import { promisify } from "node:util";
|
|
17
17
|
import { lookup } from "node:dns/promises";
|
|
18
18
|
import { build } from "esbuild";
|
|
19
19
|
import { unzipSync, zipSync } from "fflate";
|
|
20
20
|
import { gzipSync } from "node:zlib";
|
|
21
|
+
import { emitKeypressEvents } from "node:readline";
|
|
22
|
+
var t$3 = `[data-plitzi-devtools] { display: none !important; }`;
|
|
23
|
+
//#endregion
|
|
21
24
|
//#region ../../packages/sdk-shared/dist/helpers/isRecord.mjs
|
|
22
25
|
var e$1 = (e) => typeof e == "object" && !!e && !Array.isArray(e);
|
|
23
26
|
//#endregion
|
|
27
|
+
//#region ../../packages/sdk-shared/dist/theme/themeCookie.mjs
|
|
28
|
+
var t$2 = "theme";
|
|
29
|
+
/** The port `npm start` took and the name its `/health` answers with: what `check`, `shot` and `visual` find it by. */
|
|
30
|
+
var DEV_SERVER_FILE = `tmp/dev-server.json`;
|
|
31
|
+
/** The space as last authored while developing, re-read by the server on every change: never the source of it. */
|
|
32
|
+
var DEV_SPACE_FILE = `tmp/space.json`;
|
|
33
|
+
/** What Playwright leaves of a run: traces, failures' pictures. */
|
|
34
|
+
var VISUAL_OUTPUT = `tmp/visual`;
|
|
35
|
+
//#endregion
|
|
24
36
|
//#region src/browser/index.ts
|
|
25
37
|
/**
|
|
26
38
|
* A browser on the project's own Playwright, and the project's own server to point it at — what `plitzi check`,
|
|
@@ -54,7 +66,7 @@ var launchBrowser = async (root) => {
|
|
|
54
66
|
/** What `npm start` wrote down when it took a port: the port, and the name its `/health` answers with. */
|
|
55
67
|
var recorded = async (root) => {
|
|
56
68
|
try {
|
|
57
|
-
const value = JSON.parse(await readFile(path.join(root,
|
|
69
|
+
const value = JSON.parse(await readFile(path.join(root, DEV_SERVER_FILE), "utf8"));
|
|
58
70
|
if (!e$1(value)) return {};
|
|
59
71
|
return {
|
|
60
72
|
...typeof value.port === "number" ? { port: value.port } : {},
|
|
@@ -83,6 +95,39 @@ var projectOrigin = async (root, project) => {
|
|
|
83
95
|
var dataUrl = (png) => `data:image/png;base64,${Buffer.from(png).toString("base64")}`;
|
|
84
96
|
/** A data URL's picture back as bytes, to write to a file. */
|
|
85
97
|
var fromDataUrl = (url) => Buffer.from(url.slice(url.indexOf(",") + 1), "base64");
|
|
98
|
+
/**
|
|
99
|
+
* A page for the project's own server, in the theme asked for. The machine's preference alone is not enough: a space
|
|
100
|
+
* whose default is dark paints dark whatever the machine prefers, as it would for any visitor. So a scheme asked for
|
|
101
|
+
* is also the space's own choice — the `theme` cookie a visitor's `themeToggle` writes and the server paints `<html>`
|
|
102
|
+
* by — set before the page is asked for, so the first paint is already in it.
|
|
103
|
+
*/
|
|
104
|
+
var openProjectPage = async (browser, origin, view) => {
|
|
105
|
+
const page = await browser.newPage({
|
|
106
|
+
viewport: {
|
|
107
|
+
width: view.width,
|
|
108
|
+
height: view.height
|
|
109
|
+
},
|
|
110
|
+
colorScheme: view.scheme ?? "light",
|
|
111
|
+
reducedMotion: view.reducedMotion ? "reduce" : "no-preference"
|
|
112
|
+
});
|
|
113
|
+
if (view.scheme) await page.context().addCookies([{
|
|
114
|
+
name: t$2,
|
|
115
|
+
value: view.scheme,
|
|
116
|
+
url: origin
|
|
117
|
+
}]);
|
|
118
|
+
return page;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* The theme a page was painted in: the class on `<html>` (a space's own choice, or its default), else the one the
|
|
122
|
+
* machine prefers — which is what a page with neither follows.
|
|
123
|
+
*/
|
|
124
|
+
var paintedScheme = (page, machine) => page.evaluate((prefers) => {
|
|
125
|
+
const { classList } = document.documentElement;
|
|
126
|
+
if (classList.contains("dark")) return "dark";
|
|
127
|
+
return classList.contains("light") ? "light" : prefers;
|
|
128
|
+
}, machine);
|
|
129
|
+
/** The page as a visitor sees it: the dev tools' own chrome — the badge, the panel — left out of whatever is looked at. */
|
|
130
|
+
var withoutDevTools = (page) => page.addStyleTag({ content: t$3 });
|
|
86
131
|
//#endregion
|
|
87
132
|
//#region src/commands/terminal.ts
|
|
88
133
|
/**
|
|
@@ -1494,7 +1539,7 @@ var scripts = ({ mode, source }) => ({
|
|
|
1494
1539
|
/**
|
|
1495
1540
|
* Watched by PATH, not wholesale.
|
|
1496
1541
|
*
|
|
1497
|
-
* The server compiles the project's plugins into
|
|
1542
|
+
* The server compiles the project's plugins into `tmp/.sdk-plugins/` and then IMPORTS what it built, so a
|
|
1498
1543
|
* bare `--watch` sees its own output land, restarts, compiles again, and never stops. A local space is not
|
|
1499
1544
|
* among them: `main.ts` re-authors it on save in a process of its own and the open pages load again
|
|
1500
1545
|
* (`reloadPages`), so only the server's own code and the plugins restart it.
|
|
@@ -1591,10 +1636,12 @@ var tsconfig$1 = ({ mode }) => `${JSON.stringify({
|
|
|
1591
1636
|
* writes `.yarn/install-state.gz` into the project, which is a cache and belongs in no repository.
|
|
1592
1637
|
*/
|
|
1593
1638
|
var YARN_IGNORES$1 = "\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/versions\n";
|
|
1594
|
-
/**
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1639
|
+
/**
|
|
1640
|
+
* `tmp/` is everything the project writes for itself (`./paths`); `.plitzi/` is what the CLI records about it, and is
|
|
1641
|
+
* committed — a clone without it could not pull, push or upgrade.
|
|
1642
|
+
*/
|
|
1643
|
+
var gitignore$1 = ({ packageManager }) => `node_modules\ndist\n.env\ntmp\n${packageManager === "yarn" ? YARN_IGNORES$1 : ""}`;
|
|
1644
|
+
var startLine = ({ mode, packageManager, source }) => mode === "server" ? `\`${runCommand(packageManager, "start")}\` serves pages on http://127.0.0.1:8080 — or, when something else holds 8080, the next free port, which it prints and writes to \`${DEV_SERVER_FILE}\` (set \`PORT\` to choose one). \`${runCommand(packageManager, "start:dev")}\` restarts on a save to the server's code${source === "local" ? "; a save to the space reloads the open page" : ""}. In production, \`${runCommand(packageManager, "build")}\` once and run \`${runCommand(packageManager, "start:prod")}\` with \`NODE_ENV=production\`: the compiled server, with no TypeScript in the process.` : `\`${runCommand(packageManager, "start")}\` runs Vite on http://127.0.0.1:5173, with hot module replacement.`;
|
|
1598
1645
|
var spaceSection = (answers) => {
|
|
1599
1646
|
if (answers.source === "cloud") return `## Where the space comes from
|
|
1600
1647
|
|
|
@@ -1613,9 +1660,9 @@ from what is written there, so authoring it twice writes byte-identical document
|
|
|
1613
1660
|
|
|
1614
1661
|
Nothing is fetched and nothing is signed in to: there is no account, no key and no network in the picture.
|
|
1615
1662
|
|
|
1616
|
-
\`${runCommand(answers.packageManager, "author")}\`
|
|
1617
|
-
|
|
1618
|
-
|
|
1663
|
+
\`${runCommand(answers.packageManager, "author")}\` authors it and says what it found — warnings, suggestions, what a newer SDK changed. It
|
|
1664
|
+
writes nothing: the declaration is the source, and \`npx plitzi push\` puts it on a space in Plitzi when you want it
|
|
1665
|
+
there.`;
|
|
1619
1666
|
};
|
|
1620
1667
|
var readme$1 = (answers) => `# ${answers.name}
|
|
1621
1668
|
|
|
@@ -1631,6 +1678,16 @@ ${startLine(answers)}
|
|
|
1631
1678
|
|
|
1632
1679
|
${spaceSection(answers)}
|
|
1633
1680
|
|
|
1681
|
+
## Folders that are not the source
|
|
1682
|
+
|
|
1683
|
+
- \`public/\` is served to anyone who asks, as it is: every file in it is on the internet once the project is deployed.
|
|
1684
|
+
Data and pictures for every visitor go there — never a secret, a key, a private document or what only some visitors
|
|
1685
|
+
may read.
|
|
1686
|
+
- \`tmp/\` is what the project writes for itself while it runs — the plugins it builds, the port it took, test
|
|
1687
|
+
output. Ignored by git, and rebuilt whenever it is missing.
|
|
1688
|
+
- \`.plitzi/\` is what the CLI records about the project — the space it came from, its functions' working copy, the
|
|
1689
|
+
files \`create\` wrote — so \`plitzi pull\`, \`push\` and \`upgrade\` know where they stand. Commit it.
|
|
1690
|
+
|
|
1634
1691
|
## The skills
|
|
1635
1692
|
|
|
1636
1693
|
\`.claude/skills/\` carries Plitzi's authoring skill, so an agent working in this repository knows how a space is
|
|
@@ -1656,10 +1713,10 @@ var agentsFile = (answers) => {
|
|
|
1656
1713
|
`| ${run("visual")} | open the page in a browser and check it rendered |`
|
|
1657
1714
|
];
|
|
1658
1715
|
const zeroWarnings = local ? `Zero warnings from ${run("author")}.` : "Zero warnings from authoring.";
|
|
1659
|
-
const port = answers.mode === "server" ? `${run("start")} serves on 8080, or on the next free port when something else holds it — printed, and written to ${code(
|
|
1716
|
+
const port = answers.mode === "server" ? `${run("start")} serves on 8080, or on the next free port when something else holds it — printed, and written to ${code(DEV_SERVER_FILE)}, where ${code("check")}, ${code("shot")} and ${code("visual")} read it. ${code("PORT")} chooses one.` : `${run("start")} runs Vite on 5173.`;
|
|
1660
1717
|
const pictures = answers.mode === "server" ? `- **Pictures from other sites** are resized by this server once ${code("src/main.ts")} names their hosts — ${code("images: { domains }")} in ${code("createServer")}, a list of hosts like ${code("images.example.com")} — and ${code("sharp")} is installed: an ${code("image")} then offers a ${code("srcset")} (give it ${code("sizes")}, and ${code("width")}/${code("height")} so nothing jumps).\n` : "";
|
|
1661
|
-
const serverData = answers.mode === "server" ? `; one whose ${code("runtime")} is ${code("server")} is read by the server, and the page arrives with it` : "";
|
|
1662
|
-
const generated = [
|
|
1718
|
+
const serverData = answers.mode === "server" ? `; one whose ${code("runtime")} is ${code("server")} is read by the server, and the page arrives with it — bound the same way (${code("products.data.items")}), on a page or a layout, never inside a component` : "";
|
|
1719
|
+
const generated = [`- ${code(`tmp/`)} — what the project writes for itself while it runs: the plugins it built, the port it took${local ? ", the space as last authored" : ""}, test output. Never committed, rebuilt when missing.`, `- ${code(".plitzi/")} — what the CLI records about the project: where it came from, what it wrote. Committed; the CLI's to change.`];
|
|
1663
1720
|
return `# ${answers.name} — notes for agents
|
|
1664
1721
|
|
|
1665
1722
|
A Plitzi space, rendered ${answers.mode === "server" ? "by the server in this project (SSR + RSC)" : "in the browser by a Vite app"}.
|
|
@@ -1675,7 +1732,8 @@ ${commands.join("\n")}
|
|
|
1675
1732
|
|
|
1676
1733
|
- **Port.** ${port}
|
|
1677
1734
|
- **Data with no backend** goes in ${code("public/data/*.json")}, served as it is and read by an ${code("apiContainer")} whose ${code("query")} is ${code("/data/products.json")}${serverData}.
|
|
1678
|
-
|
|
1735
|
+
- **${code("public/")} is on the internet.** Every file in it is served to anyone who asks for it, as it is, the moment the project is deployed — no sign-in, no check. Never put in it a secret, a key, a ${code(".env")}, a private document, a database dump, or data only some visitors may read: that goes through a server action or a provider that checks who is asking.
|
|
1736
|
+
${pictures}- **Check a page in text first:** ${code(`${runCommand(answers.packageManager, "check")} -- / --width 1440,390`)} says whether every element is on screen, nothing overflows and the console is clean — a picture only when it says something is wrong: ${code(`${runCommand(answers.packageManager, "shot")} -- / --width 390`)} (add ${code("--scheme dark")}; ${code("--frames 4")} to see what moves; ${code("--compare <url>")} against another site: by section, and each text measured). ${run("visual")} runs the checks as tests.
|
|
1679
1737
|
- **What the page holds, in text:** ${code(`${runCommand(answers.packageManager, "check")} -- /products --state --element <id>`)} adds its state, every source by name and one element (what it reads, its own state, whether it is on screen); every check already lists the flows that failed. Read it instead of guessing from classes in the DOM.
|
|
1680
1738
|
|
|
1681
1739
|
## Do not read
|
|
@@ -1693,12 +1751,22 @@ layouts, data, templates and flows. The types of ${code("@plitzi/sdk-authoring")
|
|
|
1693
1751
|
For a component of your own — a plugin — or anything about packing or uploading one, read
|
|
1694
1752
|
${code(".claude/skills/plitzi-cli/SKILL.md")} first: ${code("plitzi add plugin")} writes it in the shape everything reads.
|
|
1695
1753
|
|
|
1754
|
+
## Keep the project clean
|
|
1755
|
+
|
|
1756
|
+
What you leave behind is the next reader's problem — the user's, or the next agent's. Before calling a change done:
|
|
1757
|
+
|
|
1758
|
+
- **Nothing unused.** Delete what you made and no longer use — a file, a page, a component, a class, a token, a data file, an import, a plugin folder. No commented-out code, no ${code("console.log")} left from debugging, no copy of a file kept "just in case": git keeps the history.
|
|
1759
|
+
- **Scratch goes in ${code(`tmp/`)}, or nowhere.** A one-off script, a dump, a picture to look at — never at the root or beside the source, where it reads as part of the project.
|
|
1760
|
+
- **One of everything.** A look used twice is a class; a value used twice is a token; a block used twice is a component, and rows of data are one list. Change it where it is defined, and rename everywhere when you rename.
|
|
1761
|
+
- **Files a reader can find.** One part per file, named after what it is, in the folder of its kind — the shape ${code("src/space.ts")} already has. Do not start a parallel layout of your own.
|
|
1762
|
+
- **Leave it passing.** ${local ? `${run("author")} with zero warnings, ` : ""}${run("typecheck")}, ${run("lint")} and ${run("format")} clean, and the page checked (${run("check")}).
|
|
1763
|
+
|
|
1696
1764
|
## The rules that go wrong most
|
|
1697
1765
|
|
|
1698
1766
|
- Never write schema/style JSON by hand; author it. A refusal names the fix — fix the declaration; ${code("npx plitzi fix --write")} writes the ones with a single reading.
|
|
1699
1767
|
- After the ${code("@plitzi/*")} packages move, ${code("npx plitzi upgrade")}: what this project's CLI files, scripts, skills and renamed names should now be — ${code("--write")} makes it, a file you changed comes as a diff.
|
|
1700
1768
|
- ${zeroWarnings}
|
|
1701
|
-
- Chrome shared by pages is a layout; a look used twice is a class; a
|
|
1769
|
+
- Chrome shared by pages is a layout; a look used twice is a class; a block placed again with other content is a component, and rows of data are one ${code("list")} (a short menu may be a ${code("map")} in code).
|
|
1702
1770
|
- Ids are one namespace for the whole space: name what is referred to; a helper that runs more than once builds inside ${code("scope()")}.
|
|
1703
1771
|
- A file per part — the tokens, the layout, each component, each page — short enough to read whole; ${code("src/space.ts")} assembles them. ${code("npx plitzi create <dir> --template catalog")} is a complete example of the shape.
|
|
1704
1772
|
- Rebuilding a page the user owns: ${code("npx plitzi import <url>")} writes its tokens, outline and lists as a start — then split it into parts and write the content. It reads only a site whose domain the user verified on one of their spaces.
|
|
@@ -1765,14 +1833,8 @@ var prettierrc = () => `${JSON.stringify({
|
|
|
1765
1833
|
arrowParens: "avoid",
|
|
1766
1834
|
endOfLine: "lf"
|
|
1767
1835
|
}, null, 2)}\n`;
|
|
1768
|
-
/** What a project writes that is nobody's to format or lint: its builds,
|
|
1769
|
-
var PROJECT_OUTPUTS = [
|
|
1770
|
-
"dist",
|
|
1771
|
-
"space",
|
|
1772
|
-
".sdk-plugins",
|
|
1773
|
-
"visual/.results",
|
|
1774
|
-
"visual/screenshots"
|
|
1775
|
-
];
|
|
1836
|
+
/** What a project writes that is nobody's to format or lint: its builds, and everything it writes for itself. */
|
|
1837
|
+
var PROJECT_OUTPUTS = ["dist", "tmp"];
|
|
1776
1838
|
/**
|
|
1777
1839
|
* What is the agents' and not the project's: the skills copied in, whose recipes are TypeScript no program of the
|
|
1778
1840
|
* project includes — linted with types, they would stop the lint before its first file.
|
|
@@ -1781,7 +1843,7 @@ var AGENT_FILES = ".claude";
|
|
|
1781
1843
|
var prettierignore = (outputs) => `${[
|
|
1782
1844
|
"node_modules",
|
|
1783
1845
|
AGENT_FILES,
|
|
1784
|
-
...outputs
|
|
1846
|
+
...outputs
|
|
1785
1847
|
].join("\n")}\n`;
|
|
1786
1848
|
/**
|
|
1787
1849
|
* Flat config, and only what earns its place.
|
|
@@ -1892,9 +1954,9 @@ const PORT = process.env.PORT
|
|
|
1892
1954
|
: await freePort(8080, HOST);`;
|
|
1893
1955
|
/** Where the server says it is: the port it took, for `npm run shot` and the visual tests to find. */
|
|
1894
1956
|
var LISTEN_SNIPPET = `server.listen(PORT, HOST);
|
|
1895
|
-
mkdirSync(path.join(PROJECT_ROOT, '
|
|
1957
|
+
mkdirSync(path.join(PROJECT_ROOT, 'tmp'), { recursive: true });
|
|
1896
1958
|
writeFileSync(
|
|
1897
|
-
path.join(PROJECT_ROOT, '
|
|
1959
|
+
path.join(PROJECT_ROOT, '${DEV_SERVER_FILE}'),
|
|
1898
1960
|
\`\${JSON.stringify({ name: SERVER_NAME, port: PORT, url: \`http://127.0.0.1:\${PORT}\` }, null, 2)}\\n\`
|
|
1899
1961
|
);
|
|
1900
1962
|
console.log(\`pages on http://127.0.0.1:\${PORT}/\`);`;
|
|
@@ -1912,6 +1974,7 @@ import {
|
|
|
1912
1974
|
} from '@plitzi/sdk-server';
|
|
1913
1975
|
|
|
1914
1976
|
import { authorSpace } from '@plitzi/sdk-authoring';
|
|
1977
|
+
import { publicData } from '@plitzi/sdk-authoring/node';
|
|
1915
1978
|
|
|
1916
1979
|
import { declarations } from './plugins/declarations.ts';
|
|
1917
1980
|
import { space } from './space.ts';
|
|
@@ -1926,7 +1989,11 @@ ${PORT_SNIPPET}
|
|
|
1926
1989
|
* here for the same reason: this restart is the output somebody editing the space is actually watching.
|
|
1927
1990
|
*/
|
|
1928
1991
|
// \`declarations\`: what the project's plugins fire, answer and read, so the space's use of them is checked too.
|
|
1929
|
-
|
|
1992
|
+
// \`data\`: the files of \`public/\` a provider reads, so a binding onto a path one of them does not have is warned.
|
|
1993
|
+
const { schema, style, warnings } = authorSpace(space, {
|
|
1994
|
+
plugins: declarations,
|
|
1995
|
+
data: publicData(new URL('../public/', import.meta.url))
|
|
1996
|
+
});
|
|
1930
1997
|
const offlineData = { schema, style };
|
|
1931
1998
|
|
|
1932
1999
|
for (const warning of warnings) {
|
|
@@ -1936,12 +2003,13 @@ for (const warning of warnings) {
|
|
|
1936
2003
|
${PLUGINS}
|
|
1937
2004
|
|
|
1938
2005
|
/**
|
|
1939
|
-
* While developing, the documents are served from
|
|
1940
|
-
*
|
|
1941
|
-
*
|
|
2006
|
+
* While developing, the documents are served from \`${DEV_SPACE_FILE}\`, which the server reads again whenever it changes —
|
|
2007
|
+
* so a save is re-authored into it and shown without restarting anything (see \`watchSpace\` below). A by-product, in
|
|
2008
|
+
* \`tmp/\` with everything else the project writes for itself: the space is \`src/space.ts\`. A deployment keeps the
|
|
2009
|
+
* documents in memory, as authored at boot.
|
|
1942
2010
|
*/
|
|
1943
2011
|
const DEVELOPING = process.env.NODE_ENV !== 'production';
|
|
1944
|
-
const OFFLINE_DATA = path.join(PROJECT_ROOT, '
|
|
2012
|
+
const OFFLINE_DATA = path.join(PROJECT_ROOT, '${DEV_SPACE_FILE}');
|
|
1945
2013
|
if (DEVELOPING) {
|
|
1946
2014
|
mkdirSync(path.dirname(OFFLINE_DATA), { recursive: true });
|
|
1947
2015
|
writeFileSync(OFFLINE_DATA, \`\${JSON.stringify(offlineData, null, 2)}\\n\`);
|
|
@@ -1961,7 +2029,7 @@ const functions = await loadFunctions(new URL('../functions/', import.meta.url))
|
|
|
1961
2029
|
* page server makes. A real deployment swaps this for adapters onto its own database, or for
|
|
1962
2030
|
* \`createCloudAdapters\` to read the live space out of Plitzi — the server never learns the difference.
|
|
1963
2031
|
*/
|
|
1964
|
-
// What \`/health\` answers with, and
|
|
2032
|
+
// What \`/health\` answers with, and \`${DEV_SERVER_FILE}\` records: how a tool knows it reached THIS project.
|
|
1965
2033
|
const SERVER_NAME = schema.definition.permanentUrl;
|
|
1966
2034
|
const server = createServer({
|
|
1967
2035
|
port: PORT,
|
|
@@ -2010,7 +2078,7 @@ const watchSpace = (): void => {
|
|
|
2010
2078
|
}
|
|
2011
2079
|
|
|
2012
2080
|
authoring = true;
|
|
2013
|
-
const child = spawn(process.execPath, [path.join(PROJECT_ROOT, 'src/author.ts')], {
|
|
2081
|
+
const child = spawn(process.execPath, [path.join(PROJECT_ROOT, 'src/author.ts'), '--out', OFFLINE_DATA], {
|
|
2014
2082
|
cwd: PROJECT_ROOT,
|
|
2015
2083
|
stdio: 'inherit'
|
|
2016
2084
|
});
|
|
@@ -2089,7 +2157,7 @@ const functions = await loadFunctions(new URL('../functions/', import.meta.url))
|
|
|
2089
2157
|
* on every request; a published environment with no \`revision\` serves the latest and releases itself; with a
|
|
2090
2158
|
* \`revision\` it serves exactly that version, for a deployment that rolls forward on its own schedule.
|
|
2091
2159
|
*/
|
|
2092
|
-
// What \`/health\` answers with, and
|
|
2160
|
+
// What \`/health\` answers with, and \`${DEV_SERVER_FILE}\` records: how a tool knows it reached THIS project.
|
|
2093
2161
|
const SERVER_NAME = ${JSON.stringify(name)};
|
|
2094
2162
|
const server = createServer({
|
|
2095
2163
|
port: PORT,
|
|
@@ -2183,9 +2251,9 @@ var skillVersion = (skill) => /^version: (.+)$/m.exec(skill)?.[1]?.trim();
|
|
|
2183
2251
|
* sit beside it and are opened when the task needs them. Copying \`SKILL.md\` alone left every one of those links
|
|
2184
2252
|
* pointing at nothing.
|
|
2185
2253
|
*/
|
|
2186
|
-
var filesUnder$
|
|
2254
|
+
var filesUnder$2 = (root, folder = root) => readdirSync(folder, { withFileTypes: true }).flatMap((entry) => {
|
|
2187
2255
|
const path = join(folder, entry.name);
|
|
2188
|
-
if (entry.isDirectory()) return filesUnder$
|
|
2256
|
+
if (entry.isDirectory()) return filesUnder$2(root, path);
|
|
2189
2257
|
return entry.isFile() ? [[relative(root, path).split(sep).join("/"), readFileSync(path, "utf-8")]] : [];
|
|
2190
2258
|
});
|
|
2191
2259
|
/**
|
|
@@ -2199,7 +2267,7 @@ var skillFiles = (names = SKILL_NAMES, root) => {
|
|
|
2199
2267
|
const entry = resolveFrom(resolvers, `${SKILLS[name]}/skills/${name}/SKILL.md`);
|
|
2200
2268
|
if (!entry) continue;
|
|
2201
2269
|
const version = versionOf(resolvers, SKILLS[name]);
|
|
2202
|
-
for (const [path, content] of filesUnder$
|
|
2270
|
+
for (const [path, content] of filesUnder$2(dirname(entry))) files[`.claude/skills/${name}/${path}`] = path === "SKILL.md" && version ? stampVersion(content, version) : content;
|
|
2203
2271
|
}
|
|
2204
2272
|
return files;
|
|
2205
2273
|
};
|
|
@@ -2217,22 +2285,25 @@ var skillFiles = (names = SKILL_NAMES, root) => {
|
|
|
2217
2285
|
* signing up gives you cannot come apart.
|
|
2218
2286
|
*/
|
|
2219
2287
|
/**
|
|
2220
|
-
*
|
|
2288
|
+
* Authors the declaration and says what it found: the check an agent and a person run after every change.
|
|
2221
2289
|
*
|
|
2222
|
-
*
|
|
2223
|
-
*
|
|
2224
|
-
* repository with no TypeScript in it.
|
|
2290
|
+
* It writes nothing — the space is \`src/space.ts\`, and the server and the browser both author it at boot. Only the
|
|
2291
|
+
* server asks for the documents, while developing: \`--out <file>\` writes them where it re-reads them on a save.
|
|
2225
2292
|
*/
|
|
2226
2293
|
var authorScript = () => `import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2227
2294
|
import { createRequire } from 'node:module';
|
|
2295
|
+
import path from 'node:path';
|
|
2228
2296
|
|
|
2229
2297
|
import { authorSpace, planFixes, refusalOf, SpaceRefusedError } from '@plitzi/sdk-authoring';
|
|
2298
|
+
import { publicData } from '@plitzi/sdk-authoring/node';
|
|
2230
2299
|
|
|
2231
2300
|
import { declarations } from './plugins/declarations.ts';
|
|
2232
2301
|
import { space } from './space.ts';
|
|
2233
2302
|
|
|
2234
2303
|
// \`--json\` answers in one object, for a tool or an agent; otherwise one line on success, the problems on failure.
|
|
2235
2304
|
const json = process.argv.includes('--json');
|
|
2305
|
+
// \`--out <file>\`: where to write the documents — the server's, while developing. Left out, nothing is written.
|
|
2306
|
+
const out = process.argv.includes('--out') ? process.argv[process.argv.indexOf('--out') + 1] : undefined;
|
|
2236
2307
|
|
|
2237
2308
|
/** How many of the warnings and suggestions said have one fix \`plitzi fix\` can write in this source — and the line that says so. */
|
|
2238
2309
|
const fixableHint = (): string | undefined => {
|
|
@@ -2282,9 +2353,15 @@ const outdated = (): { skill?: string; files?: string; sdk: string } | undefined
|
|
|
2282
2353
|
};
|
|
2283
2354
|
|
|
2284
2355
|
try {
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2356
|
+
// \`data\`: the files of \`public/\` a provider reads, so a binding onto a path one of them does not have is said here.
|
|
2357
|
+
const { schema, style, warnings, suggestions } = authorSpace(space, {
|
|
2358
|
+
plugins: declarations,
|
|
2359
|
+
data: publicData(new URL('../public/', import.meta.url))
|
|
2360
|
+
});
|
|
2361
|
+
if (out) {
|
|
2362
|
+
mkdirSync(path.dirname(out), { recursive: true });
|
|
2363
|
+
writeFileSync(out, \`\${JSON.stringify({ schema, style }, null, 2)}\\n\`);
|
|
2364
|
+
}
|
|
2288
2365
|
|
|
2289
2366
|
const behind = outdated();
|
|
2290
2367
|
if (json) {
|
|
@@ -2319,7 +2396,7 @@ try {
|
|
|
2319
2396
|
}
|
|
2320
2397
|
|
|
2321
2398
|
console.log(
|
|
2322
|
-
\`ok · \${schema.pages.length} pages · \${warnings.length} warnings · \${suggestions.length} suggestions
|
|
2399
|
+
\`ok · \${schema.pages.length} pages · \${warnings.length} warnings · \${suggestions.length} suggestions\`
|
|
2323
2400
|
);
|
|
2324
2401
|
}
|
|
2325
2402
|
} catch (error) {
|
|
@@ -2418,7 +2495,7 @@ var spaceFiles = (answers) => {
|
|
|
2418
2495
|
var RECORDED = `/** What \`npm start\` wrote down when it took a port: the port, and the name its \`/health\` answers with. */
|
|
2419
2496
|
const recorded = (): { port?: number; name?: string } => {
|
|
2420
2497
|
try {
|
|
2421
|
-
const value: unknown = JSON.parse(readFileSync('
|
|
2498
|
+
const value: unknown = JSON.parse(readFileSync('${DEV_SERVER_FILE}', 'utf8'));
|
|
2422
2499
|
if (typeof value !== 'object' || value === null) {
|
|
2423
2500
|
return {};
|
|
2424
2501
|
}
|
|
@@ -2442,7 +2519,7 @@ const PORT = Number(process.env.PORT ?? recorded().port ?? ${mode === "server" ?
|
|
|
2442
2519
|
|
|
2443
2520
|
export default defineConfig({
|
|
2444
2521
|
testDir: './visual',
|
|
2445
|
-
outputDir: '
|
|
2522
|
+
outputDir: './${VISUAL_OUTPUT}',
|
|
2446
2523
|
use: { baseURL: \`http://127.0.0.1:\${PORT}\` },
|
|
2447
2524
|
// Playwright starts the project itself, so \`${runCommand(packageManager, "visual")}\` is one command from a
|
|
2448
2525
|
// cold checkout.
|
|
@@ -2794,7 +2871,7 @@ var tsconfig = () => `{
|
|
|
2794
2871
|
`;
|
|
2795
2872
|
/** Yarn's own recommendation: with the `node-modules` linker it writes a cache into `.yarn/` that no repository wants. */
|
|
2796
2873
|
var YARN_IGNORES = "\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/versions\n";
|
|
2797
|
-
var gitignore = ({ packageManager, inProject }) => `node_modules\ndist\n*.zip\
|
|
2874
|
+
var gitignore = ({ packageManager, inProject }) => `node_modules\ndist\n*.zip\ntmp\n${packageManager === "yarn" && !inProject ? YARN_IGNORES : ""}`;
|
|
2798
2875
|
var packageFiles = (names, answers) => ({
|
|
2799
2876
|
"package.json": packageJson(names, answers),
|
|
2800
2877
|
"tsconfig.json": tsconfig(),
|
|
@@ -2933,7 +3010,7 @@ var previewFiles = ([main, ...others]) => ({
|
|
|
2933
3010
|
//#endregion
|
|
2934
3011
|
//#region ../../packages/sdk-shared/dist/helpers/scroll.mjs
|
|
2935
3012
|
var e = ["smooth", "auto"];
|
|
2936
|
-
var t = [
|
|
3013
|
+
var t$1 = [
|
|
2937
3014
|
"start",
|
|
2938
3015
|
"center",
|
|
2939
3016
|
"end",
|
|
@@ -3049,13 +3126,13 @@ var a$1 = {
|
|
|
3049
3126
|
type: "select",
|
|
3050
3127
|
description: "Where the element lands, top to bottom.",
|
|
3051
3128
|
default: "start",
|
|
3052
|
-
options: [...t]
|
|
3129
|
+
options: [...t$1]
|
|
3053
3130
|
},
|
|
3054
3131
|
inline: {
|
|
3055
3132
|
type: "select",
|
|
3056
3133
|
description: "Where it lands, side to side.",
|
|
3057
3134
|
default: "nearest",
|
|
3058
|
-
options: [...t]
|
|
3135
|
+
options: [...t$1]
|
|
3059
3136
|
},
|
|
3060
3137
|
behavior: r$2
|
|
3061
3138
|
}
|
|
@@ -3779,7 +3856,7 @@ const PORT = 5173;
|
|
|
3779
3856
|
|
|
3780
3857
|
export default defineConfig({
|
|
3781
3858
|
testDir: './visual',
|
|
3782
|
-
outputDir: '
|
|
3859
|
+
outputDir: './${VISUAL_OUTPUT}',
|
|
3783
3860
|
use: { baseURL: \`http://127.0.0.1:\${PORT}\` },
|
|
3784
3861
|
// Playwright starts the preview itself, so \`${runCommand(packageManager, "visual")}\` is one command from a cold checkout.
|
|
3785
3862
|
webServer: {
|
|
@@ -3845,7 +3922,7 @@ var visualFiles = (elements, answers) => ({
|
|
|
3845
3922
|
//#endregion
|
|
3846
3923
|
//#region src/scaffold/pluginPackage/index.ts
|
|
3847
3924
|
/** What the build writes, and what is nobody's to format or lint. */
|
|
3848
|
-
var OUTPUTS = ["dist", "
|
|
3925
|
+
var OUTPUTS = ["dist", "tmp"];
|
|
3849
3926
|
/**
|
|
3850
3927
|
* Every file of a plugin package: the element, the build that publishes it, a space to preview it in, and what checks
|
|
3851
3928
|
* it — a project of its own, with nothing about it shared with whatever repository it sits in except the folder.
|
|
@@ -3946,7 +4023,7 @@ var LOCKFILES = [
|
|
|
3946
4023
|
["pnpm-lock.yaml", "pnpm"],
|
|
3947
4024
|
["package-lock.json", "npm"]
|
|
3948
4025
|
];
|
|
3949
|
-
var exists = async (file) => {
|
|
4026
|
+
var exists$1 = async (file) => {
|
|
3950
4027
|
try {
|
|
3951
4028
|
await fs.access(file);
|
|
3952
4029
|
return true;
|
|
@@ -3977,7 +4054,7 @@ var workspaceGlobs = async (dir, packageJson) => {
|
|
|
3977
4054
|
var nearestPackage = async (from) => {
|
|
3978
4055
|
let dir = from;
|
|
3979
4056
|
for (;;) {
|
|
3980
|
-
if (await exists(path.join(dir, "package.json"))) return dir;
|
|
4057
|
+
if (await exists$1(path.join(dir, "package.json"))) return dir;
|
|
3981
4058
|
const parent = path.dirname(dir);
|
|
3982
4059
|
if (parent === dir) return;
|
|
3983
4060
|
dir = parent;
|
|
@@ -3996,7 +4073,7 @@ var nearestWorkspace = async (from) => {
|
|
|
3996
4073
|
}
|
|
3997
4074
|
};
|
|
3998
4075
|
var lockfileManager = async (dirs) => {
|
|
3999
|
-
for (const dir of dirs) for (const [file, manager] of LOCKFILES) if (await exists(path.join(dir, file))) return manager;
|
|
4076
|
+
for (const dir of dirs) for (const [file, manager] of LOCKFILES) if (await exists$1(path.join(dir, file))) return manager;
|
|
4000
4077
|
};
|
|
4001
4078
|
var readText$1 = async (file) => {
|
|
4002
4079
|
try {
|
|
@@ -4034,7 +4111,7 @@ var projectDeclared = async (root) => {
|
|
|
4034
4111
|
* in a project written since that was how, by a list written in the file before then.
|
|
4035
4112
|
*/
|
|
4036
4113
|
var plitziProject = async (root, packageJson) => {
|
|
4037
|
-
if (packageJson.peerDependencies?.["@plitzi/plitzi-sdk"] && await exists(path.join(root, "src/elements.ts"))) return {
|
|
4114
|
+
if (packageJson.peerDependencies?.["@plitzi/plitzi-sdk"] && await exists$1(path.join(root, "src/elements.ts"))) return {
|
|
4038
4115
|
kind: "plugin",
|
|
4039
4116
|
components: await packageComponents(root)
|
|
4040
4117
|
};
|
|
@@ -4048,7 +4125,7 @@ var plitziProject = async (root, packageJson) => {
|
|
|
4048
4125
|
return {
|
|
4049
4126
|
kind: "project",
|
|
4050
4127
|
mode: server ? "server" : "client",
|
|
4051
|
-
source: await exists(path.join(root, "src/space.ts")) ? "local" : "cloud",
|
|
4128
|
+
source: await exists$1(path.join(root, "src/space.ts")) ? "local" : "cloud",
|
|
4052
4129
|
discovers: server ? main.includes("readdirSync(PLUGINS_DIR") : main.includes("import.meta.glob<{ default: RenderPlugins[string]"),
|
|
4053
4130
|
declared: await projectDeclared(root)
|
|
4054
4131
|
};
|
|
@@ -4354,12 +4431,16 @@ var loadProjectSpace = async (root) => {
|
|
|
4354
4431
|
};
|
|
4355
4432
|
//#endregion
|
|
4356
4433
|
//#region src/commands/check.ts
|
|
4357
|
-
/** The space the project declares, authored
|
|
4358
|
-
var
|
|
4434
|
+
/** The space the project declares, authored. Only for one written here. */
|
|
4435
|
+
var projectSpace = async (root) => {
|
|
4359
4436
|
const project = await loadProjectSpace(root);
|
|
4360
4437
|
if ("problem" in project) return { problem: `${project.problem} There is nothing to check the page against.` };
|
|
4361
4438
|
try {
|
|
4362
|
-
|
|
4439
|
+
const { handles, schema } = authorSpace(project.space, { plugins: project.plugins });
|
|
4440
|
+
return {
|
|
4441
|
+
handles,
|
|
4442
|
+
schema
|
|
4443
|
+
};
|
|
4363
4444
|
} catch (error) {
|
|
4364
4445
|
return { problem: `The space does not author — npm run author says why: ${error instanceof Error ? error.message.split("\n")[0] : String(error)}` };
|
|
4365
4446
|
}
|
|
@@ -4372,14 +4453,48 @@ var pageFor = (handles, pathname) => {
|
|
|
4372
4453
|
return segments.length === wanted.length && segments.every((segment, index) => segment.startsWith(":") || /^\{\{.*\}\}$/.test(segment) || segment === wanted[index]);
|
|
4373
4454
|
})?.id;
|
|
4374
4455
|
};
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4456
|
+
/** The elements a piece of HTML carries, by the id each is marked with. */
|
|
4457
|
+
var markedIn = (html) => new Set([...html.matchAll(/data-plitzi-el="([^"]+)"/g)].map(([, id]) => id));
|
|
4458
|
+
/** The `runtime: 'server'` provider an element is inside — or is — if any: what the server's HTML owes. */
|
|
4459
|
+
var serverProviderOf = (schema, id) => {
|
|
4460
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4461
|
+
let current = schema.flat[id];
|
|
4462
|
+
while (current && !seen.has(current.id)) {
|
|
4463
|
+
seen.add(current.id);
|
|
4464
|
+
if (current.definition.type === "apiContainer" && current.definition.runtime === "server") return current.id;
|
|
4465
|
+
const parent = current.definition.parentId;
|
|
4466
|
+
current = parent ? schema.flat[parent] : void 0;
|
|
4467
|
+
}
|
|
4468
|
+
};
|
|
4469
|
+
/**
|
|
4470
|
+
* What the page has once hydrated and the server's HTML did not: inside a server provider, an issue per provider — its
|
|
4471
|
+
* answer was meant to be in the page as it arrived; anywhere else, what the browser renders on its own, said apart.
|
|
4472
|
+
*/
|
|
4473
|
+
var lateElements = (schema, served, hydrated, width) => {
|
|
4474
|
+
const late = [...new Set(hydrated)].filter((id) => !served.has(id));
|
|
4475
|
+
const byProvider = /* @__PURE__ */ new Map();
|
|
4476
|
+
const browserOnly = [];
|
|
4477
|
+
for (const id of late) {
|
|
4478
|
+
const provider = schema ? serverProviderOf(schema, id) : void 0;
|
|
4479
|
+
if (provider) byProvider.set(provider, [...byProvider.get(provider) ?? [], id]);
|
|
4480
|
+
else browserOnly.push(id);
|
|
4481
|
+
}
|
|
4482
|
+
return {
|
|
4483
|
+
issues: [...byProvider].map(([provider, ids]) => ({
|
|
4484
|
+
code: "not-server-rendered",
|
|
4485
|
+
message: `apiContainer "${provider}" is resolved on the server, and ${String(ids.length)} of what it holds was not in the HTML the server sent (${ids.slice(0, 6).join(", ")}${ids.length > 6 ? ", …" : ""}) — it arrived after the page, so the first paint and a crawler miss it`,
|
|
4486
|
+
elementId: provider,
|
|
4487
|
+
width
|
|
4488
|
+
})),
|
|
4489
|
+
browserOnly
|
|
4490
|
+
};
|
|
4491
|
+
};
|
|
4492
|
+
var checkAt = async (browser, origin, pathname, width, scheme, authored, asked, ssr) => {
|
|
4493
|
+
const handles = authored?.handles;
|
|
4494
|
+
const page = await openProjectPage(browser, origin, {
|
|
4495
|
+
width,
|
|
4496
|
+
height: 900,
|
|
4497
|
+
...scheme ? { scheme } : {}
|
|
4383
4498
|
});
|
|
4384
4499
|
const consoleErrors = [];
|
|
4385
4500
|
const failedRequests = [];
|
|
@@ -4406,12 +4521,20 @@ var checkAt = async (browser, origin, pathname, width, scheme, handles, asked) =
|
|
|
4406
4521
|
hiddenAtWidth: [],
|
|
4407
4522
|
consoleErrors,
|
|
4408
4523
|
failedRequests,
|
|
4409
|
-
devTools: false
|
|
4524
|
+
devTools: false,
|
|
4525
|
+
lists: {}
|
|
4410
4526
|
};
|
|
4411
4527
|
}
|
|
4528
|
+
await withoutDevTools(page);
|
|
4412
4529
|
const pageId = handles ? pageFor(handles, pathname) : void 0;
|
|
4413
4530
|
const report = handles && pageId ? await inspectPage(page, handles, { page: pageId }) : await inspectDocument(page);
|
|
4414
|
-
const devTools = await readDevTools(page,
|
|
4531
|
+
const devTools = await readDevTools(page, {
|
|
4532
|
+
...asked,
|
|
4533
|
+
state: true
|
|
4534
|
+
});
|
|
4535
|
+
const data = authored && pageId && devTools.sources ? dataIssues(authored.schema, pageId, devTools.sources) : void 0;
|
|
4536
|
+
const served = ssr ? markedIn(await (await fetch(`${origin}${pathname}`)).text()) : void 0;
|
|
4537
|
+
const late = served ? lateElements(authored?.schema, served, await page.evaluate((selector) => Array.from(document.querySelectorAll(selector), (element) => element.getAttribute("data-plitzi-el") ?? ""), "[data-plitzi-el]"), width) : void 0;
|
|
4415
4538
|
const noPage = handles && !pageId ? [{
|
|
4416
4539
|
code: "no-page",
|
|
4417
4540
|
message: `no page of the space answers at ${pathname}`,
|
|
@@ -4429,6 +4552,11 @@ var checkAt = async (browser, origin, pathname, width, scheme, handles, asked) =
|
|
|
4429
4552
|
...issue,
|
|
4430
4553
|
width
|
|
4431
4554
|
})),
|
|
4555
|
+
...(data?.issues ?? []).map((issue) => ({
|
|
4556
|
+
...issue,
|
|
4557
|
+
width
|
|
4558
|
+
})),
|
|
4559
|
+
...late?.issues ?? [],
|
|
4432
4560
|
...failedFlows
|
|
4433
4561
|
];
|
|
4434
4562
|
return {
|
|
@@ -4442,8 +4570,10 @@ var checkAt = async (browser, origin, pathname, width, scheme, handles, asked) =
|
|
|
4442
4570
|
consoleErrors,
|
|
4443
4571
|
failedRequests,
|
|
4444
4572
|
devTools: devTools.available,
|
|
4445
|
-
|
|
4446
|
-
...
|
|
4573
|
+
lists: data?.lists ?? {},
|
|
4574
|
+
...late ? { browserOnly: late.browserOnly } : {},
|
|
4575
|
+
...!asked.state || devTools.state === void 0 ? {} : { state: devTools.state },
|
|
4576
|
+
...!asked.state || devTools.sources === void 0 ? {} : { sources: devTools.sources },
|
|
4447
4577
|
...devTools.element === void 0 ? {} : { element: devTools.element }
|
|
4448
4578
|
};
|
|
4449
4579
|
};
|
|
@@ -4467,9 +4597,22 @@ var shapeOf = (value) => {
|
|
|
4467
4597
|
const text = value === void 0 ? "undefined" : JSON.stringify(value);
|
|
4468
4598
|
return text.length > 60 ? `${text.slice(0, 57)}…` : text;
|
|
4469
4599
|
};
|
|
4600
|
+
/** Each list's rows in a few words — `plan-list 3 rows` — so an empty one is seen, not inferred from a box. */
|
|
4601
|
+
var listsText = (lists) => {
|
|
4602
|
+
const entries = Object.entries(lists);
|
|
4603
|
+
if (entries.length === 0) return [];
|
|
4604
|
+
const rows = entries.map(([id, count]) => `${id} ${count === null ? "reads nothing" : `${String(count)} row${count === 1 ? "" : "s"}`}`);
|
|
4605
|
+
return [chalk.dim(` · lists: ${rows.join(", ")}`)];
|
|
4606
|
+
};
|
|
4607
|
+
/** With `--ssr`, what only the browser renders — not a problem outside a server provider, and said so it is seen. */
|
|
4608
|
+
var browserOnlyText = (report) => report.browserOnly && report.browserOnly.length > 0 ? [chalk.dim(` · rendered only in the browser, outside any server provider: ${report.browserOnly.slice(0, 10).join(", ")}${report.browserOnly.length > 10 ? `, … (${String(report.browserOnly.length)})` : ""}`)] : [];
|
|
4470
4609
|
var reportText$1 = (report, asked) => {
|
|
4471
4610
|
const head = `${report.path} at ${String(report.width)} px`;
|
|
4472
|
-
const held =
|
|
4611
|
+
const held = [
|
|
4612
|
+
...listsText(report.lists),
|
|
4613
|
+
...browserOnlyText(report),
|
|
4614
|
+
...heldText(report, asked)
|
|
4615
|
+
];
|
|
4473
4616
|
const atWidth = report.hiddenAtWidth.length > 0 ? chalk.dim(` · hidden at this width by a breakpoint, on purpose: ${report.hiddenAtWidth.join(", ")}`) : void 0;
|
|
4474
4617
|
if (report.ok) return [
|
|
4475
4618
|
chalk.green(`✓ ${head} — ${report.checked > 0 ? `${String(report.checked)} elements on screen, ` : ""}nothing wrong`),
|
|
@@ -4493,9 +4636,9 @@ var check = async (route, options) => {
|
|
|
4493
4636
|
fail(where.problem);
|
|
4494
4637
|
return;
|
|
4495
4638
|
}
|
|
4496
|
-
const
|
|
4497
|
-
if (
|
|
4498
|
-
fail(
|
|
4639
|
+
const authored = plitzi?.source === "local" ? await projectSpace(project.root) : void 0;
|
|
4640
|
+
if (authored && "problem" in authored) {
|
|
4641
|
+
fail(authored.problem);
|
|
4499
4642
|
return;
|
|
4500
4643
|
}
|
|
4501
4644
|
const browser = await launchBrowser(project.root);
|
|
@@ -4505,13 +4648,13 @@ var check = async (route, options) => {
|
|
|
4505
4648
|
}
|
|
4506
4649
|
try {
|
|
4507
4650
|
const widths = options.width ?? [1440, 390];
|
|
4508
|
-
const scheme = options
|
|
4651
|
+
const { scheme } = options;
|
|
4509
4652
|
const asked = {
|
|
4510
4653
|
state: Boolean(options.state),
|
|
4511
4654
|
...options.element ? { element: options.element } : {}
|
|
4512
4655
|
};
|
|
4513
4656
|
const reports = [];
|
|
4514
|
-
for (const width of widths) reports.push(await checkAt(browser, where.origin, route ?? "/", width, scheme,
|
|
4657
|
+
for (const width of widths) reports.push(await checkAt(browser, where.origin, route ?? "/", width, scheme, authored, asked, Boolean(options.ssr)));
|
|
4515
4658
|
console.log(options.json ? JSON.stringify(reports) : reports.map((report) => reportText$1(report, asked)).join("\n"));
|
|
4516
4659
|
if (reports.some((report) => !report.ok)) process.exitCode = 1;
|
|
4517
4660
|
} finally {
|
|
@@ -4570,6 +4713,15 @@ var writeFunctionsState = async (root, state) => {
|
|
|
4570
4713
|
};
|
|
4571
4714
|
/** The files that differ between two copies — added, removed or changed — sorted. */
|
|
4572
4715
|
var changedFiles = (a, b) => [.../* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])].filter((file) => a[file] !== b[file]).sort();
|
|
4716
|
+
/**
|
|
4717
|
+
* What `functions/` holds that the space does not: nothing at all (`none`, no `functions/index.ts`), something changed
|
|
4718
|
+
* since it was pulled from this space or never pulled (`changed`), or exactly what was pulled (`unchanged`).
|
|
4719
|
+
*/
|
|
4720
|
+
var functionsChange = async (root, spaceId) => {
|
|
4721
|
+
const [local, state] = await Promise.all([readLocal(root), readState(root)]);
|
|
4722
|
+
if (!Object.hasOwn(local, "index.ts")) return "none";
|
|
4723
|
+
return state?.space === spaceId && !changedFiles(local, state.files).length ? "unchanged" : "changed";
|
|
4724
|
+
};
|
|
4573
4725
|
var taskList = (draft) => draft.manifest?.tasks.length ? draft.manifest.tasks.map((task) => `${task.namespace}.${task.action}`).join(", ") : "no tasks";
|
|
4574
4726
|
var readDraft = async (connection, spaceId) => {
|
|
4575
4727
|
const answered = await authorizedRequest(connection, `/spaces/${spaceId}/functions`);
|
|
@@ -4622,30 +4774,29 @@ var printProblems = (problems) => {
|
|
|
4622
4774
|
});
|
|
4623
4775
|
process.exitCode = 1;
|
|
4624
4776
|
};
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4777
|
+
/**
|
|
4778
|
+
* `functions/` saved as the draft of the space the connection works in: refused when the space's copy moved on since the
|
|
4779
|
+
* pull, and — for a project that never pulled them — when the space already has functions of its own. `force` replaces
|
|
4780
|
+
* whatever the space holds now, which is what `plitzi push --force` asks of every part.
|
|
4781
|
+
*/
|
|
4782
|
+
var pushFunctionsOf = async (root, connection, space, { force = false } = {}) => {
|
|
4629
4783
|
const [local, state] = await Promise.all([readLocal(root), readState(root)]);
|
|
4630
4784
|
if (!Object.hasOwn(local, "index.ts")) {
|
|
4631
4785
|
fail("There is no functions/index.ts here: it is where a space’s functions start. Pull them, or write it.");
|
|
4632
|
-
return;
|
|
4786
|
+
return "failed";
|
|
4633
4787
|
}
|
|
4634
|
-
|
|
4788
|
+
if (state?.space === space.id && !changedFiles(local, state.files).length) return "unchanged";
|
|
4789
|
+
let base = state?.space === space.id && !force ? state.version : void 0;
|
|
4635
4790
|
if (base === void 0) {
|
|
4636
|
-
const draft = await readDraft(connection,
|
|
4637
|
-
if (!draft) return;
|
|
4638
|
-
if (Object.keys(draft.files).length) {
|
|
4639
|
-
fail(`${
|
|
4640
|
-
return;
|
|
4791
|
+
const draft = await readDraft(connection, space.id);
|
|
4792
|
+
if (!draft) return "failed";
|
|
4793
|
+
if (Object.keys(draft.files).length && !force) {
|
|
4794
|
+
fail(`${space.name} already has functions. Pull them first: plitzi functions pull.`);
|
|
4795
|
+
return "failed";
|
|
4641
4796
|
}
|
|
4642
4797
|
base = draft.version;
|
|
4643
4798
|
}
|
|
4644
|
-
|
|
4645
|
-
console.log("Nothing to push: functions/ is what was pulled.");
|
|
4646
|
-
return;
|
|
4647
|
-
}
|
|
4648
|
-
const answered = await authorizedRequest(connection, `/spaces/${connection.space.id}/functions`, {
|
|
4799
|
+
const answered = await authorizedRequest(connection, `/spaces/${space.id}/functions`, {
|
|
4649
4800
|
method: "PUT",
|
|
4650
4801
|
headers: { "Content-Type": "application/json" },
|
|
4651
4802
|
body: JSON.stringify({
|
|
@@ -4655,27 +4806,36 @@ var pushFunctions = async (options) => {
|
|
|
4655
4806
|
});
|
|
4656
4807
|
if (!answered.ok) {
|
|
4657
4808
|
fail(answered.error);
|
|
4658
|
-
return;
|
|
4809
|
+
return "failed";
|
|
4659
4810
|
}
|
|
4660
4811
|
const { reply } = answered.value;
|
|
4661
4812
|
if (reply.status === 422 && reply.data.problems) {
|
|
4662
4813
|
printProblems(reply.data.problems);
|
|
4663
|
-
return;
|
|
4814
|
+
return "failed";
|
|
4664
4815
|
}
|
|
4665
4816
|
if (reply.status === 409) {
|
|
4666
|
-
fail(`${
|
|
4667
|
-
return;
|
|
4817
|
+
fail(`${space.name}’s functions changed since your pull — in the builder, or from another copy. Keep your changes aside, pull, and apply them again.`);
|
|
4818
|
+
return "failed";
|
|
4668
4819
|
}
|
|
4669
4820
|
if (reply.status !== 200 || !reply.data.version) {
|
|
4670
4821
|
fail(reply.data.refusal?.error ?? reply.data.error ?? `The functions were not saved (${reply.status}).`);
|
|
4671
|
-
return;
|
|
4822
|
+
return "failed";
|
|
4672
4823
|
}
|
|
4673
4824
|
await writeFunctionsState(root, {
|
|
4674
|
-
space:
|
|
4825
|
+
space: space.id,
|
|
4675
4826
|
version: reply.data.version,
|
|
4676
4827
|
files: local
|
|
4677
4828
|
});
|
|
4678
|
-
console.log(chalk.green(`
|
|
4829
|
+
console.log(chalk.green(`Functions pushed to ${space.name}’s draft — ${taskList({ manifest: reply.data.manifest ?? null })}.`));
|
|
4830
|
+
return "pushed";
|
|
4831
|
+
};
|
|
4832
|
+
var pushFunctions = async (options) => {
|
|
4833
|
+
const root = await rootOf();
|
|
4834
|
+
const connection = root && await connectToSpace(options, "to push to");
|
|
4835
|
+
if (!root || !connection || !connection.space) return;
|
|
4836
|
+
const outcome = await pushFunctionsOf(root, connection, connection.space);
|
|
4837
|
+
if (outcome === "unchanged") console.log("Nothing to push: functions/ is what was pulled.");
|
|
4838
|
+
else if (outcome === "pushed") console.log(chalk.dim("The live site runs them once the space is published."));
|
|
4679
4839
|
};
|
|
4680
4840
|
/** What one run of a task did: its logs, then its value — or why it failed, as a failure of the command. */
|
|
4681
4841
|
var printRun = (task, report) => {
|
|
@@ -4974,7 +5134,7 @@ var fromSpaceMain = (options) => {
|
|
|
4974
5134
|
import path from 'node:path';
|
|
4975
5135
|
|
|
4976
5136
|
import { closeOnSignals, consoleLogger, ${local ? "createJsonAdapters" : "createCloudAdapters"}, createServer, loadFunctions } from '@plitzi/sdk-server';
|
|
4977
|
-
${hasRuntime ? importLine(`{ ${runtimeImports} }`, "@plitzi/sdk-server/runtime") : ""}${local ? `\n${importLine("{ authorSpace }", "@plitzi/sdk-authoring")}` : ""}
|
|
5137
|
+
${hasRuntime ? importLine(`{ ${runtimeImports} }`, "@plitzi/sdk-server/runtime") : ""}${local ? `\n${importLine("{ authorSpace }", "@plitzi/sdk-authoring")}${importLine("{ publicData }", "@plitzi/sdk-authoring/node")}` : ""}
|
|
4978
5138
|
${importLine("{ lookups }", "./actions.ts")}${local ? importLine("{ declarations }", "./plugins/declarations.ts") : ""}${runtimeEntry ? importLine("spaceRuntime", runtimeEntry) : ""}${local ? importLine("{ space }", "./space.ts") : ""}
|
|
4979
5139
|
/**
|
|
4980
5140
|
* The project's settings — the key its actions sign with, the variables the space was given on Plitzi — kept in \`.env\`,
|
|
@@ -4996,7 +5156,10 @@ ${builtPlugins ? BUILT_PLUGINS : ""}${local ? `
|
|
|
4996
5156
|
* The space, held in this project: \`src/space/\`, authored at boot — saving a page and letting \`--watch\` restart is
|
|
4997
5157
|
* the whole edit loop, and the warnings are printed where somebody editing it is watching.
|
|
4998
5158
|
*/
|
|
4999
|
-
const { schema, style, warnings } = authorSpace(space, {
|
|
5159
|
+
const { schema, style, warnings } = authorSpace(space, {
|
|
5160
|
+
plugins: declarations,${builtPlugins ? "\n pluginTypes: builtTypes," : ""}
|
|
5161
|
+
data: publicData(new URL('../public/', import.meta.url))
|
|
5162
|
+
});
|
|
5000
5163
|
|
|
5001
5164
|
for (const warning of warnings) {
|
|
5002
5165
|
console.warn(\`[author] \${warning.message}\`);
|
|
@@ -5051,6 +5214,22 @@ var placeOf = (paths) => {
|
|
|
5051
5214
|
const prefix = paths.length > 0 && paths.every((path) => path.startsWith("src/")) ? "" : "src/";
|
|
5052
5215
|
return (path) => `${prefix}${path}`;
|
|
5053
5216
|
};
|
|
5217
|
+
/**
|
|
5218
|
+
* Where the project keeps what the space's runtime and plugins are built from: the module the runtime starts at, and the
|
|
5219
|
+
* files each plugin's elements start at — the paths `create --from` writes them at, which `plitzi push` packs again.
|
|
5220
|
+
*/
|
|
5221
|
+
var projectEntries = ({ source }) => {
|
|
5222
|
+
const place = placeOf(Object.keys(source.files));
|
|
5223
|
+
const runtime = source.runtime?.entries[0];
|
|
5224
|
+
return {
|
|
5225
|
+
runtime: runtime ? place(runtime) : null,
|
|
5226
|
+
plugins: source.plugins.map(({ type, entries }) => ({
|
|
5227
|
+
type,
|
|
5228
|
+
entries: entries.map(place)
|
|
5229
|
+
})),
|
|
5230
|
+
files: Object.keys(source.files).map(place)
|
|
5231
|
+
};
|
|
5232
|
+
};
|
|
5054
5233
|
/** The import `src/main.ts` names the runtime's module by. */
|
|
5055
5234
|
var fromMain = (path) => `./${path.replace(/^src\//, "")}`;
|
|
5056
5235
|
/** What the space was given on Plitzi, by name, as `.env.example` lists it — and `.env`, with values where they are known. */
|
|
@@ -5212,7 +5391,7 @@ var digestsOf = async (root, file, format) => {
|
|
|
5212
5391
|
var readOrigin = async (root) => {
|
|
5213
5392
|
try {
|
|
5214
5393
|
const value = JSON.parse(await fs.readFile(path.join(root, ORIGIN_FILE), "utf-8"));
|
|
5215
|
-
if (!e$1(value) || value.format !== 1 || typeof value.api !== "string" || !e$1(value.space) || typeof value.space.id !== "number" || typeof value.space.name !== "string" || typeof value.space.permanentUrl !== "string" || value.source !== "local" && value.source !== "cloud" || !e$1(value.version) || typeof value.version.environment !== "string" || value.version.revision !== void 0 && typeof value.version.revision !== "number" || !isStrings(value.files) || !isStrings(value.downloads) || !isStrings(value.dependencies)) return;
|
|
5394
|
+
if (!e$1(value) || value.format !== 1 || typeof value.api !== "string" || !e$1(value.space) || typeof value.space.id !== "number" || typeof value.space.name !== "string" || typeof value.space.permanentUrl !== "string" || value.source !== "local" && value.source !== "cloud" || !e$1(value.version) || typeof value.version.environment !== "string" || value.version.revision !== void 0 && typeof value.version.revision !== "number" || value.draft !== void 0 && typeof value.draft !== "string" || !isStrings(value.files) || !isStrings(value.downloads) || !isStrings(value.dependencies)) return;
|
|
5216
5395
|
return {
|
|
5217
5396
|
format: 1,
|
|
5218
5397
|
api: value.api,
|
|
@@ -5226,6 +5405,7 @@ var readOrigin = async (root) => {
|
|
|
5226
5405
|
environment: value.version.environment,
|
|
5227
5406
|
...typeof value.version.revision === "number" ? { revision: value.version.revision } : {}
|
|
5228
5407
|
},
|
|
5408
|
+
...typeof value.draft === "string" ? { draft: value.draft } : {},
|
|
5229
5409
|
files: value.files,
|
|
5230
5410
|
downloads: value.downloads,
|
|
5231
5411
|
dependencies: value.dependencies
|
|
@@ -5250,6 +5430,17 @@ var digestsOnDisk = async (root, files, format) => {
|
|
|
5250
5430
|
const digests = await Promise.all(files.map(async (file) => [file, (await digestsOf(root, file, format))?.formatted]));
|
|
5251
5431
|
return Object.fromEntries(digests.filter((entry) => entry[1] !== void 0));
|
|
5252
5432
|
};
|
|
5433
|
+
/**
|
|
5434
|
+
* The files a space gives a project — its text as the project's formatter writes it, its binaries as they are — by
|
|
5435
|
+
* path: what a pull compares with what is on disk, and what a push records as given once the space holds the project.
|
|
5436
|
+
* The files of its CDN are not among them: they are fetched, and only when the space names another address for one.
|
|
5437
|
+
*/
|
|
5438
|
+
var givenFiles = async (project, format) => {
|
|
5439
|
+
const given = /* @__PURE__ */ new Map();
|
|
5440
|
+
for (const [file, text] of Object.entries(project.files)) given.set(file, Buffer.from(await format(file, text)));
|
|
5441
|
+
Object.entries(project.binaries).forEach(([file, base64]) => given.set(file, Buffer.from(base64, "base64")));
|
|
5442
|
+
return given;
|
|
5443
|
+
};
|
|
5253
5444
|
//#endregion
|
|
5254
5445
|
//#region src/commands/createFrom.ts
|
|
5255
5446
|
/**
|
|
@@ -5264,30 +5455,49 @@ var versionLabel = ({ environment, revision }) => {
|
|
|
5264
5455
|
return revision ? `${environment} r${String(revision)}` : `${environment} (latest)`;
|
|
5265
5456
|
};
|
|
5266
5457
|
/**
|
|
5267
|
-
* A version of the space named — its id or its permanent URL — put together as a project,
|
|
5268
|
-
*
|
|
5458
|
+
* A version of the space named — its id or its permanent URL — put together as a project, asked for on a connection that
|
|
5459
|
+
* is signed in: the export, or why not in words. `name` is what the space is called in them.
|
|
5269
5460
|
*/
|
|
5270
|
-
var
|
|
5271
|
-
const connection = await signedIn(api, `to take ${from} out of Plitzi`);
|
|
5272
|
-
if (!connection) return;
|
|
5273
|
-
console.log(`\nTaking ${chalk.bold(name)} (${versionLabel(version)}) out of ${api}…`);
|
|
5461
|
+
var requestExport = async (connection, from, { source, version, name = from }) => {
|
|
5274
5462
|
const query = new URLSearchParams({
|
|
5275
5463
|
source,
|
|
5276
5464
|
environment: version.environment,
|
|
5277
5465
|
...version.revision ? { revision: String(version.revision) } : {}
|
|
5278
5466
|
});
|
|
5279
5467
|
const answered = await authorizedRequest(connection, `/spaces/${encodeURIComponent(from)}/export?${query.toString()}`);
|
|
5280
|
-
if (!answered.ok)
|
|
5281
|
-
fail(answered.error);
|
|
5282
|
-
return;
|
|
5283
|
-
}
|
|
5468
|
+
if (!answered.ok) return answered;
|
|
5284
5469
|
const { status, data } = answered.value.reply;
|
|
5285
|
-
if (status === 200 && isExport(data)) return
|
|
5470
|
+
if (status === 200 && isExport(data)) return {
|
|
5471
|
+
ok: true,
|
|
5472
|
+
value: data
|
|
5473
|
+
};
|
|
5286
5474
|
const said = e$1(data) && typeof data.error === "string" ? data.error : void 0;
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5475
|
+
return {
|
|
5476
|
+
ok: false,
|
|
5477
|
+
error: {
|
|
5478
|
+
403: `You may not take ${name} out: it takes being able to change it — its owner, an administrator or a writer.`,
|
|
5479
|
+
404: said ?? `There is no space ${name} you can reach.`
|
|
5480
|
+
}[status] ?? said ?? `The platform would not hand ${name} over (${String(status)}).`
|
|
5481
|
+
};
|
|
5482
|
+
};
|
|
5483
|
+
/**
|
|
5484
|
+
* A version of the space named put together as a project — signing in first when there is no session — or why not,
|
|
5485
|
+
* said and `undefined`. `name` is what it is called in what is printed: `from`, unless a pull names it by its id.
|
|
5486
|
+
*/
|
|
5487
|
+
var fetchExport = async (api, from, { source, version, name = from }) => {
|
|
5488
|
+
const connection = await signedIn(api, `to take ${from} out of Plitzi`);
|
|
5489
|
+
if (!connection) return;
|
|
5490
|
+
console.log(`\nTaking ${chalk.bold(name)} (${versionLabel(version)}) out of ${api}…`);
|
|
5491
|
+
const exported = await requestExport(connection, from, {
|
|
5492
|
+
source,
|
|
5493
|
+
version,
|
|
5494
|
+
name
|
|
5495
|
+
});
|
|
5496
|
+
if (!exported.ok) {
|
|
5497
|
+
fail(exported.error);
|
|
5498
|
+
return;
|
|
5499
|
+
}
|
|
5500
|
+
return exported.value;
|
|
5291
5501
|
};
|
|
5292
5502
|
/** A file of the space's CDN, fetched. */
|
|
5293
5503
|
var download = async (url) => {
|
|
@@ -5335,6 +5545,7 @@ var recordOrigin = async (root, { api, exported, source, pinned, project }) => {
|
|
|
5335
5545
|
environment: exported.version.environment,
|
|
5336
5546
|
...pinned ? { revision: exported.version.revision } : {}
|
|
5337
5547
|
},
|
|
5548
|
+
...exported.draft ? { draft: exported.draft } : {},
|
|
5338
5549
|
files,
|
|
5339
5550
|
downloads: Object.fromEntries(project.downloads.filter(({ to }) => Object.hasOwn(files, to)).map(({ url, to }) => [to, url])),
|
|
5340
5551
|
dependencies: project.dependencies
|
|
@@ -5902,7 +6113,7 @@ var explainCommand = (name, options) => {
|
|
|
5902
6113
|
}
|
|
5903
6114
|
const explanations = explain(name);
|
|
5904
6115
|
if (explanations.length === 0) {
|
|
5905
|
-
fail(`"${name}" is no element, step, trigger, code or
|
|
6116
|
+
fail(`"${name}" is no element, step, trigger, code, transformer or helper. See what there is: --list ${kinds}.`);
|
|
5906
6117
|
return;
|
|
5907
6118
|
}
|
|
5908
6119
|
console.log(options.json ? JSON.stringify(explanations) : explanations.map(explanationText).join("\n\n"));
|
|
@@ -6540,18 +6751,18 @@ var measure = async (browser, url, width, scheme, read = []) => {
|
|
|
6540
6751
|
};
|
|
6541
6752
|
};
|
|
6542
6753
|
/** Every file under a folder, by its path inside it — none when there is no such folder. */
|
|
6543
|
-
var filesUnder = async (folder) => {
|
|
6754
|
+
var filesUnder$1 = async (folder) => {
|
|
6544
6755
|
return (await readdir(folder, {
|
|
6545
6756
|
recursive: true,
|
|
6546
6757
|
withFileTypes: true
|
|
6547
6758
|
}).catch(() => [])).filter((entry) => entry.isFile()).map((entry) => path.relative(folder, path.join(entry.parentPath, entry.name)));
|
|
6548
6759
|
};
|
|
6549
|
-
var count = (amount, one, many = `${one}s`) => `${String(amount)} ${amount === 1 ? one : many}`;
|
|
6760
|
+
var count$1 = (amount, one, many = `${one}s`) => `${String(amount)} ${amount === 1 ? one : many}`;
|
|
6550
6761
|
var summaryText = (summary, out, stale) => [
|
|
6551
6762
|
chalk.green(`Imported ${summary.url} into ${out}`),
|
|
6552
|
-
` tokens ${count(summary.colours, "colour")} (${summary.dark ? "light and dark" : "no dark scheme: dark repeats light"}), ${count(summary.shadows, "shadow")}, ${count(summary.radii, "radius", "radii")}${summary.fonts.length > 0 ? `, fonts ${summary.fonts.join(", ")}` : ""}`,
|
|
6553
|
-
` outline ${count(summary.blocks, "block")} at ${summary.widths.join(", ")} px`,
|
|
6554
|
-
...summary.lists.length > 0 ? [` lists ${summary.lists.map((list) => `${list.file} (${count(list.rows, "row")})`).join(", ")}`] : [],
|
|
6763
|
+
` tokens ${count$1(summary.colours, "colour")} (${summary.dark ? "light and dark" : "no dark scheme: dark repeats light"}), ${count$1(summary.shadows, "shadow")}, ${count$1(summary.radii, "radius", "radii")}${summary.fonts.length > 0 ? `, fonts ${summary.fonts.join(", ")}` : ""}`,
|
|
6764
|
+
` outline ${count$1(summary.blocks, "block")} at ${summary.widths.join(", ")} px`,
|
|
6765
|
+
...summary.lists.length > 0 ? [` lists ${summary.lists.map((list) => `${list.file} (${count$1(list.rows, "row")})`).join(", ")}`] : [],
|
|
6555
6766
|
` pictures ${String(summary.pictures)} in assets.json`,
|
|
6556
6767
|
...stale.length > 0 ? [chalk.yellow(` earlier ${stale.join(", ")} ${stale.length === 1 ? "was" : "were"} there before and not written this time: delete what is stale`)] : [],
|
|
6557
6768
|
` next ${path.join(out, "IMPORT.md")} says what was not carried over`
|
|
@@ -6576,7 +6787,7 @@ var importPage = async (address, options) => {
|
|
|
6576
6787
|
fail(`${out} is outside the project: the import is written inside it.`);
|
|
6577
6788
|
return;
|
|
6578
6789
|
}
|
|
6579
|
-
const existing = await filesUnder(target);
|
|
6790
|
+
const existing = await filesUnder$1(target);
|
|
6580
6791
|
if (existing.length > 0 && !options.force) {
|
|
6581
6792
|
fail(`${out} already has files: choose another --out, or --force to write over them.`);
|
|
6582
6793
|
return;
|
|
@@ -6674,16 +6885,16 @@ var a = [
|
|
|
6674
6885
|
pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/
|
|
6675
6886
|
}
|
|
6676
6887
|
];
|
|
6677
|
-
var o = (e) => {
|
|
6888
|
+
var o$1 = (e) => {
|
|
6678
6889
|
let t = e.split("/");
|
|
6679
6890
|
if (!e || e.startsWith("/") || /^[a-zA-Z]:/.test(e) || e.includes("\\")) return `"${e}" is not a path relative to the project, with forward slashes`;
|
|
6680
6891
|
if (t.some((e) => e === "" || e === "." || e === "..")) return `"${e}" climbs out of the project, or has an empty part`;
|
|
6681
6892
|
if (t.some((e) => e === "node_modules" || e === ".git")) return `"${e}" is in node_modules or .git: a package is a dependency, not a file of the project`;
|
|
6682
6893
|
if (i.some((t) => t.test(e))) return `"${e}" is a file of credentials: they stay where the project runs, never in what Plitzi keeps`;
|
|
6683
6894
|
};
|
|
6684
|
-
var s = (e) => a.find(({ pattern: t }) => t.test(e))?.what;
|
|
6685
|
-
var c = (e) => typeof e == "string" && r.some((t) => t === e);
|
|
6686
|
-
var l = (e) => typeof e == "string" && /^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(e);
|
|
6895
|
+
var s$1 = (e) => a.find(({ pattern: t }) => t.test(e))?.what;
|
|
6896
|
+
var c$1 = (e) => typeof e == "string" && r.some((t) => t === e);
|
|
6897
|
+
var l$1 = (e) => typeof e == "string" && /^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(e);
|
|
6687
6898
|
var u = (e) => Array.isArray(e) && e.length > 0 && e.every((e) => typeof e == "string");
|
|
6688
6899
|
var d = (t) => e$1(t) && Object.values(t).every((e) => typeof e == "string");
|
|
6689
6900
|
var f = (e) => Math.floor(e.length * 3 / 4) - (e.endsWith("==") ? 2 : +!!e.endsWith("="));
|
|
@@ -6693,7 +6904,7 @@ var p = (t, i) => {
|
|
|
6693
6904
|
problems: ["Not a source snapshot of format 1"]
|
|
6694
6905
|
};
|
|
6695
6906
|
let { kind: a, name: p, entries: m, files: h, dependencies: g } = t, _ = [];
|
|
6696
|
-
if (c(a) || _.push(`Its kind is one of ${r.join(", ")}`), l(p) || _.push("Its name is a plugin type or \"runtime\": letters, digits, dashes and underscores"), !d(h) || !d(g)) return {
|
|
6907
|
+
if (c$1(a) || _.push(`Its kind is one of ${r.join(", ")}`), l$1(p) || _.push("Its name is a plugin type or \"runtime\": letters, digits, dashes and underscores"), !d(h) || !d(g)) return {
|
|
6697
6908
|
ok: !1,
|
|
6698
6909
|
problems: [..._, "Its files and dependencies are maps of text"]
|
|
6699
6910
|
};
|
|
@@ -6702,7 +6913,7 @@ var p = (t, i) => {
|
|
|
6702
6913
|
v.length > n.files && _.push(`It holds ${String(v.length)} files; a snapshot holds at most ${String(n.files)}`);
|
|
6703
6914
|
let y = 0;
|
|
6704
6915
|
return v.forEach((e) => {
|
|
6705
|
-
let t = o(e);
|
|
6916
|
+
let t = o$1(e);
|
|
6706
6917
|
if (t) {
|
|
6707
6918
|
_.push(t);
|
|
6708
6919
|
return;
|
|
@@ -6712,9 +6923,9 @@ var p = (t, i) => {
|
|
|
6712
6923
|
_.push(`"${e}" is ${(r / 1024 / 1024).toFixed(1)} MB; a file of source is at most 4 MB`);
|
|
6713
6924
|
return;
|
|
6714
6925
|
}
|
|
6715
|
-
let a = s(i(h[e]));
|
|
6926
|
+
let a = s$1(i(h[e]));
|
|
6716
6927
|
a && _.push(`"${e}" holds ${a}: take it out of the code and give it to the project as a variable`);
|
|
6717
|
-
}), y > n.totalBytes && _.push(`It holds ${(y / 1024 / 1024).toFixed(1)} MB of files; a snapshot holds at most 24 MB`), _.length > 0 || !c(a) || !l(p) || !u(m) ? {
|
|
6928
|
+
}), y > n.totalBytes && _.push(`It holds ${(y / 1024 / 1024).toFixed(1)} MB of files; a snapshot holds at most 24 MB`), _.length > 0 || !c$1(a) || !l$1(p) || !u(m) ? {
|
|
6718
6929
|
ok: !1,
|
|
6719
6930
|
problems: _
|
|
6720
6931
|
} : {
|
|
@@ -7332,9 +7543,7 @@ var pull = async (options) => {
|
|
|
7332
7543
|
if (!exported) return;
|
|
7333
7544
|
const next = projectFromSpace(exported, origin.source);
|
|
7334
7545
|
const format = await projectFormatter(root);
|
|
7335
|
-
const given =
|
|
7336
|
-
for (const [file, text] of Object.entries(next.files)) given.set(file, Buffer.from(await format(file, text)));
|
|
7337
|
-
Object.entries(next.binaries).forEach(([file, base64]) => given.set(file, Buffer.from(base64, "base64")));
|
|
7546
|
+
const given = await givenFiles(next, format);
|
|
7338
7547
|
const missing = [];
|
|
7339
7548
|
const untouched = /* @__PURE__ */ new Set();
|
|
7340
7549
|
for (const { url, to } of next.downloads) {
|
|
@@ -7381,10 +7590,12 @@ var pull = async (options) => {
|
|
|
7381
7590
|
if (dependencies.changed.length > 0) await fs.writeFile(packageFile, `${JSON.stringify(dependencies.manifest, null, 2)}\n`);
|
|
7382
7591
|
const files = Object.fromEntries([...[...given].map(([file, bytes]) => [file, digest(bytes)]), ...[...untouched].flatMap((file) => origin.files[file] ? [[file, origin.files[file]]] : [])]);
|
|
7383
7592
|
await writeOrigin(root, {
|
|
7384
|
-
|
|
7593
|
+
format: origin.format,
|
|
7385
7594
|
api,
|
|
7595
|
+
source: origin.source,
|
|
7386
7596
|
space: exported.space,
|
|
7387
7597
|
version: asked.version,
|
|
7598
|
+
...exported.draft ? { draft: exported.draft } : {},
|
|
7388
7599
|
files,
|
|
7389
7600
|
downloads: Object.fromEntries(next.downloads.flatMap(({ url, to }) => {
|
|
7390
7601
|
if (given.has(to)) return [[to, url]];
|
|
@@ -7418,6 +7629,202 @@ var pull = async (options) => {
|
|
|
7418
7629
|
console.log("");
|
|
7419
7630
|
};
|
|
7420
7631
|
//#endregion
|
|
7632
|
+
//#region src/commands/askChecks.ts
|
|
7633
|
+
/** What a key does to the list: a new state, or the end of the question — taken or given up. */
|
|
7634
|
+
var checksAfter = (state, key) => {
|
|
7635
|
+
const count = state.checked.length;
|
|
7636
|
+
if (key.name === "escape" || key.ctrl && key.name === "c") return "cancel";
|
|
7637
|
+
if (key.name === "return" || key.name === "enter") return "done";
|
|
7638
|
+
if (key.name === "up" || key.name === "k") return {
|
|
7639
|
+
...state,
|
|
7640
|
+
cursor: (state.cursor - 1 + count) % count
|
|
7641
|
+
};
|
|
7642
|
+
if (key.name === "down" || key.name === "j") return {
|
|
7643
|
+
...state,
|
|
7644
|
+
cursor: (state.cursor + 1) % count
|
|
7645
|
+
};
|
|
7646
|
+
if (key.name === "space") return {
|
|
7647
|
+
...state,
|
|
7648
|
+
checked: state.checked.map((on, index) => index === state.cursor ? !on : on)
|
|
7649
|
+
};
|
|
7650
|
+
if (key.name === "a") {
|
|
7651
|
+
const all = state.checked.every(Boolean);
|
|
7652
|
+
return {
|
|
7653
|
+
...state,
|
|
7654
|
+
checked: state.checked.map(() => !all)
|
|
7655
|
+
};
|
|
7656
|
+
}
|
|
7657
|
+
return state;
|
|
7658
|
+
};
|
|
7659
|
+
/** The list as it is drawn: the question, a line per option, and the keys. */
|
|
7660
|
+
var renderChecks = (question, options, state) => [
|
|
7661
|
+
chalk.bold(question),
|
|
7662
|
+
...options.map(({ label, hint }, index) => {
|
|
7663
|
+
return `${index === state.cursor ? chalk.cyan("›") : " "} ${state.checked[index] ? chalk.green("[x]") : "[ ]"} ${label}${hint ? chalk.dim(` ${hint}`) : ""}`;
|
|
7664
|
+
}),
|
|
7665
|
+
chalk.dim(" ↑/↓ move · space ticks · a all · enter takes · esc cancels")
|
|
7666
|
+
];
|
|
7667
|
+
/** What is ticked once the person takes the list — `undefined` when they give it up. */
|
|
7668
|
+
var askChecks = async (question, options) => {
|
|
7669
|
+
const { stdin, stdout } = process;
|
|
7670
|
+
let state = {
|
|
7671
|
+
cursor: 0,
|
|
7672
|
+
checked: options.map(({ checked }) => checked)
|
|
7673
|
+
};
|
|
7674
|
+
let drawn = 0;
|
|
7675
|
+
const draw = () => {
|
|
7676
|
+
const lines = renderChecks(question, options, state);
|
|
7677
|
+
stdout.write(`${drawn ? `\x1b[${String(drawn)}A\x1b[0J` : ""}${lines.join("\n")}\n`);
|
|
7678
|
+
drawn = lines.length;
|
|
7679
|
+
};
|
|
7680
|
+
emitKeypressEvents(stdin);
|
|
7681
|
+
const wasRaw = stdin.isRaw;
|
|
7682
|
+
stdin.setRawMode(true);
|
|
7683
|
+
stdin.resume();
|
|
7684
|
+
stdout.write("\x1B[?25l");
|
|
7685
|
+
draw();
|
|
7686
|
+
return new Promise((resolve) => {
|
|
7687
|
+
const onKey = (_text, key) => {
|
|
7688
|
+
const next = checksAfter(state, key ?? {});
|
|
7689
|
+
if (next !== "done" && next !== "cancel") {
|
|
7690
|
+
state = next;
|
|
7691
|
+
draw();
|
|
7692
|
+
return;
|
|
7693
|
+
}
|
|
7694
|
+
stdin.off("keypress", onKey);
|
|
7695
|
+
stdin.setRawMode(wasRaw);
|
|
7696
|
+
stdin.pause();
|
|
7697
|
+
stdout.write("\x1B[?25h");
|
|
7698
|
+
resolve(next === "done" ? options.filter((_option, index) => state.checked[index]).map(({ value }) => value) : void 0);
|
|
7699
|
+
};
|
|
7700
|
+
stdin.on("keypress", onKey);
|
|
7701
|
+
});
|
|
7702
|
+
};
|
|
7703
|
+
//#endregion
|
|
7704
|
+
//#region src/commands/pushSpace.ts
|
|
7705
|
+
/** The JSON documents of a folder, by file: none when there is no such folder. */
|
|
7706
|
+
var readJsonFolder = async (dir) => {
|
|
7707
|
+
let files;
|
|
7708
|
+
try {
|
|
7709
|
+
files = (await fs.readdir(dir)).filter((file) => file.endsWith(".json")).sort();
|
|
7710
|
+
} catch {
|
|
7711
|
+
return;
|
|
7712
|
+
}
|
|
7713
|
+
return Promise.all(files.map(async (file) => ({
|
|
7714
|
+
file,
|
|
7715
|
+
value: JSON.parse(await fs.readFile(path.join(dir, file), "utf-8"))
|
|
7716
|
+
})));
|
|
7717
|
+
};
|
|
7718
|
+
/** The project's server actions, as its `src/actions.ts` serves them — `undefined` when it has no such module. */
|
|
7719
|
+
var projectActions = async (root) => {
|
|
7720
|
+
const file = path.join(root, "src/actions.ts");
|
|
7721
|
+
try {
|
|
7722
|
+
await fs.access(file);
|
|
7723
|
+
} catch {
|
|
7724
|
+
return;
|
|
7725
|
+
}
|
|
7726
|
+
const loaded = await import(pathToFileURL(file).href);
|
|
7727
|
+
const lookups = e$1(loaded) && e$1(loaded.lookups) ? loaded.lookups : void 0;
|
|
7728
|
+
if (!lookups || typeof lookups.listActions !== "function") return { problem: "src/actions.ts exports no `lookups` with a listActions: it is where the project’s actions are." };
|
|
7729
|
+
const listed = await lookups.listActions();
|
|
7730
|
+
if (!Array.isArray(listed)) return { problem: "src/actions.ts: lookups.listActions() answers no list." };
|
|
7731
|
+
return listed.flatMap((entry) => e$1(entry) && typeof entry.id === "string" && e$1(entry.document) ? [{
|
|
7732
|
+
identifier: entry.id,
|
|
7733
|
+
name: typeof entry.document.name === "string" ? entry.document.name : entry.id,
|
|
7734
|
+
document: entry.document
|
|
7735
|
+
}] : []);
|
|
7736
|
+
};
|
|
7737
|
+
/** The connectors in `src/connectors/`, one manifest a file — `undefined` when there is no such folder. */
|
|
7738
|
+
var projectConnectors = async (root) => {
|
|
7739
|
+
const read = await readJsonFolder(path.join(root, "src/connectors"));
|
|
7740
|
+
if (!read) return;
|
|
7741
|
+
const broken = read.find(({ value }) => !e$1(value) || typeof value.id !== "string" || !e$1(value.manifest));
|
|
7742
|
+
return broken ? { problem: `src/connectors/${broken.file} is not a connector: { "id", "name", "manifest" }.` } : read.flatMap(({ value }) => e$1(value) && typeof value.id === "string" && e$1(value.manifest) ? [{
|
|
7743
|
+
identifier: value.id,
|
|
7744
|
+
name: typeof value.name === "string" ? value.name : value.id,
|
|
7745
|
+
manifest: value.manifest
|
|
7746
|
+
}] : []);
|
|
7747
|
+
};
|
|
7748
|
+
/** The element types of plugins the project runs as they were built (`vendor/plugins/<type>/`): authored as known. */
|
|
7749
|
+
var builtPluginTypes = async (root) => {
|
|
7750
|
+
try {
|
|
7751
|
+
return (await fs.readdir(path.join(root, "vendor/plugins"), { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
7752
|
+
} catch {
|
|
7753
|
+
return [];
|
|
7754
|
+
}
|
|
7755
|
+
};
|
|
7756
|
+
var count = (entries, noun) => entries ? `${String(entries.length)} ${noun}${entries.length === 1 ? "" : "s"}` : `the space’s ${noun}s as they are`;
|
|
7757
|
+
/**
|
|
7758
|
+
* The project's space sent as the draft of the space the connection works in: refused when the draft moved on since
|
|
7759
|
+
* `base` (the draft the project last had), or — with no base — when the space holds work of its own; `force` takes it
|
|
7760
|
+
* anyway.
|
|
7761
|
+
*/
|
|
7762
|
+
var pushSpaceOf = async (root, connection, space, { base, force }) => {
|
|
7763
|
+
const project = await loadProjectSpace(root);
|
|
7764
|
+
if ("problem" in project) {
|
|
7765
|
+
fail(project.problem);
|
|
7766
|
+
return { outcome: "failed" };
|
|
7767
|
+
}
|
|
7768
|
+
let documents;
|
|
7769
|
+
try {
|
|
7770
|
+
const { schema, style } = authorSpace(project.space, {
|
|
7771
|
+
plugins: project.plugins,
|
|
7772
|
+
pluginTypes: await builtPluginTypes(root)
|
|
7773
|
+
});
|
|
7774
|
+
documents = {
|
|
7775
|
+
schema,
|
|
7776
|
+
style
|
|
7777
|
+
};
|
|
7778
|
+
} catch (error) {
|
|
7779
|
+
fail(`The space does not author, so nothing was pushed — npm run author says why:\n${String(error instanceof Error ? error.message : error)}`);
|
|
7780
|
+
return { outcome: "failed" };
|
|
7781
|
+
}
|
|
7782
|
+
const [actions, connectors] = await Promise.all([projectActions(root), projectConnectors(root)]);
|
|
7783
|
+
for (const read of [actions, connectors]) if (read && "problem" in read) {
|
|
7784
|
+
fail(read.problem);
|
|
7785
|
+
return { outcome: "failed" };
|
|
7786
|
+
}
|
|
7787
|
+
const sent = {
|
|
7788
|
+
format: 1,
|
|
7789
|
+
documents,
|
|
7790
|
+
...Array.isArray(actions) ? { actions } : {},
|
|
7791
|
+
...Array.isArray(connectors) ? { connectors } : {},
|
|
7792
|
+
base,
|
|
7793
|
+
force
|
|
7794
|
+
};
|
|
7795
|
+
const answered = await authorizedRequest(connection, `/spaces/${String(space.id)}/import`, {
|
|
7796
|
+
method: "PUT",
|
|
7797
|
+
headers: { "Content-Type": "application/json" },
|
|
7798
|
+
body: JSON.stringify(sent)
|
|
7799
|
+
});
|
|
7800
|
+
if (!answered.ok) {
|
|
7801
|
+
fail(answered.error);
|
|
7802
|
+
return { outcome: "failed" };
|
|
7803
|
+
}
|
|
7804
|
+
const { status, data } = answered.value.reply;
|
|
7805
|
+
if (status === 409) {
|
|
7806
|
+
fail(data.refusal?.code === "DRAFT_NOT_EMPTY" ? `${space.name} already holds work of its own, and this project never had it: pushing would replace it.\nTake it out first (plitzi create --from ${space.permanentUrl}), or pass --force to replace it with this project.` : `${space.name}’s draft changed since this project last had it — in the builder, or from another copy.\nPull first (plitzi pull) and push again, or pass --force to replace it with this project.`);
|
|
7807
|
+
return { outcome: "failed" };
|
|
7808
|
+
}
|
|
7809
|
+
if (status === 422 && data.problems) {
|
|
7810
|
+
fail(`${space.name} would not take the space as it is:${data.problems.map((problem) => `\n ${problem}`).join("")}`);
|
|
7811
|
+
return { outcome: "failed" };
|
|
7812
|
+
}
|
|
7813
|
+
if (status !== 200 || !data.draft) {
|
|
7814
|
+
fail(data.error ?? `The space was not pushed (${String(status)}).`);
|
|
7815
|
+
return { outcome: "failed" };
|
|
7816
|
+
}
|
|
7817
|
+
if (!data.changed) return {
|
|
7818
|
+
outcome: "unchanged",
|
|
7819
|
+
draft: data.draft
|
|
7820
|
+
};
|
|
7821
|
+
console.log(chalk.green(`The space is ${space.name}’s draft now — its pages and styles, ${count(sent.actions, "action")}, ${count(sent.connectors, "connector")}.`) + chalk.dim(" Any builder open on it shows it now."));
|
|
7822
|
+
return {
|
|
7823
|
+
outcome: "pushed",
|
|
7824
|
+
draft: data.draft
|
|
7825
|
+
};
|
|
7826
|
+
};
|
|
7827
|
+
//#endregion
|
|
7421
7828
|
//#region src/commands/keepSource.ts
|
|
7422
7829
|
/**
|
|
7423
7830
|
* A packed source snapshot sent beside what was just uploaded or pushed, and kept on the space (docs/en/projects-from-spaces.md): what lets
|
|
@@ -7445,7 +7852,6 @@ var keepSource = async (connection, spaceId, bytes) => {
|
|
|
7445
7852
|
};
|
|
7446
7853
|
//#endregion
|
|
7447
7854
|
//#region src/commands/runtime.ts
|
|
7448
|
-
var DEFAULT_ENTRY = path.join("src", "runtime.ts");
|
|
7449
7855
|
var isPacker = (value) => e$1(value) && typeof value.packRuntime === "function";
|
|
7450
7856
|
/** This project's own packer — read, since it is the project's. */
|
|
7451
7857
|
var projectPacker = async (root) => {
|
|
@@ -7456,42 +7862,42 @@ var projectPacker = async (root) => {
|
|
|
7456
7862
|
} catch {}
|
|
7457
7863
|
fail("plitzi runtime packs it with this project’s own @plitzi/sdk-server:\n npm install @plitzi/sdk-server");
|
|
7458
7864
|
};
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7865
|
+
var DEFAULT_RUNTIME_ENTRY = path.join("src", "runtime.ts");
|
|
7866
|
+
/**
|
|
7867
|
+
* The project's runtime module packed and kept as the draft runtime of the space the connection works in — which a
|
|
7868
|
+
* publish takes live — with the source it was packed from beside it.
|
|
7869
|
+
*/
|
|
7870
|
+
var pushRuntimeOf = async (root, connection, space, entry) => {
|
|
7465
7871
|
try {
|
|
7466
7872
|
await fs.access(entry);
|
|
7467
7873
|
} catch {
|
|
7468
7874
|
fail(`There is no ${path.relative(root, entry)}: the module whose default export is defineRuntime({ start }).`);
|
|
7469
|
-
return;
|
|
7875
|
+
return "failed";
|
|
7470
7876
|
}
|
|
7471
7877
|
const packer = await projectPacker(root);
|
|
7472
|
-
if (!packer) return;
|
|
7878
|
+
if (!packer) return "failed";
|
|
7473
7879
|
let bytes;
|
|
7474
7880
|
try {
|
|
7475
7881
|
bytes = await packer.packRuntime(entry);
|
|
7476
7882
|
} catch (error) {
|
|
7477
7883
|
fail(`It does not pack: ${error instanceof Error ? error.message : String(error)}`);
|
|
7478
|
-
return;
|
|
7884
|
+
return "failed";
|
|
7479
7885
|
}
|
|
7480
|
-
const answered = await authorizedRequest(connection, `/spaces/${String(
|
|
7886
|
+
const answered = await authorizedRequest(connection, `/spaces/${String(space.id)}/runtime`, {
|
|
7481
7887
|
method: "PUT",
|
|
7482
7888
|
headers: { "content-type": "application/octet-stream" },
|
|
7483
7889
|
body: new Uint8Array(bytes)
|
|
7484
7890
|
});
|
|
7485
7891
|
if (!answered.ok) {
|
|
7486
7892
|
fail(answered.error);
|
|
7487
|
-
return;
|
|
7893
|
+
return "failed";
|
|
7488
7894
|
}
|
|
7489
7895
|
const { reply } = answered.value;
|
|
7490
7896
|
if (reply.status !== 200 || !reply.data.digest) {
|
|
7491
7897
|
fail(reply.data.error ?? `The runtime was not kept (${String(reply.status)}).`);
|
|
7492
|
-
return;
|
|
7898
|
+
return "failed";
|
|
7493
7899
|
}
|
|
7494
|
-
console.log(chalk.green(`${
|
|
7900
|
+
console.log(chalk.green(`${space.name}’s draft runtime is ${reply.data.digest.slice(0, 12)} (${(bytes.byteLength / 1024).toFixed(0)} KB).`) + chalk.dim(" It starts in a moment; publish the space to take it live."));
|
|
7495
7901
|
try {
|
|
7496
7902
|
const source = await packSource({
|
|
7497
7903
|
root,
|
|
@@ -7499,11 +7905,19 @@ var pushRuntime = async (options) => {
|
|
|
7499
7905
|
name: "runtime",
|
|
7500
7906
|
entries: [entry]
|
|
7501
7907
|
});
|
|
7502
|
-
await keepSource(answered.value.connection,
|
|
7908
|
+
await keepSource(answered.value.connection, space.id, source.bytes);
|
|
7503
7909
|
} catch (error) {
|
|
7504
7910
|
console.log(chalk.yellow(" Its source is not kept, so a project taken from the space gets it built only:"));
|
|
7505
7911
|
console.log(chalk.yellow(` ${error instanceof Error ? error.message : String(error)}`));
|
|
7506
7912
|
}
|
|
7913
|
+
return "pushed";
|
|
7914
|
+
};
|
|
7915
|
+
/** Packs the project's runtime module and keeps it as the space's draft runtime — which a publish takes live. */
|
|
7916
|
+
var pushRuntime = async (options) => {
|
|
7917
|
+
const root = (await projectHere("whose runtime to push"))?.root;
|
|
7918
|
+
const connection = root && await connectToSpace(options, "to push to");
|
|
7919
|
+
if (!root || !connection || !connection.space) return;
|
|
7920
|
+
await pushRuntimeOf(root, connection, connection.space, path.resolve(root, options.entry ?? DEFAULT_RUNTIME_ENTRY));
|
|
7507
7921
|
};
|
|
7508
7922
|
var readRuntime = async (connection, spaceId) => {
|
|
7509
7923
|
const answered = await authorizedRequest(connection, `/spaces/${String(spaceId)}/runtime`);
|
|
@@ -7644,55 +8058,840 @@ var unsetRuntimeVariable = async (name, options) => {
|
|
|
7644
8058
|
console.log(chalk.green(`${name} removed — the runtime starts again without it.`));
|
|
7645
8059
|
};
|
|
7646
8060
|
//#endregion
|
|
7647
|
-
//#region src/commands/
|
|
7648
|
-
/**
|
|
7649
|
-
var
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
height: view.height
|
|
7657
|
-
},
|
|
7658
|
-
colorScheme: view.scheme,
|
|
7659
|
-
reducedMotion: view.reducedMotion ? "reduce" : "no-preference"
|
|
7660
|
-
});
|
|
7661
|
-
if (!await page.goto(url, { waitUntil: "networkidle" }).catch(() => null)) return { problem: `Nothing answers at ${url}.` };
|
|
7662
|
-
if (view.waitFor) {
|
|
7663
|
-
if (!await page.waitForSelector(selectorOf(view.waitFor), { timeout: 1e4 }).catch(() => null)) return { problem: `${view.waitFor} did not appear on ${url} within 10 s.` };
|
|
7664
|
-
}
|
|
7665
|
-
for (const target of [view.scrollTo, view.clip]) if (target && !await page.waitForSelector(selectorOf(target), { timeout: 1e4 }).catch(() => null)) return { problem: `${target} is not on ${url}.` };
|
|
7666
|
-
if (view.scrollTo) await page.locator(selectorOf(view.scrollTo)).first().scrollIntoViewIfNeeded();
|
|
7667
|
-
return {
|
|
7668
|
-
page,
|
|
7669
|
-
png: await capture(page, framing(view))
|
|
7670
|
-
};
|
|
7671
|
-
};
|
|
7672
|
-
var framing = (view) => {
|
|
7673
|
-
if (view.clip) return {
|
|
7674
|
-
kind: "element",
|
|
7675
|
-
selector: selectorOf(view.clip)
|
|
8061
|
+
//#region src/commands/uploadPlugin.ts
|
|
8062
|
+
/** The zips `plitzi pack plugin` leaves: at a plugin package's root, or under `dist/plugins` of any other project. */
|
|
8063
|
+
var packedZips = async (root) => {
|
|
8064
|
+
const inFolder = async (dir) => {
|
|
8065
|
+
try {
|
|
8066
|
+
return (await fs.readdir(dir)).filter((file) => file.endsWith(".zip")).map((file) => path.join(dir, file));
|
|
8067
|
+
} catch {
|
|
8068
|
+
return [];
|
|
8069
|
+
}
|
|
7676
8070
|
};
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
};
|
|
7683
|
-
var regionsText = (regions) => regions.map((region) => `${region.name} ${String(region.changed)}%`).join(", ");
|
|
7684
|
-
/** Compared frame to frame: the most each region changed between two that follow each other. */
|
|
7685
|
-
var framesMoved = (diffs) => {
|
|
7686
|
-
const most = /* @__PURE__ */ new Map();
|
|
7687
|
-
for (const diff of diffs) for (const region of diff.regions) most.set(region.name, Math.max(most.get(region.name) ?? 0, region.changed));
|
|
7688
|
-
return [...most].map(([name, changed]) => ({
|
|
7689
|
-
name,
|
|
7690
|
-
changed
|
|
7691
|
-
}));
|
|
8071
|
+
const zips = [...await inFolder(root), ...await inFolder(path.join(root, "dist/plugins"))];
|
|
8072
|
+
const modified = await Promise.all(zips.map(async (zip) => (await fs.stat(zip)).mtimeMs));
|
|
8073
|
+
return zips.map((zip, index) => ({
|
|
8074
|
+
zip,
|
|
8075
|
+
at: modified[index]
|
|
8076
|
+
})).sort((a, b) => b.at - a.at).map(({ zip }) => zip);
|
|
7692
8077
|
};
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
if (
|
|
8078
|
+
/** The zip named, or the one packed here — asked for at a terminal when there are several. */
|
|
8079
|
+
var chooseZip = async (given) => {
|
|
8080
|
+
if (given) return path.resolve(given);
|
|
8081
|
+
const project = await findProject(process.cwd());
|
|
8082
|
+
const zips = project ? await packedZips(project.root) : [];
|
|
8083
|
+
if (zips.length === 0) {
|
|
8084
|
+
fail("No packed plugin here. Build one with plitzi pack plugin, or name it: plitzi upload plugin <file.zip>.");
|
|
8085
|
+
return;
|
|
8086
|
+
}
|
|
8087
|
+
if (zips.length === 1) return zips[0];
|
|
8088
|
+
const choices = zips.map((zip) => path.relative(process.cwd(), zip));
|
|
8089
|
+
if (!atTerminal()) {
|
|
8090
|
+
refuseWithoutTerminal("upload", [{
|
|
8091
|
+
flag: "<zip> (the argument)",
|
|
8092
|
+
choices,
|
|
8093
|
+
question: "Which packed plugin is to be uploaded?"
|
|
8094
|
+
}], "plitzi upload plugin stopped before uploading anything: which plugin goes to the space is a choice");
|
|
8095
|
+
return;
|
|
8096
|
+
}
|
|
8097
|
+
const rl = readline.createInterface({
|
|
8098
|
+
input: process.stdin,
|
|
8099
|
+
output: process.stdout
|
|
8100
|
+
});
|
|
8101
|
+
try {
|
|
8102
|
+
return await askPick(rl, "Which packed plugin? The newest is first.", zips.map((zip, index) => ({
|
|
8103
|
+
label: choices[index],
|
|
8104
|
+
value: zip
|
|
8105
|
+
})));
|
|
8106
|
+
} finally {
|
|
8107
|
+
rl.close();
|
|
8108
|
+
}
|
|
8109
|
+
};
|
|
8110
|
+
/**
|
|
8111
|
+
* The manifest a zip carries at its root, read here before anything is sent: a zip that is not a plugin is refused by
|
|
8112
|
+
* the platform too, but only after it has travelled.
|
|
8113
|
+
*/
|
|
8114
|
+
var manifestOf = (zip) => {
|
|
8115
|
+
try {
|
|
8116
|
+
const entry = Object.values(unzipSync(zip, { filter: (file) => file.name === "plugin-manifest.json" })).at(0);
|
|
8117
|
+
const parsed = entry ? JSON.parse(new TextDecoder().decode(entry)) : {};
|
|
8118
|
+
return typeof parsed.root === "string" && parsed.root ? {
|
|
8119
|
+
root: parsed.root,
|
|
8120
|
+
version: typeof parsed.version === "string" ? parsed.version : ""
|
|
8121
|
+
} : void 0;
|
|
8122
|
+
} catch {
|
|
8123
|
+
return;
|
|
8124
|
+
}
|
|
8125
|
+
};
|
|
8126
|
+
/**
|
|
8127
|
+
* The public bucket a plugin goes in — the one `--cdn` and `--bucket` name, the only one there is, or the one picked.
|
|
8128
|
+
* A private bucket keeps server code and has no public address, so it is never one.
|
|
8129
|
+
*/
|
|
8130
|
+
var chooseTarget = async (cdns, { cdn: givenCdn, bucket: givenBucket }, spaceName) => {
|
|
8131
|
+
if (givenCdn && !cdns.some((cdn) => cdn.identifier === givenCdn)) {
|
|
8132
|
+
fail(`${spaceName} has no CDN "${givenCdn}". Its CDNs: ${cdns.map((cdn) => cdn.identifier).join(", ")}.`);
|
|
8133
|
+
return;
|
|
8134
|
+
}
|
|
8135
|
+
const candidates = cdns.filter((cdn) => !givenCdn || cdn.identifier === givenCdn).flatMap((cdn) => cdn.buckets.map((bucket) => ({
|
|
8136
|
+
cdn,
|
|
8137
|
+
bucket
|
|
8138
|
+
})));
|
|
8139
|
+
if (givenBucket) {
|
|
8140
|
+
const named = candidates.find(({ bucket }) => bucket.identifier === givenBucket);
|
|
8141
|
+
if (!named) {
|
|
8142
|
+
fail(`${spaceName} has no bucket "${givenBucket}"${givenCdn ? ` in the CDN "${givenCdn}"` : ""}.`);
|
|
8143
|
+
return;
|
|
8144
|
+
}
|
|
8145
|
+
if (named.bucket.visibility === "private") {
|
|
8146
|
+
fail(`"${givenBucket}" is a private bucket: it has no public address, so no page could load a plugin from it.`);
|
|
8147
|
+
return;
|
|
8148
|
+
}
|
|
8149
|
+
return named;
|
|
8150
|
+
}
|
|
8151
|
+
const targets = candidates.filter(({ bucket }) => bucket.visibility === "public");
|
|
8152
|
+
if (targets.length === 0) {
|
|
8153
|
+
fail(`${spaceName} has no public bucket to put a plugin in. Add one to a CDN in the builder, under Resources.`);
|
|
8154
|
+
return;
|
|
8155
|
+
}
|
|
8156
|
+
if (targets.length === 1) return targets[0];
|
|
8157
|
+
if (!atTerminal()) {
|
|
8158
|
+
refuseWithoutTerminal("upload", [{
|
|
8159
|
+
flag: "--bucket",
|
|
8160
|
+
choices: targets.map(({ bucket }) => bucket.identifier),
|
|
8161
|
+
question: `Which of ${spaceName}'s public buckets does the plugin go in? (narrow with --cdn)`
|
|
8162
|
+
}], "plitzi upload plugin stopped before uploading anything: which bucket serves the plugin is a choice");
|
|
8163
|
+
return;
|
|
8164
|
+
}
|
|
8165
|
+
const rl = readline.createInterface({
|
|
8166
|
+
input: process.stdin,
|
|
8167
|
+
output: process.stdout
|
|
8168
|
+
});
|
|
8169
|
+
try {
|
|
8170
|
+
return await askPick(rl, `Which of ${spaceName}'s public buckets does the plugin go in?`, targets.map((target) => ({
|
|
8171
|
+
label: `${target.cdn.name} — ${target.bucket.name} ${chalk.dim(`${target.bucket.identifier} · ${target.bucket.domain}`)}`,
|
|
8172
|
+
value: target
|
|
8173
|
+
})));
|
|
8174
|
+
} finally {
|
|
8175
|
+
rl.close();
|
|
8176
|
+
}
|
|
8177
|
+
};
|
|
8178
|
+
/** The space's CDNs and their buckets, asked for — or why not, in words. */
|
|
8179
|
+
var listCdns = async (connection, space) => {
|
|
8180
|
+
const listed = await authorizedRequest(connection, `/spaces/${space.id}/cdns`);
|
|
8181
|
+
if (!listed.ok) return listed;
|
|
8182
|
+
const { reply } = listed.value;
|
|
8183
|
+
if (reply.status !== 200) return {
|
|
8184
|
+
ok: false,
|
|
8185
|
+
error: reply.data.error ?? `Could not list ${space.name}'s CDNs (${reply.status}).`
|
|
8186
|
+
};
|
|
8187
|
+
return {
|
|
8188
|
+
ok: true,
|
|
8189
|
+
value: {
|
|
8190
|
+
connection: listed.value.connection,
|
|
8191
|
+
cdns: reply.data.cdns ?? []
|
|
8192
|
+
}
|
|
8193
|
+
};
|
|
8194
|
+
};
|
|
8195
|
+
/**
|
|
8196
|
+
* A packed plugin uploaded to a bucket of the space and installed there — and its source kept beside it, when it was
|
|
8197
|
+
* packed with one.
|
|
8198
|
+
*/
|
|
8199
|
+
var uploadZip = async (connection, space, { zip, filename, target, source }) => {
|
|
8200
|
+
const manifest = manifestOf(zip);
|
|
8201
|
+
if (!manifest) {
|
|
8202
|
+
fail(`${filename} is not a plugin: it has no plugin-manifest.json at its root. Build it with plitzi pack plugin.`);
|
|
8203
|
+
return "failed";
|
|
8204
|
+
}
|
|
8205
|
+
const { cdn, bucket } = target;
|
|
8206
|
+
const label = `${manifest.root}${manifest.version ? ` ${manifest.version}` : ""}`;
|
|
8207
|
+
console.log(`\nUploading ${chalk.bold(label)} to ${chalk.bold(space.name)}, in ${cdn.name} — ${bucket.name}…`);
|
|
8208
|
+
const query = new URLSearchParams({
|
|
8209
|
+
filename,
|
|
8210
|
+
bucket: bucket.identifier
|
|
8211
|
+
});
|
|
8212
|
+
const uploaded = await authorizedRequest(connection, `/spaces/${space.id}/cdns/${encodeURIComponent(cdn.identifier)}/plugins?${query}`, {
|
|
8213
|
+
method: "POST",
|
|
8214
|
+
headers: { "Content-Type": "application/zip" },
|
|
8215
|
+
body: new Uint8Array(zip)
|
|
8216
|
+
});
|
|
8217
|
+
if (!uploaded.ok) {
|
|
8218
|
+
fail(uploaded.error);
|
|
8219
|
+
return "failed";
|
|
8220
|
+
}
|
|
8221
|
+
const { status, data } = uploaded.value.reply;
|
|
8222
|
+
if (status === 413) {
|
|
8223
|
+
fail(`${filename} is larger than ${connection.api} accepts.`);
|
|
8224
|
+
return "failed";
|
|
8225
|
+
}
|
|
8226
|
+
if (status !== 201) {
|
|
8227
|
+
fail(data.error ?? `The upload was refused (${status}).`);
|
|
8228
|
+
return "failed";
|
|
8229
|
+
}
|
|
8230
|
+
const installed = data.installed === "updated" ? `now loads ${label}` : `loads ${label} from now on`;
|
|
8231
|
+
console.log(chalk.green(`${space.name} ${installed}.`));
|
|
8232
|
+
if (data.resource?.path) console.log(chalk.dim(` ${data.resource.path}`));
|
|
8233
|
+
console.log(chalk.dim(" Any builder open on the space loads it now."));
|
|
8234
|
+
if (source) await keepSource(uploaded.value.connection, space.id, source);
|
|
8235
|
+
return "pushed";
|
|
8236
|
+
};
|
|
8237
|
+
var uploadPluginCommand = async (zipGiven, options) => {
|
|
8238
|
+
const api = await apiFor(options);
|
|
8239
|
+
const zipPath = api ? await chooseZip(zipGiven) : void 0;
|
|
8240
|
+
if (!api || !zipPath) return;
|
|
8241
|
+
let zip;
|
|
8242
|
+
try {
|
|
8243
|
+
zip = await fs.readFile(zipPath);
|
|
8244
|
+
} catch {
|
|
8245
|
+
fail(`Could not read ${zipPath}.`);
|
|
8246
|
+
return;
|
|
8247
|
+
}
|
|
8248
|
+
if (!manifestOf(zip)) {
|
|
8249
|
+
fail(`${path.basename(zipPath)} is not a plugin: it has no plugin-manifest.json at its root. Build it with plitzi pack plugin.`);
|
|
8250
|
+
return;
|
|
8251
|
+
}
|
|
8252
|
+
const connection = await connectionWithSpace(api, "to upload to");
|
|
8253
|
+
if (!connection?.space) return;
|
|
8254
|
+
const { space } = connection;
|
|
8255
|
+
const listed = await listCdns(connection, space);
|
|
8256
|
+
if (!listed.ok) {
|
|
8257
|
+
fail(listed.error);
|
|
8258
|
+
return;
|
|
8259
|
+
}
|
|
8260
|
+
const target = await chooseTarget(listed.value.cdns, options, space.name);
|
|
8261
|
+
if (!target) return;
|
|
8262
|
+
const source = await fs.readFile(sourceFileOf(zipPath)).catch(() => void 0);
|
|
8263
|
+
await uploadZip(listed.value.connection, space, {
|
|
8264
|
+
zip,
|
|
8265
|
+
filename: path.basename(zipPath),
|
|
8266
|
+
target,
|
|
8267
|
+
...source ? { source } : {}
|
|
8268
|
+
});
|
|
8269
|
+
console.log("");
|
|
8270
|
+
};
|
|
8271
|
+
//#endregion
|
|
8272
|
+
//#region src/commands/push.ts
|
|
8273
|
+
/**
|
|
8274
|
+
* `plitzi push`: the project put back on the space it works with — the way back of `plitzi pull`
|
|
8275
|
+
* (docs/en/projects-from-spaces.md). Whatever changed since the project last had the space, or the parts named:
|
|
8276
|
+
*
|
|
8277
|
+
* - `plugins` — each plugin whose source changed, packed and uploaded to the space's CDN, as `plitzi upload plugin`;
|
|
8278
|
+
* - `functions` — `functions/`, as `plitzi functions push`;
|
|
8279
|
+
* - `runtime` — the runtime module, packed, as `plitzi runtime push`;
|
|
8280
|
+
* - `space` — the pages, styles, actions and connectors, as the space's draft.
|
|
8281
|
+
*
|
|
8282
|
+
* In that order: the pages name the plugins' elements and the actions name the functions' tasks, so what is named goes
|
|
8283
|
+
* up before what names it. A part that fails stops the push there.
|
|
8284
|
+
*
|
|
8285
|
+
* Always to the space the CLI is connected to, which must be the one the project came from; always the draft, never a
|
|
8286
|
+
* published environment. A draft edited in the builder since the project last had it is never replaced unseen: the push
|
|
8287
|
+
* is refused, and `--force` replaces it.
|
|
8288
|
+
*/
|
|
8289
|
+
var PUSH_PARTS = [
|
|
8290
|
+
"space",
|
|
8291
|
+
"functions",
|
|
8292
|
+
"runtime",
|
|
8293
|
+
"plugins"
|
|
8294
|
+
];
|
|
8295
|
+
/** What goes up first: what the rest names. */
|
|
8296
|
+
var ORDER = [
|
|
8297
|
+
"plugins",
|
|
8298
|
+
"functions",
|
|
8299
|
+
"runtime",
|
|
8300
|
+
"space"
|
|
8301
|
+
];
|
|
8302
|
+
var isPart = (value) => PUSH_PARTS.some((part) => part === value);
|
|
8303
|
+
var exists = async (file) => {
|
|
8304
|
+
try {
|
|
8305
|
+
await fs.access(file);
|
|
8306
|
+
return true;
|
|
8307
|
+
} catch {
|
|
8308
|
+
return false;
|
|
8309
|
+
}
|
|
8310
|
+
};
|
|
8311
|
+
/** Every file under a folder of the project, by its path in the project: none when there is no such folder. */
|
|
8312
|
+
var filesUnder = async (root, folder) => {
|
|
8313
|
+
try {
|
|
8314
|
+
return (await fs.readdir(path.join(root, folder), {
|
|
8315
|
+
recursive: true,
|
|
8316
|
+
withFileTypes: true
|
|
8317
|
+
})).filter((entry) => entry.isFile()).map((entry) => path.relative(root, path.join(entry.parentPath, entry.name)));
|
|
8318
|
+
} catch {
|
|
8319
|
+
return [];
|
|
8320
|
+
}
|
|
8321
|
+
};
|
|
8322
|
+
/**
|
|
8323
|
+
* Whether any of the files changed since the project last had the space: one not recorded, or not as it was recorded —
|
|
8324
|
+
* formatting aside. Everything has, for a project that never had it.
|
|
8325
|
+
*/
|
|
8326
|
+
var changedSince = async (root, origin, files, format) => {
|
|
8327
|
+
if (!origin) return true;
|
|
8328
|
+
for (const file of files) {
|
|
8329
|
+
const was = origin.files[file];
|
|
8330
|
+
const here = await digestsOf(root, file, format);
|
|
8331
|
+
if (!was || !here || here.raw !== was && here.formatted !== was) return true;
|
|
8332
|
+
}
|
|
8333
|
+
return false;
|
|
8334
|
+
};
|
|
8335
|
+
/** The project's files a build of the runtime or of a plugin is made of — none when that cannot be told. */
|
|
8336
|
+
var closureOf = async (root, kind, name, entries) => {
|
|
8337
|
+
try {
|
|
8338
|
+
return Object.keys((await packSource({
|
|
8339
|
+
root,
|
|
8340
|
+
kind,
|
|
8341
|
+
name,
|
|
8342
|
+
entries
|
|
8343
|
+
})).snapshot.files);
|
|
8344
|
+
} catch {
|
|
8345
|
+
return;
|
|
8346
|
+
}
|
|
8347
|
+
};
|
|
8348
|
+
/** The files the space part is made of, as `create --from` lays them out: the pages, the actions, the connectors. */
|
|
8349
|
+
var SPACE_FILES = ["src/space.ts", "src/actions.ts"];
|
|
8350
|
+
var SPACE_FOLDERS = [
|
|
8351
|
+
"src/space/",
|
|
8352
|
+
"src/actions/",
|
|
8353
|
+
"src/connectors/"
|
|
8354
|
+
];
|
|
8355
|
+
var isSpaceFile = (file) => SPACE_FILES.includes(file) || SPACE_FOLDERS.some((folder) => file.startsWith(folder));
|
|
8356
|
+
/** A plugin's type as the project's folder spells it: `src/plugins/SeatPicker` is `seatPicker`. */
|
|
8357
|
+
var typeOfFolder = (folder) => {
|
|
8358
|
+
const name = path.basename(folder);
|
|
8359
|
+
return `${name.charAt(0).toLowerCase()}${name.slice(1)}`;
|
|
8360
|
+
};
|
|
8361
|
+
/**
|
|
8362
|
+
* What the project can send, and whether each changed since the project last had the space. The plugins are grouped as
|
|
8363
|
+
* the space keeps them — a plugin can be several elements — and an element folder the space has no plugin of is a
|
|
8364
|
+
* plugin of its own.
|
|
8365
|
+
*/
|
|
8366
|
+
var survey = async (root, { origin, source, before, spaceId, format }) => {
|
|
8367
|
+
const found = [];
|
|
8368
|
+
if (source === "local" && await exists(path.join(root, "src/space.ts"))) {
|
|
8369
|
+
const files = [...await Promise.all(SPACE_FILES.map(async (file) => await exists(path.join(root, file)) ? [file] : [])), ...await Promise.all(SPACE_FOLDERS.map((folder) => filesUnder(root, folder)))].flat();
|
|
8370
|
+
const gone = Object.keys(origin?.files ?? {}).some((file) => isSpaceFile(file) && !files.includes(file));
|
|
8371
|
+
found.push({
|
|
8372
|
+
part: "space",
|
|
8373
|
+
label: "space — its pages, styles, actions and connectors",
|
|
8374
|
+
changed: gone || await changedSince(root, origin, files.filter((file) => isSpaceFile(file)), format)
|
|
8375
|
+
});
|
|
8376
|
+
}
|
|
8377
|
+
const functions = await functionsChange(root, spaceId);
|
|
8378
|
+
if (functions !== "none") found.push({
|
|
8379
|
+
part: "functions",
|
|
8380
|
+
label: "functions — functions/",
|
|
8381
|
+
changed: functions === "changed"
|
|
8382
|
+
});
|
|
8383
|
+
const entries = before && projectEntries(before);
|
|
8384
|
+
const candidates = [...entries?.runtime ? [entries.runtime] : [], DEFAULT_RUNTIME_ENTRY];
|
|
8385
|
+
const present = await Promise.all(candidates.map((entry) => exists(path.join(root, entry))));
|
|
8386
|
+
const runtimeEntry = candidates.find((_entry, index) => present[index]);
|
|
8387
|
+
if (runtimeEntry) {
|
|
8388
|
+
const entry = path.join(root, runtimeEntry);
|
|
8389
|
+
const files = await closureOf(root, "runtime", "runtime", [entry]);
|
|
8390
|
+
found.push({
|
|
8391
|
+
part: "runtime",
|
|
8392
|
+
label: `runtime — ${runtimeEntry}`,
|
|
8393
|
+
changed: !files || await changedSince(root, origin, files, format),
|
|
8394
|
+
entry,
|
|
8395
|
+
files: files ?? []
|
|
8396
|
+
});
|
|
8397
|
+
}
|
|
8398
|
+
const folders = await elementFolders({ root });
|
|
8399
|
+
const grouped = (entries?.plugins ?? []).map(({ type, entries: starts }) => ({
|
|
8400
|
+
type,
|
|
8401
|
+
folders: starts.map((start) => path.join(root, path.dirname(start)))
|
|
8402
|
+
})).filter((group) => group.folders.every((folder) => folders.includes(folder)));
|
|
8403
|
+
const alone = folders.filter((folder) => !grouped.some((group) => group.folders.includes(folder))).map((folder) => ({
|
|
8404
|
+
type: typeOfFolder(folder),
|
|
8405
|
+
folders: [folder]
|
|
8406
|
+
}));
|
|
8407
|
+
for (const { type, folders: group } of [...grouped, ...alone]) {
|
|
8408
|
+
const files = await closureOf(root, "plugin", type, group.map((folder) => path.join(folder, "index.ts")));
|
|
8409
|
+
found.push({
|
|
8410
|
+
part: "plugins",
|
|
8411
|
+
label: `plugin ${type} — ${group.map((folder) => path.relative(root, folder)).join(", ")}`,
|
|
8412
|
+
changed: !files || await changedSince(root, origin, files, format),
|
|
8413
|
+
folders: group,
|
|
8414
|
+
files: files ?? []
|
|
8415
|
+
});
|
|
8416
|
+
}
|
|
8417
|
+
return found;
|
|
8418
|
+
};
|
|
8419
|
+
/**
|
|
8420
|
+
* What to send: the parts named; at a terminal, what the person ticks — what changed, ticked to begin with; with nobody
|
|
8421
|
+
* there, what changed. `undefined` when there is nothing to send, said why.
|
|
8422
|
+
*/
|
|
8423
|
+
var choose = async (found, asked, spaceName) => {
|
|
8424
|
+
if (asked.length > 0) {
|
|
8425
|
+
const unknown = asked.filter((part) => !isPart(part));
|
|
8426
|
+
if (unknown.length > 0) {
|
|
8427
|
+
fail(`plitzi push sends ${PUSH_PARTS.join(", ")} — not ${unknown.join(", ")}.`);
|
|
8428
|
+
return;
|
|
8429
|
+
}
|
|
8430
|
+
const missing = asked.filter((part) => !found.some((item) => item.part === part));
|
|
8431
|
+
if (missing.length > 0) {
|
|
8432
|
+
fail(`This project has no ${missing.join(", ")} to push.`);
|
|
8433
|
+
return;
|
|
8434
|
+
}
|
|
8435
|
+
return found.filter((item) => asked.includes(item.part));
|
|
8436
|
+
}
|
|
8437
|
+
if (found.length === 0) {
|
|
8438
|
+
fail("This project has nothing to push: no src/space.ts, functions/, runtime or plugin.");
|
|
8439
|
+
return;
|
|
8440
|
+
}
|
|
8441
|
+
if (!atTerminal()) return found.filter((item) => item.changed);
|
|
8442
|
+
const chosen = await askChecks(`What goes to ${spaceName}? Ticked: what changed since the project last had it.`, found.map((item) => ({
|
|
8443
|
+
label: item.label,
|
|
8444
|
+
value: item,
|
|
8445
|
+
checked: item.changed,
|
|
8446
|
+
hint: item.changed ? "changed" : "unchanged"
|
|
8447
|
+
})));
|
|
8448
|
+
if (!chosen) console.log("Nothing was pushed.");
|
|
8449
|
+
return chosen;
|
|
8450
|
+
};
|
|
8451
|
+
/** A plugin packed as `plitzi pack plugin` packs it, and uploaded and installed as `plitzi upload plugin` does. */
|
|
8452
|
+
var pushPlugin = async (root, connection, space, { folders, target, version }) => {
|
|
8453
|
+
const base = fileNameOf(folders[0]);
|
|
8454
|
+
const zip = path.join(root, "dist/plugins", `${base}-${version}.zip`);
|
|
8455
|
+
let packed;
|
|
8456
|
+
try {
|
|
8457
|
+
packed = await packPlugin({
|
|
8458
|
+
root,
|
|
8459
|
+
source: {
|
|
8460
|
+
kind: "elements",
|
|
8461
|
+
folders
|
|
8462
|
+
},
|
|
8463
|
+
base,
|
|
8464
|
+
version,
|
|
8465
|
+
outDir: path.join(root, "dist/plugins", base),
|
|
8466
|
+
zip
|
|
8467
|
+
});
|
|
8468
|
+
} catch (error) {
|
|
8469
|
+
if (!(error instanceof PackError)) throw error;
|
|
8470
|
+
fail(error.message);
|
|
8471
|
+
return "failed";
|
|
8472
|
+
}
|
|
8473
|
+
if ("problem" in packed.source) {
|
|
8474
|
+
console.log(chalk.yellow(` Its source is not kept, so the space can be taken out with ${base} built only:`));
|
|
8475
|
+
console.log(chalk.yellow(` ${packed.source.problem}`));
|
|
8476
|
+
}
|
|
8477
|
+
return uploadZip(connection, space, {
|
|
8478
|
+
zip: await fs.readFile(zip),
|
|
8479
|
+
filename: path.basename(zip),
|
|
8480
|
+
target,
|
|
8481
|
+
..."file" in packed.source ? { source: await fs.readFile(packed.source.file) } : {}
|
|
8482
|
+
});
|
|
8483
|
+
};
|
|
8484
|
+
/**
|
|
8485
|
+
* `.plitzi/space.json` as the space now holds the project, so `plitzi pull` compares with it from here on: the files of
|
|
8486
|
+
* the parts sent recorded as the space gives them back, the rest as they were — a page changed in the builder and not
|
|
8487
|
+
* pushed over is still the builder's change to the next pull. A project that never had the space records it whole.
|
|
8488
|
+
*/
|
|
8489
|
+
var recordPush = async (root, connection, space, { origin, source, found, sent, draft }) => {
|
|
8490
|
+
const exported = await requestExport(connection, String(space.id), {
|
|
8491
|
+
source,
|
|
8492
|
+
version: { environment: "main" },
|
|
8493
|
+
name: space.name
|
|
8494
|
+
});
|
|
8495
|
+
if (!exported.ok) {
|
|
8496
|
+
console.log(chalk.yellow(`Pushed, but .plitzi/space.json is not brought up to date — ${exported.error}\nThe next plitzi pull compares with what it recorded before.`));
|
|
8497
|
+
return;
|
|
8498
|
+
}
|
|
8499
|
+
const format = await projectFormatter(root);
|
|
8500
|
+
const next = projectFromSpace(exported.value, source);
|
|
8501
|
+
const given = await givenFiles(next, format);
|
|
8502
|
+
const parts = new Set(sent.map((item) => item.part));
|
|
8503
|
+
const codeOf = (items) => items.flatMap((item) => item.part === "runtime" || item.part === "plugins" ? item.files : []);
|
|
8504
|
+
const built = new Set(codeOf(sent));
|
|
8505
|
+
const code = /* @__PURE__ */ new Set([...codeOf(found), ...projectEntries(exported.value).files]);
|
|
8506
|
+
const downloads = new Set(Object.keys(origin?.downloads ?? {}));
|
|
8507
|
+
const isSent = (file) => {
|
|
8508
|
+
if (!origin || built.has(file)) return true;
|
|
8509
|
+
if (file.startsWith("functions/")) return parts.has("functions");
|
|
8510
|
+
return !code.has(file) && !file.startsWith("vendor/") && parts.has("space");
|
|
8511
|
+
};
|
|
8512
|
+
const files = Object.fromEntries(Object.entries(origin?.files ?? {}).filter(([file]) => downloads.has(file) || !isSent(file) || given.has(file)));
|
|
8513
|
+
given.forEach((bytes, file) => {
|
|
8514
|
+
if (isSent(file)) files[file] = digest(bytes);
|
|
8515
|
+
});
|
|
8516
|
+
const recorded = draft ?? origin?.draft;
|
|
8517
|
+
await writeOrigin(root, {
|
|
8518
|
+
format: 1,
|
|
8519
|
+
api: connection.api,
|
|
8520
|
+
space: exported.value.space,
|
|
8521
|
+
source,
|
|
8522
|
+
version: { environment: "main" },
|
|
8523
|
+
...recorded ? { draft: recorded } : {},
|
|
8524
|
+
files,
|
|
8525
|
+
downloads: origin?.downloads ?? {},
|
|
8526
|
+
dependencies: !origin || parts.has("runtime") || parts.has("plugins") ? next.dependencies : origin.dependencies
|
|
8527
|
+
});
|
|
8528
|
+
};
|
|
8529
|
+
var push = async (asked, options) => {
|
|
8530
|
+
const project = await findProject(process.cwd());
|
|
8531
|
+
if (!project) {
|
|
8532
|
+
fail("There is no package.json here or above: run plitzi push in the project to send.");
|
|
8533
|
+
return;
|
|
8534
|
+
}
|
|
8535
|
+
if (project.plitzi?.kind === "plugin") {
|
|
8536
|
+
fail("This is a plugin package: plitzi pack plugin and plitzi upload plugin put it on a space.");
|
|
8537
|
+
return;
|
|
8538
|
+
}
|
|
8539
|
+
const { root } = project;
|
|
8540
|
+
const origin = await readOrigin(root);
|
|
8541
|
+
if (origin && origin.version.environment !== "main") {
|
|
8542
|
+
fail(`This project follows ${versionLabel(origin.version)} of ${origin.space.name}, and a push writes the draft.\nFollow the draft first: plitzi pull --environment main.`);
|
|
8543
|
+
return;
|
|
8544
|
+
}
|
|
8545
|
+
const connection = await connectToSpace({
|
|
8546
|
+
...options,
|
|
8547
|
+
api: options.api ?? origin?.api
|
|
8548
|
+
}, "to push to");
|
|
8549
|
+
const space = connection?.space;
|
|
8550
|
+
if (!connection || !space) return;
|
|
8551
|
+
if (origin && origin.space.id !== space.id) {
|
|
8552
|
+
fail(`This project is ${origin.space.name}’s, and the CLI works in ${space.name}. Switch to it with plitzi space, and push again.`);
|
|
8553
|
+
return;
|
|
8554
|
+
}
|
|
8555
|
+
const source = origin?.source ?? (project.plitzi?.kind === "project" ? project.plitzi.source : "local");
|
|
8556
|
+
let before;
|
|
8557
|
+
if (origin) {
|
|
8558
|
+
const exported = await requestExport(connection, String(space.id), {
|
|
8559
|
+
source: "cloud",
|
|
8560
|
+
version: { environment: "main" },
|
|
8561
|
+
name: space.name
|
|
8562
|
+
});
|
|
8563
|
+
if (!exported.ok) {
|
|
8564
|
+
fail(exported.error);
|
|
8565
|
+
return;
|
|
8566
|
+
}
|
|
8567
|
+
before = exported.value;
|
|
8568
|
+
}
|
|
8569
|
+
const format = await projectFormatter(root);
|
|
8570
|
+
const found = await survey(root, {
|
|
8571
|
+
origin,
|
|
8572
|
+
source,
|
|
8573
|
+
before,
|
|
8574
|
+
spaceId: space.id,
|
|
8575
|
+
format
|
|
8576
|
+
});
|
|
8577
|
+
const chosen = await choose(found, asked, space.name);
|
|
8578
|
+
if (!chosen) return;
|
|
8579
|
+
if (chosen.length === 0) {
|
|
8580
|
+
console.log(`Nothing to push: the project is what ${space.name} last gave it.`);
|
|
8581
|
+
return;
|
|
8582
|
+
}
|
|
8583
|
+
const force = options.force ?? false;
|
|
8584
|
+
const sent = [];
|
|
8585
|
+
const settled = [];
|
|
8586
|
+
const unchanged = [];
|
|
8587
|
+
let draft;
|
|
8588
|
+
let target;
|
|
8589
|
+
const version = (await readPackageJson(root))?.version ?? "0.0.0";
|
|
8590
|
+
for (const item of [...chosen].sort((a, b) => ORDER.indexOf(a.part) - ORDER.indexOf(b.part))) {
|
|
8591
|
+
let outcome;
|
|
8592
|
+
if (item.part === "plugins") {
|
|
8593
|
+
if (!target) {
|
|
8594
|
+
const listed = await listCdns(connection, space);
|
|
8595
|
+
if (!listed.ok) {
|
|
8596
|
+
fail(listed.error);
|
|
8597
|
+
return;
|
|
8598
|
+
}
|
|
8599
|
+
target = await chooseTarget(listed.value.cdns, options, space.name);
|
|
8600
|
+
if (!target) return;
|
|
8601
|
+
}
|
|
8602
|
+
outcome = await pushPlugin(root, connection, space, {
|
|
8603
|
+
folders: item.folders,
|
|
8604
|
+
target,
|
|
8605
|
+
version
|
|
8606
|
+
});
|
|
8607
|
+
} else if (item.part === "functions") outcome = await pushFunctionsOf(root, connection, space, { force });
|
|
8608
|
+
else if (item.part === "runtime") outcome = await pushRuntimeOf(root, connection, space, item.entry);
|
|
8609
|
+
else {
|
|
8610
|
+
const pushed = await pushSpaceOf(root, connection, space, {
|
|
8611
|
+
base: origin?.draft ?? null,
|
|
8612
|
+
force
|
|
8613
|
+
});
|
|
8614
|
+
outcome = pushed.outcome;
|
|
8615
|
+
draft = pushed.draft;
|
|
8616
|
+
}
|
|
8617
|
+
if (outcome === "failed") {
|
|
8618
|
+
if (sent.length > 0) {
|
|
8619
|
+
console.log(chalk.yellow(`\nPushed before it stopped: ${sent.map((done) => done.label).join("; ")}.`));
|
|
8620
|
+
await recordPush(root, connection, space, {
|
|
8621
|
+
origin,
|
|
8622
|
+
source,
|
|
8623
|
+
found,
|
|
8624
|
+
sent: settled,
|
|
8625
|
+
draft
|
|
8626
|
+
});
|
|
8627
|
+
}
|
|
8628
|
+
return;
|
|
8629
|
+
}
|
|
8630
|
+
if (outcome === "pushed") sent.push(item);
|
|
8631
|
+
else unchanged.push(item.label);
|
|
8632
|
+
if (outcome === "pushed" || item.part === "space") settled.push(item);
|
|
8633
|
+
}
|
|
8634
|
+
if (settled.length > 0) await recordPush(root, connection, space, {
|
|
8635
|
+
origin,
|
|
8636
|
+
source,
|
|
8637
|
+
found,
|
|
8638
|
+
sent: settled,
|
|
8639
|
+
draft
|
|
8640
|
+
});
|
|
8641
|
+
if (unchanged.length > 0) console.log(chalk.dim(`Already ${space.name}’s: ${unchanged.join("; ")}.`));
|
|
8642
|
+
if (sent.length > 0) console.log(chalk.green(`\nPushed to ${space.name}’s draft.`) + chalk.dim(" Publish it in the builder to take it live."));
|
|
8643
|
+
};
|
|
8644
|
+
//#endregion
|
|
8645
|
+
//#region ../../packages/sdk-shared/dist/schema/motion.mjs
|
|
8646
|
+
var t = [
|
|
8647
|
+
"fade",
|
|
8648
|
+
"fade-up",
|
|
8649
|
+
"fade-down",
|
|
8650
|
+
"slide-left",
|
|
8651
|
+
"slide-right",
|
|
8652
|
+
"slide-up",
|
|
8653
|
+
"scale",
|
|
8654
|
+
"zoom-in",
|
|
8655
|
+
"zoom-out",
|
|
8656
|
+
"pop",
|
|
8657
|
+
"bounce-in",
|
|
8658
|
+
"tilt",
|
|
8659
|
+
"spin-in"
|
|
8660
|
+
];
|
|
8661
|
+
var o = {
|
|
8662
|
+
fade: [{
|
|
8663
|
+
at: 0,
|
|
8664
|
+
opacity: 0
|
|
8665
|
+
}],
|
|
8666
|
+
"fade-up": [{
|
|
8667
|
+
at: 0,
|
|
8668
|
+
opacity: 0,
|
|
8669
|
+
translate: "0 24px"
|
|
8670
|
+
}],
|
|
8671
|
+
"fade-down": [{
|
|
8672
|
+
at: 0,
|
|
8673
|
+
opacity: 0,
|
|
8674
|
+
translate: "0 -24px"
|
|
8675
|
+
}],
|
|
8676
|
+
"slide-left": [{
|
|
8677
|
+
at: 0,
|
|
8678
|
+
opacity: 0,
|
|
8679
|
+
translate: "32px 0"
|
|
8680
|
+
}],
|
|
8681
|
+
"slide-right": [{
|
|
8682
|
+
at: 0,
|
|
8683
|
+
opacity: 0,
|
|
8684
|
+
translate: "-32px 0"
|
|
8685
|
+
}],
|
|
8686
|
+
"slide-up": [{
|
|
8687
|
+
at: 0,
|
|
8688
|
+
opacity: 0,
|
|
8689
|
+
translate: "0 72px"
|
|
8690
|
+
}],
|
|
8691
|
+
scale: [{
|
|
8692
|
+
at: 0,
|
|
8693
|
+
opacity: 0,
|
|
8694
|
+
scale: "0.94"
|
|
8695
|
+
}],
|
|
8696
|
+
"zoom-in": [{
|
|
8697
|
+
at: 0,
|
|
8698
|
+
opacity: 0,
|
|
8699
|
+
scale: "0.6"
|
|
8700
|
+
}],
|
|
8701
|
+
"zoom-out": [{
|
|
8702
|
+
at: 0,
|
|
8703
|
+
opacity: 0,
|
|
8704
|
+
scale: "1.25"
|
|
8705
|
+
}],
|
|
8706
|
+
pop: [
|
|
8707
|
+
{
|
|
8708
|
+
at: 0,
|
|
8709
|
+
opacity: 0,
|
|
8710
|
+
scale: "0.5"
|
|
8711
|
+
},
|
|
8712
|
+
{
|
|
8713
|
+
at: 55,
|
|
8714
|
+
opacity: 1,
|
|
8715
|
+
scale: "1.08"
|
|
8716
|
+
},
|
|
8717
|
+
{
|
|
8718
|
+
at: 78,
|
|
8719
|
+
scale: "0.97"
|
|
8720
|
+
}
|
|
8721
|
+
],
|
|
8722
|
+
"bounce-in": [
|
|
8723
|
+
{
|
|
8724
|
+
at: 0,
|
|
8725
|
+
opacity: 0,
|
|
8726
|
+
translate: "0 -64px"
|
|
8727
|
+
},
|
|
8728
|
+
{
|
|
8729
|
+
at: 55,
|
|
8730
|
+
opacity: 1,
|
|
8731
|
+
translate: "0 10px"
|
|
8732
|
+
},
|
|
8733
|
+
{
|
|
8734
|
+
at: 75,
|
|
8735
|
+
translate: "0 -5px"
|
|
8736
|
+
},
|
|
8737
|
+
{
|
|
8738
|
+
at: 90,
|
|
8739
|
+
translate: "0 2px"
|
|
8740
|
+
}
|
|
8741
|
+
],
|
|
8742
|
+
tilt: [{
|
|
8743
|
+
at: 0,
|
|
8744
|
+
opacity: 0,
|
|
8745
|
+
translate: "0 32px",
|
|
8746
|
+
rotate: "-6deg"
|
|
8747
|
+
}],
|
|
8748
|
+
"spin-in": [{
|
|
8749
|
+
at: 0,
|
|
8750
|
+
opacity: 0,
|
|
8751
|
+
scale: "0.4",
|
|
8752
|
+
rotate: "-180deg"
|
|
8753
|
+
}]
|
|
8754
|
+
};
|
|
8755
|
+
Object.fromEntries(t.map((e) => {
|
|
8756
|
+
let [{ at: t, ...n }] = o[e];
|
|
8757
|
+
return [e, n];
|
|
8758
|
+
}));
|
|
8759
|
+
var c = (e, t) => {
|
|
8760
|
+
let n = t * 2 * Math.PI, r = Math.round(Math.sin(n) * e * 10) / 10, i = Math.round(-Math.cos(n) * e * 10) / 10;
|
|
8761
|
+
return `translate(${String(r)}px, ${String(i)}px)`;
|
|
8762
|
+
};
|
|
8763
|
+
Array.from({ length: 9 }, (e, t) => ({
|
|
8764
|
+
at: t * 100 / 8,
|
|
8765
|
+
transform: c(6, t / 8)
|
|
8766
|
+
}));
|
|
8767
|
+
var _ = `[data-motion-enter], [data-motion-stagger] > *, [data-motion-loop] { animation: none !important; }`;
|
|
8768
|
+
[
|
|
8769
|
+
"[data-motion-loop] { animation-play-state: running, running !important; }",
|
|
8770
|
+
"[data-motion-on=\"view\"][data-motion-enter], [data-motion-on=\"view\"][data-motion-stagger] > * { animation-play-state: running, running !important; }",
|
|
8771
|
+
"[data-motion-on=\"scroll\"][data-motion-enter], [data-motion-on=\"scroll\"][data-motion-stagger] > * { animation-timeline: auto, auto !important; }"
|
|
8772
|
+
].join("\n");
|
|
8773
|
+
//#endregion
|
|
8774
|
+
//#region src/commands/shot.ts
|
|
8775
|
+
/** A region moves when more than this share of it changed between two frames: below is anti-aliasing and carets. */
|
|
8776
|
+
var MOVING_PERCENT = .5;
|
|
8777
|
+
/** How long a whole page waits for its pictures before it is taken anyway. */
|
|
8778
|
+
var IMAGES_BUDGET_MS = 8e3;
|
|
8779
|
+
/** How many texts that differ are written out; `--json` has every one. */
|
|
8780
|
+
var TEXTS_SHOWN = 12;
|
|
8781
|
+
/** A shift within this is where it was: a pixel or two is rounding. */
|
|
8782
|
+
var DRIFT_PX = 2;
|
|
8783
|
+
/** An arrival that waits for the scroll and has not had it: one waiting to be seen, or one tied to the scroll. */
|
|
8784
|
+
var WAITING_FOR_SCROLL = "[data-motion-on=\"view\"]:not([data-motion-seen]), [data-motion-on=\"scroll\"]";
|
|
8785
|
+
/** A selector, or an element's name — what `data-plitzi-el` carries. */
|
|
8786
|
+
var selectorOf = (target) => /^[#.[]/.test(target) ? target : `[data-plitzi-el="${target}"]`;
|
|
8787
|
+
var framing = (view) => {
|
|
8788
|
+
if (view.clip) return {
|
|
8789
|
+
kind: "element",
|
|
8790
|
+
selector: selectorOf(view.clip)
|
|
8791
|
+
};
|
|
8792
|
+
return view.viewport || view.scrollTo ? { kind: "viewport" } : { kind: "page" };
|
|
8793
|
+
};
|
|
8794
|
+
var capture = (page, how) => {
|
|
8795
|
+
if (how.kind === "element") return page.locator(how.selector).first().screenshot();
|
|
8796
|
+
return page.screenshot({ fullPage: how.kind === "page" });
|
|
8797
|
+
};
|
|
8798
|
+
/**
|
|
8799
|
+
* One page, pictured: the project's own (`origin` given — in the space's theme when one is asked for) or another site,
|
|
8800
|
+
* in the scheme it is told.
|
|
8801
|
+
*/
|
|
8802
|
+
var pictureOf = async (browser, url, view, origin) => {
|
|
8803
|
+
const page = origin ? await openProjectPage(browser, origin, view) : await browser.newPage({
|
|
8804
|
+
viewport: {
|
|
8805
|
+
width: view.width,
|
|
8806
|
+
height: view.height
|
|
8807
|
+
},
|
|
8808
|
+
colorScheme: view.scheme ?? "light",
|
|
8809
|
+
reducedMotion: view.reducedMotion ? "reduce" : "no-preference"
|
|
8810
|
+
});
|
|
8811
|
+
if (!await page.goto(url, { waitUntil: "networkidle" }).catch(() => null)) return { problem: `Nothing answers at ${url}.` };
|
|
8812
|
+
if (view.waitFor) {
|
|
8813
|
+
if (!await page.waitForSelector(selectorOf(view.waitFor), { timeout: 1e4 }).catch(() => null)) return { problem: `${view.waitFor} did not appear on ${url} within 10 s.` };
|
|
8814
|
+
}
|
|
8815
|
+
for (const target of [view.scrollTo, view.clip]) if (target && !await page.waitForSelector(selectorOf(target), { timeout: 1e4 }).catch(() => null)) return { problem: `${target} is not on ${url}.` };
|
|
8816
|
+
if (view.scrollTo) await page.locator(selectorOf(view.scrollTo)).first().scrollIntoViewIfNeeded();
|
|
8817
|
+
await withoutDevTools(page);
|
|
8818
|
+
const settled = view.still ? await page.evaluate((selector) => document.querySelectorAll(selector).length, WAITING_FOR_SCROLL) : 0;
|
|
8819
|
+
const whole = framing(view).kind === "page";
|
|
8820
|
+
const pictures = whole ? await page.evaluate(loadImages, IMAGES_BUDGET_MS) : void 0;
|
|
8821
|
+
if (whole) await page.evaluate(unrollPage, void 0);
|
|
8822
|
+
if (view.still) await page.addStyleTag({ content: _ });
|
|
8823
|
+
return {
|
|
8824
|
+
page,
|
|
8825
|
+
png: await capture(page, framing(view)),
|
|
8826
|
+
scheme: await paintedScheme(page, view.scheme ?? "light"),
|
|
8827
|
+
settled,
|
|
8828
|
+
unloaded: pictures?.missing ?? 0
|
|
8829
|
+
};
|
|
8830
|
+
};
|
|
8831
|
+
var shiftText = (shift) => shift !== void 0 && Math.abs(shift) > DRIFT_PX ? ` (${shift > 0 ? "+" : ""}${String(shift)}px there)` : "";
|
|
8832
|
+
var regionsText = (regions) => regions.map((region) => `${region.name} ${String(region.changed)}%${shiftText(region.shift)}`).join(", ");
|
|
8833
|
+
/** Compared frame to frame: the most each region changed between two that follow each other. */
|
|
8834
|
+
var framesMoved = (diffs) => {
|
|
8835
|
+
const most = /* @__PURE__ */ new Map();
|
|
8836
|
+
for (const diff of diffs) for (const region of diff.regions) most.set(region.name, Math.max(most.get(region.name) ?? 0, region.changed));
|
|
8837
|
+
return [...most].map(([name, changed]) => ({
|
|
8838
|
+
name,
|
|
8839
|
+
changed
|
|
8840
|
+
}));
|
|
8841
|
+
};
|
|
8842
|
+
var differenceText = (entry) => ` ${entry.tag} ${JSON.stringify(entry.text.length > 48 ? `${entry.text.slice(0, 47)}…` : entry.text)} — ${entry.differences.join(" · ")}`;
|
|
8843
|
+
var someOf = (texts) => {
|
|
8844
|
+
return `${texts.slice(0, 3).map((text) => JSON.stringify(text.length > 32 ? `${text.slice(0, 31)}…` : text)).join(", ")}${texts.length > 3 ? ` (+${String(texts.length - 3)})` : ""}`;
|
|
8845
|
+
};
|
|
8846
|
+
/** What a comparison found, in the lines an agent reads before it opens a picture. */
|
|
8847
|
+
var compareText = (compare) => {
|
|
8848
|
+
const lines = [`${chalk.bold(`${String(compare.changed)}% differs`)} from ${compare.url}${compare.regions.length > 0 ? ` — ${regionsText(compare.regions)}` : ""}`];
|
|
8849
|
+
const longer = compare.heights.there - compare.heights.here;
|
|
8850
|
+
const drift = compare.driftFrom;
|
|
8851
|
+
const placed = [Math.abs(longer) > DRIFT_PX ? `the page there is ${String(Math.abs(longer))}px ${longer > 0 ? "longer" : "shorter"} (${String(compare.heights.here)} → ${String(compare.heights.there)})` : "", drift ? `from ${drift.name} on, it sits ${String(Math.abs(drift.shift))}px ${drift.shift > 0 ? "lower" : "higher"} there — each section is compared where it is` : ""].filter(Boolean);
|
|
8852
|
+
if (placed.length > 0) lines.push(placed.join("; "));
|
|
8853
|
+
const { texts } = compare;
|
|
8854
|
+
if (texts.differ.length > 0) lines.push(`texts: ${String(texts.same)} the same, ${String(texts.differ.length)} differ (here → there):`, ...texts.differ.slice(0, TEXTS_SHOWN).map(differenceText), ...texts.differ.length > TEXTS_SHOWN ? [chalk.dim(` …and ${String(texts.differ.length - TEXTS_SHOWN)} more: --json lists them`)] : []);
|
|
8855
|
+
else if (texts.same > 0) lines.push(`texts: the ${String(texts.same)} both pages have look the same`);
|
|
8856
|
+
if (texts.onlyHere.length > 0 || texts.onlyThere.length > 0) lines.push(chalk.dim([texts.onlyHere.length > 0 ? `only here: ${someOf(texts.onlyHere)}` : "", texts.onlyThere.length > 0 ? `only there: ${someOf(texts.onlyThere)}` : ""].filter(Boolean).join(" · ")));
|
|
8857
|
+
if (compare.unloaded > 0) lines.push(chalk.yellow(`${String(compare.unloaded)} picture(s) there had not loaded when it was taken: those parts compare against a hole`));
|
|
8858
|
+
lines.push(chalk.dim(`${compare.sideBySide}\n${compare.diff}`));
|
|
8859
|
+
return lines;
|
|
8860
|
+
};
|
|
8861
|
+
/** The other site beside this page: section by section where each one is, and the words both have, measured. */
|
|
8862
|
+
var compareWith = async (browser, taken, target, view, regions, base) => {
|
|
8863
|
+
const theirs = await pictureOf(browser, target, {
|
|
8864
|
+
...view,
|
|
8865
|
+
scheme: taken.scheme
|
|
8866
|
+
});
|
|
8867
|
+
if ("problem" in theirs) return theirs;
|
|
8868
|
+
const diff = await comparePictures(taken.page, dataUrl(taken.png), dataUrl(theirs.png), {
|
|
8869
|
+
regions,
|
|
8870
|
+
align: true
|
|
8871
|
+
});
|
|
8872
|
+
await writeFile(`${base}-compare.png`, fromDataUrl(diff.sideBySide));
|
|
8873
|
+
await writeFile(`${base}-diff.png`, fromDataUrl(diff.diff));
|
|
8874
|
+
const rowShift = diff.rowShift ?? [];
|
|
8875
|
+
const shiftAt = (y) => rowShift[Math.min(Math.max(0, Math.round(y)), rowShift.length - 1)] ?? 0;
|
|
8876
|
+
const texts = compareTexts(await taken.page.evaluate(pageTexts, PAGE_TEXT_LIMIT), await theirs.page.evaluate(pageTexts, PAGE_TEXT_LIMIT), shiftAt);
|
|
8877
|
+
return {
|
|
8878
|
+
url: target,
|
|
8879
|
+
changed: diff.changed,
|
|
8880
|
+
heights: {
|
|
8881
|
+
here: diff.heights.a,
|
|
8882
|
+
there: diff.heights.b
|
|
8883
|
+
},
|
|
8884
|
+
...diff.driftFrom ? { driftFrom: diff.driftFrom } : {},
|
|
8885
|
+
regions: diff.regions,
|
|
8886
|
+
texts,
|
|
8887
|
+
unloaded: theirs.unloaded,
|
|
8888
|
+
sideBySide: `${base}-compare.png`,
|
|
8889
|
+
diff: `${base}-diff.png`
|
|
8890
|
+
};
|
|
8891
|
+
};
|
|
8892
|
+
var shot = async (route, options) => {
|
|
8893
|
+
const project = await projectHere("whose page to picture");
|
|
8894
|
+
if (!project) return;
|
|
7696
8895
|
const where = await projectOrigin(project.root, project.plitzi?.kind === "project" ? project.plitzi : void 0);
|
|
7697
8896
|
if ("problem" in where) {
|
|
7698
8897
|
fail(where.problem);
|
|
@@ -7713,49 +8912,42 @@ var shot = async (route, options) => {
|
|
|
7713
8912
|
const view = {
|
|
7714
8913
|
width: options.width ?? 1280,
|
|
7715
8914
|
height: options.height ?? 800,
|
|
7716
|
-
scheme: options.scheme
|
|
8915
|
+
...options.scheme ? { scheme: options.scheme } : {},
|
|
7717
8916
|
reducedMotion: options.reducedMotion === true,
|
|
7718
8917
|
...options.waitFor ? { waitFor: options.waitFor } : {},
|
|
7719
8918
|
...options.clip ? { clip: options.clip } : {},
|
|
7720
8919
|
...options.scrollTo ? { scrollTo: options.scrollTo } : {},
|
|
7721
|
-
...options.viewport ? { viewport: true } : {}
|
|
8920
|
+
...options.viewport ? { viewport: true } : {},
|
|
8921
|
+
still: !options.clip && !options.scrollTo && !options.viewport && (options.frames ?? 0) <= 1
|
|
7722
8922
|
};
|
|
7723
8923
|
const how = framing(view);
|
|
7724
|
-
const
|
|
7725
|
-
const out = path.resolve(project.root, options.out ?? `visual/.shots/${name}-${String(view.width)}-${view.scheme}.png`);
|
|
7726
|
-
const base = out.replace(/\.png$/, "");
|
|
7727
|
-
const taken = await pictureOf(browser, `${where.origin}${pathname}`, view);
|
|
8924
|
+
const taken = await pictureOf(browser, `${where.origin}${pathname}`, view, where.origin);
|
|
7728
8925
|
if ("problem" in taken) {
|
|
7729
8926
|
fail(`${taken.problem} Start the project first: npm start`);
|
|
7730
8927
|
return;
|
|
7731
8928
|
}
|
|
8929
|
+
const name = pathname === "/" ? "home" : pathname.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "");
|
|
8930
|
+
const out = path.resolve(project.root, options.out ?? `tmp/shots/${name}-${String(view.width)}-${taken.scheme}.png`);
|
|
8931
|
+
const base = out.replace(/\.png$/, "");
|
|
7732
8932
|
await mkdir(path.dirname(out), { recursive: true });
|
|
7733
8933
|
await writeFile(out, taken.png);
|
|
7734
8934
|
const regions = how.kind === "page" ? await taken.page.evaluate(pageRegions, void 0) : [];
|
|
7735
8935
|
const report = {
|
|
7736
8936
|
path: pathname,
|
|
7737
8937
|
width: view.width,
|
|
7738
|
-
scheme:
|
|
7739
|
-
out
|
|
8938
|
+
scheme: taken.scheme,
|
|
8939
|
+
out,
|
|
8940
|
+
...taken.settled > 0 ? { settled: taken.settled } : {},
|
|
8941
|
+
...taken.unloaded > 0 ? { unloaded: taken.unloaded } : {}
|
|
7740
8942
|
};
|
|
7741
8943
|
if (options.compare) {
|
|
7742
8944
|
const other = new URL(options.compare);
|
|
7743
|
-
const
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
fail(theirs.problem);
|
|
8945
|
+
const compared = await compareWith(browser, taken, other.pathname === "/" ? new URL(pathname, other).href : other.href, view, regions, base);
|
|
8946
|
+
if ("problem" in compared) {
|
|
8947
|
+
fail(compared.problem);
|
|
7747
8948
|
return;
|
|
7748
8949
|
}
|
|
7749
|
-
|
|
7750
|
-
await writeFile(`${base}-compare.png`, fromDataUrl(diff.sideBySide));
|
|
7751
|
-
await writeFile(`${base}-diff.png`, fromDataUrl(diff.diff));
|
|
7752
|
-
report.compare = {
|
|
7753
|
-
url: target,
|
|
7754
|
-
changed: diff.changed,
|
|
7755
|
-
regions: diff.regions,
|
|
7756
|
-
sideBySide: `${base}-compare.png`,
|
|
7757
|
-
diff: `${base}-diff.png`
|
|
7758
|
-
};
|
|
8950
|
+
report.compare = compared;
|
|
7759
8951
|
}
|
|
7760
8952
|
const frames = options.frames ?? 0;
|
|
7761
8953
|
if (frames > 1) {
|
|
@@ -7766,7 +8958,7 @@ var shot = async (route, options) => {
|
|
|
7766
8958
|
pictures.push(await capture(taken.page, how));
|
|
7767
8959
|
}
|
|
7768
8960
|
const diffs = [];
|
|
7769
|
-
for (let frame = 1; frame < pictures.length; frame += 1) diffs.push(await comparePictures(taken.page, dataUrl(pictures[frame - 1]), dataUrl(pictures[frame]), regions));
|
|
8961
|
+
for (let frame = 1; frame < pictures.length; frame += 1) diffs.push(await comparePictures(taken.page, dataUrl(pictures[frame - 1]), dataUrl(pictures[frame]), { regions }));
|
|
7770
8962
|
const whole = how.kind === "element" ? options.clip ?? "element" : "viewport";
|
|
7771
8963
|
const moved = how.kind === "page" ? framesMoved(diffs) : [{
|
|
7772
8964
|
name: whole,
|
|
@@ -7784,10 +8976,9 @@ var shot = async (route, options) => {
|
|
|
7784
8976
|
return;
|
|
7785
8977
|
}
|
|
7786
8978
|
console.log(out);
|
|
7787
|
-
if (report.
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
}
|
|
8979
|
+
if (report.settled) console.log(chalk.dim(`${String(report.settled)} arrival${report.settled === 1 ? "" : "s"} waiting for the scroll shown as they end — --scroll-to <id> --viewport --frames 4 shows one arriving`));
|
|
8980
|
+
if (report.unloaded) console.log(chalk.yellow(`${String(report.unloaded)} picture(s) had not loaded when the page was taken`));
|
|
8981
|
+
if (report.compare) console.log(compareText(report.compare).join("\n"));
|
|
7791
8982
|
if (report.frames) console.log(report.frames.moving.length > 0 ? `moving over ${String(report.frames.count)} frames: ${regionsText(report.frames.moving)}` : `nothing moved over ${String(report.frames.count)} frames, ${String(report.frames.every)} ms apart`);
|
|
7792
8983
|
} finally {
|
|
7793
8984
|
await browser.close();
|
|
@@ -8138,187 +9329,6 @@ var upgrade = async (parts, options) => {
|
|
|
8138
9329
|
}
|
|
8139
9330
|
};
|
|
8140
9331
|
//#endregion
|
|
8141
|
-
//#region src/commands/uploadPlugin.ts
|
|
8142
|
-
/** The zips `plitzi pack plugin` leaves: at a plugin package's root, or under `dist/plugins` of any other project. */
|
|
8143
|
-
var packedZips = async (root) => {
|
|
8144
|
-
const inFolder = async (dir) => {
|
|
8145
|
-
try {
|
|
8146
|
-
return (await fs.readdir(dir)).filter((file) => file.endsWith(".zip")).map((file) => path.join(dir, file));
|
|
8147
|
-
} catch {
|
|
8148
|
-
return [];
|
|
8149
|
-
}
|
|
8150
|
-
};
|
|
8151
|
-
const zips = [...await inFolder(root), ...await inFolder(path.join(root, "dist/plugins"))];
|
|
8152
|
-
const modified = await Promise.all(zips.map(async (zip) => (await fs.stat(zip)).mtimeMs));
|
|
8153
|
-
return zips.map((zip, index) => ({
|
|
8154
|
-
zip,
|
|
8155
|
-
at: modified[index]
|
|
8156
|
-
})).sort((a, b) => b.at - a.at).map(({ zip }) => zip);
|
|
8157
|
-
};
|
|
8158
|
-
/** The zip named, or the one packed here — asked for at a terminal when there are several. */
|
|
8159
|
-
var chooseZip = async (given) => {
|
|
8160
|
-
if (given) return path.resolve(given);
|
|
8161
|
-
const project = await findProject(process.cwd());
|
|
8162
|
-
const zips = project ? await packedZips(project.root) : [];
|
|
8163
|
-
if (zips.length === 0) {
|
|
8164
|
-
fail("No packed plugin here. Build one with plitzi pack plugin, or name it: plitzi upload plugin <file.zip>.");
|
|
8165
|
-
return;
|
|
8166
|
-
}
|
|
8167
|
-
if (zips.length === 1) return zips[0];
|
|
8168
|
-
const choices = zips.map((zip) => path.relative(process.cwd(), zip));
|
|
8169
|
-
if (!atTerminal()) {
|
|
8170
|
-
refuseWithoutTerminal("upload", [{
|
|
8171
|
-
flag: "<zip> (the argument)",
|
|
8172
|
-
choices,
|
|
8173
|
-
question: "Which packed plugin is to be uploaded?"
|
|
8174
|
-
}], "plitzi upload plugin stopped before uploading anything: which plugin goes to the space is a choice");
|
|
8175
|
-
return;
|
|
8176
|
-
}
|
|
8177
|
-
const rl = readline.createInterface({
|
|
8178
|
-
input: process.stdin,
|
|
8179
|
-
output: process.stdout
|
|
8180
|
-
});
|
|
8181
|
-
try {
|
|
8182
|
-
return await askPick(rl, "Which packed plugin? The newest is first.", zips.map((zip, index) => ({
|
|
8183
|
-
label: choices[index],
|
|
8184
|
-
value: zip
|
|
8185
|
-
})));
|
|
8186
|
-
} finally {
|
|
8187
|
-
rl.close();
|
|
8188
|
-
}
|
|
8189
|
-
};
|
|
8190
|
-
/**
|
|
8191
|
-
* The manifest a zip carries at its root, read here before anything is sent: a zip that is not a plugin is refused by
|
|
8192
|
-
* the platform too, but only after it has travelled.
|
|
8193
|
-
*/
|
|
8194
|
-
var manifestOf = (zip) => {
|
|
8195
|
-
try {
|
|
8196
|
-
const entry = Object.values(unzipSync(zip, { filter: (file) => file.name === "plugin-manifest.json" })).at(0);
|
|
8197
|
-
const parsed = entry ? JSON.parse(new TextDecoder().decode(entry)) : {};
|
|
8198
|
-
return typeof parsed.root === "string" && parsed.root ? {
|
|
8199
|
-
root: parsed.root,
|
|
8200
|
-
version: typeof parsed.version === "string" ? parsed.version : ""
|
|
8201
|
-
} : void 0;
|
|
8202
|
-
} catch {
|
|
8203
|
-
return;
|
|
8204
|
-
}
|
|
8205
|
-
};
|
|
8206
|
-
/**
|
|
8207
|
-
* The public bucket a plugin goes in — the one `--cdn` and `--bucket` name, the only one there is, or the one picked.
|
|
8208
|
-
* A private bucket keeps server code and has no public address, so it is never one.
|
|
8209
|
-
*/
|
|
8210
|
-
var chooseTarget = async (cdns, { cdn: givenCdn, bucket: givenBucket }, spaceName) => {
|
|
8211
|
-
if (givenCdn && !cdns.some((cdn) => cdn.identifier === givenCdn)) {
|
|
8212
|
-
fail(`${spaceName} has no CDN "${givenCdn}". Its CDNs: ${cdns.map((cdn) => cdn.identifier).join(", ")}.`);
|
|
8213
|
-
return;
|
|
8214
|
-
}
|
|
8215
|
-
const candidates = cdns.filter((cdn) => !givenCdn || cdn.identifier === givenCdn).flatMap((cdn) => cdn.buckets.map((bucket) => ({
|
|
8216
|
-
cdn,
|
|
8217
|
-
bucket
|
|
8218
|
-
})));
|
|
8219
|
-
if (givenBucket) {
|
|
8220
|
-
const named = candidates.find(({ bucket }) => bucket.identifier === givenBucket);
|
|
8221
|
-
if (!named) {
|
|
8222
|
-
fail(`${spaceName} has no bucket "${givenBucket}"${givenCdn ? ` in the CDN "${givenCdn}"` : ""}.`);
|
|
8223
|
-
return;
|
|
8224
|
-
}
|
|
8225
|
-
if (named.bucket.visibility === "private") {
|
|
8226
|
-
fail(`"${givenBucket}" is a private bucket: it has no public address, so no page could load a plugin from it.`);
|
|
8227
|
-
return;
|
|
8228
|
-
}
|
|
8229
|
-
return named;
|
|
8230
|
-
}
|
|
8231
|
-
const targets = candidates.filter(({ bucket }) => bucket.visibility === "public");
|
|
8232
|
-
if (targets.length === 0) {
|
|
8233
|
-
fail(`${spaceName} has no public bucket to put a plugin in. Add one to a CDN in the builder, under Resources.`);
|
|
8234
|
-
return;
|
|
8235
|
-
}
|
|
8236
|
-
if (targets.length === 1) return targets[0];
|
|
8237
|
-
if (!atTerminal()) {
|
|
8238
|
-
refuseWithoutTerminal("upload", [{
|
|
8239
|
-
flag: "--bucket",
|
|
8240
|
-
choices: targets.map(({ bucket }) => bucket.identifier),
|
|
8241
|
-
question: `Which of ${spaceName}'s public buckets does the plugin go in? (narrow with --cdn)`
|
|
8242
|
-
}], "plitzi upload plugin stopped before uploading anything: which bucket serves the plugin is a choice");
|
|
8243
|
-
return;
|
|
8244
|
-
}
|
|
8245
|
-
const rl = readline.createInterface({
|
|
8246
|
-
input: process.stdin,
|
|
8247
|
-
output: process.stdout
|
|
8248
|
-
});
|
|
8249
|
-
try {
|
|
8250
|
-
return await askPick(rl, `Which of ${spaceName}'s public buckets does the plugin go in?`, targets.map((target) => ({
|
|
8251
|
-
label: `${target.cdn.name} — ${target.bucket.name} ${chalk.dim(`${target.bucket.identifier} · ${target.bucket.domain}`)}`,
|
|
8252
|
-
value: target
|
|
8253
|
-
})));
|
|
8254
|
-
} finally {
|
|
8255
|
-
rl.close();
|
|
8256
|
-
}
|
|
8257
|
-
};
|
|
8258
|
-
var uploadPluginCommand = async (zipGiven, options) => {
|
|
8259
|
-
const api = await apiFor(options);
|
|
8260
|
-
const zipPath = api ? await chooseZip(zipGiven) : void 0;
|
|
8261
|
-
if (!api || !zipPath) return;
|
|
8262
|
-
let zip;
|
|
8263
|
-
try {
|
|
8264
|
-
zip = await fs.readFile(zipPath);
|
|
8265
|
-
} catch {
|
|
8266
|
-
fail(`Could not read ${zipPath}.`);
|
|
8267
|
-
return;
|
|
8268
|
-
}
|
|
8269
|
-
const manifest = manifestOf(zip);
|
|
8270
|
-
if (!manifest) {
|
|
8271
|
-
fail(`${path.basename(zipPath)} is not a plugin: it has no plugin-manifest.json at its root. Build it with plitzi pack plugin.`);
|
|
8272
|
-
return;
|
|
8273
|
-
}
|
|
8274
|
-
const connection = await connectionWithSpace(api, "to upload to");
|
|
8275
|
-
if (!connection?.space) return;
|
|
8276
|
-
const { space } = connection;
|
|
8277
|
-
const listed = await authorizedRequest(connection, `/spaces/${space.id}/cdns`);
|
|
8278
|
-
if (!listed.ok) {
|
|
8279
|
-
fail(listed.error);
|
|
8280
|
-
return;
|
|
8281
|
-
}
|
|
8282
|
-
if (listed.value.reply.status !== 200) {
|
|
8283
|
-
fail(listed.value.reply.data.error ?? `Could not list ${space.name}'s CDNs (${listed.value.reply.status}).`);
|
|
8284
|
-
return;
|
|
8285
|
-
}
|
|
8286
|
-
const target = await chooseTarget(listed.value.reply.data.cdns ?? [], options, space.name);
|
|
8287
|
-
if (!target) return;
|
|
8288
|
-
const { cdn, bucket } = target;
|
|
8289
|
-
const label = `${manifest.root}${manifest.version ? ` ${manifest.version}` : ""}`;
|
|
8290
|
-
console.log(`\nUploading ${chalk.bold(label)} to ${chalk.bold(space.name)}, in ${cdn.name} — ${bucket.name}…`);
|
|
8291
|
-
const query = new URLSearchParams({
|
|
8292
|
-
filename: path.basename(zipPath),
|
|
8293
|
-
bucket: bucket.identifier
|
|
8294
|
-
});
|
|
8295
|
-
const uploaded = await authorizedRequest(listed.value.connection, `/spaces/${space.id}/cdns/${encodeURIComponent(cdn.identifier)}/plugins?${query}`, {
|
|
8296
|
-
method: "POST",
|
|
8297
|
-
headers: { "Content-Type": "application/zip" },
|
|
8298
|
-
body: new Uint8Array(zip)
|
|
8299
|
-
});
|
|
8300
|
-
if (!uploaded.ok) {
|
|
8301
|
-
fail(uploaded.error);
|
|
8302
|
-
return;
|
|
8303
|
-
}
|
|
8304
|
-
const { status, data } = uploaded.value.reply;
|
|
8305
|
-
if (status === 413) {
|
|
8306
|
-
fail(`${path.basename(zipPath)} is larger than ${api} accepts.`);
|
|
8307
|
-
return;
|
|
8308
|
-
}
|
|
8309
|
-
if (status !== 201) {
|
|
8310
|
-
fail(data.error ?? `The upload was refused (${status}).`);
|
|
8311
|
-
return;
|
|
8312
|
-
}
|
|
8313
|
-
const installed = data.installed === "updated" ? `now loads ${label}` : `loads ${label} from now on`;
|
|
8314
|
-
console.log(chalk.green(`\n${space.name} ${installed}.`));
|
|
8315
|
-
if (data.resource?.path) console.log(chalk.dim(` ${data.resource.path}`));
|
|
8316
|
-
console.log(chalk.dim(" Any builder open on the space loads it now."));
|
|
8317
|
-
const source = await fs.readFile(sourceFileOf(zipPath)).catch(() => void 0);
|
|
8318
|
-
if (source) await keepSource(uploaded.value.connection, space.id, source);
|
|
8319
|
-
console.log("");
|
|
8320
|
-
};
|
|
8321
|
-
//#endregion
|
|
8322
9332
|
//#region src/options.ts
|
|
8323
9333
|
/**
|
|
8324
9334
|
* What a flag's value has to be, read once where the command line is declared. A value that is not one is refused
|
|
@@ -8362,7 +9372,7 @@ var PROJECT_ONLY = [
|
|
|
8362
9372
|
];
|
|
8363
9373
|
/** The widths a page is looked at, as one flag every command that opens a page shares. */
|
|
8364
9374
|
var widthsOption = (description, fallback) => new Option("--width <px,px>", description).argParser(widths).default(fallback, fallback.join(","));
|
|
8365
|
-
var schemeOption = () => new Option("--scheme <scheme>", "The
|
|
9375
|
+
var schemeOption = () => new Option("--scheme <scheme>", "The space’s theme, as a visitor’s toggle sets it. The space’s own default when left out").choices(SCHEMES);
|
|
8366
9376
|
var API_OPTION = ["--api <url>", "The platform’s API. Defaults to PLITZI_API_URL, else the one signed in to, else https://api.plitzi.com."];
|
|
8367
9377
|
program.command("create").argument("[directory]", "Where to write it. Defaults to the current directory, or asked for with --plugin.").description("Scaffold a project that renders a Plitzi space, or with --plugin a plugin package").addOption(new Option("-m, --mode <mode>", "server (SSR + RSC on a Node tier) or client (browser only)").choices(["server", "client"])).addOption(new Option("-s, --source <source>", "local (the space travels in the project) or cloud (read it from Plitzi)").choices(["local", "cloud"])).option("-k, --key <key>", "Cloud only: the space key (asked for when omitted)").option("-e, --environment <environment>", "Which version: main (the draft), or a published environment — what --from takes out, and what a cloud project serves", "main").option("--revision <n>", "A published environment’s revision, pinned rather than its latest (with --from or --source cloud)").addOption(new Option("-p, --package-manager <manager>", "The package manager the project is written for. Asked for when left out.").choices([...PACKAGE_MANAGERS])).option("--from <space>", "Make it from a space on Plitzi (its id or permanent URL): its pages, actions, functions, runtime, plugins and files, served by the project alone. Signs in as you; the space must be one you can change.").option(...API_OPTION).option("--plugin", "A plugin package instead: one element any space can load, with a preview and a build").option("--name <name>", "Plugin only: its package name (plitzi-plugin-seat-picker). Asked for when left out.").option("--title <title>", "Plugin only: what the builder calls the element").option("--description <description>", "Plugin only: what the element is for, in a sentence").option("--owner <owner>", "Plugin only: who publishes it").option("--elements <names>", "Plugin only: other elements the package holds, by name, separated by commas").addOption(new Option("-t, --template <template>", "What a space written here starts as: welcome (a tour, the default), blank (tokens, a layout, one empty page) or catalog (a shop: layout, card component, data, filtered list, a page per product)").choices([...CREATE_TEMPLATES])).option("--no-install", "Write the files without installing dependencies").option("-f, --force", "Write into a directory that is not empty").option("-y, --yes", "At a terminal: take the defaults for any choice not passed. Without one, every choice must be passed").action((directory, options) => {
|
|
8368
9378
|
if (!options.plugin) return create(directory, options);
|
|
@@ -8371,6 +9381,7 @@ program.command("create").argument("[directory]", "Where to write it. Defaults t
|
|
|
8371
9381
|
return createPlugin(directory, options);
|
|
8372
9382
|
});
|
|
8373
9383
|
program.command("pull").description("Bring a project made with create --from up to date with its space: what changed there is written, what changed here is kept").option("-f, --force", "Where a file changed both here and on the space, take the space’s copy").option("-e, --environment <environment>", "Follow another version from now on: main (the draft), or a published environment").option("--revision <n>", "Pin a published revision of it, or latest to follow its newest again").option(...API_OPTION).action((options) => pull(options));
|
|
9384
|
+
program.command("push").argument("[parts...]", `Only these: ${PUSH_PARTS.join(", ")}. None named: what changed — ticked at a terminal, to choose from`).description("Put the project back on the space it works with, as its draft: the way back of pull. Refused when the draft changed since").option("-f, --force", "Replace the space’s draft even when it changed since, or holds work this project never had").option("--cdn <identifier>", "Which of the space’s CDNs a plugin goes to, when it has several").option("--bucket <identifier>", "Which public bucket a plugin goes in, when there are several").option(...API_OPTION).action((parts, options) => push(parts, options));
|
|
8374
9385
|
program.command("add").description("Add something to the project you are in").command("plugin").argument("[names...]", "What each element is called: seat-picker. Asked for when left out.").description("Add elements of your own to this project: each one its component, declaration and builder panel").option("-d, --dir <folder>", "The folder that holds the project’s components. Asked for, unless the project says.").option("--title <title>", "One element only: what the builder calls it").option("--description <description>", "One element only: what it is for, in a sentence").option("--prop <name:type=default>", "One element only: an attribute it takes — interval:number=5000, paused:boolean, label:string=Hi. Repeat it", collect, []).option("--trigger <name:fields>", "An event it fires and what a flow reads — onTick:count. Repeat it", collect, []).option("--callback <name>", "An action a flow can call on it — reset. Repeat it", collect, []).option("--headless", "Nothing to see: hidden on a page, a badge in the builder (a clock, a listener)").option("-f, --force", "Write into a folder that is not empty").action((names, options) => addPlugin(names, options));
|
|
8375
9386
|
var pack = program.command("pack").description("Build something of this project into what the platform takes");
|
|
8376
9387
|
pack.command("plugin").argument("[folders...]", "Element folders to pack, the main one first. In a plugin package, left out: all of them.").description("Build a plugin: one ES module, its plugin-manifest.json, and the zip the builder takes under Resources").option("-o, --out <folder>", "Where the build goes. Emptied first; it must be inside the project.").option("--no-zip", "Build without the zip").option("--plugin-version <version>", "The version the manifest carries. Defaults to the one in package.json.").option("--source-root <folder>", "The project its source is kept relative to, when the elements are a project of their own inside this one").action((folders, options) => packPluginCommand(folders, options));
|
|
@@ -8379,15 +9390,15 @@ program.command("login").description("Sign in, in your browser. The session is k
|
|
|
8379
9390
|
program.command("logout").description("Sign out: the session is revoked on the platform and forgotten here").action(() => logout());
|
|
8380
9391
|
program.command("whoami").description("Who the CLI is signed in as, and the space it works in").option("--json", "One object: { api, user, space }, for a tool or an agent").option(...API_OPTION).action((options) => whoami(options));
|
|
8381
9392
|
program.command("space").description("Choose the space to work in, in your browser. One at a time: it replaces the one chosen before").option(...API_OPTION).action((options) => space(options));
|
|
8382
|
-
program.command("check").argument("[path]", "The page: /about. The home page when left out").description("Whether a page of the running project is whole, in text: every element on screen, no broken image, no console error, no failed flow").addOption(widthsOption("The widths to check it at, separated by commas", [1440, 390])).addOption(schemeOption()).option("--state", "Also what the page holds: its state, and every source by name").option("--element <id>", "Also one element: what it reads, its own state, whether it is on screen").option("--json", "One object per width, for a tool or an agent").action((path, options) => check(path, options));
|
|
9393
|
+
program.command("check").argument("[path]", "The page: /about. The home page when left out").description("Whether a page of the running project is whole, in text: every element on screen, no broken image, no console error, no failed flow").addOption(widthsOption("The widths to check it at, separated by commas", [1440, 390])).addOption(schemeOption()).option("--state", "Also what the page holds: its state, and every source by name").option("--element <id>", "Also one element: what it reads, its own state, whether it is on screen").option("--ssr", "Also the HTML the server sent against the hydrated page: what a server provider sent late").option("--json", "One object per width, for a tool or an agent").action((path, options) => check(path, options));
|
|
8383
9394
|
program.command("fix").description("What authoring would fix in the space, as edits to your source: shown as a diff; with --write written, formatted and checked").option("--write", "Write the edits — kept only when the space authors with each fix gone and no problem added").option("--json", "The plan as one object, for a tool or an agent").action((options) => fix(options));
|
|
8384
9395
|
program.command("import").argument("<url>", "The page to start from: https://example.com/pricing").description("A page of a site you verified, as a place to start writing from: its tokens, the outline of its blocks per breakpoint, its lists as JSON — never its words").option("-o, --out <dir>", "Where it is written, inside the project", "src/imported").addOption(widthsOption("The widths it is measured at: the widest is desktop, then tablet and mobile", [
|
|
8385
9396
|
1440,
|
|
8386
9397
|
768,
|
|
8387
9398
|
390
|
|
8388
9399
|
])).option("-f, --force", "Write over what --out already holds").option("--json", "One object, for a tool or an agent").option(...API_OPTION).action((url, options) => importPage(url, options));
|
|
8389
|
-
program.command("shot").argument("[path]", "The page: /about. The home page when left out").description("A picture of one page of the running project — and, asked, how it differs from another or what moves").addOption(new Option("--width <px>", "The viewport width").argParser(width).default(1280)).addOption(new Option("--height <px>", "The viewport height (the picture is the whole page unless --viewport, --scroll-to or --clip)").argParser(positiveInteger).default(800)).addOption(schemeOption()).option("-o, --out <file>", "Where the picture goes.
|
|
8390
|
-
program.command("explain").argument("[name]", "An element, a step, a trigger, a problem’s code or a
|
|
9400
|
+
program.command("shot").argument("[path]", "The page: /about. The home page when left out").description("A picture of one page of the running project — and, asked, how it differs from another or what moves").addOption(new Option("--width <px>", "The viewport width").argParser(width).default(1280)).addOption(new Option("--height <px>", "The viewport height (the picture is the whole page unless --viewport, --scroll-to or --clip)").argParser(positiveInteger).default(800)).addOption(schemeOption()).option("-o, --out <file>", "Where the picture goes. tmp/shots/<page>-<width>-<scheme>.png by default").option("--compare <url>", "Another site: the same page there, side by side, and how much differs by section").addOption(new Option("--frames <n>", "Pictures taken one after another: which sections move (a marquee, an autoplay)").argParser(positiveInteger)).addOption(new Option("--every <ms>", "How far apart --frames are taken").argParser(positiveInteger).default(500)).option("--wait-for <element>", "An element to wait for first: its name (data-plitzi-el) or a CSS selector").option("--clip <element>", "Only this element — its name or a CSS selector — scrolled to wherever it is").option("--scroll-to <element>", "The page scrolled until this element is in view; the picture is the screen there").option("--viewport", "Only what fits the viewport, not the whole page").option("--reduced-motion", "As a visitor who asked for less motion").option("--json", "One object, for a tool or an agent").action((path, options) => shot(path, options));
|
|
9401
|
+
program.command("explain").argument("[name]", "An element, a step, a trigger, a problem’s code, a transformer or a helper: container, navigate, class-and-css, bindTemplate, motion").description("What a name means when authoring a space — what it takes, fires, reads and how it is written").option("--list <kind>", "Every name of a kind: elements, steps, triggers, codes, transformers, helpers").option("--json", "One object, for a tool or an agent").action((name, options) => explainCommand(name, options));
|
|
8391
9402
|
program.command("upgrade").alias("update").argument("[parts...]", `Only these: ${UPGRADE_PARTS.join(", ")} — every one when none is named`).description("Bring the project up to this CLI: its machinery files, package.json, the skills and renamed names — shown; with --write made").option("--write", "Make the changes: files nobody changed replaced, package.json merged and installed, names renamed").option("--take <files...>", "Replace these files of your own too (`all` for every one), after reading their diff").option("--no-install", "Write package.json and leave the install to you").option("--json", "One object, for a tool or an agent").action((parts, options) => upgrade(parts, options));
|
|
8392
9403
|
program.command("skills").description("The Plitzi skills an agent reads in .claude/skills/").command("update").description("Bring them up to the Plitzi packages this project has installed, each replaced whole (`upgrade skills --write`)").action(() => upgrade(["skills"], { write: true }));
|
|
8393
9404
|
program.command("data").description("The JSON a project serves (public/data), without reading it whole").command("describe").argument("<file>", "A JSON file: public/data/products.json").description("Its shape — every field, its type, whether every row has it — and one row of its longest list").option("--json", "One object: { shape, example }").action((file, options) => dataDescribe(file, options));
|