pressship 0.1.13 → 0.1.15
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/assets/web/app.js +2234 -212
- package/assets/web/style.css +2951 -988
- package/dist/checks/plugin-check.d.ts +1 -0
- package/dist/checks/plugin-check.js +72 -1
- package/dist/checks/plugin-check.js.map +1 -1
- package/dist/package/archive.d.ts +12 -0
- package/dist/package/archive.js +44 -6
- package/dist/package/archive.js.map +1 -1
- package/dist/package/ignore.d.ts +18 -0
- package/dist/package/ignore.js +156 -8
- package/dist/package/ignore.js.map +1 -1
- package/dist/svn/release.js +105 -8
- package/dist/svn/release.js.map +1 -1
- package/dist/ui.d.ts +1 -0
- package/dist/ui.js +76 -0
- package/dist/ui.js.map +1 -1
- package/dist/web/index.js +1 -0
- package/dist/web/index.js.map +1 -1
- package/dist/web/release.js +10 -6
- package/dist/web/release.js.map +1 -1
- package/dist/web/server.d.ts +7 -0
- package/dist/web/server.js +502 -67
- package/dist/web/server.js.map +1 -1
- package/dist/web/version-state.js +14 -10
- package/dist/web/version-state.js.map +1 -1
- package/dist/wordpress-org/submit.js +4 -2
- package/dist/wordpress-org/submit.js.map +1 -1
- package/package.json +1 -1
package/dist/web/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { createReadStream } from "node:fs";
|
|
3
|
-
import { cp, mkdir, mkdtemp, readFile, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { cp, mkdir, mkdtemp, readdir, readFile, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { createServer } from "node:http";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
@@ -12,6 +12,8 @@ import fg from "fast-glob";
|
|
|
12
12
|
import { z } from "zod";
|
|
13
13
|
import { hasSavedSession } from "../auth/session.js";
|
|
14
14
|
import { getWordPressOrgAccount } from "../auth/whoami.js";
|
|
15
|
+
import { analyzePluginPackage, stagePluginDirectory } from "../package/archive.js";
|
|
16
|
+
import { addPressshipIgnorePattern, createPressshipIgnoreMatcher, hardIgnoreDirectories, isHardIgnoredPath, pressshipIgnoreFile, readPressshipIgnorePatterns, removePressshipIgnorePattern } from "../package/ignore.js";
|
|
15
17
|
import { createPluginPack, summarizePackResult, validatePluginPack } from "../package/pack.js";
|
|
16
18
|
import { discoverPluginProject, resolvePluginProjectPath } from "../plugin/discover.js";
|
|
17
19
|
import { assertDemoLaunchPlanSupported, createDemoLaunchPlan, prepareDemoRuntime, publicDemoLaunchPlan, resetPlaygroundSite } from "../plugin/demo.js";
|
|
@@ -25,7 +27,7 @@ import { publish } from "../wordpress-org/publish.js";
|
|
|
25
27
|
import { fetchPluginStates, matchesPluginState } from "../wordpress-org/state.js";
|
|
26
28
|
import { runPluginCheck } from "../checks/plugin-check.js";
|
|
27
29
|
import { hasBlockingFindings } from "../checks/summary.js";
|
|
28
|
-
import { ensureCacheDir, getConfigDir } from "../utils/paths.js";
|
|
30
|
+
import { ensureCacheDir, getConfigDir, pathExists } from "../utils/paths.js";
|
|
29
31
|
import { addLocalPluginPath, getLocalPlugin, listLocalPlugins, removeLocalPlugin } from "./registry.js";
|
|
30
32
|
import { WebJobManager } from "./jobs.js";
|
|
31
33
|
import { getVersionState } from "./version-state.js";
|
|
@@ -37,11 +39,13 @@ import { addStudioPluginCheckLineHints, normalizeStudioPluginCheckFindings, summ
|
|
|
37
39
|
import { readStudioPluginCheckState, removeStudioPluginCheckFindingsForFiles, removeStudioPluginCheckState, writeStudioPluginCheckState } from "./plugin-check-state.js";
|
|
38
40
|
const nodeRequire = createRequire(import.meta.url);
|
|
39
41
|
const mutationMethods = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
42
|
+
const spaRoutePrefixes = new Set(["dashboard", "studio", "wordpress.org", "remote", "local", "release", "settings"]);
|
|
40
43
|
const addLocalPluginSchema = z.object({ path: z.string().min(1) });
|
|
41
44
|
const bumpVersionSchema = z.object({ bump: z.enum(["patch", "minor", "major"]) });
|
|
42
45
|
const setVersionSchema = z.object({ version: z.string().min(1) });
|
|
43
46
|
const createSvnTagSchema = z.object({ name: z.string().min(1) });
|
|
44
47
|
const switchSvnTagSchema = z.object({ conflictResolution: z.enum(["override", "revert"]).optional() });
|
|
48
|
+
const ignoreRuleSchema = z.object({ pattern: z.string().min(1) });
|
|
45
49
|
const writeStudioFileSchema = z.object({
|
|
46
50
|
path: z.string().min(1),
|
|
47
51
|
content: z.string()
|
|
@@ -102,7 +106,9 @@ export async function startWebServer(options = {}) {
|
|
|
102
106
|
const approvals = new Map();
|
|
103
107
|
const playgrounds = new Map();
|
|
104
108
|
const playgroundPortReservations = new Set();
|
|
109
|
+
const packageSizeCache = new Map();
|
|
105
110
|
const staticDir = resolveStaticDir();
|
|
111
|
+
const dependencies = options.dependencies ?? {};
|
|
106
112
|
const server = createServer((request, response) => {
|
|
107
113
|
void handleRequest(request, response, {
|
|
108
114
|
token,
|
|
@@ -110,7 +116,9 @@ export async function startWebServer(options = {}) {
|
|
|
110
116
|
approvals,
|
|
111
117
|
playgrounds,
|
|
112
118
|
playgroundPortReservations,
|
|
113
|
-
|
|
119
|
+
packageSizeCache,
|
|
120
|
+
staticDir,
|
|
121
|
+
dependencies
|
|
114
122
|
});
|
|
115
123
|
});
|
|
116
124
|
await new Promise((resolve, reject) => {
|
|
@@ -271,6 +279,62 @@ async function handleApi(request, response, url, context) {
|
|
|
271
279
|
sendJson(response, 200, await listStudioFiles(plugin.path));
|
|
272
280
|
return;
|
|
273
281
|
}
|
|
282
|
+
const studioFilesDirectoryMatch = url.pathname.match(/^\/api\/plugins\/local\/([^/]+)\/files\/directory$/);
|
|
283
|
+
if (method === "GET" && studioFilesDirectoryMatch) {
|
|
284
|
+
const plugin = await requireLocalPlugin(decodeURIComponent(studioFilesDirectoryMatch[1]));
|
|
285
|
+
const relativePath = url.searchParams.get("path") ?? "";
|
|
286
|
+
sendJson(response, 200, await listStudioDirectory(plugin.path, relativePath));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const studioIgnoreStateMatch = url.pathname.match(/^\/api\/plugins\/local\/([^/]+)\/ignore-state$/);
|
|
290
|
+
if (method === "GET" && studioIgnoreStateMatch) {
|
|
291
|
+
const plugin = await requireLocalPlugin(decodeURIComponent(studioIgnoreStateMatch[1]));
|
|
292
|
+
sendJson(response, 200, await readStudioIgnoreState(plugin.path));
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const studioPackageSizeMatch = url.pathname.match(/^\/api\/plugins\/local\/([^/]+)\/package-size$/);
|
|
296
|
+
if (method === "GET" && studioPackageSizeMatch) {
|
|
297
|
+
const plugin = await requireLocalPlugin(decodeURIComponent(studioPackageSizeMatch[1]));
|
|
298
|
+
sendJson(response, 200, readStudioPackageSize(plugin.path, context.packageSizeCache));
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const studioIgnoreRulesMatch = url.pathname.match(/^\/api\/plugins\/local\/([^/]+)\/ignore-rules$/);
|
|
302
|
+
if (method === "POST" && studioIgnoreRulesMatch) {
|
|
303
|
+
const plugin = await requireLocalPlugin(decodeURIComponent(studioIgnoreRulesMatch[1]));
|
|
304
|
+
const body = ignoreRuleSchema.parse(await readJson(request));
|
|
305
|
+
try {
|
|
306
|
+
await addPressshipIgnorePattern(path.resolve(plugin.path), body.pattern);
|
|
307
|
+
invalidateStudioPackageSize(plugin.path, context.packageSizeCache);
|
|
308
|
+
sendJson(response, 200, await readStudioIgnoreState(plugin.path));
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
sendJson(response, 400, {
|
|
312
|
+
error: {
|
|
313
|
+
message: error instanceof Error ? error.message : String(error),
|
|
314
|
+
code: "invalid_ignore_pattern"
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (method === "DELETE" && studioIgnoreRulesMatch) {
|
|
321
|
+
const plugin = await requireLocalPlugin(decodeURIComponent(studioIgnoreRulesMatch[1]));
|
|
322
|
+
const body = ignoreRuleSchema.parse(await readJson(request));
|
|
323
|
+
try {
|
|
324
|
+
await removePressshipIgnorePattern(path.resolve(plugin.path), body.pattern);
|
|
325
|
+
invalidateStudioPackageSize(plugin.path, context.packageSizeCache);
|
|
326
|
+
sendJson(response, 200, await readStudioIgnoreState(plugin.path));
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
sendJson(response, 400, {
|
|
330
|
+
error: {
|
|
331
|
+
message: error instanceof Error ? error.message : String(error),
|
|
332
|
+
code: "invalid_ignore_pattern"
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
274
338
|
const studioCheckStateMatch = url.pathname.match(/^\/api\/plugins\/local\/([^/]+)\/check-state$/);
|
|
275
339
|
if (method === "GET" && studioCheckStateMatch) {
|
|
276
340
|
const localId = decodeURIComponent(studioCheckStateMatch[1]);
|
|
@@ -290,6 +354,7 @@ async function handleApi(request, response, url, context) {
|
|
|
290
354
|
const plugin = await requireLocalPlugin(localId);
|
|
291
355
|
const body = writeStudioFileSchema.parse(await readJson(request));
|
|
292
356
|
const saved = await writeStudioFile(plugin.path, body.path, body.content);
|
|
357
|
+
invalidateStudioPackageSize(plugin.path, context.packageSizeCache);
|
|
293
358
|
sendJson(response, 200, {
|
|
294
359
|
...saved,
|
|
295
360
|
checkState: await removeStudioPluginCheckFindingsForFiles(localId, [saved.path])
|
|
@@ -303,6 +368,7 @@ async function handleApi(request, response, url, context) {
|
|
|
303
368
|
const body = studioAiChangeSchema.parse(await readJson(request));
|
|
304
369
|
try {
|
|
305
370
|
const applied = await applyStudioAiChange(plugin.path, body);
|
|
371
|
+
invalidateStudioPackageSize(plugin.path, context.packageSizeCache);
|
|
306
372
|
const checkState = await removeStudioPluginCheckFindingsForFiles(localId, [applied.path]);
|
|
307
373
|
sendJson(response, 200, {
|
|
308
374
|
...applied,
|
|
@@ -325,6 +391,7 @@ async function handleApi(request, response, url, context) {
|
|
|
325
391
|
const localId = decodeURIComponent(bumpMatch[1]);
|
|
326
392
|
const plugin = await requireLocalPlugin(localId);
|
|
327
393
|
await bumpLocalPluginVersion(plugin.path, body.bump);
|
|
394
|
+
invalidateStudioPackageSize(plugin.path, context.packageSizeCache);
|
|
328
395
|
await addLocalPluginPath(plugin.path, plugin.source);
|
|
329
396
|
await removeStudioPluginCheckState(localId);
|
|
330
397
|
sendJson(response, 200, { ...(await getVersionState(plugin.path)), checkState: null });
|
|
@@ -347,6 +414,7 @@ async function handleApi(request, response, url, context) {
|
|
|
347
414
|
}
|
|
348
415
|
try {
|
|
349
416
|
await setLocalPluginVersion(plugin.path, trimmed);
|
|
417
|
+
invalidateStudioPackageSize(plugin.path, context.packageSizeCache);
|
|
350
418
|
await addLocalPluginPath(plugin.path, plugin.source);
|
|
351
419
|
await removeStudioPluginCheckState(localId);
|
|
352
420
|
sendJson(response, 200, { ...(await getVersionState(plugin.path)), checkState: null });
|
|
@@ -410,7 +478,7 @@ async function handleApi(request, response, url, context) {
|
|
|
410
478
|
}
|
|
411
479
|
if (method === "POST" && url.pathname === "/api/jobs") {
|
|
412
480
|
const body = jobSchema.parse(await readJson(request));
|
|
413
|
-
const job = createWebJob(body, context.jobs, context.approvals, context.playgrounds, context.playgroundPortReservations);
|
|
481
|
+
const job = createWebJob(body, context.jobs, context.approvals, context.playgrounds, context.playgroundPortReservations, context.dependencies);
|
|
414
482
|
sendJson(response, 202, job);
|
|
415
483
|
return;
|
|
416
484
|
}
|
|
@@ -437,7 +505,7 @@ async function handleApi(request, response, url, context) {
|
|
|
437
505
|
}
|
|
438
506
|
sendJson(response, 404, { error: { message: "Not found.", code: "not_found" } });
|
|
439
507
|
}
|
|
440
|
-
function createWebJob(input, jobs, approvals, playgrounds, playgroundPortReservations) {
|
|
508
|
+
function createWebJob(input, jobs, approvals, playgrounds, playgroundPortReservations, dependencies = {}) {
|
|
441
509
|
if (input.type === "clone") {
|
|
442
510
|
return jobs.create("clone", `Clone/update ${input.slug}`, (context) => clonePluginJob(input, context));
|
|
443
511
|
}
|
|
@@ -445,7 +513,7 @@ function createWebJob(input, jobs, approvals, playgrounds, playgroundPortReserva
|
|
|
445
513
|
return jobs.create("play", `Start Playground for ${input.id}`, (context) => playPluginJob(input, playgrounds, playgroundPortReservations, context));
|
|
446
514
|
}
|
|
447
515
|
if (input.type === "check") {
|
|
448
|
-
return jobs.create("check", "Plugin Check", (context) => pluginCheckJob(input.localId, context));
|
|
516
|
+
return jobs.create("check", "Plugin Check", (context) => pluginCheckJob(input.localId, context, dependencies));
|
|
449
517
|
}
|
|
450
518
|
if (input.type === "ai-chat") {
|
|
451
519
|
return jobs.create("ai-chat", "AI Assistance", (context) => aiChatJob(input, context));
|
|
@@ -569,41 +637,50 @@ async function playPluginJob(input, playgrounds, playgroundPortReservations, con
|
|
|
569
637
|
throw error;
|
|
570
638
|
}
|
|
571
639
|
}
|
|
572
|
-
async function pluginCheckJob(localId, context) {
|
|
640
|
+
async function pluginCheckJob(localId, context, dependencies = {}) {
|
|
573
641
|
const local = await requireLocalPlugin(localId);
|
|
574
642
|
const source = resolvePluginProjectPath(local.path);
|
|
575
643
|
const project = await discoverPluginProject(source.rootDir);
|
|
644
|
+
const stagePlugin = dependencies.stagePluginDirectory ?? stagePluginDirectory;
|
|
645
|
+
const pluginChecker = dependencies.runPluginCheck ?? runPluginCheck;
|
|
646
|
+
const stageRoot = await mkdtemp(path.join(tmpdir(), "pressship-studio-check-"));
|
|
576
647
|
context.status(`Running WordPress.org Plugin Check for ${project.headers.pluginName}.`);
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
skipped: result.skipped,
|
|
587
|
-
available: result.available,
|
|
588
|
-
findings,
|
|
589
|
-
summary,
|
|
590
|
-
checkedAt
|
|
591
|
-
});
|
|
592
|
-
context.log(`Plugin Check finished: ${summary.error} errors, ${summary.warning} warnings, ${summary.info} info.`);
|
|
593
|
-
return {
|
|
594
|
-
plugin: {
|
|
595
|
-
id: local.id,
|
|
596
|
-
name: local.name,
|
|
648
|
+
try {
|
|
649
|
+
const checkTarget = await stagePlugin(project, { outputDir: stageRoot });
|
|
650
|
+
const result = await pluginChecker(checkTarget.path, { mode: "new" });
|
|
651
|
+
const findings = await addStudioPluginCheckLineHints(normalizeStudioPluginCheckFindings(result.findings, checkTarget.path, project.slug), source.rootDir);
|
|
652
|
+
const summary = summarizeStudioPluginCheckFindings(findings);
|
|
653
|
+
const checkedAt = new Date().toISOString();
|
|
654
|
+
const persisted = await writeStudioPluginCheckState({
|
|
655
|
+
pluginId: local.id,
|
|
656
|
+
pluginPath: local.path,
|
|
597
657
|
slug: local.slug,
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
658
|
+
name: local.name,
|
|
659
|
+
skipped: result.skipped,
|
|
660
|
+
available: result.available,
|
|
661
|
+
findings,
|
|
662
|
+
summary,
|
|
663
|
+
checkedAt
|
|
664
|
+
});
|
|
665
|
+
context.log(`Plugin Check finished: ${summary.error} errors, ${summary.warning} warnings, ${summary.info} info.`);
|
|
666
|
+
return {
|
|
667
|
+
plugin: {
|
|
668
|
+
id: local.id,
|
|
669
|
+
name: local.name,
|
|
670
|
+
slug: local.slug,
|
|
671
|
+
path: local.path
|
|
672
|
+
},
|
|
673
|
+
skipped: result.skipped,
|
|
674
|
+
available: result.available,
|
|
675
|
+
findings,
|
|
676
|
+
summary,
|
|
677
|
+
checkedAt: persisted.checkedAt,
|
|
678
|
+
rawOutput: result.rawOutput
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
finally {
|
|
682
|
+
await rm(stageRoot, { recursive: true, force: true });
|
|
683
|
+
}
|
|
607
684
|
}
|
|
608
685
|
async function aiChatJob(input, context) {
|
|
609
686
|
const local = await requireLocalPlugin(input.localId);
|
|
@@ -680,12 +757,13 @@ async function dryRunPublishJob(localId, requestedAction, approvals, context) {
|
|
|
680
757
|
if (route.action === "release" && versionState.releaseBlocked) {
|
|
681
758
|
context.log("Release is blocked by version state.", versionState);
|
|
682
759
|
}
|
|
760
|
+
const ignorePatterns = await readPressshipIgnorePatterns(source.rootDir);
|
|
683
761
|
context.status("Validating package.");
|
|
684
|
-
const validation = await validatePluginPack(project, {});
|
|
762
|
+
const validation = await validatePluginPack(project, { ignore: ignorePatterns });
|
|
685
763
|
const validationBlocked = hasBlockingFindings(validation.readmeFindings) || hasBlockingFindings(validation.pluginCheckFindings);
|
|
686
764
|
const cacheDir = path.join(await ensureCacheDir(), "studio-packages");
|
|
687
765
|
await mkdir(cacheDir, { recursive: true, mode: 0o700 });
|
|
688
|
-
const pack = summarizePackResult(await createPluginPack(source.rootDir, { outputDir: cacheDir }), validation);
|
|
766
|
+
const pack = summarizePackResult(await createPluginPack(source.rootDir, { outputDir: cacheDir, ignore: ignorePatterns }), validation);
|
|
689
767
|
const releasePlan = route.action === "release"
|
|
690
768
|
? createReleaseCommandPlan(project.slug, path.resolve(source.svnRootDir ?? path.join(process.cwd(), ".pressship-svn", project.slug)), project.version ?? "unknown", `Release ${project.slug} ${project.version ?? "unknown"}`, await inferWordPressOrgUsername())
|
|
691
769
|
: undefined;
|
|
@@ -695,7 +773,8 @@ async function dryRunPublishJob(localId, requestedAction, approvals, context) {
|
|
|
695
773
|
localId,
|
|
696
774
|
pluginPath: source.inputDir,
|
|
697
775
|
action: route.action,
|
|
698
|
-
version: project.version
|
|
776
|
+
version: project.version,
|
|
777
|
+
ignore: ignorePatterns
|
|
699
778
|
})
|
|
700
779
|
: undefined;
|
|
701
780
|
return {
|
|
@@ -728,6 +807,7 @@ async function confirmPublishJob(approvalId, overview, approvals, context) {
|
|
|
728
807
|
yes: true,
|
|
729
808
|
submit: approval.action === "submit",
|
|
730
809
|
release: approval.action === "release",
|
|
810
|
+
ignore: approval.ignore ?? [],
|
|
731
811
|
overview: overview ?? project.headers.description ?? ""
|
|
732
812
|
});
|
|
733
813
|
approvals.delete(approvalId);
|
|
@@ -749,39 +829,366 @@ async function readPluginDetail(scope, id) {
|
|
|
749
829
|
readme: await fetchHostedReadme(id).catch(() => undefined)
|
|
750
830
|
};
|
|
751
831
|
}
|
|
832
|
+
const studioHiddenFilePatterns = [
|
|
833
|
+
"**/.git/**",
|
|
834
|
+
"**/.svn/**",
|
|
835
|
+
"**/node_modules/**",
|
|
836
|
+
"**/vendor/**",
|
|
837
|
+
"**/build/**",
|
|
838
|
+
"**/dist/**",
|
|
839
|
+
"**/playground/**",
|
|
840
|
+
"**/.wordpress-playground/**"
|
|
841
|
+
];
|
|
842
|
+
const maxStudioEditableFileBytes = 1_000_000;
|
|
843
|
+
const studioAlwaysHiddenDirectoryNames = new Set([".git"]);
|
|
844
|
+
const studioDeferredDirectoryNames = new Set([
|
|
845
|
+
...hardIgnoreDirectories,
|
|
846
|
+
"vendor",
|
|
847
|
+
".wordpress-playground",
|
|
848
|
+
"playground"
|
|
849
|
+
]);
|
|
850
|
+
const studioDeferredDirectoryPatterns = Array.from(studioDeferredDirectoryNames).flatMap((directory) => [
|
|
851
|
+
`${directory}/**`,
|
|
852
|
+
`**/${directory}/**`
|
|
853
|
+
]);
|
|
854
|
+
const studioEditableExtensions = new Set([
|
|
855
|
+
".php",
|
|
856
|
+
".js",
|
|
857
|
+
".jsx",
|
|
858
|
+
".ts",
|
|
859
|
+
".tsx",
|
|
860
|
+
".css",
|
|
861
|
+
".scss",
|
|
862
|
+
".sass",
|
|
863
|
+
".html",
|
|
864
|
+
".htm",
|
|
865
|
+
".json",
|
|
866
|
+
".md",
|
|
867
|
+
".txt",
|
|
868
|
+
".xml",
|
|
869
|
+
".yml",
|
|
870
|
+
".yaml",
|
|
871
|
+
".po",
|
|
872
|
+
".pot",
|
|
873
|
+
".ini",
|
|
874
|
+
".sh",
|
|
875
|
+
".svg"
|
|
876
|
+
]);
|
|
877
|
+
const studioEditableFileNames = new Set([
|
|
878
|
+
"composer.json",
|
|
879
|
+
"package.json",
|
|
880
|
+
"readme.txt",
|
|
881
|
+
"license",
|
|
882
|
+
"license.txt",
|
|
883
|
+
pressshipIgnoreFile
|
|
884
|
+
]);
|
|
752
885
|
async function listStudioFiles(pluginPath) {
|
|
753
886
|
const root = path.resolve(pluginPath);
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
"
|
|
758
|
-
|
|
759
|
-
|
|
887
|
+
const patterns = await readPressshipIgnorePatterns(root);
|
|
888
|
+
const matcher = createPressshipIgnoreMatcher(patterns);
|
|
889
|
+
const [files, deferredDirectories, ignoredDirectories] = await Promise.all([
|
|
890
|
+
fg("**/*", {
|
|
891
|
+
cwd: root,
|
|
892
|
+
onlyFiles: true,
|
|
893
|
+
dot: true,
|
|
894
|
+
unique: true,
|
|
895
|
+
ignore: studioDeferredDirectoryPatterns
|
|
896
|
+
}),
|
|
897
|
+
listStudioDeferredDirectories(root, matcher),
|
|
898
|
+
listStudioIgnoredPatternDirectories(root, patterns, matcher)
|
|
899
|
+
]);
|
|
900
|
+
const directories = mergeStudioDirectoryEntries([...deferredDirectories, ...ignoredDirectories]);
|
|
901
|
+
const entries = await Promise.all(files.sort((a, b) => a.localeCompare(b)).map(async (relativePath) => {
|
|
902
|
+
const fileStats = await stat(path.join(root, relativePath));
|
|
903
|
+
const ignoredBy = matcher.ignoredBy(relativePath);
|
|
904
|
+
return {
|
|
905
|
+
path: relativePath,
|
|
906
|
+
name: path.basename(relativePath),
|
|
907
|
+
directory: path.dirname(relativePath) === "." ? "" : path.dirname(relativePath),
|
|
908
|
+
size: fileStats.size,
|
|
909
|
+
hardIgnored: isHardIgnoredPath(relativePath),
|
|
910
|
+
ignored: Boolean(ignoredBy),
|
|
911
|
+
ignoredBy
|
|
912
|
+
};
|
|
913
|
+
}));
|
|
914
|
+
return { files: entries, directories };
|
|
915
|
+
}
|
|
916
|
+
async function listStudioDirectory(pluginPath, relativePath) {
|
|
917
|
+
const root = path.resolve(pluginPath);
|
|
918
|
+
const normalized = normalizeStudioRelativePath(relativePath);
|
|
919
|
+
if (!normalized) {
|
|
920
|
+
throw new Error("Choose a folder first.");
|
|
921
|
+
}
|
|
922
|
+
if (studioExplorerDirectoryIsAlwaysHidden(normalized)) {
|
|
923
|
+
return { files: [], directories: [] };
|
|
924
|
+
}
|
|
925
|
+
const directoryPath = path.resolve(root, normalized);
|
|
926
|
+
if (directoryPath !== root && !directoryPath.startsWith(`${root}${path.sep}`)) {
|
|
927
|
+
throw new Error("Folder is outside the plugin directory.");
|
|
928
|
+
}
|
|
929
|
+
const directoryStats = await stat(directoryPath);
|
|
930
|
+
if (!directoryStats.isDirectory()) {
|
|
931
|
+
throw new Error("Studio can only load folders.");
|
|
932
|
+
}
|
|
933
|
+
const patterns = await readPressshipIgnorePatterns(root);
|
|
934
|
+
const matcher = createPressshipIgnoreMatcher(patterns);
|
|
935
|
+
const ignoredBy = studioIgnoredBy(normalized, matcher);
|
|
936
|
+
const entries = await readdir(directoryPath, { withFileTypes: true });
|
|
937
|
+
const files = [];
|
|
938
|
+
const directories = [
|
|
939
|
+
{
|
|
940
|
+
path: normalized,
|
|
941
|
+
name: path.basename(normalized),
|
|
942
|
+
directory: path.dirname(normalized) === "." ? "" : path.dirname(normalized),
|
|
943
|
+
deferred: false,
|
|
944
|
+
ignored: Boolean(ignoredBy),
|
|
945
|
+
ignoredBy,
|
|
946
|
+
hardIgnored: isHardIgnoredPath(normalized)
|
|
947
|
+
}
|
|
948
|
+
];
|
|
949
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
950
|
+
const childPath = `${normalized}/${entry.name}`;
|
|
951
|
+
if (studioExplorerDirectoryIsAlwaysHidden(childPath)) {
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
const childIgnoredBy = studioIgnoredBy(childPath, matcher);
|
|
955
|
+
if (entry.isDirectory()) {
|
|
956
|
+
directories.push({
|
|
957
|
+
path: childPath,
|
|
958
|
+
name: entry.name,
|
|
959
|
+
directory: normalized,
|
|
960
|
+
deferred: true,
|
|
961
|
+
ignored: Boolean(childIgnoredBy),
|
|
962
|
+
ignoredBy: childIgnoredBy,
|
|
963
|
+
hardIgnored: isHardIgnoredPath(childPath)
|
|
964
|
+
});
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
if (!entry.isFile()) {
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
const fileStats = await stat(path.join(directoryPath, entry.name));
|
|
971
|
+
files.push({
|
|
972
|
+
path: childPath,
|
|
973
|
+
name: entry.name,
|
|
974
|
+
directory: normalized,
|
|
975
|
+
size: fileStats.size,
|
|
976
|
+
hardIgnored: isHardIgnoredPath(childPath),
|
|
977
|
+
ignored: Boolean(childIgnoredBy),
|
|
978
|
+
ignoredBy: childIgnoredBy
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
return { files, directories };
|
|
982
|
+
}
|
|
983
|
+
async function listStudioDeferredDirectories(root, matcher) {
|
|
984
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
985
|
+
return entries
|
|
986
|
+
.filter((entry) => entry.isDirectory() &&
|
|
987
|
+
studioDeferredDirectoryNames.has(entry.name) &&
|
|
988
|
+
!studioExplorerDirectoryIsAlwaysHidden(entry.name))
|
|
989
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
990
|
+
.map((entry) => {
|
|
991
|
+
const ignoredBy = isHardIgnoredPath(entry.name)
|
|
992
|
+
? "Pressship package rules"
|
|
993
|
+
: matcher.ignoredBy(entry.name) ?? matcher.ignoredBy(`${entry.name}/.pressship-directory`);
|
|
994
|
+
return {
|
|
995
|
+
path: entry.name,
|
|
996
|
+
name: entry.name,
|
|
997
|
+
directory: "",
|
|
998
|
+
deferred: true,
|
|
999
|
+
ignored: Boolean(ignoredBy),
|
|
1000
|
+
ignoredBy,
|
|
1001
|
+
hardIgnored: isHardIgnoredPath(entry.name)
|
|
1002
|
+
};
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
async function listStudioIgnoredPatternDirectories(root, patterns, matcher) {
|
|
1006
|
+
const candidates = Array.from(new Set(patterns.flatMap(studioIgnoredDirectoryCandidates)));
|
|
1007
|
+
const entries = await Promise.all(candidates.map(async (relativePath) => {
|
|
1008
|
+
if (studioExplorerDirectoryIsAlwaysHidden(relativePath)) {
|
|
1009
|
+
return undefined;
|
|
1010
|
+
}
|
|
1011
|
+
const absolutePath = path.join(root, relativePath);
|
|
1012
|
+
if (!pathExists(absolutePath)) {
|
|
1013
|
+
return undefined;
|
|
1014
|
+
}
|
|
1015
|
+
const entryStats = await stat(absolutePath);
|
|
1016
|
+
if (!entryStats.isDirectory()) {
|
|
1017
|
+
return undefined;
|
|
1018
|
+
}
|
|
1019
|
+
const ignoredBy = matcher.ignoredBy(relativePath) ?? matcher.ignoredBy(`${relativePath}/.pressship-directory`);
|
|
1020
|
+
if (!ignoredBy) {
|
|
1021
|
+
return undefined;
|
|
1022
|
+
}
|
|
1023
|
+
return {
|
|
1024
|
+
path: relativePath,
|
|
1025
|
+
name: path.basename(relativePath),
|
|
1026
|
+
directory: path.dirname(relativePath) === "." ? "" : path.dirname(relativePath),
|
|
1027
|
+
deferred: false,
|
|
1028
|
+
ignored: true,
|
|
1029
|
+
ignoredBy,
|
|
1030
|
+
hardIgnored: isHardIgnoredPath(relativePath)
|
|
1031
|
+
};
|
|
1032
|
+
}));
|
|
1033
|
+
return entries
|
|
1034
|
+
.filter((entry) => Boolean(entry))
|
|
1035
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
1036
|
+
}
|
|
1037
|
+
function studioIgnoredDirectoryCandidates(pattern) {
|
|
1038
|
+
const trimmed = pattern.trim();
|
|
1039
|
+
if (!trimmed || trimmed.startsWith("!")) {
|
|
1040
|
+
return [];
|
|
1041
|
+
}
|
|
1042
|
+
let candidate = "";
|
|
1043
|
+
if (trimmed.endsWith("/**")) {
|
|
1044
|
+
candidate = trimmed.slice(0, -3);
|
|
1045
|
+
}
|
|
1046
|
+
else if (trimmed.endsWith("/")) {
|
|
1047
|
+
candidate = trimmed.slice(0, -1);
|
|
1048
|
+
}
|
|
1049
|
+
else if (!hasStudioGlobMagic(trimmed)) {
|
|
1050
|
+
candidate = trimmed;
|
|
1051
|
+
}
|
|
1052
|
+
candidate = candidate.replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
1053
|
+
if (!candidate || candidate === "." || hasStudioGlobMagic(candidate)) {
|
|
1054
|
+
return [];
|
|
1055
|
+
}
|
|
1056
|
+
return [candidate];
|
|
1057
|
+
}
|
|
1058
|
+
function hasStudioGlobMagic(value) {
|
|
1059
|
+
return /[*?[\]{}]/.test(value);
|
|
1060
|
+
}
|
|
1061
|
+
function studioIgnoredBy(relativePath, matcher) {
|
|
1062
|
+
return isHardIgnoredPath(relativePath)
|
|
1063
|
+
? "Pressship package rules"
|
|
1064
|
+
: matcher.ignoredBy(relativePath);
|
|
1065
|
+
}
|
|
1066
|
+
function studioExplorerDirectoryIsAlwaysHidden(relativePath) {
|
|
1067
|
+
return relativePath
|
|
1068
|
+
.split("/")
|
|
1069
|
+
.filter(Boolean)
|
|
1070
|
+
.some((segment) => studioAlwaysHiddenDirectoryNames.has(segment));
|
|
1071
|
+
}
|
|
1072
|
+
function mergeStudioDirectoryEntries(entries) {
|
|
1073
|
+
const merged = new Map();
|
|
1074
|
+
for (const entry of entries) {
|
|
1075
|
+
const existing = merged.get(entry.path);
|
|
1076
|
+
if (!existing) {
|
|
1077
|
+
merged.set(entry.path, entry);
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
merged.set(entry.path, {
|
|
1081
|
+
...existing,
|
|
1082
|
+
...entry,
|
|
1083
|
+
deferred: Boolean(existing.deferred || entry.deferred),
|
|
1084
|
+
ignored: Boolean(existing.ignored || entry.ignored),
|
|
1085
|
+
ignoredBy: existing.hardIgnored ? existing.ignoredBy : entry.ignoredBy ?? existing.ignoredBy,
|
|
1086
|
+
hardIgnored: Boolean(existing.hardIgnored || entry.hardIgnored)
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
return Array.from(merged.values()).sort((a, b) => a.path.localeCompare(b.path));
|
|
1090
|
+
}
|
|
1091
|
+
async function readStudioIgnoreState(pluginPath) {
|
|
1092
|
+
const root = path.resolve(pluginPath);
|
|
1093
|
+
const patterns = await readPressshipIgnorePatterns(root);
|
|
1094
|
+
const ignoredFiles = await listStudioIgnoredFiles(root, patterns);
|
|
1095
|
+
return {
|
|
1096
|
+
ignorePath: path.join(root, pressshipIgnoreFile),
|
|
1097
|
+
patterns,
|
|
1098
|
+
ignoredFiles
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
async function listStudioIgnoredFiles(root, patterns) {
|
|
1102
|
+
if (!patterns.length) {
|
|
1103
|
+
return [];
|
|
1104
|
+
}
|
|
1105
|
+
const matcher = createPressshipIgnoreMatcher(patterns);
|
|
1106
|
+
const files = await fg("**/*", {
|
|
760
1107
|
cwd: root,
|
|
761
1108
|
onlyFiles: true,
|
|
762
1109
|
dot: false,
|
|
763
1110
|
unique: true,
|
|
764
|
-
ignore:
|
|
765
|
-
"**/.git/**",
|
|
766
|
-
"**/.svn/**",
|
|
767
|
-
"**/node_modules/**",
|
|
768
|
-
"**/vendor/**",
|
|
769
|
-
"**/build/**",
|
|
770
|
-
"**/dist/**",
|
|
771
|
-
"**/playground/**",
|
|
772
|
-
"**/.wordpress-playground/**"
|
|
773
|
-
]
|
|
1111
|
+
ignore: studioHiddenFilePatterns
|
|
774
1112
|
});
|
|
775
|
-
const
|
|
1113
|
+
const ignored = files
|
|
1114
|
+
.map((relativePath) => ({ relativePath, ignoredBy: matcher.ignoredBy(relativePath) }))
|
|
1115
|
+
.filter((entry) => Boolean(entry.ignoredBy));
|
|
1116
|
+
const entries = await Promise.all(ignored.sort((a, b) => a.relativePath.localeCompare(b.relativePath)).map(async ({ relativePath, ignoredBy }) => {
|
|
776
1117
|
const fileStats = await stat(path.join(root, relativePath));
|
|
777
1118
|
return {
|
|
778
1119
|
path: relativePath,
|
|
779
1120
|
name: path.basename(relativePath),
|
|
780
1121
|
directory: path.dirname(relativePath) === "." ? "" : path.dirname(relativePath),
|
|
781
|
-
size: fileStats.size
|
|
1122
|
+
size: fileStats.size,
|
|
1123
|
+
ignoredBy
|
|
782
1124
|
};
|
|
783
1125
|
}));
|
|
784
|
-
return
|
|
1126
|
+
return entries;
|
|
1127
|
+
}
|
|
1128
|
+
function readStudioPackageSize(pluginPath, cache) {
|
|
1129
|
+
const source = resolvePluginProjectPath(pluginPath);
|
|
1130
|
+
const key = source.rootDir;
|
|
1131
|
+
const cached = cache.get(key);
|
|
1132
|
+
if (cached) {
|
|
1133
|
+
return publicStudioPackageSizeEntry(cached);
|
|
1134
|
+
}
|
|
1135
|
+
const entry = {
|
|
1136
|
+
status: "calculating",
|
|
1137
|
+
requestedAt: new Date().toISOString()
|
|
1138
|
+
};
|
|
1139
|
+
cache.set(key, entry);
|
|
1140
|
+
entry.promise = calculateStudioPackageSize(source.rootDir)
|
|
1141
|
+
.then((result) => {
|
|
1142
|
+
entry.status = "ready";
|
|
1143
|
+
entry.result = result;
|
|
1144
|
+
entry.error = undefined;
|
|
1145
|
+
entry.updatedAt = new Date().toISOString();
|
|
1146
|
+
})
|
|
1147
|
+
.catch((error) => {
|
|
1148
|
+
entry.status = "error";
|
|
1149
|
+
entry.error = error instanceof Error ? error.message : String(error);
|
|
1150
|
+
entry.updatedAt = new Date().toISOString();
|
|
1151
|
+
});
|
|
1152
|
+
return publicStudioPackageSizeEntry(entry);
|
|
1153
|
+
}
|
|
1154
|
+
function invalidateStudioPackageSize(pluginPath, cache) {
|
|
1155
|
+
cache.delete(resolvePluginProjectPath(pluginPath).rootDir);
|
|
1156
|
+
}
|
|
1157
|
+
async function calculateStudioPackageSize(rootDir) {
|
|
1158
|
+
const project = await discoverPluginProject(rootDir);
|
|
1159
|
+
const ignorePatterns = await readPressshipIgnorePatterns(rootDir);
|
|
1160
|
+
const analysis = await analyzePluginPackage(project, { ignore: ignorePatterns });
|
|
1161
|
+
return {
|
|
1162
|
+
sizeBytes: analysis.sizeBytes,
|
|
1163
|
+
maxSizeBytes: analysis.maxSizeBytes,
|
|
1164
|
+
overLimit: analysis.overLimit,
|
|
1165
|
+
fileCount: analysis.files.length,
|
|
1166
|
+
topLevelFolder: analysis.topLevelFolder,
|
|
1167
|
+
largestFiles: analysis.largestFiles
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
function publicStudioPackageSizeEntry(entry) {
|
|
1171
|
+
if (entry.status === "ready" && entry.result) {
|
|
1172
|
+
return {
|
|
1173
|
+
status: "ready",
|
|
1174
|
+
cached: true,
|
|
1175
|
+
calculatedAt: entry.updatedAt,
|
|
1176
|
+
...entry.result
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
if (entry.status === "error") {
|
|
1180
|
+
return {
|
|
1181
|
+
status: "error",
|
|
1182
|
+
cached: true,
|
|
1183
|
+
error: entry.error ?? "Package size could not be calculated.",
|
|
1184
|
+
calculatedAt: entry.updatedAt
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
return {
|
|
1188
|
+
status: "calculating",
|
|
1189
|
+
cached: false,
|
|
1190
|
+
requestedAt: entry.requestedAt
|
|
1191
|
+
};
|
|
785
1192
|
}
|
|
786
1193
|
async function readStudioFile(pluginPath, relativePath) {
|
|
787
1194
|
const filePath = await resolveStudioFilePath(pluginPath, relativePath);
|
|
@@ -887,11 +1294,18 @@ async function resolveStudioFilePath(pluginPath, relativePath) {
|
|
|
887
1294
|
if (!fileStats.isFile()) {
|
|
888
1295
|
throw new Error("Studio can only open files.");
|
|
889
1296
|
}
|
|
1297
|
+
if (fileStats.size > maxStudioEditableFileBytes) {
|
|
1298
|
+
throw new Error(`Studio can list ${normalized}, but files larger than 1 MB are not opened in the editor.`);
|
|
1299
|
+
}
|
|
890
1300
|
return filePath;
|
|
891
1301
|
}
|
|
892
1302
|
function normalizeStudioRelativePath(relativePath) {
|
|
893
1303
|
return relativePath.replace(/\\/g, "/").replace(/^\/+/, "").split("/").filter(Boolean).join("/");
|
|
894
1304
|
}
|
|
1305
|
+
function isStudioEditablePath(relativePath) {
|
|
1306
|
+
const name = path.basename(relativePath).toLowerCase();
|
|
1307
|
+
return studioEditableFileNames.has(name) || studioEditableExtensions.has(path.extname(name));
|
|
1308
|
+
}
|
|
895
1309
|
async function selectFolder() {
|
|
896
1310
|
if (process.platform === "darwin") {
|
|
897
1311
|
const result = await execa("osascript", [
|
|
@@ -1248,7 +1662,7 @@ async function createStudioAiPreviewWorkspace(pluginPath) {
|
|
|
1248
1662
|
async function snapshotStudioFileContents(pluginPath) {
|
|
1249
1663
|
const root = path.resolve(pluginPath);
|
|
1250
1664
|
const { files } = await listStudioFiles(root);
|
|
1251
|
-
const entries = await Promise.all(files.map(async (file) => {
|
|
1665
|
+
const entries = await Promise.all(files.filter((file) => file.size <= maxStudioEditableFileBytes && isStudioEditablePath(file.path)).map(async (file) => {
|
|
1252
1666
|
const filePath = path.join(root, file.path);
|
|
1253
1667
|
const [fileStats, content] = await Promise.all([stat(filePath), readFile(filePath, "utf8")]);
|
|
1254
1668
|
return [
|
|
@@ -1358,18 +1772,26 @@ function streamJobEvents(response, jobs, id) {
|
|
|
1358
1772
|
response.on("close", unsubscribe);
|
|
1359
1773
|
}
|
|
1360
1774
|
async function serveStatic(response, staticDir, requestPath, token) {
|
|
1361
|
-
|
|
1362
|
-
|
|
1775
|
+
if (requestPath === "/" || isSpaRoutePath(requestPath)) {
|
|
1776
|
+
await serveIndex(response, staticDir, token);
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
const rootDir = path.resolve(staticDir);
|
|
1780
|
+
const relativePath = requestPath.replace(/^\/+/, "");
|
|
1781
|
+
const filePath = path.resolve(rootDir, relativePath);
|
|
1782
|
+
if (filePath !== rootDir && !filePath.startsWith(`${rootDir}${path.sep}`)) {
|
|
1363
1783
|
sendJson(response, 403, { error: { message: "Forbidden." } });
|
|
1364
1784
|
return;
|
|
1365
1785
|
}
|
|
1366
1786
|
if (path.basename(filePath) === "index.html") {
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1787
|
+
await serveIndex(response, rootDir, token);
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
try {
|
|
1791
|
+
await stat(filePath);
|
|
1792
|
+
}
|
|
1793
|
+
catch {
|
|
1794
|
+
sendJson(response, 404, { error: { message: "Not found." } });
|
|
1373
1795
|
return;
|
|
1374
1796
|
}
|
|
1375
1797
|
const type = contentType(filePath);
|
|
@@ -1381,6 +1803,19 @@ async function serveStatic(response, staticDir, requestPath, token) {
|
|
|
1381
1803
|
.on("error", () => sendJson(response, 404, { error: { message: "Not found." } }))
|
|
1382
1804
|
.pipe(response);
|
|
1383
1805
|
}
|
|
1806
|
+
function isSpaRoutePath(requestPath) {
|
|
1807
|
+
const firstSegment = requestPath.split("/").filter(Boolean)[0] ?? "";
|
|
1808
|
+
return spaRoutePrefixes.has(firstSegment);
|
|
1809
|
+
}
|
|
1810
|
+
async function serveIndex(response, staticDir, token) {
|
|
1811
|
+
const filePath = path.join(staticDir, "index.html");
|
|
1812
|
+
const html = await readFile(filePath, "utf8");
|
|
1813
|
+
response.writeHead(200, {
|
|
1814
|
+
"Cache-Control": "no-store",
|
|
1815
|
+
"Content-Type": "text/html; charset=utf-8"
|
|
1816
|
+
});
|
|
1817
|
+
response.end(html.replace("__PRESSSHIP_TOKEN__", token));
|
|
1818
|
+
}
|
|
1384
1819
|
async function readJson(request) {
|
|
1385
1820
|
const chunks = [];
|
|
1386
1821
|
for await (const chunk of request) {
|