@plitzi/cli 0.37.7 → 0.37.9
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 +253 -0
- package/README.md +64 -2
- package/dist/index.js +1664 -483
- package/package.json +3 -3
- package/skills/plitzi-cli/SKILL.md +37 -5
package/dist/index.js
CHANGED
|
@@ -9,12 +9,13 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
9
9
|
import { createServer } from "node:http";
|
|
10
10
|
import tls from "node:tls";
|
|
11
11
|
import readline from "node:readline/promises";
|
|
12
|
-
import { createRequire } from "node:module";
|
|
12
|
+
import { builtinModules, createRequire } from "node:module";
|
|
13
13
|
import { readFileSync, readdirSync, watch } from "node:fs";
|
|
14
|
-
import { blankSpaceSource, elementTypeNames } from "@plitzi/sdk-authoring";
|
|
14
|
+
import { actionSpecFromEntry, actionToSource, blankSpaceSource, elementTypeNames } from "@plitzi/sdk-authoring";
|
|
15
15
|
import { pathToFileURL } from "node:url";
|
|
16
16
|
import { build } from "esbuild";
|
|
17
17
|
import { unzipSync, zipSync } from "fflate";
|
|
18
|
+
import { gzipSync } from "node:zlib";
|
|
18
19
|
/** `$XDG_CONFIG_HOME/plitzi`, `%APPDATA%\plitzi` on Windows, `~/.config/plitzi` otherwise. */
|
|
19
20
|
var configDir = () => {
|
|
20
21
|
if (process.env.XDG_CONFIG_HOME) return path.join(process.env.XDG_CONFIG_HOME, "plitzi");
|
|
@@ -642,6 +643,22 @@ var connectionWithSpace = async (api, doing = "to work in") => {
|
|
|
642
643
|
}
|
|
643
644
|
return chosen.value;
|
|
644
645
|
};
|
|
646
|
+
/**
|
|
647
|
+
* A signed-in connection, whichever space it works in — or none: signing in in the browser when there is no session.
|
|
648
|
+
* What a command that names its space itself needs (`plitzi create --from`), since the session reaches every space the
|
|
649
|
+
* person can.
|
|
650
|
+
*/
|
|
651
|
+
var signedIn = async (api, doing) => {
|
|
652
|
+
const current = await currentConnection(api);
|
|
653
|
+
if (current.ok && current.value) return current.value;
|
|
654
|
+
console.log(`\nSign in ${doing}.`);
|
|
655
|
+
const connected = await connect$2(api, { open: openInBrowser });
|
|
656
|
+
if (!connected.ok) {
|
|
657
|
+
fail(connected.error);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
return connected.value;
|
|
661
|
+
};
|
|
645
662
|
//#endregion
|
|
646
663
|
//#region src/scaffold/client.ts
|
|
647
664
|
/**
|
|
@@ -1263,13 +1280,21 @@ var scripts = ({ mode, source }) => ({
|
|
|
1263
1280
|
visual: "playwright test",
|
|
1264
1281
|
shot: "node scripts/shot.ts"
|
|
1265
1282
|
});
|
|
1266
|
-
|
|
1283
|
+
/**
|
|
1284
|
+
* `extra` is what a project made from a space needs besides (`plitzi create --from`): the packages its plugins and
|
|
1285
|
+
* runtime import. The scaffold's own win where both name one — the SDK and React are this CLI's versions, which the
|
|
1286
|
+
* project's plugins are rebuilt against.
|
|
1287
|
+
*/
|
|
1288
|
+
var packageJson$1 = (answers, extra = {}) => `${JSON.stringify({
|
|
1267
1289
|
name: answers.name,
|
|
1268
1290
|
version: "0.0.0",
|
|
1269
1291
|
private: true,
|
|
1270
1292
|
type: "module",
|
|
1271
1293
|
scripts: scripts(answers),
|
|
1272
|
-
dependencies:
|
|
1294
|
+
dependencies: Object.fromEntries(Object.entries({
|
|
1295
|
+
...extra,
|
|
1296
|
+
...dependencies(answers)
|
|
1297
|
+
}).sort(([a], [b]) => a.localeCompare(b))),
|
|
1273
1298
|
devDependencies: devDependencies(answers),
|
|
1274
1299
|
engines: NODE_ENGINES,
|
|
1275
1300
|
...managerPackageFields(answers.packageManager)
|
|
@@ -1430,14 +1455,14 @@ ${code(".claude/skills/plitzi-cli/SKILL.md")} first: ${code("plitzi add plugin")
|
|
|
1430
1455
|
* prefix — ship a server credential to every visitor. The two modes therefore name different variables, because
|
|
1431
1456
|
* they hold different keys.
|
|
1432
1457
|
*/
|
|
1433
|
-
var envFile = ({ key, environment, mode }) => mode === "server" ? `# The space's self-hosting key. Secret: never commit it, never ship it in a page.
|
|
1458
|
+
var envFile = ({ key, environment, revision, mode }) => mode === "server" ? `# The space's self-hosting key. Secret: never commit it, never ship it in a page.
|
|
1434
1459
|
# Credentials, in the builder.
|
|
1435
1460
|
PLITZI_HOST_KEY=${key}
|
|
1436
1461
|
|
|
1437
1462
|
# Which version this serves. 'main' is what the builder is editing; a published environment serves the latest
|
|
1438
1463
|
# release, and PLITZI_REVISION pins one exact version.
|
|
1439
1464
|
PLITZI_ENVIRONMENT=${environment}
|
|
1440
|
-
# PLITZI_REVISION=12
|
|
1465
|
+
${revision ? `PLITZI_REVISION=${String(revision)}` : "# PLITZI_REVISION=12"}
|
|
1441
1466
|
|
|
1442
1467
|
PORT=8080
|
|
1443
1468
|
` : `# The space's public render key. It ships in the page by design; the origin the browser states is what
|
|
@@ -3368,198 +3393,1126 @@ This project lists its plugins in src/main.ts (projects created since find them
|
|
|
3368
3393
|
}
|
|
3369
3394
|
};
|
|
3370
3395
|
//#endregion
|
|
3371
|
-
//#region
|
|
3372
|
-
var
|
|
3373
|
-
var
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
* Assumed, they are the wrong project for somebody, and an agent running this was the one assuming. So what was not
|
|
3380
|
-
* passed is asked for when a person is at the terminal, and refused when nobody is — with the exact questions to put
|
|
3381
|
-
* to the person.
|
|
3382
|
-
*/
|
|
3383
|
-
var resolveDecisions = async (options) => {
|
|
3384
|
-
const detected = detectPackageManager();
|
|
3385
|
-
const packageManager = {
|
|
3386
|
-
flag: "--package-manager",
|
|
3387
|
-
choices: PACKAGE_MANAGERS,
|
|
3388
|
-
question: `Which package manager will you work in? (this was run through ${detected})`,
|
|
3389
|
-
given: PACKAGE_MANAGERS.find((manager) => manager === options.packageManager),
|
|
3390
|
-
fallback: detected
|
|
3391
|
-
};
|
|
3392
|
-
const mode = {
|
|
3393
|
-
flag: "--mode",
|
|
3394
|
-
choices: MODES,
|
|
3395
|
-
question: "Should pages render on a Node server of your own (server: SSR + RSC), or only in the browser (client)?",
|
|
3396
|
-
given: MODES.find((candidate) => candidate === options.mode),
|
|
3397
|
-
fallback: "server"
|
|
3398
|
-
};
|
|
3399
|
-
const source = {
|
|
3400
|
-
flag: "--source",
|
|
3401
|
-
choices: SOURCES,
|
|
3402
|
-
question: "Should the space live in the project as code you edit (local), or be read live from your Plitzi account (cloud, which needs a key from Credentials in the builder)?",
|
|
3403
|
-
given: SOURCES.find((candidate) => candidate === options.source),
|
|
3404
|
-
fallback: "local"
|
|
3405
|
-
};
|
|
3406
|
-
const missing = [
|
|
3407
|
-
packageManager,
|
|
3408
|
-
mode,
|
|
3409
|
-
source
|
|
3410
|
-
].filter((question) => question.given === void 0);
|
|
3411
|
-
if (missing.length === 0 || options.yes && atTerminal()) return {
|
|
3412
|
-
packageManager: packageManager.given ?? packageManager.fallback,
|
|
3413
|
-
mode: mode.given ?? mode.fallback,
|
|
3414
|
-
source: source.given ?? source.fallback
|
|
3415
|
-
};
|
|
3416
|
-
if (!atTerminal()) {
|
|
3417
|
-
refuseWithoutTerminal("project", missing);
|
|
3418
|
-
return;
|
|
3419
|
-
}
|
|
3420
|
-
const rl = readline.createInterface({
|
|
3421
|
-
input: process.stdin,
|
|
3422
|
-
output: process.stdout
|
|
3423
|
-
});
|
|
3424
|
-
try {
|
|
3425
|
-
const manager = packageManager.given ?? await ask(rl, packageManager);
|
|
3426
|
-
const tier = manager && (mode.given ?? await ask(rl, mode));
|
|
3427
|
-
const origin = tier && (source.given ?? await ask(rl, source));
|
|
3428
|
-
if (!manager || !tier || !origin) {
|
|
3429
|
-
process.exitCode = 1;
|
|
3396
|
+
//#region ../../packages/sdk-shared/dist/actions/functions.mjs
|
|
3397
|
+
var r$1 = (e) => /^[A-Za-z0-9_][A-Za-z0-9_./-]*\.(ts|js|mjs|json)$/.test(e) && !e.split("/").some((e) => e === ".." || e === "." || e === "");
|
|
3398
|
+
var i$1 = async (e, t) => {
|
|
3399
|
+
let n = {}, i = async (a) => {
|
|
3400
|
+
let o;
|
|
3401
|
+
try {
|
|
3402
|
+
o = await t.list(a ? `${e}/${a}` : e);
|
|
3403
|
+
} catch {
|
|
3430
3404
|
return;
|
|
3431
3405
|
}
|
|
3406
|
+
for (let s of o) {
|
|
3407
|
+
let o = a ? `${a}/${s.name}` : s.name;
|
|
3408
|
+
s.directory ? s.name !== "node_modules" && !s.name.startsWith(".") && await i(o) : r$1(o) && (n[o] = await t.read(`${e}/${o}`));
|
|
3409
|
+
}
|
|
3410
|
+
};
|
|
3411
|
+
return await i(""), n;
|
|
3412
|
+
};
|
|
3413
|
+
//#endregion
|
|
3414
|
+
//#region src/commands/functions.ts
|
|
3415
|
+
var FUNCTIONS_DIR = "functions";
|
|
3416
|
+
var STATE_FILE = path.join(".plitzi", "functions.json");
|
|
3417
|
+
var isRecord$6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3418
|
+
/** The project's root: where `functions/` and `.plitzi/` go. */
|
|
3419
|
+
var rootOf = async () => (await findProject(process.cwd()))?.root ?? process.cwd();
|
|
3420
|
+
/** Every source file under `functions/`, by its path there — read by the one rule of what a source is. */
|
|
3421
|
+
var readLocal = (root) => i$1(path.join(root, FUNCTIONS_DIR), {
|
|
3422
|
+
list: async (dir) => (await fs.readdir(dir, { withFileTypes: true })).map((entry) => ({
|
|
3423
|
+
name: entry.name,
|
|
3424
|
+
directory: entry.isDirectory()
|
|
3425
|
+
})),
|
|
3426
|
+
read: (file) => fs.readFile(file, "utf8")
|
|
3427
|
+
});
|
|
3428
|
+
var readState = async (root) => {
|
|
3429
|
+
try {
|
|
3430
|
+
const value = JSON.parse(await fs.readFile(path.join(root, STATE_FILE), "utf8"));
|
|
3431
|
+
if (!isRecord$6(value) || typeof value.space !== "number" || typeof value.version !== "string" || !isRecord$6(value.files)) return;
|
|
3432
|
+
const files = Object.fromEntries(Object.entries(value.files).flatMap(([name, text]) => typeof text === "string" ? [[name, text]] : []));
|
|
3432
3433
|
return {
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3434
|
+
space: value.space,
|
|
3435
|
+
version: value.version,
|
|
3436
|
+
files
|
|
3436
3437
|
};
|
|
3437
|
-
}
|
|
3438
|
-
|
|
3438
|
+
} catch {
|
|
3439
|
+
return;
|
|
3439
3440
|
}
|
|
3440
3441
|
};
|
|
3441
|
-
var
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
input: process.stdin,
|
|
3445
|
-
output: process.stdout
|
|
3446
|
-
});
|
|
3447
|
-
try {
|
|
3448
|
-
return (await rl.question(`\n${keyQuestion(mode)}\n> `)).trim();
|
|
3449
|
-
} finally {
|
|
3450
|
-
rl.close();
|
|
3451
|
-
}
|
|
3442
|
+
var writeFunctionsState = async (root, state) => {
|
|
3443
|
+
await fs.mkdir(path.join(root, ".plitzi"), { recursive: true });
|
|
3444
|
+
await fs.writeFile(path.join(root, STATE_FILE), `${JSON.stringify(state, null, 2)}\n`);
|
|
3452
3445
|
};
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3446
|
+
/** The files that differ between two copies — added, removed or changed — sorted. */
|
|
3447
|
+
var changedFiles = (a, b) => [.../* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])].filter((file) => a[file] !== b[file]).sort();
|
|
3448
|
+
var taskList = (draft) => draft.manifest?.tasks.length ? draft.manifest.tasks.map((task) => `${task.namespace}.${task.action}`).join(", ") : "no tasks";
|
|
3449
|
+
var connect$1 = async (options, doing) => {
|
|
3450
|
+
const api = await apiFor(options);
|
|
3451
|
+
return api ? connectionWithSpace(api, doing) : void 0;
|
|
3452
|
+
};
|
|
3453
|
+
var readDraft = async (connection, spaceId) => {
|
|
3454
|
+
const answered = await authorizedRequest(connection, `/spaces/${spaceId}/functions`);
|
|
3455
|
+
if (!answered.ok) {
|
|
3456
|
+
fail(answered.error);
|
|
3460
3457
|
return;
|
|
3461
3458
|
}
|
|
3462
|
-
const
|
|
3463
|
-
if (
|
|
3464
|
-
|
|
3465
|
-
if (source === "cloud" && !options.key && !atTerminal()) {
|
|
3466
|
-
refuseWithoutTerminal("project", [{
|
|
3467
|
-
flag: "--key",
|
|
3468
|
-
choices: ["<key>"],
|
|
3469
|
-
question: keyQuestion(mode)
|
|
3470
|
-
}]);
|
|
3459
|
+
const { reply } = answered.value;
|
|
3460
|
+
if (reply.status !== 200) {
|
|
3461
|
+
fail(reply.data.error ?? `Could not read the space’s functions (${reply.status}).`);
|
|
3471
3462
|
return;
|
|
3472
3463
|
}
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3464
|
+
return reply.data;
|
|
3465
|
+
};
|
|
3466
|
+
var pullFunctions = async (options) => {
|
|
3467
|
+
const connection = await connect$1(options, "to pull from");
|
|
3468
|
+
if (!connection?.space) return;
|
|
3469
|
+
const root = await rootOf();
|
|
3470
|
+
const [local, state] = await Promise.all([readLocal(root), readState(root)]);
|
|
3471
|
+
const unpushed = state ? changedFiles(local, state.files) : Object.keys(local);
|
|
3472
|
+
if (!options.force && (state?.space ?? connection.space.id) !== connection.space.id) {
|
|
3473
|
+
fail("functions/ is a copy of another space’s. Pull into another project, or pass --force to replace it.");
|
|
3477
3474
|
return;
|
|
3478
3475
|
}
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
source,
|
|
3483
|
-
key,
|
|
3484
|
-
environment: options.environment ?? "main",
|
|
3485
|
-
packageManager,
|
|
3486
|
-
managerVersion: detectManagerVersion(packageManager, await nearestExisting(target))
|
|
3487
|
-
}));
|
|
3488
|
-
const wantsInstall = options.install !== false;
|
|
3489
|
-
const installed = wantsInstall && await install(packageManager, target);
|
|
3490
|
-
/**
|
|
3491
|
-
* A failed install is a failed command, not a next step.
|
|
3492
|
-
*
|
|
3493
|
-
* The files are already written, so the summary below still prints — but printed on its own after a screen of
|
|
3494
|
-
* resolver errors it reads as success, and a script that ran `create` carries on into a project with no
|
|
3495
|
-
* `node_modules`.
|
|
3496
|
-
*/
|
|
3497
|
-
if (wantsInstall && !installed) {
|
|
3498
|
-
console.error(chalk.red(`\n\`${installCommand(packageManager)}\` failed. The project is written; the reason is in the output above.`));
|
|
3499
|
-
console.error(chalk.dim(INSTALL_HINTS[packageManager]));
|
|
3500
|
-
process.exitCode = 1;
|
|
3501
|
-
}
|
|
3502
|
-
/**
|
|
3503
|
-
* Formatted by the project's own Prettier, once, so the first commit is already in its style — the scaffold writes
|
|
3504
|
-
* tables and lists whose layout depends on what they hold. Only into a folder that was empty: `--force` into one
|
|
3505
|
-
* with work in it must never reformat that work.
|
|
3506
|
-
*/
|
|
3507
|
-
if (installed && wasEmpty) await runScript(packageManager, "format", target);
|
|
3508
|
-
const where = cdPrefix(target);
|
|
3509
|
-
console.log(chalk.green(`\n${name} — ${mode === "server" ? "server-rendered" : "browser-rendered"}, space ${source === "cloud" ? "from Plitzi" : "in the project"}.`));
|
|
3510
|
-
console.log("");
|
|
3511
|
-
if (!installed) console.log(` ${where}${installCommand(packageManager)}`);
|
|
3512
|
-
console.log(` ${where}${runCommand(packageManager, "start")}`);
|
|
3513
|
-
console.log("");
|
|
3514
|
-
console.log(chalk.dim(`.claude/skills carries Plitzi's authoring skill; \`${runCommand(packageManager, "visual")}\` opens the page and checks it.`));
|
|
3515
|
-
if (source === "cloud") console.log(chalk.dim(".env holds the key and is already git-ignored."));
|
|
3516
|
-
console.log("");
|
|
3517
|
-
};
|
|
3518
|
-
//#endregion
|
|
3519
|
-
//#region src/commands/createPlugin.ts
|
|
3520
|
-
var DEFAULT_NAME = "plitzi-plugin-my-element";
|
|
3521
|
-
/** What an agent is told to ask for, and the flags that carry the answers. */
|
|
3522
|
-
var QUESTIONS = {
|
|
3523
|
-
name: {
|
|
3524
|
-
flag: "--name",
|
|
3525
|
-
choices: ["<package name>"],
|
|
3526
|
-
question: "What is the plugin called? Its package name, e.g. plitzi-plugin-seat-picker."
|
|
3527
|
-
},
|
|
3528
|
-
directory: {
|
|
3529
|
-
flag: "<directory> (the first argument)",
|
|
3530
|
-
choices: ["<folder>"],
|
|
3531
|
-
question: "Which folder should the plugin be written to?"
|
|
3476
|
+
if (!options.force && unpushed.length) {
|
|
3477
|
+
fail(`Pulling would overwrite what is not pushed yet: ${unpushed.join(", ")}.\nPush it first, or pass --force to throw it away.`);
|
|
3478
|
+
return;
|
|
3532
3479
|
}
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
for (const name of others) {
|
|
3541
|
-
const problem = pluginNameProblem(name);
|
|
3542
|
-
if (problem) return problem;
|
|
3480
|
+
const draft = await readDraft(connection, connection.space.id);
|
|
3481
|
+
if (!draft) return;
|
|
3482
|
+
const base = path.join(root, FUNCTIONS_DIR);
|
|
3483
|
+
await Promise.all(Object.keys(local).filter((file) => !Object.hasOwn(draft.files, file)).map((file) => fs.rm(path.join(base, file))));
|
|
3484
|
+
for (const [file, text] of Object.entries(draft.files)) {
|
|
3485
|
+
await fs.mkdir(path.dirname(path.join(base, file)), { recursive: true });
|
|
3486
|
+
await fs.writeFile(path.join(base, file), text);
|
|
3543
3487
|
}
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
/** The folders a repository keeps its packages in, as places for this one — or `./<name>` outside of any. */
|
|
3549
|
-
var folderOptions = (project, base) => {
|
|
3550
|
-
if (!project) return [];
|
|
3551
|
-
const root = project.workspaceRoot ?? project.root;
|
|
3552
|
-
return [.../* @__PURE__ */ new Set([...project.workspaceFolders, "plugins"])].map((folder) => {
|
|
3553
|
-
const value = path.join(root, folder, base);
|
|
3554
|
-
return {
|
|
3555
|
-
label: path.relative(process.cwd(), value) || ".",
|
|
3556
|
-
value
|
|
3557
|
-
};
|
|
3488
|
+
await writeFunctionsState(root, {
|
|
3489
|
+
space: connection.space.id,
|
|
3490
|
+
version: draft.version,
|
|
3491
|
+
files: draft.files
|
|
3558
3492
|
});
|
|
3493
|
+
const count = Object.keys(draft.files).length;
|
|
3494
|
+
console.log(count ? `Pulled ${count} file${count === 1 ? "" : "s"} of ${chalk.bold(connection.space.name)}’s functions into functions/ — ${taskList(draft)}.` : `${chalk.bold(connection.space.name)} has no functions yet. Write functions/index.ts and plitzi functions push.`);
|
|
3559
3495
|
};
|
|
3560
|
-
var
|
|
3561
|
-
|
|
3562
|
-
|
|
3496
|
+
var printProblems = (problems) => {
|
|
3497
|
+
console.error(chalk.red("The functions were not saved:"));
|
|
3498
|
+
problems.forEach(({ file, line, column, message }) => {
|
|
3499
|
+
const where = file ? `functions/${file}${line ? `:${String(line)}${column ? `:${String(column)}` : ""}` : ""}` : "functions";
|
|
3500
|
+
console.error(` ${chalk.bold(where)} ${message}`);
|
|
3501
|
+
});
|
|
3502
|
+
process.exitCode = 1;
|
|
3503
|
+
};
|
|
3504
|
+
var pushFunctions = async (options) => {
|
|
3505
|
+
const connection = await connect$1(options, "to push to");
|
|
3506
|
+
if (!connection?.space) return;
|
|
3507
|
+
const root = await rootOf();
|
|
3508
|
+
const [local, state] = await Promise.all([readLocal(root), readState(root)]);
|
|
3509
|
+
if (!Object.hasOwn(local, "index.ts")) {
|
|
3510
|
+
fail("There is no functions/index.ts here: it is where a space’s functions start. Pull them, or write it.");
|
|
3511
|
+
return;
|
|
3512
|
+
}
|
|
3513
|
+
let base = state?.space === connection.space.id ? state.version : void 0;
|
|
3514
|
+
if (base === void 0) {
|
|
3515
|
+
const draft = await readDraft(connection, connection.space.id);
|
|
3516
|
+
if (!draft) return;
|
|
3517
|
+
if (Object.keys(draft.files).length) {
|
|
3518
|
+
fail(`${connection.space.name} already has functions. Pull them first: plitzi functions pull.`);
|
|
3519
|
+
return;
|
|
3520
|
+
}
|
|
3521
|
+
base = draft.version;
|
|
3522
|
+
}
|
|
3523
|
+
if (state?.space === connection.space.id && !changedFiles(local, state.files).length) {
|
|
3524
|
+
console.log("Nothing to push: functions/ is what was pulled.");
|
|
3525
|
+
return;
|
|
3526
|
+
}
|
|
3527
|
+
const answered = await authorizedRequest(connection, `/spaces/${connection.space.id}/functions`, {
|
|
3528
|
+
method: "PUT",
|
|
3529
|
+
headers: { "Content-Type": "application/json" },
|
|
3530
|
+
body: JSON.stringify({
|
|
3531
|
+
files: local,
|
|
3532
|
+
base
|
|
3533
|
+
})
|
|
3534
|
+
});
|
|
3535
|
+
if (!answered.ok) {
|
|
3536
|
+
fail(answered.error);
|
|
3537
|
+
return;
|
|
3538
|
+
}
|
|
3539
|
+
const { reply } = answered.value;
|
|
3540
|
+
if (reply.status === 422 && reply.data.problems) {
|
|
3541
|
+
printProblems(reply.data.problems);
|
|
3542
|
+
return;
|
|
3543
|
+
}
|
|
3544
|
+
if (reply.status === 409) {
|
|
3545
|
+
fail(`${connection.space.name}’s functions changed since your pull — in the builder, or from another copy. Keep your changes aside, pull, and apply them again.`);
|
|
3546
|
+
return;
|
|
3547
|
+
}
|
|
3548
|
+
if (reply.status !== 200 || !reply.data.version) {
|
|
3549
|
+
fail(reply.data.refusal?.error ?? reply.data.error ?? `The functions were not saved (${reply.status}).`);
|
|
3550
|
+
return;
|
|
3551
|
+
}
|
|
3552
|
+
await writeFunctionsState(root, {
|
|
3553
|
+
space: connection.space.id,
|
|
3554
|
+
version: reply.data.version,
|
|
3555
|
+
files: local
|
|
3556
|
+
});
|
|
3557
|
+
console.log(`Pushed to ${chalk.bold(connection.space.name)}’s draft — ${taskList({ manifest: reply.data.manifest ?? null })}. ` + chalk.dim("The live site runs them once the space is published."));
|
|
3558
|
+
};
|
|
3559
|
+
/** What one run of a task did: its logs, then its value — or why it failed, as a failure of the command. */
|
|
3560
|
+
var printRun = (task, report) => {
|
|
3561
|
+
const step = report.steps?.find((entry) => entry.action === task);
|
|
3562
|
+
const took = step ? `${String(step.endTime - step.startTime)} ms` : "";
|
|
3563
|
+
step?.logs?.forEach((line) => console.log(chalk.dim(`log ${line}`)));
|
|
3564
|
+
if (step?.error) {
|
|
3565
|
+
fail(`${task} failed after ${took}: ${step.error}`);
|
|
3566
|
+
return;
|
|
3567
|
+
}
|
|
3568
|
+
console.log(`${chalk.green(report.status ?? "completed")} ${chalk.dim(took ? `in ${took}` : "")}`);
|
|
3569
|
+
console.log(JSON.stringify(report.output?.value ?? null, null, 2));
|
|
3570
|
+
};
|
|
3571
|
+
/** `--params`, read: an object of the task's params by name, or nothing when it is not one. */
|
|
3572
|
+
var paramsOf = (given) => {
|
|
3573
|
+
try {
|
|
3574
|
+
const params = given ? JSON.parse(given) : {};
|
|
3575
|
+
if (isRecord$6(params)) return params;
|
|
3576
|
+
} catch {}
|
|
3577
|
+
fail("--params is a JSON object of the task’s params, by name: --params '{\"name\":\"Ada\"}'");
|
|
3578
|
+
};
|
|
3579
|
+
var tryFunction = async (task, options) => {
|
|
3580
|
+
const params = paramsOf(options.params);
|
|
3581
|
+
if (!params) return;
|
|
3582
|
+
const connection = await connect$1(options, "to try it in");
|
|
3583
|
+
if (!connection?.space) return;
|
|
3584
|
+
const answered = await authorizedRequest(connection, `/spaces/${connection.space.id}/functions/try`, {
|
|
3585
|
+
method: "POST",
|
|
3586
|
+
headers: { "Content-Type": "application/json" },
|
|
3587
|
+
body: JSON.stringify({
|
|
3588
|
+
task,
|
|
3589
|
+
params
|
|
3590
|
+
})
|
|
3591
|
+
});
|
|
3592
|
+
if (!answered.ok) {
|
|
3593
|
+
fail(answered.error);
|
|
3594
|
+
return;
|
|
3595
|
+
}
|
|
3596
|
+
const { reply } = answered.value;
|
|
3597
|
+
if (reply.status !== 200) {
|
|
3598
|
+
fail(reply.data.error ?? `The task could not be tried (${reply.status}).`);
|
|
3599
|
+
return;
|
|
3600
|
+
}
|
|
3601
|
+
printRun(task, reply.data);
|
|
3602
|
+
};
|
|
3603
|
+
var isLocalRunner = (value) => isRecord$6(value) && typeof value.createLocalFunctions === "function";
|
|
3604
|
+
/**
|
|
3605
|
+
* This project's own runner: `dev` runs the functions with the `@plitzi/sdk-server` the project installed, so the build,
|
|
3606
|
+
* the checks, the isolates and the limits are the platform's own, at the version the project is on.
|
|
3607
|
+
*/
|
|
3608
|
+
var projectRunner = async (root) => {
|
|
3609
|
+
try {
|
|
3610
|
+
const entry = createRequire(path.join(root, "package.json")).resolve("@plitzi/sdk-server/functions-runner");
|
|
3611
|
+
const loaded = await import(pathToFileURL(entry).href);
|
|
3612
|
+
if (isLocalRunner(loaded)) return loaded;
|
|
3613
|
+
} catch {}
|
|
3614
|
+
fail("plitzi functions dev runs them with this project’s own @plitzi/sdk-server, and the isolates it needs:\n npm install --save-dev @plitzi/sdk-server isolated-vm core-js");
|
|
3615
|
+
};
|
|
3616
|
+
/** `PLITZI_FUNCTIONS_CREDENTIALS`, a JSON object of credential id → its keys: what `ctx.fetch` may name locally. */
|
|
3617
|
+
var localCredentials = () => {
|
|
3618
|
+
const raw = process.env.PLITZI_FUNCTIONS_CREDENTIALS;
|
|
3619
|
+
try {
|
|
3620
|
+
const value = raw ? JSON.parse(raw) : {};
|
|
3621
|
+
return isRecord$6(value) ? Object.fromEntries(Object.entries(value).flatMap(([id, keys]) => isRecord$6(keys) ? [[id, Object.fromEntries(Object.entries(keys).filter((pair) => typeof pair[1] === "string"))]] : [])) : {};
|
|
3622
|
+
} catch {
|
|
3623
|
+
return {};
|
|
3624
|
+
}
|
|
3625
|
+
};
|
|
3626
|
+
var NO_NODE_SNAPSHOT = "--no-node-snapshot";
|
|
3627
|
+
/**
|
|
3628
|
+
* The isolates `dev` runs need Node's startup snapshot off, and a process cannot turn it off once started: `dev` runs
|
|
3629
|
+
* itself again with it, and answers with that run's exit code.
|
|
3630
|
+
*/
|
|
3631
|
+
var rerunWithoutNodeSnapshot = () => new Promise((resolve, reject) => {
|
|
3632
|
+
const child = spawn(process.execPath, [
|
|
3633
|
+
...process.execArgv,
|
|
3634
|
+
NO_NODE_SNAPSHOT,
|
|
3635
|
+
...process.argv.slice(1)
|
|
3636
|
+
], { stdio: "inherit" });
|
|
3637
|
+
child.once("error", reject);
|
|
3638
|
+
child.once("exit", (code) => {
|
|
3639
|
+
resolve(code ?? 1);
|
|
3640
|
+
});
|
|
3641
|
+
});
|
|
3642
|
+
/**
|
|
3643
|
+
* `plitzi functions dev <task>`: the task run from `functions/` on this machine, as the platform would run it — and,
|
|
3644
|
+
* with `--watch`, again every time a file is saved. Nothing reaches the space: its draft is what `push` sends.
|
|
3645
|
+
*/
|
|
3646
|
+
var devFunction = async (task, options) => {
|
|
3647
|
+
if (!process.execArgv.includes(NO_NODE_SNAPSHOT)) {
|
|
3648
|
+
process.exitCode = await rerunWithoutNodeSnapshot();
|
|
3649
|
+
return;
|
|
3650
|
+
}
|
|
3651
|
+
const params = paramsOf(options.params);
|
|
3652
|
+
const root = await rootOf();
|
|
3653
|
+
const runner = params ? await projectRunner(root) : void 0;
|
|
3654
|
+
if (!params || !runner) return;
|
|
3655
|
+
const local = runner.createLocalFunctions({ credentials: localCredentials() });
|
|
3656
|
+
const run = async () => {
|
|
3657
|
+
const loaded = await local.load(await readLocal(root));
|
|
3658
|
+
if (!loaded.ok) {
|
|
3659
|
+
printProblems(loaded.problems);
|
|
3660
|
+
return;
|
|
3661
|
+
}
|
|
3662
|
+
if (!loaded.tasks.includes(task)) {
|
|
3663
|
+
fail(`functions/ declares no task ${task}. It declares: ${loaded.tasks.join(", ") || "none"}.`);
|
|
3664
|
+
return;
|
|
3665
|
+
}
|
|
3666
|
+
printRun(task, await local.tryTask(task, params));
|
|
3667
|
+
};
|
|
3668
|
+
await run();
|
|
3669
|
+
if (!options.watch) return;
|
|
3670
|
+
console.log(chalk.dim("\nWatching functions/ — every save runs it again. ^C to stop."));
|
|
3671
|
+
let pending;
|
|
3672
|
+
watch(path.join(root, FUNCTIONS_DIR), { recursive: true }, () => {
|
|
3673
|
+
clearTimeout(pending);
|
|
3674
|
+
pending = setTimeout(() => {
|
|
3675
|
+
process.exitCode = void 0;
|
|
3676
|
+
console.log(chalk.dim(`\n— ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`));
|
|
3677
|
+
run();
|
|
3678
|
+
}, 150);
|
|
3679
|
+
});
|
|
3680
|
+
};
|
|
3681
|
+
//#endregion
|
|
3682
|
+
//#region src/commands/projectFormatter.ts
|
|
3683
|
+
var isRecord$5 = (value) => typeof value === "object" && value !== null;
|
|
3684
|
+
var isPrettier = (value) => isRecord$5(value) && [
|
|
3685
|
+
"format",
|
|
3686
|
+
"resolveConfig",
|
|
3687
|
+
"getFileInfo"
|
|
3688
|
+
].every((name) => typeof value[name] === "function");
|
|
3689
|
+
var unformatted = (_file, text) => Promise.resolve(text);
|
|
3690
|
+
/** Loads the project's Prettier once. A project without one formats nothing. */
|
|
3691
|
+
var projectFormatter = async (root) => {
|
|
3692
|
+
let loaded;
|
|
3693
|
+
try {
|
|
3694
|
+
const resolved = createRequire(path.join(root, "package.json")).resolve("prettier");
|
|
3695
|
+
loaded = await import(pathToFileURL(resolved).href);
|
|
3696
|
+
} catch {
|
|
3697
|
+
return unformatted;
|
|
3698
|
+
}
|
|
3699
|
+
const prettier = isPrettier(loaded) ? loaded : isRecord$5(loaded) ? loaded.default : void 0;
|
|
3700
|
+
if (!isPrettier(prettier)) return unformatted;
|
|
3701
|
+
const api = prettier;
|
|
3702
|
+
const ignorePath = [".prettierignore", ".gitignore"].map((file) => path.join(root, file));
|
|
3703
|
+
return async (file, text) => {
|
|
3704
|
+
const filepath = path.join(root, file);
|
|
3705
|
+
const info = await api.getFileInfo(filepath, {
|
|
3706
|
+
ignorePath,
|
|
3707
|
+
resolveConfig: true
|
|
3708
|
+
});
|
|
3709
|
+
if (info.ignored || !info.inferredParser) return text;
|
|
3710
|
+
return api.format(text, {
|
|
3711
|
+
...await api.resolveConfig(filepath),
|
|
3712
|
+
filepath
|
|
3713
|
+
});
|
|
3714
|
+
};
|
|
3715
|
+
};
|
|
3716
|
+
//#endregion
|
|
3717
|
+
//#region src/scaffold/fromSpace.ts
|
|
3718
|
+
var TEXT = /\.((m|c)?(t|j)sx?|json|css|scss|sass|less|md|txt|html|svg|ya?ml|xml|csv|graphql)$/i;
|
|
3719
|
+
/** Whether a file of the project is text — written, compared and formatted as such — rather than bytes. */
|
|
3720
|
+
var isTextFile = (path) => TEXT.test(path);
|
|
3721
|
+
/** The example plugin and space `create` writes for a project of its own, which one made from a space replaces. */
|
|
3722
|
+
var EXAMPLE = ["src/plugins/StatCard/StatCard.tsx", "src/plugins/StatCard/index.ts"];
|
|
3723
|
+
var decode = (base64) => Buffer.from(base64, "base64").toString("utf-8");
|
|
3724
|
+
/** One import line of the generated code. */
|
|
3725
|
+
var importLine = (names, from) => `import ${names} from '${from}';\n`;
|
|
3726
|
+
/** `export { pizarra as space }`: the name the project's own code imports the space under, whatever it was exported as. */
|
|
3727
|
+
var spaceEntry = (exportName) => `/**
|
|
3728
|
+
* The space, as Plitzi kept it, written out as the code that authors it (\`./space/\`): a file per page, the shared
|
|
3729
|
+
* classes in \`styles.ts\`. It is this project's now — edit it, and the next start renders the change.
|
|
3730
|
+
*/
|
|
3731
|
+
export { ${exportName} as space } from './space/index.ts';
|
|
3732
|
+
`;
|
|
3733
|
+
/** The lines that import every action written as code, each under a name of its own however their ids camelCase. */
|
|
3734
|
+
var codedImports = (coded) => {
|
|
3735
|
+
const taken = /* @__PURE__ */ new Set();
|
|
3736
|
+
const imports = coded.map(({ identifier, exportName }) => {
|
|
3737
|
+
let name = exportName;
|
|
3738
|
+
for (let next = 2; taken.has(name); next += 1) name = `${exportName}${String(next)}`;
|
|
3739
|
+
taken.add(name);
|
|
3740
|
+
return {
|
|
3741
|
+
name,
|
|
3742
|
+
line: importLine(`{ ${name === exportName ? name : `${exportName} as ${name}`} }`, `./actions/${identifier}.ts`)
|
|
3743
|
+
};
|
|
3744
|
+
});
|
|
3745
|
+
return {
|
|
3746
|
+
lines: imports.map(({ line }) => line).join(""),
|
|
3747
|
+
names: imports.map(({ name }) => name)
|
|
3748
|
+
};
|
|
3749
|
+
};
|
|
3750
|
+
var actionsModule = (coded) => {
|
|
3751
|
+
const { lines, names } = codedImports(coded);
|
|
3752
|
+
return `import { readdirSync, readFileSync } from 'node:fs';
|
|
3753
|
+
import path from 'node:path';
|
|
3754
|
+
${lines ? `\n${lines}` : ""}
|
|
3755
|
+
import type { ActionLookups } from '@plitzi/sdk-server/actions';
|
|
3756
|
+
|
|
3757
|
+
type ActionEntry = NonNullable<Awaited<ReturnType<ActionLookups['getAction']>>>;
|
|
3758
|
+
type Connector = NonNullable<Awaited<ReturnType<NonNullable<ActionLookups['getConnector']>>>>;
|
|
3759
|
+
|
|
3760
|
+
/** The JSON documents of a folder beside this file: none when there is no such folder. */
|
|
3761
|
+
const read = (folder: string): unknown[] => {
|
|
3762
|
+
const dir = path.join(import.meta.dirname, folder);
|
|
3763
|
+
try {
|
|
3764
|
+
return readdirSync(dir)
|
|
3765
|
+
.filter(file => file.endsWith('.json'))
|
|
3766
|
+
.map((file): unknown => JSON.parse(readFileSync(path.join(dir, file), 'utf-8')));
|
|
3767
|
+
} catch {
|
|
3768
|
+
return [];
|
|
3769
|
+
}
|
|
3770
|
+
};
|
|
3771
|
+
|
|
3772
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
3773
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3774
|
+
|
|
3775
|
+
/**
|
|
3776
|
+
* The space's server actions, in \`src/actions/\`: one \`defineAction\` each — edit one, and the next start runs it —
|
|
3777
|
+
* and, as JSON, any the builder wrote in a form code has no words for. Their ways in say whether they are on.
|
|
3778
|
+
*/
|
|
3779
|
+
const actions: ActionEntry[] = [${names.length > 0 ? `\n ${names.join(",\n ")},` : ""}
|
|
3780
|
+
...read('actions').flatMap(entry =>
|
|
3781
|
+
isRecord(entry) && typeof entry.id === 'string' && isRecord(entry.document)
|
|
3782
|
+
? // A document Plitzi checked when it was saved, and that the server checks again on every run.
|
|
3783
|
+
[{ id: entry.id, document: entry.document as ActionEntry['document'] }]
|
|
3784
|
+
: []
|
|
3785
|
+
)
|
|
3786
|
+
];
|
|
3787
|
+
|
|
3788
|
+
/** The space's connectors, as Plitzi kept them: one JSON manifest each, in \`src/connectors/\`. */
|
|
3789
|
+
const connectors = new Map(
|
|
3790
|
+
read('connectors').flatMap(entry =>
|
|
3791
|
+
isRecord(entry) && typeof entry.id === 'string' && isRecord(entry.manifest)
|
|
3792
|
+
? // A manifest Plitzi checked when it was saved.
|
|
3793
|
+
[[entry.id, entry.manifest as Connector] as const]
|
|
3794
|
+
: []
|
|
3795
|
+
)
|
|
3796
|
+
);
|
|
3797
|
+
|
|
3798
|
+
/**
|
|
3799
|
+
* The one space this server serves, as its adapters name it — 1, whether they read it from this project or from
|
|
3800
|
+
* Plitzi: what the scheduler watches for actions on a clock.
|
|
3801
|
+
*/
|
|
3802
|
+
const SPACE_ID = 1;
|
|
3803
|
+
|
|
3804
|
+
export const lookups: ActionLookups = {
|
|
3805
|
+
getAction: (_spaceId, actionId) => Promise.resolve(actions.find(entry => entry.id === actionId)),
|
|
3806
|
+
listActions: () => Promise.resolve(actions),
|
|
3807
|
+
getConnector: (_spaceId, connectorId) => Promise.resolve(connectors.get(connectorId)),
|
|
3808
|
+
listScheduledSpaces: () => Promise.resolve([SPACE_ID])
|
|
3809
|
+
};
|
|
3810
|
+
`;
|
|
3811
|
+
};
|
|
3812
|
+
/** What a space with visitor roles needs of a server of its own, said where its auth would be handed over. */
|
|
3813
|
+
var visitorsNote = (roles) => `
|
|
3814
|
+
/**
|
|
3815
|
+
* The space declares visitor roles (${roles.join(", ")}). On Plitzi its visitors sign in with their Plitzi account, and
|
|
3816
|
+
* hold the roles the space gives them by email. Here nobody signs in until this server does it itself: \`createAuth\`
|
|
3817
|
+
* from \`@plitzi/sdk-server/auth\`, handed to \`createServer\` as \`auth\`, over the accounts it keeps — giving each
|
|
3818
|
+
* person the permissions of the roles they hold (\`visitorAccess\` from \`@plitzi/sdk-shared/auth/visitorRoles\`, over
|
|
3819
|
+
* the space's \`settings.visitorRoles\`). Until then every action that asks for a role refuses. See Self-hosting, in
|
|
3820
|
+
* Plitzi's docs.
|
|
3821
|
+
*/`;
|
|
3822
|
+
var BUILT_PLUGINS = `
|
|
3823
|
+
/**
|
|
3824
|
+
* The space's plugins no source of was kept, as they were built: \`vendor/plugins/<type>/\`, each beside the manifest it
|
|
3825
|
+
* was published with. They run and render as they are — on the server too — and cannot be changed here: upload one
|
|
3826
|
+
* again from its source (\`plitzi upload plugin\`), and \`plitzi pull\` brings its code in their place.
|
|
3827
|
+
*/
|
|
3828
|
+
type BuiltManifest = {
|
|
3829
|
+
version?: string;
|
|
3830
|
+
pluginSchema?: Record<string, unknown>;
|
|
3831
|
+
assets?: Record<string, { src: string; type: string; isMain?: boolean }>;
|
|
3832
|
+
};
|
|
3833
|
+
|
|
3834
|
+
const VENDOR_PLUGINS_DIR = path.resolve(import.meta.dirname, '../vendor/plugins');
|
|
3835
|
+
const built = readdirSync(VENDOR_PLUGINS_DIR, { withFileTypes: true })
|
|
3836
|
+
.filter(entry => entry.isDirectory())
|
|
3837
|
+
.map(entry => {
|
|
3838
|
+
const dir = path.join(VENDOR_PLUGINS_DIR, entry.name);
|
|
3839
|
+
// What \`plitzi pack plugin\` wrote beside the bundle when it was published.
|
|
3840
|
+
const manifest = JSON.parse(readFileSync(path.join(dir, 'plugin-manifest.json'), 'utf-8')) as BuiltManifest;
|
|
3841
|
+
const assets = Object.values(manifest.assets ?? {});
|
|
3842
|
+
const script = assets.find(asset => asset.type === 'script' && asset.isMain) ?? assets.find(asset => asset.type === 'script');
|
|
3843
|
+
const style = assets.find(asset => asset.type === 'style');
|
|
3844
|
+
if (!script) {
|
|
3845
|
+
throw new Error(\`vendor/plugins/\${entry.name}/plugin-manifest.json names no script to run.\`);
|
|
3846
|
+
}
|
|
3847
|
+
|
|
3848
|
+
return {
|
|
3849
|
+
type: entry.name,
|
|
3850
|
+
provides: Object.keys(manifest.pluginSchema ?? {}),
|
|
3851
|
+
source: {
|
|
3852
|
+
js: path.join(dir, script.src),
|
|
3853
|
+
...(style ? { css: path.join(dir, style.src) } : {}),
|
|
3854
|
+
action: 'copy' as const,
|
|
3855
|
+
version: manifest.version ?? '1.0.0'
|
|
3856
|
+
}
|
|
3857
|
+
};
|
|
3858
|
+
});
|
|
3859
|
+
const builtPlugins = Object.fromEntries(built.map(({ type, source }) => [type, source]));
|
|
3860
|
+
/** Every element type they provide, for the space's use of them to be authored as a plugin's rather than a typo's. */
|
|
3861
|
+
const builtTypes = built.flatMap(({ type, provides }) => [type, ...provides]);
|
|
3862
|
+
`;
|
|
3863
|
+
var runtimeLines = ({ runtimeEntry, packedRuntime }) => {
|
|
3864
|
+
if (runtimeEntry) return `/**
|
|
3865
|
+
* The space's runtime — its own server code, run by Plitzi beside the space — run here, in this process: its tasks join
|
|
3866
|
+
* the functions, and its endpoints answer before anything else. Its variables are this process's environment.
|
|
3867
|
+
*/
|
|
3868
|
+
const runtime = await serveRuntime(spaceRuntime, { env: process.env, publicUrl });`;
|
|
3869
|
+
if (packedRuntime) return `/**
|
|
3870
|
+
* The space's runtime, as it was built: no source of it was kept, so it runs as it is and cannot be changed here. Push
|
|
3871
|
+
* it again from its source (\`plitzi runtime push\`), and \`plitzi pull\` brings its code in its place.
|
|
3872
|
+
*/
|
|
3873
|
+
const runtime = await serveRuntime(
|
|
3874
|
+
await loadRuntime(readFileSync(path.join(PROJECT_ROOT, 'vendor/runtime.bundle')), path.join(PROJECT_ROOT, '.runtime')),
|
|
3875
|
+
{ env: process.env, publicUrl }
|
|
3876
|
+
);`;
|
|
3877
|
+
return "";
|
|
3878
|
+
};
|
|
3879
|
+
var fromSpaceMain = (options) => {
|
|
3880
|
+
const { source, runtimeEntry, packedRuntime, builtPlugins, visitorRoles } = options;
|
|
3881
|
+
const hasRuntime = Boolean(runtimeEntry) || packedRuntime;
|
|
3882
|
+
const local = source === "local";
|
|
3883
|
+
const fsImports = ["readdirSync", ...packedRuntime || builtPlugins ? ["readFileSync"] : []].join(", ");
|
|
3884
|
+
const allPlugins = builtPlugins ? "{ ...plugins, ...builtPlugins }" : "plugins";
|
|
3885
|
+
const allNames = builtPlugins ? "[...pluginNames, ...Object.keys(builtPlugins)]" : "pluginNames";
|
|
3886
|
+
const runtimeImports = [...packedRuntime ? ["loadRuntime"] : [], ...hasRuntime ? ["serveRuntime"] : []].join(", ");
|
|
3887
|
+
return `import { ${fsImports} } from 'node:fs';
|
|
3888
|
+
import path from 'node:path';
|
|
3889
|
+
|
|
3890
|
+
import { closeOnSignals, consoleLogger, ${local ? "createJsonAdapters" : "createCloudAdapters"}, createServer, loadFunctions } from '@plitzi/sdk-server';
|
|
3891
|
+
${hasRuntime ? importLine(`{ ${runtimeImports} }`, "@plitzi/sdk-server/runtime") : ""}${local ? `\n${importLine("{ authorSpace }", "@plitzi/sdk-authoring")}` : ""}
|
|
3892
|
+
${importLine("{ lookups }", "./actions.ts")}${local ? importLine("{ declarations }", "./plugins/declarations.ts") : ""}${runtimeEntry ? importLine("spaceRuntime", runtimeEntry) : ""}${local ? importLine("{ space }", "./space.ts") : ""}
|
|
3893
|
+
/**
|
|
3894
|
+
* The project's settings — the key its actions sign with, the variables the space was given on Plitzi — kept in \`.env\`,
|
|
3895
|
+
* out of git. \`plitzi create\` wrote one with a fresh signing key; \`.env.example\` names the rest.
|
|
3896
|
+
*/
|
|
3897
|
+
try {
|
|
3898
|
+
process.loadEnvFile(new URL('../.env', import.meta.url));
|
|
3899
|
+
} catch {
|
|
3900
|
+
// None: the environment the process was started with is all there is.
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3903
|
+
const PORT = Number(process.env.PORT ?? 8080);
|
|
3904
|
+
// Loopback unless told otherwise: a container publishes a port only from an address it listens on (\`HOST=0.0.0.0\`).
|
|
3905
|
+
const HOST = process.env.HOST ?? '127.0.0.1';
|
|
3906
|
+
/** Where people reach this server: \`PUBLIC_URL\` behind a proxy, its own address otherwise. */
|
|
3907
|
+
const publicUrl = (process.env.PUBLIC_URL ?? \`http://127.0.0.1:\${String(PORT)}\`).replace(/\\/+$/, '');
|
|
3908
|
+
${builtPlugins ? BUILT_PLUGINS : ""}${local ? `
|
|
3909
|
+
/**
|
|
3910
|
+
* The space, held in this project: \`src/space/\`, authored at boot — saving a page and letting \`--watch\` restart is
|
|
3911
|
+
* the whole edit loop, and the warnings are printed where somebody editing it is watching.
|
|
3912
|
+
*/
|
|
3913
|
+
const { schema, style, warnings } = authorSpace(space, { plugins: declarations${builtPlugins ? ", pluginTypes: builtTypes" : ""} });
|
|
3914
|
+
|
|
3915
|
+
for (const warning of warnings) {
|
|
3916
|
+
console.warn(\`[author] \${warning.message}\`);
|
|
3917
|
+
}
|
|
3918
|
+
` : `
|
|
3919
|
+
/** The space's HOST key (Credentials, in the builder): secret, never committed, never shipped in a page. */
|
|
3920
|
+
const HOST_KEY = process.env.PLITZI_HOST_KEY ?? '';
|
|
3921
|
+
|
|
3922
|
+
if (!HOST_KEY) {
|
|
3923
|
+
throw new Error('Set PLITZI_HOST_KEY in .env — Credentials, in the builder.');
|
|
3924
|
+
}
|
|
3925
|
+
`}
|
|
3926
|
+
${PLUGINS}
|
|
3927
|
+
|
|
3928
|
+
/** The space's functions: \`functions/\`, built the way Plitzi builds them and run here, in this process. */
|
|
3929
|
+
const functions = await loadFunctions(new URL('../functions/', import.meta.url));
|
|
3930
|
+
${hasRuntime ? `\n${runtimeLines(options)}\n` : ""}${visitorRoles.length > 0 ? `${visitorsNote(visitorRoles)}\n` : ""}
|
|
3931
|
+
/**
|
|
3932
|
+
* Everything the space is, served by this server alone: its pages, its plugins, its server actions (\`src/actions/\`),
|
|
3933
|
+
* its functions${hasRuntime ? " and runtime" : ""}, and its files (\`public/\`, where Plitzi's CDN served them from).
|
|
3934
|
+
*/
|
|
3935
|
+
const server = createServer(
|
|
3936
|
+
{
|
|
3937
|
+
port: PORT,
|
|
3938
|
+
devMode: process.env.NODE_ENV !== 'production',
|
|
3939
|
+
adapters: ${local ? `createJsonAdapters({
|
|
3940
|
+
offlineData: { schema, style },
|
|
3941
|
+
deployment: { spaceId: 1, environment: 'main', revision: 0, pluginNames: ${allNames} }
|
|
3942
|
+
})` : `createCloudAdapters({
|
|
3943
|
+
webKey: HOST_KEY,
|
|
3944
|
+
...(process.env.PLITZI_SERVER_URL ? { serverUrl: process.env.PLITZI_SERVER_URL } : {}),
|
|
3945
|
+
environment: (process.env.PLITZI_ENVIRONMENT ?? 'main') as 'main' | 'production',
|
|
3946
|
+
deployment: { pluginNames: ${allNames} }
|
|
3947
|
+
})`},
|
|
3948
|
+
plugins: ${allPlugins},
|
|
3949
|
+
publicDir: path.join(PROJECT_ROOT, 'public'),
|
|
3950
|
+
functions: { native: ${hasRuntime ? "[...functions, ...runtime.native]" : "functions"} },
|
|
3951
|
+
// What \`ctx.sign\` and \`ctx.verify\` sign with: the key that was the space's on Plitzi stays there.
|
|
3952
|
+
action: { lookups, signingSecret: process.env.PLITZI_SIGNING_SECRET },
|
|
3953
|
+
logger: consoleLogger
|
|
3954
|
+
}${hasRuntime ? ",\n { preAuth: [runtime.stage] }" : ""}
|
|
3955
|
+
);
|
|
3956
|
+
|
|
3957
|
+
server.listen(PORT, HOST);
|
|
3958
|
+
console.log(\`pages on \${publicUrl}/\`);
|
|
3959
|
+
|
|
3960
|
+
closeOnSignals(server${hasRuntime ? ", { afterClose: () => runtime.close() }" : ""});
|
|
3961
|
+
`;
|
|
3962
|
+
};
|
|
3963
|
+
/** A path of the source tree, where the project keeps it: under `src/`, unless it was already a project's `src/`. */
|
|
3964
|
+
var placeOf = (paths) => {
|
|
3965
|
+
const prefix = paths.length > 0 && paths.every((path) => path.startsWith("src/")) ? "" : "src/";
|
|
3966
|
+
return (path) => `${prefix}${path}`;
|
|
3967
|
+
};
|
|
3968
|
+
/** The import `src/main.ts` names the runtime's module by. */
|
|
3969
|
+
var fromMain = (path) => `./${path.replace(/^src\//, "")}`;
|
|
3970
|
+
/** What the space was given on Plitzi, by name, as `.env.example` lists it — and `.env`, with values where they are known. */
|
|
3971
|
+
var settingsLines = (exported) => [
|
|
3972
|
+
"# What the space was given on Plitzi, by name — the values never leave it. Copy this to .env and fill them in.",
|
|
3973
|
+
"# What the project signs with (ctx.sign): at least 32 characters, secret.",
|
|
3974
|
+
"PLITZI_SIGNING_SECRET=",
|
|
3975
|
+
...exported.variables.map((name) => `${name}=`),
|
|
3976
|
+
...exported.credentials.map(({ identifier, name, provider }) => `# ${name} (${provider}): the credential "${identifier}"`),
|
|
3977
|
+
"PORT=8080",
|
|
3978
|
+
""
|
|
3979
|
+
];
|
|
3980
|
+
/**
|
|
3981
|
+
* The project's own `.env`, ready to run: a signing key made for it — the space's own stays on Plitzi — and the rest
|
|
3982
|
+
* left for whoever has the values. Read from Plitzi, the key it is read with comes first, as a project of its own has
|
|
3983
|
+
* it. Written once, by `create`: it is the project's, and nothing pulled ever touches it.
|
|
3984
|
+
*/
|
|
3985
|
+
var envFromSpace = (exported, answers, signingSecret) => {
|
|
3986
|
+
const settings = settingsLines(exported).join("\n").replace("PLITZI_SIGNING_SECRET=", `PLITZI_SIGNING_SECRET=${signingSecret}`);
|
|
3987
|
+
return answers.source === "cloud" ? `${envFile(answers)}\n${settings}\n` : `${settings}\nPORT=8080\n`;
|
|
3988
|
+
};
|
|
3989
|
+
/** What a project made from a space holds, from what the platform answered for it — the same for `create` and `pull`. */
|
|
3990
|
+
var projectFromSpace = (exported, spaceSource) => {
|
|
3991
|
+
const { source, builtOnly, assets, report } = exported;
|
|
3992
|
+
const place = placeOf(Object.keys(source.files));
|
|
3993
|
+
const local = (text) => assets.reduce((written, { url, path }) => written.split(url).join(`/${path}`), text);
|
|
3994
|
+
const files = {};
|
|
3995
|
+
const binaries = {};
|
|
3996
|
+
Object.entries(source.files).forEach(([path, content]) => {
|
|
3997
|
+
if (isTextFile(path)) files[place(path)] = local(decode(content));
|
|
3998
|
+
else binaries[place(path)] = content;
|
|
3999
|
+
});
|
|
4000
|
+
if (exported.authoring) {
|
|
4001
|
+
Object.entries(exported.authoring.files).forEach(([path, text]) => {
|
|
4002
|
+
files[`src/space/${path}`] = local(text);
|
|
4003
|
+
});
|
|
4004
|
+
files["src/space.ts"] = spaceEntry(exported.authoring.exportName);
|
|
4005
|
+
files["src/plugins/declarations.ts"] = projectDeclarations(Object.keys(files).map((path) => /^src\/plugins\/([^/]+)\/declaration\.ts$/.exec(path)?.[1]).filter((folder) => folder !== void 0).sort());
|
|
4006
|
+
}
|
|
4007
|
+
const coded = [];
|
|
4008
|
+
const asJson = [];
|
|
4009
|
+
exported.actions.forEach(({ identifier, name, document }) => {
|
|
4010
|
+
const reading = actionSpecFromEntry({
|
|
4011
|
+
id: identifier,
|
|
4012
|
+
document
|
|
4013
|
+
});
|
|
4014
|
+
if (reading.ok) {
|
|
4015
|
+
const { exportName, source: code } = actionToSource(reading.spec);
|
|
4016
|
+
files[`src/actions/${identifier}.ts`] = local(code);
|
|
4017
|
+
coded.push({
|
|
4018
|
+
identifier,
|
|
4019
|
+
exportName
|
|
4020
|
+
});
|
|
4021
|
+
return;
|
|
4022
|
+
}
|
|
4023
|
+
files[`src/actions/${identifier}.json`] = `${local(JSON.stringify({
|
|
4024
|
+
id: identifier,
|
|
4025
|
+
name,
|
|
4026
|
+
document
|
|
4027
|
+
}, null, 2))}\n`;
|
|
4028
|
+
asJson.push(`src/actions/${identifier}.json stays JSON: ${reading.reason}`);
|
|
4029
|
+
});
|
|
4030
|
+
exported.connectors.forEach(({ identifier, name, manifest }) => {
|
|
4031
|
+
files[`src/connectors/${identifier}.json`] = `${JSON.stringify({
|
|
4032
|
+
id: identifier,
|
|
4033
|
+
name,
|
|
4034
|
+
manifest
|
|
4035
|
+
}, null, 2)}\n`;
|
|
4036
|
+
});
|
|
4037
|
+
files["src/actions.ts"] = actionsModule(coded);
|
|
4038
|
+
const functions = Object.fromEntries(Object.entries(exported.functions.files).map(([path, text]) => [path, local(text)]));
|
|
4039
|
+
Object.entries(functions).forEach(([path, text]) => {
|
|
4040
|
+
files[`functions/${path}`] = text;
|
|
4041
|
+
});
|
|
4042
|
+
const runtimeEntry = source.runtime?.entries[0];
|
|
4043
|
+
if (builtOnly.runtime) binaries["vendor/runtime.bundle"] = builtOnly.runtime;
|
|
4044
|
+
files["src/main.ts"] = fromSpaceMain({
|
|
4045
|
+
source: spaceSource,
|
|
4046
|
+
...runtimeEntry ? { runtimeEntry: fromMain(place(runtimeEntry)) } : {},
|
|
4047
|
+
packedRuntime: Boolean(builtOnly.runtime),
|
|
4048
|
+
builtPlugins: builtOnly.plugins.length > 0,
|
|
4049
|
+
visitorRoles: exported.visitorRoles
|
|
4050
|
+
});
|
|
4051
|
+
const extra = Object.fromEntries(Object.entries(source.dependencies).filter(([name]) => !name.startsWith("@plitzi/") && !["react", "react-dom"].includes(name)));
|
|
4052
|
+
const plitzi = Object.fromEntries([...Object.keys(source.dependencies), ...coded.length > 0 ? ["@plitzi/sdk-authoring"] : []].filter((name) => name.startsWith("@plitzi/")).map((name) => [name, SDK_VERSION]));
|
|
4053
|
+
const variables = [...exported.variables, ...exported.credentials.map(({ identifier }) => identifier)];
|
|
4054
|
+
files[".env.example"] = `${settingsLines(exported).join("\n")}\nPORT=8080\n`;
|
|
4055
|
+
files[".prettierignore"] = `${prettierignore(PROJECT_OUTPUTS)}public\nvendor\n`;
|
|
4056
|
+
const lines = [
|
|
4057
|
+
...report.conflicts.map(({ path, kept, others }) => `${place(path)}: ${kept} and ${others.join(", ")} held different copies; ${kept}'s is kept`),
|
|
4058
|
+
...report.rangeConflicts.map(({ name, kept, others }) => `${name}: asked for at ${[kept, ...others].join(" and ")}; ${kept} is written`),
|
|
4059
|
+
...Object.entries(source.dependencies).filter(([name, range]) => name.startsWith("@plitzi/") && range !== SDK_VERSION).map(([name, range]) => `${name}: the source was written against ${range}, and the project runs ${SDK_VERSION}`),
|
|
4060
|
+
...source.plugins.flatMap(({ type, entries }) => {
|
|
4061
|
+
const entry = place(entries[0] ?? "");
|
|
4062
|
+
const folder = /^src\/plugins\/([^/]+)\/index\.ts$/.exec(entry)?.[1];
|
|
4063
|
+
return folder && `${folder.charAt(0).toLowerCase()}${folder.slice(1)}` === type ? [] : [`${type}: its source starts at ${entry}, and the project registers src/plugins/<Name>/index.ts — move it there`];
|
|
4064
|
+
}),
|
|
4065
|
+
...builtOnly.plugins.map(({ type }) => `${type}: no source of this plugin was kept, so it runs as it was built (vendor/plugins/${type}/) and cannot be changed — upload it again from its source with plitzi upload plugin`),
|
|
4066
|
+
...builtOnly.runtime ? ["The runtime came across built only (vendor/runtime.bundle): it runs, and cannot be changed"] : [],
|
|
4067
|
+
...asJson,
|
|
4068
|
+
...exported.visitorRoles.length > 0 ? [`Its visitors (${exported.visitorRoles.join(", ")}) signed in with Plitzi: here nobody signs in until the server does it itself — see the note in src/main.ts`] : [],
|
|
4069
|
+
...report.corrections,
|
|
4070
|
+
...variables.length > 0 ? [`Give the project its variables and credentials in .env: ${variables.join(", ")}`] : []
|
|
4071
|
+
];
|
|
4072
|
+
return {
|
|
4073
|
+
files,
|
|
4074
|
+
binaries,
|
|
4075
|
+
downloads: [...assets.map(({ url, path }) => ({
|
|
4076
|
+
url,
|
|
4077
|
+
to: `public/${path}`
|
|
4078
|
+
})), ...builtOnly.plugins.flatMap(({ type, files: built }) => built.map(({ url, path }) => ({
|
|
4079
|
+
url,
|
|
4080
|
+
to: `vendor/plugins/${type}/${path}`
|
|
4081
|
+
})))],
|
|
4082
|
+
dependencies: {
|
|
4083
|
+
...extra,
|
|
4084
|
+
...plitzi
|
|
4085
|
+
},
|
|
4086
|
+
functions: {
|
|
4087
|
+
version: exported.functions.version,
|
|
4088
|
+
files: functions
|
|
4089
|
+
},
|
|
4090
|
+
omit: EXAMPLE.filter((path) => !Object.hasOwn(files, path)),
|
|
4091
|
+
report: lines
|
|
4092
|
+
};
|
|
4093
|
+
};
|
|
4094
|
+
//#endregion
|
|
4095
|
+
//#region src/commands/spaceOrigin.ts
|
|
4096
|
+
/**
|
|
4097
|
+
* Where a project `plitzi create --from` made came from, and what the space gave it: `.plitzi/space.json`, committed
|
|
4098
|
+
* with the project so everyone working on it pulls from the same point.
|
|
4099
|
+
*
|
|
4100
|
+
* What it was given is kept as a digest of each file as it was written, never the file: that is enough for `plitzi
|
|
4101
|
+
* pull` to tell a file changed here from one the space changed — the two questions a pull asks of every file — and a
|
|
4102
|
+
* copy of the whole project beside the project would be the second source of it nobody keeps in step.
|
|
4103
|
+
*/
|
|
4104
|
+
var ORIGIN_FILE = path.join(".plitzi", "space.json");
|
|
4105
|
+
var isRecord$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4106
|
+
var isStrings = (value) => isRecord$4(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
4107
|
+
var digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
4108
|
+
/**
|
|
4109
|
+
* A file of the project as a pull compares it: the digest of what the project's formatter makes of it — so a file only
|
|
4110
|
+
* formatted since is not a file changed — and of its bytes as they are, which is what a project recorded before it had
|
|
4111
|
+
* a formatter. Nothing when there is no such file.
|
|
4112
|
+
*/
|
|
4113
|
+
var digestsOf = async (root, file, format) => {
|
|
4114
|
+
let bytes;
|
|
4115
|
+
try {
|
|
4116
|
+
bytes = await fs.readFile(path.join(root, file));
|
|
4117
|
+
} catch {
|
|
4118
|
+
return;
|
|
4119
|
+
}
|
|
4120
|
+
const raw = digest(bytes);
|
|
4121
|
+
return {
|
|
4122
|
+
formatted: isTextFile(file) ? digest(await format(file, bytes.toString("utf-8"))) : raw,
|
|
4123
|
+
raw
|
|
4124
|
+
};
|
|
4125
|
+
};
|
|
4126
|
+
/** The project's origin, or `undefined` when it has none — it was not made from a space, or the file is not one. */
|
|
4127
|
+
var readOrigin = async (root) => {
|
|
4128
|
+
try {
|
|
4129
|
+
const value = JSON.parse(await fs.readFile(path.join(root, ORIGIN_FILE), "utf-8"));
|
|
4130
|
+
if (!isRecord$4(value) || value.format !== 1 || typeof value.api !== "string" || !isRecord$4(value.space) || typeof value.space.id !== "number" || typeof value.space.name !== "string" || typeof value.space.permanentUrl !== "string" || value.source !== "local" && value.source !== "cloud" || !isRecord$4(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;
|
|
4131
|
+
return {
|
|
4132
|
+
format: 1,
|
|
4133
|
+
api: value.api,
|
|
4134
|
+
space: {
|
|
4135
|
+
id: value.space.id,
|
|
4136
|
+
name: value.space.name,
|
|
4137
|
+
permanentUrl: value.space.permanentUrl
|
|
4138
|
+
},
|
|
4139
|
+
source: value.source,
|
|
4140
|
+
version: {
|
|
4141
|
+
environment: value.version.environment,
|
|
4142
|
+
...typeof value.version.revision === "number" ? { revision: value.version.revision } : {}
|
|
4143
|
+
},
|
|
4144
|
+
files: value.files,
|
|
4145
|
+
downloads: value.downloads,
|
|
4146
|
+
dependencies: value.dependencies
|
|
4147
|
+
};
|
|
4148
|
+
} catch {
|
|
4149
|
+
return;
|
|
4150
|
+
}
|
|
4151
|
+
};
|
|
4152
|
+
/** Kept sorted, so a pull that changed one file changes one line of it. */
|
|
4153
|
+
var sorted = (record) => Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b)));
|
|
4154
|
+
var writeOrigin = async (root, origin) => {
|
|
4155
|
+
await fs.mkdir(path.join(root, ".plitzi"), { recursive: true });
|
|
4156
|
+
await fs.writeFile(path.join(root, ORIGIN_FILE), `${JSON.stringify({
|
|
4157
|
+
...origin,
|
|
4158
|
+
files: sorted(origin.files),
|
|
4159
|
+
downloads: sorted(origin.downloads),
|
|
4160
|
+
dependencies: sorted(origin.dependencies)
|
|
4161
|
+
}, null, 2)}\n`);
|
|
4162
|
+
};
|
|
4163
|
+
/** What the project was given, once it is on disk: each file's digest, formatted as the project formats it. */
|
|
4164
|
+
var digestsOnDisk = async (root, files, format) => {
|
|
4165
|
+
const digests = await Promise.all(files.map(async (file) => [file, (await digestsOf(root, file, format))?.formatted]));
|
|
4166
|
+
return Object.fromEntries(digests.filter((entry) => entry[1] !== void 0));
|
|
4167
|
+
};
|
|
4168
|
+
//#endregion
|
|
4169
|
+
//#region src/commands/createFrom.ts
|
|
4170
|
+
/**
|
|
4171
|
+
* `plitzi create --from <space>`: the space as Plitzi keeps it (docs/en/projects-from-spaces.md), asked for as the person — who must be able
|
|
4172
|
+
* to change it — and the files of it the project serves itself, fetched once.
|
|
4173
|
+
*/
|
|
4174
|
+
var isRecord$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4175
|
+
/** What the platform answered is an export: of the format this CLI writes, with every part it reads. */
|
|
4176
|
+
var isExport = (value) => isRecord$3(value) && value.format === 1 && isRecord$3(value.space) && isRecord$3(value.version) && isRecord$3(value.source) && isRecord$3(value.builtOnly) && isRecord$3(value.report) && isRecord$3(value.functions) && Array.isArray(value.actions) && Array.isArray(value.assets) && Array.isArray(value.visitorRoles);
|
|
4177
|
+
/** How a version is said: `the draft`, `production (latest)`, `production r3`. */
|
|
4178
|
+
var versionLabel = ({ environment, revision }) => {
|
|
4179
|
+
if (environment === "main") return "the draft";
|
|
4180
|
+
return revision ? `${environment} r${String(revision)}` : `${environment} (latest)`;
|
|
4181
|
+
};
|
|
4182
|
+
/**
|
|
4183
|
+
* A version of the space named — its id or its permanent URL — put together as a project, or why not, said and
|
|
4184
|
+
* `undefined`. `name` is what it is called in what is printed: `from`, unless a pull names it by its id.
|
|
4185
|
+
*/
|
|
4186
|
+
var fetchExport = async (api, from, { source, version, name = from }) => {
|
|
4187
|
+
const connection = await signedIn(api, `to take ${from} out of Plitzi`);
|
|
4188
|
+
if (!connection) return;
|
|
4189
|
+
console.log(`\nTaking ${chalk.bold(name)} (${versionLabel(version)}) out of ${api}…`);
|
|
4190
|
+
const query = new URLSearchParams({
|
|
4191
|
+
source,
|
|
4192
|
+
environment: version.environment,
|
|
4193
|
+
...version.revision ? { revision: String(version.revision) } : {}
|
|
4194
|
+
});
|
|
4195
|
+
const answered = await authorizedRequest(connection, `/spaces/${encodeURIComponent(from)}/export?${query.toString()}`);
|
|
4196
|
+
if (!answered.ok) {
|
|
4197
|
+
console.error(chalk.red(answered.error));
|
|
4198
|
+
process.exitCode = 1;
|
|
4199
|
+
return;
|
|
4200
|
+
}
|
|
4201
|
+
const { status, data } = answered.value.reply;
|
|
4202
|
+
if (status === 200 && isExport(data)) return data;
|
|
4203
|
+
const said = isRecord$3(data) && typeof data.error === "string" ? data.error : void 0;
|
|
4204
|
+
const why = {
|
|
4205
|
+
403: `You may not take ${from} out: it takes being able to change it — its owner, an administrator or a writer.`,
|
|
4206
|
+
404: said ?? `There is no space ${from} you can reach.`
|
|
4207
|
+
};
|
|
4208
|
+
console.error(chalk.red(why[status] ?? said ?? `The platform would not hand ${from} over (${String(status)}).`));
|
|
4209
|
+
process.exitCode = 1;
|
|
4210
|
+
};
|
|
4211
|
+
/** A file of the space's CDN, fetched. */
|
|
4212
|
+
var download = async (url) => {
|
|
4213
|
+
const response = await fetch(url);
|
|
4214
|
+
if (!response.ok) throw new Error(String(response.status));
|
|
4215
|
+
return Buffer.from(await response.arrayBuffer());
|
|
4216
|
+
};
|
|
4217
|
+
/** Why a file could not be fetched, as the report says it. */
|
|
4218
|
+
var notFetched = (to, url, error) => `${to}: could not be fetched from ${url} (${error instanceof Error ? error.message : String(error)})`;
|
|
4219
|
+
/** The project's binary files written, and the space's files fetched from its CDN into the project to be served by it. */
|
|
4220
|
+
var writeFromSpace = async (target, project) => {
|
|
4221
|
+
for (const [file, base64] of Object.entries(project.binaries)) {
|
|
4222
|
+
await fs.mkdir(path.dirname(path.join(target, file)), { recursive: true });
|
|
4223
|
+
await fs.writeFile(path.join(target, file), Buffer.from(base64, "base64"));
|
|
4224
|
+
}
|
|
4225
|
+
const missing = [];
|
|
4226
|
+
for (const { url, to } of project.downloads) try {
|
|
4227
|
+
const bytes = await download(url);
|
|
4228
|
+
await fs.mkdir(path.dirname(path.join(target, to)), { recursive: true });
|
|
4229
|
+
await fs.writeFile(path.join(target, to), bytes);
|
|
4230
|
+
} catch (error) {
|
|
4231
|
+
missing.push(notFetched(to, url, error));
|
|
4232
|
+
}
|
|
4233
|
+
return missing;
|
|
4234
|
+
};
|
|
4235
|
+
/** The space's functions as they are on disk: what `plitzi functions push` compares the project's with. */
|
|
4236
|
+
var functionsOnDisk = async (root, project) => Object.fromEntries(await Promise.all(Object.keys(project.functions.files).map(async (file) => [file, await fs.readFile(path.join(root, "functions", file), "utf-8")])));
|
|
4237
|
+
/**
|
|
4238
|
+
* What the project was given, recorded once it is on disk as it will stay — installed and formatted — so `plitzi pull`
|
|
4239
|
+
* tells what changed here from what changed on the space; and its functions as a pulled working copy, so `plitzi
|
|
4240
|
+
* functions push` sends them back to the space they came from.
|
|
4241
|
+
*/
|
|
4242
|
+
var recordOrigin = async (root, { api, exported, source, pinned, project }) => {
|
|
4243
|
+
const files = await digestsOnDisk(root, [
|
|
4244
|
+
...Object.keys(project.files),
|
|
4245
|
+
...Object.keys(project.binaries),
|
|
4246
|
+
...project.downloads.map(({ to }) => to)
|
|
4247
|
+
], await projectFormatter(root));
|
|
4248
|
+
await writeOrigin(root, {
|
|
4249
|
+
format: 1,
|
|
4250
|
+
api,
|
|
4251
|
+
space: exported.space,
|
|
4252
|
+
source,
|
|
4253
|
+
version: {
|
|
4254
|
+
environment: exported.version.environment,
|
|
4255
|
+
...pinned ? { revision: exported.version.revision } : {}
|
|
4256
|
+
},
|
|
4257
|
+
files,
|
|
4258
|
+
downloads: Object.fromEntries(project.downloads.filter(({ to }) => Object.hasOwn(files, to)).map(({ url, to }) => [to, url])),
|
|
4259
|
+
dependencies: project.dependencies
|
|
4260
|
+
});
|
|
4261
|
+
await writeFunctionsState(root, {
|
|
4262
|
+
space: exported.space.id,
|
|
4263
|
+
version: project.functions.version,
|
|
4264
|
+
files: await functionsOnDisk(root, project)
|
|
4265
|
+
});
|
|
4266
|
+
};
|
|
4267
|
+
//#endregion
|
|
4268
|
+
//#region src/commands/create.ts
|
|
4269
|
+
var MODES = ["server", "client"];
|
|
4270
|
+
var SOURCES = ["local", "cloud"];
|
|
4271
|
+
/**
|
|
4272
|
+
* The choices a project is BUILT around, which belong to whoever it is for — never to a default nobody chose.
|
|
4273
|
+
*
|
|
4274
|
+
* Each one is written into the project: the package manager into every command its README, lockfile, Playwright config
|
|
4275
|
+
* and install use; the mode into whether there is a Node tier at all; the source into whether the space is in the repo.
|
|
4276
|
+
* Assumed, they are the wrong project for somebody, and an agent running this was the one assuming. So what was not
|
|
4277
|
+
* passed is asked for when a person is at the terminal, and refused when nobody is — with the exact questions to put
|
|
4278
|
+
* to the person.
|
|
4279
|
+
*/
|
|
4280
|
+
var resolveDecisions = async (options) => {
|
|
4281
|
+
const detected = detectPackageManager();
|
|
4282
|
+
const packageManager = {
|
|
4283
|
+
flag: "--package-manager",
|
|
4284
|
+
choices: PACKAGE_MANAGERS,
|
|
4285
|
+
question: `Which package manager will you work in? (this was run through ${detected})`,
|
|
4286
|
+
given: PACKAGE_MANAGERS.find((manager) => manager === options.packageManager),
|
|
4287
|
+
fallback: detected
|
|
4288
|
+
};
|
|
4289
|
+
const mode = {
|
|
4290
|
+
flag: "--mode",
|
|
4291
|
+
choices: MODES,
|
|
4292
|
+
question: "Should pages render on a Node server of your own (server: SSR + RSC), or only in the browser (client)?",
|
|
4293
|
+
given: MODES.find((candidate) => candidate === options.mode),
|
|
4294
|
+
fallback: "server"
|
|
4295
|
+
};
|
|
4296
|
+
const source = {
|
|
4297
|
+
flag: "--source",
|
|
4298
|
+
choices: SOURCES,
|
|
4299
|
+
question: "Should the space live in the project as code you edit (local), or be read live from your Plitzi account (cloud, which needs a key from Credentials in the builder)?",
|
|
4300
|
+
given: SOURCES.find((candidate) => candidate === options.source),
|
|
4301
|
+
fallback: "local"
|
|
4302
|
+
};
|
|
4303
|
+
const missing = [
|
|
4304
|
+
packageManager,
|
|
4305
|
+
mode,
|
|
4306
|
+
source
|
|
4307
|
+
].filter((question) => question.given === void 0);
|
|
4308
|
+
if (missing.length === 0 || options.yes && atTerminal()) return {
|
|
4309
|
+
packageManager: packageManager.given ?? packageManager.fallback,
|
|
4310
|
+
mode: mode.given ?? mode.fallback,
|
|
4311
|
+
source: source.given ?? source.fallback
|
|
4312
|
+
};
|
|
4313
|
+
if (!atTerminal()) {
|
|
4314
|
+
refuseWithoutTerminal("project", missing);
|
|
4315
|
+
return;
|
|
4316
|
+
}
|
|
4317
|
+
const rl = readline.createInterface({
|
|
4318
|
+
input: process.stdin,
|
|
4319
|
+
output: process.stdout
|
|
4320
|
+
});
|
|
4321
|
+
try {
|
|
4322
|
+
const manager = packageManager.given ?? await ask(rl, packageManager);
|
|
4323
|
+
const tier = manager && (mode.given ?? await ask(rl, mode));
|
|
4324
|
+
const origin = tier && (source.given ?? await ask(rl, source));
|
|
4325
|
+
if (!manager || !tier || !origin) {
|
|
4326
|
+
process.exitCode = 1;
|
|
4327
|
+
return;
|
|
4328
|
+
}
|
|
4329
|
+
return {
|
|
4330
|
+
packageManager: manager,
|
|
4331
|
+
mode: tier,
|
|
4332
|
+
source: origin
|
|
4333
|
+
};
|
|
4334
|
+
} finally {
|
|
4335
|
+
rl.close();
|
|
4336
|
+
}
|
|
4337
|
+
};
|
|
4338
|
+
var keyQuestion = (mode) => `The ${mode === "server" ? "self-hosting" : "public render"} key the project reads the space with (Credentials, in the builder).`;
|
|
4339
|
+
var askForKey = async (mode) => {
|
|
4340
|
+
const rl = readline.createInterface({
|
|
4341
|
+
input: process.stdin,
|
|
4342
|
+
output: process.stdout
|
|
4343
|
+
});
|
|
4344
|
+
try {
|
|
4345
|
+
return (await rl.question(`\n${keyQuestion(mode)}\n> `)).trim();
|
|
4346
|
+
} finally {
|
|
4347
|
+
rl.close();
|
|
4348
|
+
}
|
|
4349
|
+
};
|
|
4350
|
+
var create = async (directory, options) => {
|
|
4351
|
+
const target = path.resolve(directory ?? ".");
|
|
4352
|
+
const name = path.basename(target);
|
|
4353
|
+
const wasEmpty = await isEmpty(target);
|
|
4354
|
+
if (!options.force && !wasEmpty) {
|
|
4355
|
+
console.error(chalk.red(`${target} is not empty. Pass --force to write into it anyway.`));
|
|
4356
|
+
process.exitCode = 1;
|
|
4357
|
+
return;
|
|
4358
|
+
}
|
|
4359
|
+
if (options.from && options.mode === "client") {
|
|
4360
|
+
console.error(chalk.red("A project made from a space runs its own server: leave out --mode, or pass --mode server."));
|
|
4361
|
+
process.exitCode = 1;
|
|
4362
|
+
return;
|
|
4363
|
+
}
|
|
4364
|
+
const environment = options.environment ?? "main";
|
|
4365
|
+
const revision = options.revision === void 0 ? void 0 : Number(options.revision);
|
|
4366
|
+
if (revision !== void 0 && (!Number.isInteger(revision) || revision < 1 || environment === "main")) {
|
|
4367
|
+
console.error(chalk.red(environment === "main" ? "The draft (main) has no revisions: name a published environment with --environment, then --revision." : `--revision takes a revision number from 1, not "${options.revision ?? ""}".`));
|
|
4368
|
+
process.exitCode = 1;
|
|
4369
|
+
return;
|
|
4370
|
+
}
|
|
4371
|
+
const decisions = await resolveDecisions(options.from ? {
|
|
4372
|
+
...options,
|
|
4373
|
+
mode: "server"
|
|
4374
|
+
} : options);
|
|
4375
|
+
if (!decisions) return;
|
|
4376
|
+
const { packageManager, mode, source } = decisions;
|
|
4377
|
+
if (revision !== void 0 && !options.from && source === "local") {
|
|
4378
|
+
console.error(chalk.red("--revision pins a published version: it goes with --from, or with --source cloud."));
|
|
4379
|
+
process.exitCode = 1;
|
|
4380
|
+
return;
|
|
4381
|
+
}
|
|
4382
|
+
if (source === "cloud" && !options.key && !atTerminal()) {
|
|
4383
|
+
refuseWithoutTerminal("project", [{
|
|
4384
|
+
flag: "--key",
|
|
4385
|
+
choices: ["<key>"],
|
|
4386
|
+
question: keyQuestion(mode)
|
|
4387
|
+
}]);
|
|
4388
|
+
return;
|
|
4389
|
+
}
|
|
4390
|
+
const key = source === "cloud" ? options.key ?? await askForKey(mode) : "";
|
|
4391
|
+
if (source === "cloud" && !key) {
|
|
4392
|
+
console.error(chalk.red("\nNo key. Mint one under Credentials in the builder, then run this again."));
|
|
4393
|
+
process.exitCode = 1;
|
|
4394
|
+
return;
|
|
4395
|
+
}
|
|
4396
|
+
const answers = {
|
|
4397
|
+
name,
|
|
4398
|
+
mode,
|
|
4399
|
+
source,
|
|
4400
|
+
key,
|
|
4401
|
+
environment,
|
|
4402
|
+
...revision ? { revision } : {},
|
|
4403
|
+
packageManager,
|
|
4404
|
+
managerVersion: detectManagerVersion(packageManager, await nearestExisting(target))
|
|
4405
|
+
};
|
|
4406
|
+
const api = options.from ? await apiFor(options) : void 0;
|
|
4407
|
+
const exported = options.from && api ? await fetchExport(api, options.from, {
|
|
4408
|
+
source,
|
|
4409
|
+
version: {
|
|
4410
|
+
environment,
|
|
4411
|
+
...revision ? { revision } : {}
|
|
4412
|
+
}
|
|
4413
|
+
}) : void 0;
|
|
4414
|
+
if (options.from && !exported) return;
|
|
4415
|
+
const fromSpace = exported ? projectFromSpace(exported, source) : void 0;
|
|
4416
|
+
const files = Object.fromEntries(Object.entries(scaffold(answers)).filter(([file]) => !fromSpace?.omit.includes(file)));
|
|
4417
|
+
await writeFiles(target, exported && fromSpace ? {
|
|
4418
|
+
...files,
|
|
4419
|
+
...fromSpace.files,
|
|
4420
|
+
"package.json": packageJson$1(answers, fromSpace.dependencies),
|
|
4421
|
+
".env": envFromSpace(exported, answers, randomBytes(32).toString("hex"))
|
|
4422
|
+
} : files);
|
|
4423
|
+
const missing = fromSpace ? await writeFromSpace(target, fromSpace) : [];
|
|
4424
|
+
const wantsInstall = options.install !== false;
|
|
4425
|
+
const installed = wantsInstall && await install(packageManager, target);
|
|
4426
|
+
/**
|
|
4427
|
+
* A failed install is a failed command, not a next step.
|
|
4428
|
+
*
|
|
4429
|
+
* The files are already written, so the summary below still prints — but printed on its own after a screen of
|
|
4430
|
+
* resolver errors it reads as success, and a script that ran `create` carries on into a project with no
|
|
4431
|
+
* `node_modules`.
|
|
4432
|
+
*/
|
|
4433
|
+
if (wantsInstall && !installed) {
|
|
4434
|
+
console.error(chalk.red(`\n\`${installCommand(packageManager)}\` failed. The project is written; the reason is in the output above.`));
|
|
4435
|
+
console.error(chalk.dim(INSTALL_HINTS[packageManager]));
|
|
4436
|
+
process.exitCode = 1;
|
|
4437
|
+
}
|
|
4438
|
+
/**
|
|
4439
|
+
* Formatted by the project's own Prettier, once, so the first commit is already in its style — the scaffold writes
|
|
4440
|
+
* tables and lists whose layout depends on what they hold. Only into a folder that was empty: `--force` into one
|
|
4441
|
+
* with work in it must never reformat that work.
|
|
4442
|
+
*/
|
|
4443
|
+
if (installed && wasEmpty) await runScript(packageManager, "format", target);
|
|
4444
|
+
if (api && exported && fromSpace) await recordOrigin(target, {
|
|
4445
|
+
api,
|
|
4446
|
+
exported,
|
|
4447
|
+
source,
|
|
4448
|
+
pinned: revision !== void 0,
|
|
4449
|
+
project: fromSpace
|
|
4450
|
+
});
|
|
4451
|
+
const where = cdPrefix(target);
|
|
4452
|
+
console.log(chalk.green(`\n${name} — ${mode === "server" ? "server-rendered" : "browser-rendered"}, space ${source === "cloud" ? "from Plitzi" : "in the project"}.`));
|
|
4453
|
+
console.log("");
|
|
4454
|
+
if (!installed) console.log(` ${where}${installCommand(packageManager)}`);
|
|
4455
|
+
console.log(` ${where}${runCommand(packageManager, "start")}`);
|
|
4456
|
+
console.log("");
|
|
4457
|
+
console.log(chalk.dim(`.claude/skills carries Plitzi's authoring skill; \`${runCommand(packageManager, "visual")}\` opens the page and checks it.`));
|
|
4458
|
+
if (source === "cloud") console.log(chalk.dim(".env holds the key and is already git-ignored."));
|
|
4459
|
+
if (exported) {
|
|
4460
|
+
const { version } = exported;
|
|
4461
|
+
const said = version.snapshot ? ` — “${version.snapshot.description}”, made ${version.snapshot.publishedAt}` : "";
|
|
4462
|
+
console.log(chalk.dim(`Taken from ${versionLabel(version)}${said}.`));
|
|
4463
|
+
}
|
|
4464
|
+
const notes = [...fromSpace?.report ?? [], ...missing];
|
|
4465
|
+
if (exported && notes.length > 0) {
|
|
4466
|
+
console.log(chalk.yellow(`\nWhat to know about ${exported.space.name} here:`));
|
|
4467
|
+
notes.forEach((note) => console.log(chalk.yellow(` - ${note}`)));
|
|
4468
|
+
}
|
|
4469
|
+
console.log("");
|
|
4470
|
+
};
|
|
4471
|
+
//#endregion
|
|
4472
|
+
//#region src/commands/createPlugin.ts
|
|
4473
|
+
var DEFAULT_NAME = "plitzi-plugin-my-element";
|
|
4474
|
+
/** What an agent is told to ask for, and the flags that carry the answers. */
|
|
4475
|
+
var QUESTIONS = {
|
|
4476
|
+
name: {
|
|
4477
|
+
flag: "--name",
|
|
4478
|
+
choices: ["<package name>"],
|
|
4479
|
+
question: "What is the plugin called? Its package name, e.g. plitzi-plugin-seat-picker."
|
|
4480
|
+
},
|
|
4481
|
+
directory: {
|
|
4482
|
+
flag: "<directory> (the first argument)",
|
|
4483
|
+
choices: ["<folder>"],
|
|
4484
|
+
question: "Which folder should the plugin be written to?"
|
|
4485
|
+
}
|
|
4486
|
+
};
|
|
4487
|
+
var splitNames = (list) => list.split(",").map((name) => name.trim()).filter(Boolean);
|
|
4488
|
+
/**
|
|
4489
|
+
* What is wrong with the other elements asked for, if anything: a name that makes no element, or two elements — the
|
|
4490
|
+
* package's own among them — that would share a type, which a space could not tell apart.
|
|
4491
|
+
*/
|
|
4492
|
+
var elementListProblem = (main, others) => {
|
|
4493
|
+
for (const name of others) {
|
|
4494
|
+
const problem = pluginNameProblem(name);
|
|
4495
|
+
if (problem) return problem;
|
|
4496
|
+
}
|
|
4497
|
+
const types = [pluginNames(main).type, ...others.map((name) => pluginNames(name).type)];
|
|
4498
|
+
const repeated = types.find((type, index) => types.indexOf(type) !== index);
|
|
4499
|
+
return repeated ? `Two elements would both be "${repeated}": give each a name of its own.` : void 0;
|
|
4500
|
+
};
|
|
4501
|
+
/** The folders a repository keeps its packages in, as places for this one — or `./<name>` outside of any. */
|
|
4502
|
+
var folderOptions = (project, base) => {
|
|
4503
|
+
if (!project) return [];
|
|
4504
|
+
const root = project.workspaceRoot ?? project.root;
|
|
4505
|
+
return [.../* @__PURE__ */ new Set([...project.workspaceFolders, "plugins"])].map((folder) => {
|
|
4506
|
+
const value = path.join(root, folder, base);
|
|
4507
|
+
return {
|
|
4508
|
+
label: path.relative(process.cwd(), value) || ".",
|
|
4509
|
+
value
|
|
4510
|
+
};
|
|
4511
|
+
});
|
|
4512
|
+
};
|
|
4513
|
+
var askDirectory = async (rl, project, base) => {
|
|
4514
|
+
const typed = {
|
|
4515
|
+
question: "Which folder? (relative to here)",
|
|
3563
4516
|
fallback: `./${base}`
|
|
3564
4517
|
};
|
|
3565
4518
|
if (!project) return path.resolve(await askText(rl, "Which folder should the plugin be written to?", typed.fallback));
|
|
@@ -3658,316 +4611,251 @@ var createPlugin = async (directory, options) => {
|
|
|
3658
4611
|
inProject: project !== void 0
|
|
3659
4612
|
}));
|
|
3660
4613
|
/**
|
|
3661
|
-
* Installed where it will be installed from. Inside a workspace that covers the folder, installing there installs
|
|
3662
|
-
* the workspace; inside a repository that does not list it, Yarn refuses to install a package it does not know —
|
|
3663
|
-
* so that is said, with the fix, rather than failed at.
|
|
3664
|
-
*/
|
|
3665
|
-
const unlisted = project?.workspaceRoot !== void 0 && !coveredByWorkspace(project, target);
|
|
3666
|
-
const wantsInstall = options.install !== false && !unlisted;
|
|
3667
|
-
const installed = wantsInstall && await install(packageManager, target);
|
|
3668
|
-
if (wantsInstall && !installed) {
|
|
3669
|
-
console.error(chalk.red(`\n\`${installCommand(packageManager)}\` failed. The plugin is written; the reason is in the output above.`));
|
|
3670
|
-
console.error(chalk.dim(INSTALL_HINTS[packageManager]));
|
|
3671
|
-
process.exitCode = 1;
|
|
3672
|
-
}
|
|
3673
|
-
if (installed && wasEmpty) await runScript(packageManager, "format", target);
|
|
3674
|
-
const where = cdPrefix(target);
|
|
3675
|
-
console.log(chalk.green(`\n${name} — the ${names.type} element, as a package any space can load.`));
|
|
3676
|
-
if (unlisted) console.log(chalk.yellow(`\n${path.relative(process.cwd(), target)} is not one of the workspace's packages: add it to \`workspaces\`, then install.`));
|
|
3677
|
-
console.log("");
|
|
3678
|
-
if (!installed) console.log(` ${where}${installCommand(packageManager)}`);
|
|
3679
|
-
console.log(` ${where}${runCommand(packageManager, "start")} # the plugin inside a space`);
|
|
3680
|
-
console.log(` ${where}${runCommand(packageManager, "zip")} # what the builder takes, under Resources`);
|
|
3681
|
-
console.log("");
|
|
3682
|
-
} finally {
|
|
3683
|
-
rl?.close();
|
|
3684
|
-
}
|
|
3685
|
-
};
|
|
3686
|
-
//#endregion
|
|
3687
|
-
//#region ../../packages/sdk-shared/dist/actions/functions.mjs
|
|
3688
|
-
var n = (e) => /^[A-Za-z0-9_][A-Za-z0-9_./-]*\.(ts|js|mjs|json)$/.test(e) && !e.split("/").some((e) => e === ".." || e === "." || e === "");
|
|
3689
|
-
var r = async (e, t) => {
|
|
3690
|
-
let r = {}, i = async (a) => {
|
|
3691
|
-
let o;
|
|
3692
|
-
try {
|
|
3693
|
-
o = await t.list(a ? `${e}/${a}` : e);
|
|
3694
|
-
} catch {
|
|
3695
|
-
return;
|
|
3696
|
-
}
|
|
3697
|
-
for (let s of o) {
|
|
3698
|
-
let o = a ? `${a}/${s.name}` : s.name;
|
|
3699
|
-
s.directory ? s.name !== "node_modules" && !s.name.startsWith(".") && await i(o) : n(o) && (r[o] = await t.read(`${e}/${o}`));
|
|
3700
|
-
}
|
|
3701
|
-
};
|
|
3702
|
-
return await i(""), r;
|
|
3703
|
-
};
|
|
3704
|
-
//#endregion
|
|
3705
|
-
//#region src/commands/functions.ts
|
|
3706
|
-
var FUNCTIONS_DIR = "functions";
|
|
3707
|
-
var STATE_FILE = path.join(".plitzi", "functions.json");
|
|
3708
|
-
var isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3709
|
-
/** The project's root: where `functions/` and `.plitzi/` go. */
|
|
3710
|
-
var rootOf = async () => (await findProject(process.cwd()))?.root ?? process.cwd();
|
|
3711
|
-
/** Every source file under `functions/`, by its path there — read by the one rule of what a source is. */
|
|
3712
|
-
var readLocal = (root) => r(path.join(root, FUNCTIONS_DIR), {
|
|
3713
|
-
list: async (dir) => (await fs.readdir(dir, { withFileTypes: true })).map((entry) => ({
|
|
3714
|
-
name: entry.name,
|
|
3715
|
-
directory: entry.isDirectory()
|
|
3716
|
-
})),
|
|
3717
|
-
read: (file) => fs.readFile(file, "utf8")
|
|
3718
|
-
});
|
|
3719
|
-
var readState = async (root) => {
|
|
3720
|
-
try {
|
|
3721
|
-
const value = JSON.parse(await fs.readFile(path.join(root, STATE_FILE), "utf8"));
|
|
3722
|
-
if (!isRecord$2(value) || typeof value.space !== "number" || typeof value.version !== "string" || !isRecord$2(value.files)) return;
|
|
3723
|
-
const files = Object.fromEntries(Object.entries(value.files).flatMap(([name, text]) => typeof text === "string" ? [[name, text]] : []));
|
|
3724
|
-
return {
|
|
3725
|
-
space: value.space,
|
|
3726
|
-
version: value.version,
|
|
3727
|
-
files
|
|
3728
|
-
};
|
|
3729
|
-
} catch {
|
|
3730
|
-
return;
|
|
3731
|
-
}
|
|
3732
|
-
};
|
|
3733
|
-
var writeState = async (root, state) => {
|
|
3734
|
-
await fs.mkdir(path.join(root, ".plitzi"), { recursive: true });
|
|
3735
|
-
await fs.writeFile(path.join(root, STATE_FILE), `${JSON.stringify(state, null, 2)}\n`);
|
|
3736
|
-
};
|
|
3737
|
-
/** The files that differ between two copies — added, removed or changed — sorted. */
|
|
3738
|
-
var changedFiles = (a, b) => [.../* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])].filter((file) => a[file] !== b[file]).sort();
|
|
3739
|
-
var taskList = (draft) => draft.manifest?.tasks.length ? draft.manifest.tasks.map((task) => `${task.namespace}.${task.action}`).join(", ") : "no tasks";
|
|
3740
|
-
var connect$1 = async (options, doing) => {
|
|
3741
|
-
const api = await apiFor(options);
|
|
3742
|
-
return api ? connectionWithSpace(api, doing) : void 0;
|
|
3743
|
-
};
|
|
3744
|
-
var readDraft = async (connection, spaceId) => {
|
|
3745
|
-
const answered = await authorizedRequest(connection, `/spaces/${spaceId}/functions`);
|
|
3746
|
-
if (!answered.ok) {
|
|
3747
|
-
fail(answered.error);
|
|
3748
|
-
return;
|
|
3749
|
-
}
|
|
3750
|
-
const { reply } = answered.value;
|
|
3751
|
-
if (reply.status !== 200) {
|
|
3752
|
-
fail(reply.data.error ?? `Could not read the space’s functions (${reply.status}).`);
|
|
3753
|
-
return;
|
|
3754
|
-
}
|
|
3755
|
-
return reply.data;
|
|
3756
|
-
};
|
|
3757
|
-
var pullFunctions = async (options) => {
|
|
3758
|
-
const connection = await connect$1(options, "to pull from");
|
|
3759
|
-
if (!connection?.space) return;
|
|
3760
|
-
const root = await rootOf();
|
|
3761
|
-
const [local, state] = await Promise.all([readLocal(root), readState(root)]);
|
|
3762
|
-
const unpushed = state ? changedFiles(local, state.files) : Object.keys(local);
|
|
3763
|
-
if (!options.force && (state?.space ?? connection.space.id) !== connection.space.id) {
|
|
3764
|
-
fail("functions/ is a copy of another space’s. Pull into another project, or pass --force to replace it.");
|
|
3765
|
-
return;
|
|
3766
|
-
}
|
|
3767
|
-
if (!options.force && unpushed.length) {
|
|
3768
|
-
fail(`Pulling would overwrite what is not pushed yet: ${unpushed.join(", ")}.\nPush it first, or pass --force to throw it away.`);
|
|
3769
|
-
return;
|
|
3770
|
-
}
|
|
3771
|
-
const draft = await readDraft(connection, connection.space.id);
|
|
3772
|
-
if (!draft) return;
|
|
3773
|
-
const base = path.join(root, FUNCTIONS_DIR);
|
|
3774
|
-
await Promise.all(Object.keys(local).filter((file) => !Object.hasOwn(draft.files, file)).map((file) => fs.rm(path.join(base, file))));
|
|
3775
|
-
for (const [file, text] of Object.entries(draft.files)) {
|
|
3776
|
-
await fs.mkdir(path.dirname(path.join(base, file)), { recursive: true });
|
|
3777
|
-
await fs.writeFile(path.join(base, file), text);
|
|
3778
|
-
}
|
|
3779
|
-
await writeState(root, {
|
|
3780
|
-
space: connection.space.id,
|
|
3781
|
-
version: draft.version,
|
|
3782
|
-
files: draft.files
|
|
3783
|
-
});
|
|
3784
|
-
const count = Object.keys(draft.files).length;
|
|
3785
|
-
console.log(count ? `Pulled ${count} file${count === 1 ? "" : "s"} of ${chalk.bold(connection.space.name)}’s functions into functions/ — ${taskList(draft)}.` : `${chalk.bold(connection.space.name)} has no functions yet. Write functions/index.ts and plitzi functions push.`);
|
|
3786
|
-
};
|
|
3787
|
-
var printProblems = (problems) => {
|
|
3788
|
-
console.error(chalk.red("The functions were not saved:"));
|
|
3789
|
-
problems.forEach(({ file, line, column, message }) => {
|
|
3790
|
-
const where = file ? `functions/${file}${line ? `:${String(line)}${column ? `:${String(column)}` : ""}` : ""}` : "functions";
|
|
3791
|
-
console.error(` ${chalk.bold(where)} ${message}`);
|
|
3792
|
-
});
|
|
3793
|
-
process.exitCode = 1;
|
|
3794
|
-
};
|
|
3795
|
-
var pushFunctions = async (options) => {
|
|
3796
|
-
const connection = await connect$1(options, "to push to");
|
|
3797
|
-
if (!connection?.space) return;
|
|
3798
|
-
const root = await rootOf();
|
|
3799
|
-
const [local, state] = await Promise.all([readLocal(root), readState(root)]);
|
|
3800
|
-
if (!Object.hasOwn(local, "index.ts")) {
|
|
3801
|
-
fail("There is no functions/index.ts here: it is where a space’s functions start. Pull them, or write it.");
|
|
3802
|
-
return;
|
|
3803
|
-
}
|
|
3804
|
-
let base = state?.space === connection.space.id ? state.version : void 0;
|
|
3805
|
-
if (base === void 0) {
|
|
3806
|
-
const draft = await readDraft(connection, connection.space.id);
|
|
3807
|
-
if (!draft) return;
|
|
3808
|
-
if (Object.keys(draft.files).length) {
|
|
3809
|
-
fail(`${connection.space.name} already has functions. Pull them first: plitzi functions pull.`);
|
|
3810
|
-
return;
|
|
3811
|
-
}
|
|
3812
|
-
base = draft.version;
|
|
3813
|
-
}
|
|
3814
|
-
if (state?.space === connection.space.id && !changedFiles(local, state.files).length) {
|
|
3815
|
-
console.log("Nothing to push: functions/ is what was pulled.");
|
|
3816
|
-
return;
|
|
3817
|
-
}
|
|
3818
|
-
const answered = await authorizedRequest(connection, `/spaces/${connection.space.id}/functions`, {
|
|
3819
|
-
method: "PUT",
|
|
3820
|
-
headers: { "Content-Type": "application/json" },
|
|
3821
|
-
body: JSON.stringify({
|
|
3822
|
-
files: local,
|
|
3823
|
-
base
|
|
3824
|
-
})
|
|
3825
|
-
});
|
|
3826
|
-
if (!answered.ok) {
|
|
3827
|
-
fail(answered.error);
|
|
3828
|
-
return;
|
|
3829
|
-
}
|
|
3830
|
-
const { reply } = answered.value;
|
|
3831
|
-
if (reply.status === 422 && reply.data.problems) {
|
|
3832
|
-
printProblems(reply.data.problems);
|
|
3833
|
-
return;
|
|
3834
|
-
}
|
|
3835
|
-
if (reply.status === 409) {
|
|
3836
|
-
fail(`${connection.space.name}’s functions changed since your pull — in the builder, or from another copy. Keep your changes aside, pull, and apply them again.`);
|
|
3837
|
-
return;
|
|
3838
|
-
}
|
|
3839
|
-
if (reply.status !== 200 || !reply.data.version) {
|
|
3840
|
-
fail(reply.data.refusal?.error ?? reply.data.error ?? `The functions were not saved (${reply.status}).`);
|
|
3841
|
-
return;
|
|
4614
|
+
* Installed where it will be installed from. Inside a workspace that covers the folder, installing there installs
|
|
4615
|
+
* the workspace; inside a repository that does not list it, Yarn refuses to install a package it does not know —
|
|
4616
|
+
* so that is said, with the fix, rather than failed at.
|
|
4617
|
+
*/
|
|
4618
|
+
const unlisted = project?.workspaceRoot !== void 0 && !coveredByWorkspace(project, target);
|
|
4619
|
+
const wantsInstall = options.install !== false && !unlisted;
|
|
4620
|
+
const installed = wantsInstall && await install(packageManager, target);
|
|
4621
|
+
if (wantsInstall && !installed) {
|
|
4622
|
+
console.error(chalk.red(`\n\`${installCommand(packageManager)}\` failed. The plugin is written; the reason is in the output above.`));
|
|
4623
|
+
console.error(chalk.dim(INSTALL_HINTS[packageManager]));
|
|
4624
|
+
process.exitCode = 1;
|
|
4625
|
+
}
|
|
4626
|
+
if (installed && wasEmpty) await runScript(packageManager, "format", target);
|
|
4627
|
+
const where = cdPrefix(target);
|
|
4628
|
+
console.log(chalk.green(`\n${name} — the ${names.type} element, as a package any space can load.`));
|
|
4629
|
+
if (unlisted) console.log(chalk.yellow(`\n${path.relative(process.cwd(), target)} is not one of the workspace's packages: add it to \`workspaces\`, then install.`));
|
|
4630
|
+
console.log("");
|
|
4631
|
+
if (!installed) console.log(` ${where}${installCommand(packageManager)}`);
|
|
4632
|
+
console.log(` ${where}${runCommand(packageManager, "start")} # the plugin inside a space`);
|
|
4633
|
+
console.log(` ${where}${runCommand(packageManager, "zip")} # what the builder takes, under Resources`);
|
|
4634
|
+
console.log("");
|
|
4635
|
+
} finally {
|
|
4636
|
+
rl?.close();
|
|
3842
4637
|
}
|
|
3843
|
-
await writeState(root, {
|
|
3844
|
-
space: connection.space.id,
|
|
3845
|
-
version: reply.data.version,
|
|
3846
|
-
files: local
|
|
3847
|
-
});
|
|
3848
|
-
console.log(`Pushed to ${chalk.bold(connection.space.name)}’s draft — ${taskList({ manifest: reply.data.manifest ?? null })}. ` + chalk.dim("The live site runs them once the space is published."));
|
|
3849
4638
|
};
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
4639
|
+
//#endregion
|
|
4640
|
+
//#region ../../packages/sdk-shared/dist/source/snapshot.mjs
|
|
4641
|
+
var t = {
|
|
4642
|
+
files: 2e3,
|
|
4643
|
+
fileBytes: 4194304,
|
|
4644
|
+
totalBytes: 25165824
|
|
4645
|
+
};
|
|
4646
|
+
var n = ["plugin", "runtime"];
|
|
4647
|
+
var r = [
|
|
4648
|
+
/(^|\/)\.env(\.|$)/,
|
|
4649
|
+
/\.(pem|key|p12|pfx)$/i,
|
|
4650
|
+
/(^|\/)id_(rsa|dsa|ecdsa|ed25519)$/
|
|
4651
|
+
];
|
|
4652
|
+
var i = [
|
|
4653
|
+
{
|
|
4654
|
+
what: "a private key",
|
|
4655
|
+
pattern: /-----BEGIN (?:[A-Z]+ )?PRIVATE KEY-----/
|
|
4656
|
+
},
|
|
4657
|
+
{
|
|
4658
|
+
what: "an AWS access key",
|
|
4659
|
+
pattern: /\bAKIA[0-9A-Z]{16}\b/
|
|
4660
|
+
},
|
|
4661
|
+
{
|
|
4662
|
+
what: "a Stripe live key",
|
|
4663
|
+
pattern: /\b[sr]k_live_[0-9a-zA-Z]{20,}\b/
|
|
4664
|
+
},
|
|
4665
|
+
{
|
|
4666
|
+
what: "a GitHub token",
|
|
4667
|
+
pattern: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/
|
|
4668
|
+
},
|
|
4669
|
+
{
|
|
4670
|
+
what: "a Slack token",
|
|
4671
|
+
pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/
|
|
3858
4672
|
}
|
|
3859
|
-
|
|
3860
|
-
|
|
4673
|
+
];
|
|
4674
|
+
var a = (e) => {
|
|
4675
|
+
let t = e.split("/");
|
|
4676
|
+
if (!e || e.startsWith("/") || /^[a-zA-Z]:/.test(e) || e.includes("\\")) return `"${e}" is not a path relative to the project, with forward slashes`;
|
|
4677
|
+
if (t.some((e) => e === "" || e === "." || e === "..")) return `"${e}" climbs out of the project, or has an empty part`;
|
|
4678
|
+
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`;
|
|
4679
|
+
if (r.some((t) => t.test(e))) return `"${e}" is a file of credentials: they stay where the project runs, never in what Plitzi keeps`;
|
|
4680
|
+
};
|
|
4681
|
+
var o = (e) => i.find(({ pattern: t }) => t.test(e))?.what;
|
|
4682
|
+
var s = (e) => typeof e == "string" && n.some((t) => t === e);
|
|
4683
|
+
var c = (e) => typeof e == "string" && /^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(e);
|
|
4684
|
+
var l = (e) => Array.isArray(e) && e.length > 0 && e.every((e) => typeof e == "string");
|
|
4685
|
+
var u = (e) => typeof e == "object" && !!e && !Array.isArray(e);
|
|
4686
|
+
var d = (e) => u(e) && Object.values(e).every((e) => typeof e == "string");
|
|
4687
|
+
var f = (e) => Math.floor(e.length * 3 / 4) - (e.endsWith("==") ? 2 : +!!e.endsWith("="));
|
|
4688
|
+
var p = (e, r) => {
|
|
4689
|
+
if (!u(e) || e.format !== 1) return {
|
|
4690
|
+
ok: !1,
|
|
4691
|
+
problems: ["Not a source snapshot of format 1"]
|
|
4692
|
+
};
|
|
4693
|
+
let { kind: i, name: p, entries: m, files: h, dependencies: g } = e, _ = [];
|
|
4694
|
+
if (s(i) || _.push(`Its kind is one of ${n.join(", ")}`), c(p) || _.push("Its name is a plugin type or \"runtime\": letters, digits, dashes and underscores"), !d(h) || !d(g)) return {
|
|
4695
|
+
ok: !1,
|
|
4696
|
+
problems: [..._, "Its files and dependencies are maps of text"]
|
|
4697
|
+
};
|
|
4698
|
+
l(m) ? m.filter((e) => !Object.hasOwn(h, e)).forEach((e) => _.push(`Its entry "${e}" is not among its files`)) : _.push("It names at least one entry");
|
|
4699
|
+
let v = Object.keys(h);
|
|
4700
|
+
v.length > t.files && _.push(`It holds ${String(v.length)} files; a snapshot holds at most ${String(t.files)}`);
|
|
4701
|
+
let y = 0;
|
|
4702
|
+
return v.forEach((e) => {
|
|
4703
|
+
let n = a(e);
|
|
4704
|
+
if (n) {
|
|
4705
|
+
_.push(n);
|
|
4706
|
+
return;
|
|
4707
|
+
}
|
|
4708
|
+
let i = f(h[e]);
|
|
4709
|
+
if (y += i, i > t.fileBytes) {
|
|
4710
|
+
_.push(`"${e}" is ${(i / 1024 / 1024).toFixed(1)} MB; a file of source is at most 4 MB`);
|
|
4711
|
+
return;
|
|
4712
|
+
}
|
|
4713
|
+
let s = o(r(h[e]));
|
|
4714
|
+
s && _.push(`"${e}" holds ${s}: take it out of the code and give it to the project as a variable`);
|
|
4715
|
+
}), y > t.totalBytes && _.push(`It holds ${(y / 1024 / 1024).toFixed(1)} MB of files; a snapshot holds at most 24 MB`), _.length > 0 || !s(i) || !c(p) || !l(m) ? {
|
|
4716
|
+
ok: !1,
|
|
4717
|
+
problems: _
|
|
4718
|
+
} : {
|
|
4719
|
+
ok: !0,
|
|
4720
|
+
snapshot: {
|
|
4721
|
+
format: 1,
|
|
4722
|
+
kind: i,
|
|
4723
|
+
name: p,
|
|
4724
|
+
entries: m,
|
|
4725
|
+
files: h,
|
|
4726
|
+
dependencies: g
|
|
4727
|
+
}
|
|
4728
|
+
};
|
|
3861
4729
|
};
|
|
3862
|
-
|
|
3863
|
-
|
|
4730
|
+
//#endregion
|
|
4731
|
+
//#region src/pack/source.ts
|
|
4732
|
+
var CODE = /\.(m|c)?(t|j)sx?$/;
|
|
4733
|
+
var STYLES = /\.(css|scss|sass|less)$/;
|
|
4734
|
+
/** Where an import of a file without its extension can be, in the order a bundler looks. */
|
|
4735
|
+
var EXTENSIONS = [
|
|
4736
|
+
".ts",
|
|
4737
|
+
".tsx",
|
|
4738
|
+
".mts",
|
|
4739
|
+
".cts",
|
|
4740
|
+
".js",
|
|
4741
|
+
".jsx",
|
|
4742
|
+
".mjs",
|
|
4743
|
+
".cjs",
|
|
4744
|
+
".json",
|
|
4745
|
+
".css",
|
|
4746
|
+
".scss"
|
|
4747
|
+
];
|
|
4748
|
+
var BUILTINS = new Set(builtinModules);
|
|
4749
|
+
var loadTypeScript$1 = (root) => {
|
|
3864
4750
|
try {
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
} catch {}
|
|
3868
|
-
fail("--params is a JSON object of the task’s params, by name: --params '{\"name\":\"Ada\"}'");
|
|
3869
|
-
};
|
|
3870
|
-
var tryFunction = async (task, options) => {
|
|
3871
|
-
const params = paramsOf(options.params);
|
|
3872
|
-
if (!params) return;
|
|
3873
|
-
const connection = await connect$1(options, "to try it in");
|
|
3874
|
-
if (!connection?.space) return;
|
|
3875
|
-
const answered = await authorizedRequest(connection, `/spaces/${connection.space.id}/functions/try`, {
|
|
3876
|
-
method: "POST",
|
|
3877
|
-
headers: { "Content-Type": "application/json" },
|
|
3878
|
-
body: JSON.stringify({
|
|
3879
|
-
task,
|
|
3880
|
-
params
|
|
3881
|
-
})
|
|
3882
|
-
});
|
|
3883
|
-
if (!answered.ok) {
|
|
3884
|
-
fail(answered.error);
|
|
3885
|
-
return;
|
|
3886
|
-
}
|
|
3887
|
-
const { reply } = answered.value;
|
|
3888
|
-
if (reply.status !== 200) {
|
|
3889
|
-
fail(reply.data.error ?? `The task could not be tried (${reply.status}).`);
|
|
4751
|
+
return createRequire(path.join(root, "package.json"))("typescript");
|
|
4752
|
+
} catch {
|
|
3890
4753
|
return;
|
|
3891
4754
|
}
|
|
3892
|
-
printRun(task, reply.data);
|
|
3893
|
-
};
|
|
3894
|
-
var isLocalRunner = (value) => isRecord$2(value) && typeof value.createLocalFunctions === "function";
|
|
3895
|
-
/**
|
|
3896
|
-
* This project's own runner: `dev` runs the functions with the `@plitzi/sdk-server` the project installed, so the build,
|
|
3897
|
-
* the checks, the isolates and the limits are the platform's own, at the version the project is on.
|
|
3898
|
-
*/
|
|
3899
|
-
var projectRunner = async (root) => {
|
|
3900
|
-
try {
|
|
3901
|
-
const entry = createRequire(path.join(root, "package.json")).resolve("@plitzi/sdk-server/functions-runner");
|
|
3902
|
-
const loaded = await import(pathToFileURL(entry).href);
|
|
3903
|
-
if (isLocalRunner(loaded)) return loaded;
|
|
3904
|
-
} catch {}
|
|
3905
|
-
fail("plitzi functions dev runs them with this project’s own @plitzi/sdk-server, and the isolates it needs:\n npm install --save-dev @plitzi/sdk-server isolated-vm core-js");
|
|
3906
4755
|
};
|
|
3907
|
-
|
|
3908
|
-
var localCredentials = () => {
|
|
3909
|
-
const raw = process.env.PLITZI_FUNCTIONS_CREDENTIALS;
|
|
4756
|
+
var isFile = async (file) => {
|
|
3910
4757
|
try {
|
|
3911
|
-
|
|
3912
|
-
return isRecord$2(value) ? Object.fromEntries(Object.entries(value).flatMap(([id, keys]) => isRecord$2(keys) ? [[id, Object.fromEntries(Object.entries(keys).filter((pair) => typeof pair[1] === "string"))]] : [])) : {};
|
|
4758
|
+
return (await fs.stat(file)).isFile();
|
|
3913
4759
|
} catch {
|
|
3914
|
-
return
|
|
4760
|
+
return false;
|
|
3915
4761
|
}
|
|
3916
4762
|
};
|
|
3917
|
-
var NO_NODE_SNAPSHOT = "--no-node-snapshot";
|
|
3918
4763
|
/**
|
|
3919
|
-
* The
|
|
3920
|
-
*
|
|
4764
|
+
* The file a relative import names, as a bundler finds it: as written, with an extension added, the TypeScript file a
|
|
4765
|
+
* `.js` import stands for, or a folder's index.
|
|
3921
4766
|
*/
|
|
3922
|
-
var
|
|
3923
|
-
const
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
4767
|
+
var resolveFile = async (from, specifier) => {
|
|
4768
|
+
const target = path.resolve(path.dirname(from), specifier);
|
|
4769
|
+
const candidates = [
|
|
4770
|
+
target,
|
|
4771
|
+
.../\.(m|c)?jsx?$/.test(target) ? [target.replace(/\.(m|c)?js(x?)$/, ".$1ts$2")] : [],
|
|
4772
|
+
...EXTENSIONS.map((extension) => `${target}${extension}`),
|
|
4773
|
+
...EXTENSIONS.map((extension) => path.join(target, `index${extension}`))
|
|
4774
|
+
];
|
|
4775
|
+
for (const candidate of candidates) if (await isFile(candidate)) return candidate;
|
|
4776
|
+
};
|
|
4777
|
+
/** A package's name from what an import asks for: `@scope/name/sub` is `@scope/name`, `name/sub` is `name`. */
|
|
4778
|
+
var packageOf = (specifier) => specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
|
|
4779
|
+
/** What a stylesheet brings in: its `@import`s and the files its `url()`s point at — not a data URI or an address. */
|
|
4780
|
+
var styleImports = (text) => [...text.matchAll(/@import\s+(?:url\()?\s*['"]([^'"]+)['"]/g), ...text.matchAll(/url\(\s*['"]?([^'")]+)['"]?\s*\)/g)].map((match) => match[1].trim()).filter((specifier) => !/^(data:|https?:|\/\/|#)/.test(specifier));
|
|
4781
|
+
var codeImports = (ts, text) => ts.preProcessFile(text, true, true).importedFiles.map((file) => file.fileName);
|
|
4782
|
+
/** A file's path in the snapshot: relative to the root, with forward slashes whatever the system writes. */
|
|
4783
|
+
var projectPath = (root, file) => path.relative(root, file).split(path.sep).join("/");
|
|
4784
|
+
/** The package of type declarations TypeScript reads for `name`: `geojson` is `@types/geojson`, `@a/b` is `@types/a__b`. */
|
|
4785
|
+
var typesPackageOf = (name) => `@types/${name.startsWith("@") ? name.slice(1).replace("/", "__") : name}`;
|
|
3933
4786
|
/**
|
|
3934
|
-
*
|
|
3935
|
-
*
|
|
4787
|
+
* The package an import needs, and the range the project asks for it: itself, or — for an import only its types are
|
|
4788
|
+
* read from (`import type { Feature } from 'geojson'`) — the package of its declarations.
|
|
3936
4789
|
*/
|
|
3937
|
-
var
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
const
|
|
3944
|
-
const
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
4790
|
+
var dependencyOf = async (root, name) => {
|
|
4791
|
+
const manifestDir = await nearestPackage(root);
|
|
4792
|
+
const manifest = manifestDir ? await readPackageJson(manifestDir) : void 0;
|
|
4793
|
+
const rangeOf = (dependency) => manifest?.dependencies?.[dependency] ?? manifest?.devDependencies?.[dependency] ?? manifest?.peerDependencies?.[dependency];
|
|
4794
|
+
const range = rangeOf(name);
|
|
4795
|
+
if (range) return [name, range];
|
|
4796
|
+
const types = typesPackageOf(name);
|
|
4797
|
+
const typesRange = rangeOf(types);
|
|
4798
|
+
return typesRange ? [types, typesRange] : void 0;
|
|
4799
|
+
};
|
|
4800
|
+
/**
|
|
4801
|
+
* The snapshot of what `entries` reach, gzipped as it travels, and as read. Refused, every reason at once, when an
|
|
4802
|
+
* import leaves the project, names a file that is not there or a package the project does not declare, or a file it
|
|
4803
|
+
* reaches holds a credential.
|
|
4804
|
+
*/
|
|
4805
|
+
var packSource = async ({ root, kind, name, entries }) => {
|
|
4806
|
+
const ts = loadTypeScript$1(root);
|
|
4807
|
+
if (!ts) throw new Error("Packing the source reads its imports with the project’s own TypeScript:\n npm install -D typescript");
|
|
4808
|
+
const problems = [];
|
|
4809
|
+
const files = {};
|
|
4810
|
+
const packages = /* @__PURE__ */ new Set();
|
|
4811
|
+
const queue = entries.map((entry) => path.resolve(entry));
|
|
4812
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4813
|
+
while (queue.length > 0) {
|
|
4814
|
+
const file = queue.shift();
|
|
4815
|
+
if (file === void 0 || seen.has(file)) continue;
|
|
4816
|
+
seen.add(file);
|
|
4817
|
+
const relative = projectPath(root, file);
|
|
4818
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
4819
|
+
problems.push(`${relative} is outside the project (${root}): a project's source is the files inside it`);
|
|
4820
|
+
continue;
|
|
3952
4821
|
}
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
4822
|
+
const bytes = await fs.readFile(file);
|
|
4823
|
+
files[relative] = bytes.toString("base64");
|
|
4824
|
+
let specifiers = [];
|
|
4825
|
+
if (CODE.test(file)) specifiers = codeImports(ts, bytes.toString("utf-8"));
|
|
4826
|
+
else if (STYLES.test(file)) specifiers = styleImports(bytes.toString("utf-8"));
|
|
4827
|
+
for (const raw of specifiers) {
|
|
4828
|
+
const specifier = raw.replace(/[?#].*$/, "");
|
|
4829
|
+
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
4830
|
+
const found = await resolveFile(file, specifier);
|
|
4831
|
+
if (found) queue.push(found);
|
|
4832
|
+
else problems.push(`${relative} imports "${raw}", which is not there`);
|
|
4833
|
+
continue;
|
|
4834
|
+
}
|
|
4835
|
+
if (!specifier.startsWith("node:") && !BUILTINS.has(specifier)) packages.add(packageOf(specifier));
|
|
3956
4836
|
}
|
|
3957
|
-
|
|
4837
|
+
}
|
|
4838
|
+
const dependencies = {};
|
|
4839
|
+
for (const dependency of [...packages].sort()) {
|
|
4840
|
+
const found = await dependencyOf(root, dependency);
|
|
4841
|
+
if (found) dependencies[found[0]] = found[1];
|
|
4842
|
+
else problems.push(`It imports "${dependency}", which the project's package.json does not list`);
|
|
4843
|
+
}
|
|
4844
|
+
const snapshot = {
|
|
4845
|
+
format: 1,
|
|
4846
|
+
kind,
|
|
4847
|
+
name,
|
|
4848
|
+
entries: entries.map((entry) => projectPath(root, path.resolve(entry))),
|
|
4849
|
+
files,
|
|
4850
|
+
dependencies
|
|
4851
|
+
};
|
|
4852
|
+
const reading = p(snapshot, (base64) => Buffer.from(base64, "base64").toString("utf-8"));
|
|
4853
|
+
problems.push(...reading.ok ? [] : reading.problems);
|
|
4854
|
+
if (problems.length > 0) throw new Error(`The source of ${name} cannot be kept:\n${problems.map((problem) => ` - ${problem}`).join("\n")}`);
|
|
4855
|
+
return {
|
|
4856
|
+
snapshot,
|
|
4857
|
+
bytes: new Uint8Array(gzipSync(JSON.stringify(snapshot)))
|
|
3958
4858
|
};
|
|
3959
|
-
await run();
|
|
3960
|
-
if (!options.watch) return;
|
|
3961
|
-
console.log(chalk.dim("\nWatching functions/ — every save runs it again. ^C to stop."));
|
|
3962
|
-
let pending;
|
|
3963
|
-
watch(path.join(root, FUNCTIONS_DIR), { recursive: true }, () => {
|
|
3964
|
-
clearTimeout(pending);
|
|
3965
|
-
pending = setTimeout(() => {
|
|
3966
|
-
process.exitCode = void 0;
|
|
3967
|
-
console.log(chalk.dim(`\n— ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`));
|
|
3968
|
-
run();
|
|
3969
|
-
}, 150);
|
|
3970
|
-
});
|
|
3971
4859
|
};
|
|
3972
4860
|
//#endregion
|
|
3973
4861
|
//#region src/pack/typeDeclarations.ts
|
|
@@ -4041,21 +4929,21 @@ var INLINED = [
|
|
|
4041
4929
|
];
|
|
4042
4930
|
/** A plugin that cannot be packed, with what to change — never a stack trace out of esbuild. */
|
|
4043
4931
|
var PackError = class extends Error {};
|
|
4044
|
-
var isRecord$
|
|
4932
|
+
var isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4045
4933
|
/** The declaration, or what is missing from it — named, so the fix is one edit away. */
|
|
4046
4934
|
var checkDeclaration = (value, where) => {
|
|
4047
4935
|
const missing = (field) => new PackError(`${where}: the declaration has no ${field}. Write it the way \`plitzi add plugin\` does.`);
|
|
4048
|
-
if (!isRecord$
|
|
4936
|
+
if (!isRecord$2(value) || typeof value.type !== "string" || !value.type) throw missing("`type`");
|
|
4049
4937
|
const { content } = value;
|
|
4050
|
-
if (!isRecord$
|
|
4938
|
+
if (!isRecord$2(content)) throw missing("`content`");
|
|
4051
4939
|
for (const field of [
|
|
4052
4940
|
"attributes",
|
|
4053
4941
|
"definition",
|
|
4054
4942
|
"builder",
|
|
4055
4943
|
"market",
|
|
4056
4944
|
"defaultStyle"
|
|
4057
|
-
]) if (!isRecord$
|
|
4058
|
-
if (!isRecord$
|
|
4945
|
+
]) if (!isRecord$2(content[field])) throw missing(`\`content.${field}\``);
|
|
4946
|
+
if (!isRecord$2(content.definition) || typeof content.definition.label !== "string") throw missing("`content.definition.label`");
|
|
4059
4947
|
return value;
|
|
4060
4948
|
};
|
|
4061
4949
|
var toImportPath = (file) => file.split(path.sep).join("/");
|
|
@@ -4114,7 +5002,31 @@ var integrityOf = async (file) => {
|
|
|
4114
5002
|
const content = await fs.readFile(file);
|
|
4115
5003
|
return `sha384-${createHash("sha384").update(content).digest("base64")}`;
|
|
4116
5004
|
};
|
|
4117
|
-
|
|
5005
|
+
/** The files a plugin is built from: the package's entry, or each element's index. */
|
|
5006
|
+
var sourceEntries = (source) => source.kind === "package" ? [source.entry] : source.folders.map((folder) => path.join(folder, "index.ts"));
|
|
5007
|
+
/** Where the source snapshot of a plugin goes: beside its zip, named after it. */
|
|
5008
|
+
var sourceFileOf = (zip) => zip.replace(/\.zip$/, ".source.json.gz");
|
|
5009
|
+
/**
|
|
5010
|
+
* The plugin's source snapshot written beside its zip — or, when it cannot be kept, why: the plugin still packs, and
|
|
5011
|
+
* goes up built only.
|
|
5012
|
+
*/
|
|
5013
|
+
var writeSource = async (name, source, root, zip) => {
|
|
5014
|
+
try {
|
|
5015
|
+
const { bytes } = await packSource({
|
|
5016
|
+
root,
|
|
5017
|
+
kind: "plugin",
|
|
5018
|
+
name,
|
|
5019
|
+
entries: sourceEntries(source)
|
|
5020
|
+
});
|
|
5021
|
+
const file = sourceFileOf(zip);
|
|
5022
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
5023
|
+
await fs.writeFile(file, bytes);
|
|
5024
|
+
return { file };
|
|
5025
|
+
} catch (error) {
|
|
5026
|
+
return { problem: error instanceof Error ? error.message : String(error) };
|
|
5027
|
+
}
|
|
5028
|
+
};
|
|
5029
|
+
var packPlugin = async ({ root, source, base, version, outDir, zip, sourceRoot = root, types = false }) => {
|
|
4118
5030
|
const relative = path.relative(root, outDir);
|
|
4119
5031
|
if (relative.startsWith("..") || path.isAbsolute(relative) || relative === "") throw new PackError(`${outDir} is not a folder inside ${root}: the build empties it first, so it has to be.`);
|
|
4120
5032
|
if (source.kind === "elements") for (const folder of source.folders) for (const file of ["index.ts", "declaration.ts"]) try {
|
|
@@ -4186,7 +5098,8 @@ var packPlugin = async ({ root, source, base, version, outDir, zip, types = fals
|
|
|
4186
5098
|
types: declarations.map((declaration) => declaration.type),
|
|
4187
5099
|
files,
|
|
4188
5100
|
zip,
|
|
4189
|
-
typesOutcome
|
|
5101
|
+
typesOutcome,
|
|
5102
|
+
source: await writeSource(main.type, source, sourceRoot, zip ?? path.join(outDir, `${base}.zip`))
|
|
4190
5103
|
};
|
|
4191
5104
|
};
|
|
4192
5105
|
//#endregion
|
|
@@ -4286,6 +5199,7 @@ var packPluginCommand = async (foldersGiven, options) => {
|
|
|
4286
5199
|
version,
|
|
4287
5200
|
outDir,
|
|
4288
5201
|
zip,
|
|
5202
|
+
sourceRoot: options.sourceRoot ? path.resolve(options.sourceRoot) : project.root,
|
|
4289
5203
|
types: inPackage
|
|
4290
5204
|
});
|
|
4291
5205
|
console.log(chalk.green(`\n${result.types.join(", ")} — packed at ${path.relative(process.cwd(), outDir) || "."}`));
|
|
@@ -4296,6 +5210,11 @@ var packPluginCommand = async (foldersGiven, options) => {
|
|
|
4296
5210
|
console.log(`\n ${path.relative(process.cwd(), result.zip)}`);
|
|
4297
5211
|
console.log(chalk.dim(" Upload it in the builder under Resources, as a plugin."));
|
|
4298
5212
|
}
|
|
5213
|
+
if ("file" in result.source) console.log(chalk.dim(` ${path.relative(process.cwd(), result.source.file)} — its source, which plitzi upload keeps too`));
|
|
5214
|
+
else {
|
|
5215
|
+
console.log(chalk.yellow("\n Its source is not kept, so the space can be taken back out with this plugin built only:"));
|
|
5216
|
+
console.log(chalk.yellow(` ${result.source.problem.split("\n").join("\n ")}`));
|
|
5217
|
+
}
|
|
4299
5218
|
console.log("");
|
|
4300
5219
|
} catch (error) {
|
|
4301
5220
|
if (!(error instanceof PackError)) throw error;
|
|
@@ -4304,6 +5223,250 @@ var packPluginCommand = async (foldersGiven, options) => {
|
|
|
4304
5223
|
}
|
|
4305
5224
|
};
|
|
4306
5225
|
//#endregion
|
|
5226
|
+
//#region src/commands/packSource.ts
|
|
5227
|
+
/**
|
|
5228
|
+
* `plitzi pack source`: the snapshot `plitzi upload plugin` and `plitzi runtime push` keep beside what they send (RFC
|
|
5229
|
+
* 0020), written to a file — to see what of a project would be kept, or for a tool that keeps it itself (the seeder).
|
|
5230
|
+
*/
|
|
5231
|
+
var packSourceCommand = async (entries, options) => {
|
|
5232
|
+
const root = options.root ? path.resolve(options.root) : (await findProject(process.cwd()))?.root;
|
|
5233
|
+
if (!root) {
|
|
5234
|
+
console.error(chalk.red("plitzi pack source packs a project’s source, and there is no package.json here or above."));
|
|
5235
|
+
process.exitCode = 1;
|
|
5236
|
+
return;
|
|
5237
|
+
}
|
|
5238
|
+
try {
|
|
5239
|
+
const { snapshot, bytes } = await packSource({
|
|
5240
|
+
root,
|
|
5241
|
+
kind: options.kind,
|
|
5242
|
+
name: options.name,
|
|
5243
|
+
entries: entries.map((entry) => path.resolve(entry))
|
|
5244
|
+
});
|
|
5245
|
+
const out = path.resolve(options.out);
|
|
5246
|
+
await fs.mkdir(path.dirname(out), { recursive: true });
|
|
5247
|
+
await fs.writeFile(out, bytes);
|
|
5248
|
+
console.log(chalk.green(`${options.name}: ${String(Object.keys(snapshot.files).length)} files`) + chalk.dim(` — ${path.relative(process.cwd(), out)} (${(bytes.byteLength / 1024).toFixed(0)} KB)`));
|
|
5249
|
+
} catch (error) {
|
|
5250
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
5251
|
+
process.exitCode = 1;
|
|
5252
|
+
}
|
|
5253
|
+
};
|
|
5254
|
+
//#endregion
|
|
5255
|
+
//#region src/commands/pull.ts
|
|
5256
|
+
/** The version a pull asks for: the one the project follows, or the one it is told to follow from now on. */
|
|
5257
|
+
var versionToPull = (options, followed) => {
|
|
5258
|
+
const environment = options.environment ?? followed.environment;
|
|
5259
|
+
const kept = environment === followed.environment ? followed.revision : void 0;
|
|
5260
|
+
if (options.revision === void 0 || options.revision === "latest") {
|
|
5261
|
+
const revision = options.revision === "latest" ? void 0 : kept;
|
|
5262
|
+
return {
|
|
5263
|
+
ok: true,
|
|
5264
|
+
version: {
|
|
5265
|
+
environment,
|
|
5266
|
+
...revision ? { revision } : {}
|
|
5267
|
+
}
|
|
5268
|
+
};
|
|
5269
|
+
}
|
|
5270
|
+
const revision = Number(options.revision);
|
|
5271
|
+
if (environment === "main") return {
|
|
5272
|
+
ok: false,
|
|
5273
|
+
error: "The draft (main) has no revisions: name a published environment with --environment."
|
|
5274
|
+
};
|
|
5275
|
+
if (!Number.isInteger(revision) || revision < 1) return {
|
|
5276
|
+
ok: false,
|
|
5277
|
+
error: `--revision takes a revision number from 1, or latest — not "${options.revision}".`
|
|
5278
|
+
};
|
|
5279
|
+
return {
|
|
5280
|
+
ok: true,
|
|
5281
|
+
version: {
|
|
5282
|
+
environment,
|
|
5283
|
+
revision
|
|
5284
|
+
}
|
|
5285
|
+
};
|
|
5286
|
+
};
|
|
5287
|
+
var verdictOf = ({ given, was, here }) => {
|
|
5288
|
+
if (given === void 0) {
|
|
5289
|
+
if (here === void 0 || was === void 0) return "same";
|
|
5290
|
+
return here === was ? "remove" : "keep";
|
|
5291
|
+
}
|
|
5292
|
+
if (here === given) return "same";
|
|
5293
|
+
if (here === void 0) {
|
|
5294
|
+
if (was === void 0) return "write";
|
|
5295
|
+
return given === was ? "keep" : "conflict";
|
|
5296
|
+
}
|
|
5297
|
+
if (here === was) return "write";
|
|
5298
|
+
return given === was ? "keep" : "conflict";
|
|
5299
|
+
};
|
|
5300
|
+
var isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5301
|
+
/**
|
|
5302
|
+
* The packages the space's code asks for, added to the project's `package.json`: a package it does not list is added,
|
|
5303
|
+
* one still at the range the space last asked for follows the space, and one the project changed stays as it is.
|
|
5304
|
+
*/
|
|
5305
|
+
var mergeDependencies = (manifest, asked, before) => {
|
|
5306
|
+
const current = isRecord$1(manifest.dependencies) ? manifest.dependencies : {};
|
|
5307
|
+
const dependencies = { ...current };
|
|
5308
|
+
const changed = [];
|
|
5309
|
+
const kept = [];
|
|
5310
|
+
Object.entries(asked).forEach(([name, range]) => {
|
|
5311
|
+
const listed = current[name];
|
|
5312
|
+
if (listed === range) return;
|
|
5313
|
+
if (listed === void 0 || listed === before[name]) {
|
|
5314
|
+
dependencies[name] = range;
|
|
5315
|
+
changed.push(`${name}@${range}`);
|
|
5316
|
+
return;
|
|
5317
|
+
}
|
|
5318
|
+
kept.push(`${name}: the space's code now asks for ${range}, and package.json keeps ${JSON.stringify(listed)}`);
|
|
5319
|
+
});
|
|
5320
|
+
return {
|
|
5321
|
+
manifest: {
|
|
5322
|
+
...manifest,
|
|
5323
|
+
dependencies: Object.fromEntries(Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b)))
|
|
5324
|
+
},
|
|
5325
|
+
changed,
|
|
5326
|
+
kept
|
|
5327
|
+
};
|
|
5328
|
+
};
|
|
5329
|
+
var writeBytes = async (root, file, bytes) => {
|
|
5330
|
+
await fs.mkdir(path.dirname(path.join(root, file)), { recursive: true });
|
|
5331
|
+
await fs.writeFile(path.join(root, file), bytes);
|
|
5332
|
+
};
|
|
5333
|
+
var list = (files) => files.map((file) => `\n ${file}`).join("");
|
|
5334
|
+
var plural = (count, noun) => `${String(count)} ${noun}${count === 1 ? "" : "s"}`;
|
|
5335
|
+
var pull = async (options) => {
|
|
5336
|
+
const project = await findProject(process.cwd());
|
|
5337
|
+
const root = project?.root ?? process.cwd();
|
|
5338
|
+
const origin = await readOrigin(root);
|
|
5339
|
+
if (!origin) {
|
|
5340
|
+
fail("This project was not made from a space: there is no .plitzi/space.json here. plitzi create --from <space> makes one.");
|
|
5341
|
+
return;
|
|
5342
|
+
}
|
|
5343
|
+
const api = options.api ? await apiFor(options) : origin.api;
|
|
5344
|
+
if (!api) return;
|
|
5345
|
+
const asked = versionToPull(options, origin.version);
|
|
5346
|
+
if (!asked.ok) {
|
|
5347
|
+
fail(asked.error);
|
|
5348
|
+
return;
|
|
5349
|
+
}
|
|
5350
|
+
const exported = await fetchExport(api, String(origin.space.id), {
|
|
5351
|
+
source: origin.source,
|
|
5352
|
+
version: asked.version,
|
|
5353
|
+
name: origin.space.name
|
|
5354
|
+
});
|
|
5355
|
+
if (!exported) return;
|
|
5356
|
+
const next = projectFromSpace(exported, origin.source);
|
|
5357
|
+
const format = await projectFormatter(root);
|
|
5358
|
+
const given = /* @__PURE__ */ new Map();
|
|
5359
|
+
for (const [file, text] of Object.entries(next.files)) given.set(file, Buffer.from(await format(file, text)));
|
|
5360
|
+
Object.entries(next.binaries).forEach(([file, base64]) => given.set(file, Buffer.from(base64, "base64")));
|
|
5361
|
+
const missing = [];
|
|
5362
|
+
const untouched = /* @__PURE__ */ new Set();
|
|
5363
|
+
for (const { url, to } of next.downloads) {
|
|
5364
|
+
if (origin.downloads[to] === url && await digestsOf(root, to, format) !== void 0) {
|
|
5365
|
+
untouched.add(to);
|
|
5366
|
+
continue;
|
|
5367
|
+
}
|
|
5368
|
+
try {
|
|
5369
|
+
given.set(to, await download(url));
|
|
5370
|
+
} catch (error) {
|
|
5371
|
+
missing.push(notFetched(to, url, error));
|
|
5372
|
+
untouched.add(to);
|
|
5373
|
+
}
|
|
5374
|
+
}
|
|
5375
|
+
const paths = [.../* @__PURE__ */ new Set([...given.keys(), ...Object.keys(origin.files)])].filter((file) => !untouched.has(file));
|
|
5376
|
+
const verdicts = /* @__PURE__ */ new Map();
|
|
5377
|
+
for (const file of paths) {
|
|
5378
|
+
const bytes = given.get(file);
|
|
5379
|
+
const here = await digestsOf(root, file, format);
|
|
5380
|
+
const was = origin.files[file];
|
|
5381
|
+
const unchanged = here !== void 0 && (here.raw === was || here.formatted === was);
|
|
5382
|
+
verdicts.set(file, verdictOf({
|
|
5383
|
+
given: bytes ? digest(bytes) : void 0,
|
|
5384
|
+
was: unchanged ? here.formatted : was,
|
|
5385
|
+
here: here?.formatted
|
|
5386
|
+
}));
|
|
5387
|
+
}
|
|
5388
|
+
const of = (verdict) => [...verdicts].filter(([, value]) => value === verdict).map(([file]) => file);
|
|
5389
|
+
const conflicts = of("conflict");
|
|
5390
|
+
if (conflicts.length > 0 && !options.force) {
|
|
5391
|
+
fail(`Nothing was pulled: these changed here and on ${origin.space.name} too:${list(conflicts)}\nCommit or set aside the changes here and pull again, or pass --force to take the space’s copy of them.`);
|
|
5392
|
+
return;
|
|
5393
|
+
}
|
|
5394
|
+
const written = [...of("write"), ...conflicts];
|
|
5395
|
+
for (const file of written) {
|
|
5396
|
+
const bytes = given.get(file);
|
|
5397
|
+
if (bytes) await writeBytes(root, file, bytes);
|
|
5398
|
+
}
|
|
5399
|
+
const removed = of("remove");
|
|
5400
|
+
await Promise.all(removed.map((file) => fs.rm(path.join(root, file), { force: true })));
|
|
5401
|
+
const packageFile = path.join(root, "package.json");
|
|
5402
|
+
const manifest = JSON.parse(await fs.readFile(packageFile, "utf-8"));
|
|
5403
|
+
const dependencies = mergeDependencies(isRecord$1(manifest) ? manifest : {}, next.dependencies, origin.dependencies);
|
|
5404
|
+
if (dependencies.changed.length > 0) await fs.writeFile(packageFile, `${JSON.stringify(dependencies.manifest, null, 2)}\n`);
|
|
5405
|
+
const files = Object.fromEntries([...[...given].map(([file, bytes]) => [file, digest(bytes)]), ...[...untouched].flatMap((file) => origin.files[file] ? [[file, origin.files[file]]] : [])]);
|
|
5406
|
+
await writeOrigin(root, {
|
|
5407
|
+
...origin,
|
|
5408
|
+
api,
|
|
5409
|
+
space: exported.space,
|
|
5410
|
+
version: asked.version,
|
|
5411
|
+
files,
|
|
5412
|
+
downloads: Object.fromEntries(next.downloads.flatMap(({ url, to }) => {
|
|
5413
|
+
if (given.has(to)) return [[to, url]];
|
|
5414
|
+
const before = origin.downloads[to];
|
|
5415
|
+
return before && Object.hasOwn(files, to) ? [[to, before]] : [];
|
|
5416
|
+
})),
|
|
5417
|
+
dependencies: next.dependencies
|
|
5418
|
+
});
|
|
5419
|
+
if (!of("keep").some((file) => file.startsWith("functions/"))) await writeFunctionsState(root, {
|
|
5420
|
+
space: exported.space.id,
|
|
5421
|
+
version: next.functions.version,
|
|
5422
|
+
files: await functionsOnDisk(root, next)
|
|
5423
|
+
});
|
|
5424
|
+
const kept = of("keep");
|
|
5425
|
+
console.log(chalk.green(`\nPulled ${exported.space.name} (${versionLabel(exported.version)}): ${plural(written.length, "file")} written, ${plural(removed.length, "file")} removed.`));
|
|
5426
|
+
if (written.length > 0) console.log(chalk.dim(list(written)));
|
|
5427
|
+
if (kept.length > 0) console.log(`Changed here, and kept:${list(kept)}`);
|
|
5428
|
+
if (dependencies.changed.length > 0) {
|
|
5429
|
+
const manager = project?.packageManager ?? "npm";
|
|
5430
|
+
console.log(`package.json now asks for ${dependencies.changed.join(", ")}: run ${installCommand(manager)}.`);
|
|
5431
|
+
}
|
|
5432
|
+
const notes = [
|
|
5433
|
+
...dependencies.kept,
|
|
5434
|
+
...missing,
|
|
5435
|
+
...next.report
|
|
5436
|
+
];
|
|
5437
|
+
if (notes.length > 0) {
|
|
5438
|
+
console.log(chalk.yellow(`\nWhat to know about ${exported.space.name} here:`));
|
|
5439
|
+
notes.forEach((note) => console.log(chalk.yellow(` - ${note}`)));
|
|
5440
|
+
}
|
|
5441
|
+
console.log("");
|
|
5442
|
+
};
|
|
5443
|
+
//#endregion
|
|
5444
|
+
//#region src/commands/keepSource.ts
|
|
5445
|
+
/**
|
|
5446
|
+
* 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
|
|
5447
|
+
* `plitzi create --from` take the space back out as a project. Never undoes what went up before it — the plugin or the
|
|
5448
|
+
* runtime runs either way — so a refusal is said, with why, and the command still succeeds.
|
|
5449
|
+
*/
|
|
5450
|
+
var keepSource = async (connection, spaceId, bytes) => {
|
|
5451
|
+
const sent = await authorizedRequest(connection, `/spaces/${String(spaceId)}/sources`, {
|
|
5452
|
+
method: "PUT",
|
|
5453
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
5454
|
+
body: new Uint8Array(bytes)
|
|
5455
|
+
});
|
|
5456
|
+
if (!sent.ok) {
|
|
5457
|
+
console.log(chalk.yellow(` Its source is not kept: ${sent.error}`));
|
|
5458
|
+
return;
|
|
5459
|
+
}
|
|
5460
|
+
const { status, data } = sent.value.reply;
|
|
5461
|
+
if (status === 200 && data.ok) {
|
|
5462
|
+
console.log(chalk.dim(` Its source too (${String(data.files ?? 0)} files): plitzi create --from can take it back out.`));
|
|
5463
|
+
return;
|
|
5464
|
+
}
|
|
5465
|
+
const problems = data.problems ?? [`The platform answered ${String(status)}`];
|
|
5466
|
+
console.log(chalk.yellow(" Its source is not kept, so a project taken from the space gets it built only:"));
|
|
5467
|
+
problems.forEach((problem) => console.log(chalk.yellow(` - ${problem}`)));
|
|
5468
|
+
};
|
|
5469
|
+
//#endregion
|
|
4307
5470
|
//#region src/commands/runtime.ts
|
|
4308
5471
|
var DEFAULT_ENTRY = path.join("src", "runtime.ts");
|
|
4309
5472
|
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -4357,6 +5520,18 @@ var pushRuntime = async (options) => {
|
|
|
4357
5520
|
return;
|
|
4358
5521
|
}
|
|
4359
5522
|
console.log(`${chalk.green("✓")} ${connection.space.name}’s draft runtime is ${reply.data.digest.slice(0, 12)} (${(bytes.byteLength / 1024).toFixed(0)} KB). It starts in a moment; publish the space to take it live.`);
|
|
5523
|
+
try {
|
|
5524
|
+
const source = await packSource({
|
|
5525
|
+
root,
|
|
5526
|
+
kind: "runtime",
|
|
5527
|
+
name: "runtime",
|
|
5528
|
+
entries: [entry]
|
|
5529
|
+
});
|
|
5530
|
+
await keepSource(answered.value.connection, connection.space.id, source.bytes);
|
|
5531
|
+
} catch (error) {
|
|
5532
|
+
console.log(chalk.yellow(" Its source is not kept, so a project taken from the space gets it built only:"));
|
|
5533
|
+
console.log(chalk.yellow(` ${error instanceof Error ? error.message : String(error)}`));
|
|
5534
|
+
}
|
|
4360
5535
|
};
|
|
4361
5536
|
var readRuntime = async (connection, spaceId) => {
|
|
4362
5537
|
const answered = await authorizedRequest(connection, `/spaces/${String(spaceId)}/runtime`);
|
|
@@ -4662,7 +5837,10 @@ var uploadPluginCommand = async (zipGiven, options) => {
|
|
|
4662
5837
|
const installed = data.installed === "updated" ? `now loads ${label}` : `loads ${label} from now on`;
|
|
4663
5838
|
console.log(chalk.green(`\n${space.name} ${installed}.`));
|
|
4664
5839
|
if (data.resource?.path) console.log(chalk.dim(` ${data.resource.path}`));
|
|
4665
|
-
console.log(chalk.dim(" Any builder open on the space loads it now
|
|
5840
|
+
console.log(chalk.dim(" Any builder open on the space loads it now."));
|
|
5841
|
+
const source = await fs.readFile(sourceFileOf(zipPath)).catch(() => void 0);
|
|
5842
|
+
if (source) await keepSource(uploaded.value.connection, space.id, source);
|
|
5843
|
+
console.log("");
|
|
4666
5844
|
};
|
|
4667
5845
|
//#endregion
|
|
4668
5846
|
//#region src/index.ts
|
|
@@ -4680,15 +5858,18 @@ var PROJECT_ONLY = [
|
|
|
4680
5858
|
"source",
|
|
4681
5859
|
"key"
|
|
4682
5860
|
];
|
|
4683
|
-
|
|
5861
|
+
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."];
|
|
5862
|
+
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").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) => {
|
|
4684
5863
|
if (!options.plugin) return create(directory, options);
|
|
4685
5864
|
const stray = PROJECT_ONLY.filter((flag) => options[flag] !== void 0);
|
|
4686
5865
|
if (stray.length > 0) program.error(`--${stray.join(", --")} shape a project, not a plugin: leave them out with --plugin.`);
|
|
4687
5866
|
return createPlugin(directory, options);
|
|
4688
5867
|
});
|
|
5868
|
+
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));
|
|
4689
5869
|
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("-f, --force", "Write into a folder that is not empty").action((names, options) => addPlugin(names, options));
|
|
4690
|
-
program.command("pack").description("Build something of this project into what the platform takes")
|
|
4691
|
-
|
|
5870
|
+
var pack = program.command("pack").description("Build something of this project into what the platform takes");
|
|
5871
|
+
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));
|
|
5872
|
+
pack.command("source").argument("<entries...>", "The files the build starts from: a runtime module, or each element’s index").description("Write the source snapshot plitzi upload and plitzi runtime push keep beside what they send").addOption(new Option("--kind <kind>", "What it is the source of").choices(["plugin", "runtime"]).makeOptionMandatory()).requiredOption("--name <name>", "The plugin’s type, or runtime").option("--root <folder>", "The project the paths are relative to. Defaults to the nearest package.json’s folder.").requiredOption("-o, --out <file>", "Where the gzipped snapshot goes").action((entries, options) => packSourceCommand(entries, options));
|
|
4692
5873
|
program.command("login").description("Sign in, in your browser. The session is kept and renewed until plitzi logout").option(...API_OPTION).action((options) => login(options));
|
|
4693
5874
|
program.command("logout").description("Sign out: the session is revoked on the platform and forgotten here").action(() => logout());
|
|
4694
5875
|
program.command("whoami").description("Who the CLI is signed in as, and the space it works in").option(...API_OPTION).action((options) => whoami(options));
|