@jxsuite/studio 6.0.0 → 6.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/2020-va2wwra4.js +4 -0
- package/dist/chunks/adapter-f93pafd7.js +4 -0
- package/dist/chunks/{cf-account-picker-bb2yq946.js → cf-account-picker-af1h381v.js} +3 -3
- package/dist/chunks/cf-settings-125tdm0w.js +4 -0
- package/dist/chunks/components-ax2f1fnn.js +4 -0
- package/dist/chunks/content-source-108aqtk1.js +4 -0
- package/dist/chunks/{convert-to-repeater-vqkt7s3s.js → convert-to-repeater-ry5rmppr.js} +3 -3
- package/dist/chunks/editor-pfgdmkb0.js +4 -0
- package/dist/chunks/entry-commands-t0k1q0kx.js +4 -0
- package/dist/chunks/{extension-sections-8beq9ac3.js → extension-sections-6mwh2hf8.js} +3 -3
- package/dist/chunks/file-ops-meej5mqf.js +4 -0
- package/dist/chunks/files-44vp8a2s.js +4 -0
- package/dist/chunks/format-host-7q1petb9.js +4 -0
- package/dist/chunks/fs-events-h2mwnxaz.js +4 -0
- package/dist/chunks/git-panel-zngbymvd.js +4 -0
- package/dist/chunks/{grid-view-ca10def6.js → grid-view-sx9vynsp.js} +15 -15
- package/dist/chunks/icons-p44xhjf0.js +4 -0
- package/dist/chunks/iframe-host-qk3ycq87.js +4 -0
- package/dist/chunks/{index-9ydfe04y.js → index-2veqmnhr.js} +3 -3
- package/dist/chunks/index-wkyfjeqz.js +4 -0
- package/dist/chunks/{javascript-d9pqgc2p.js → javascript-8nbvs12s.js} +3 -3
- package/dist/chunks/jsonMode-s8kt9dsj.js +13 -0
- package/dist/chunks/jx-validate-psgd7vsx.js +4 -0
- package/dist/chunks/layers-panel-1kq2jyfk.js +4 -0
- package/dist/chunks/layers-xd92fg8d.js +4 -0
- package/dist/chunks/lit-html-ac4cwwr4.js +4 -0
- package/dist/chunks/media-picker-0svzkdyb.js +4 -0
- package/dist/chunks/menu-5c55ea3d.js +4 -0
- package/dist/chunks/{monaco-binding-62dtmgbf.js → monaco-binding-z6r70pdt.js} +3 -3
- package/dist/chunks/monaco-buffer-fz6gq5y7.js +4 -0
- package/dist/chunks/monaco-setup-q4axkgj7.js +4 -0
- package/dist/chunks/notify-0xgtn8q1.js +4 -0
- package/dist/chunks/platform-v1qctbe6.js +4 -0
- package/dist/chunks/publish-panel-ym1fmgq5.js +4 -0
- package/dist/chunks/quick-search-a54e6t5n.js +4 -0
- package/dist/chunks/references-gmqp82fs.js +4 -0
- package/dist/chunks/right-panel-v7t9awbk.js +4 -0
- package/dist/chunks/section-registry-vhyf3zsr.js +4 -0
- package/dist/chunks/studio-0bhx5hz6.js +5 -0
- package/dist/chunks/studio-0k1ddnyy.js +6 -0
- package/dist/chunks/studio-1b04pd0j.js +64 -0
- package/dist/chunks/{studio-sxc0zvpt.js → studio-1q1sh2ax.js} +3 -3
- package/dist/chunks/studio-4dwbj6gb.js +5 -0
- package/dist/chunks/studio-4tanh828.js +5 -0
- package/dist/chunks/{studio-fqq890ap.js → studio-55h4mstj.js} +3 -3
- package/dist/chunks/studio-5tnvjkz4.js +5 -0
- package/dist/chunks/studio-60dqgb0c.js +5 -0
- package/dist/chunks/studio-61dp09qg.js +9 -0
- package/dist/chunks/studio-63epcrj2.js +5 -0
- package/dist/chunks/studio-74k29dy9.js +5 -0
- package/dist/chunks/{studio-qa73881a.js → studio-7vg6jcd3.js} +4 -4
- package/dist/chunks/studio-7zx31knw.js +9 -0
- package/dist/chunks/studio-8bxk97km.js +5 -0
- package/dist/chunks/{studio-edbtvcd1.js → studio-8grwxtqk.js} +4 -4
- package/dist/chunks/studio-933e30n4.js +5 -0
- package/dist/chunks/{studio-rr4xkv81.js → studio-98wnwhbg.js} +4 -4
- package/dist/chunks/studio-9mccjwjh.js +14 -0
- package/dist/chunks/studio-a2ke1e4t.js +5 -0
- package/dist/chunks/studio-e3a2a6v3.js +4 -0
- package/dist/chunks/{studio-pwqfmt4p.js → studio-e81q1vb9.js} +4 -4
- package/dist/chunks/{studio-7pgy3xaf.js → studio-epgrmqqm.js} +4 -4
- package/dist/chunks/studio-g1shz58g.js +5 -0
- package/dist/chunks/studio-gf0fz03c.js +5 -0
- package/dist/chunks/studio-gjtpe62q.js +6 -0
- package/dist/chunks/{studio-tzegmy4w.js → studio-gkskhg7m.js} +4 -4
- package/dist/chunks/studio-gx4dnjzd.js +5 -0
- package/dist/chunks/{studio-8xcwewjq.js → studio-h46vtzk8.js} +4 -4
- package/dist/chunks/studio-j03zga3y.js +9 -0
- package/dist/chunks/{studio-4r0sjtx8.js → studio-jhpxeckz.js} +4 -4
- package/dist/chunks/{studio-zd4kayn2.js → studio-k01r7ykp.js} +4 -4
- package/dist/chunks/{studio-03wwfvyt.js → studio-kggey1jt.js} +4 -4
- package/dist/chunks/{studio-hnvnfbdv.js → studio-kk7h30ne.js} +6 -6
- package/dist/chunks/{studio-01qza1n3.js → studio-kt1zqney.js} +4 -4
- package/dist/chunks/{studio-r3kaka4d.js → studio-m3cwhvye.js} +4 -4
- package/dist/chunks/{studio-evwgp1ym.js → studio-mcayt6hz.js} +4 -4
- package/dist/chunks/{studio-5vy2wfm5.js → studio-myjcghv3.js} +4 -4
- package/dist/chunks/studio-n8bxwc3y.js +4 -0
- package/dist/chunks/{studio-ztvcxm3w.js → studio-ngm8gvpb.js} +4 -4
- package/dist/chunks/{studio-01kbbqg3.js → studio-nj5a01jy.js} +196 -196
- package/dist/chunks/{studio-xkfs0dp2.js → studio-pcgz8heb.js} +4 -4
- package/dist/chunks/{studio-0bbxhhce.js → studio-pre0skq6.js} +4 -4
- package/dist/chunks/{studio-n78gjfy8.js → studio-qcsq0t15.js} +4 -4
- package/dist/chunks/studio-qkstvve9.js +5 -0
- package/dist/chunks/studio-qv1pwd3c.js +5 -0
- package/dist/chunks/{studio-rmn2dz59.js → studio-rc6dvxjb.js} +5 -5
- package/dist/chunks/{studio-st75kn99.js → studio-rv11kfwa.js} +7 -7
- package/dist/chunks/studio-s8dmmz66.js +5 -0
- package/dist/chunks/studio-sp5xzbj4.js +6 -0
- package/dist/chunks/{studio-tmeq6mjb.js → studio-szsjbj0g.js} +4 -4
- package/dist/chunks/studio-t1xzawhr.js +16 -0
- package/dist/chunks/{studio-mk00zd5d.js → studio-t6b6qe31.js} +4 -4
- package/dist/chunks/{studio-bftp4n4b.js → studio-tdcprbqb.js} +4 -4
- package/dist/chunks/{studio-ge5fqjth.js → studio-vrmcjf9g.js} +5 -5
- package/dist/chunks/studio-vyc0b4he.js +10 -0
- package/dist/chunks/{studio-semd1s8s.js → studio-w0cgf6jg.js} +4 -4
- package/dist/chunks/{studio-gp02nqnw.js → studio-w3hskzqk.js} +4 -4
- package/dist/chunks/{studio-1wvfxkyy.js → studio-wszjyh0j.js} +4 -4
- package/dist/chunks/studio-xgrgcdyh.js +9 -0
- package/dist/chunks/{studio-y7jehjrp.js → studio-xz2yxfcb.js} +4 -4
- package/dist/chunks/{studio-aje966s9.js → studio-y6n42tgs.js} +4 -4
- package/dist/chunks/{studio-yr04mv2j.js → studio-yb560vxr.js} +4 -4
- package/dist/chunks/{studio-tp642n5g.js → studio-yn9f8vtx.js} +4 -4
- package/dist/chunks/studio-yw05w8f0.js +7 -0
- package/dist/chunks/{studio-cbr9czkn.js → studio-z16sgavc.js} +4 -4
- package/dist/chunks/studio-zey7zsdw.js +12 -0
- package/dist/chunks/{studio-1q6amtph.js → studio-zyqn40w4.js} +4 -4
- package/dist/chunks/tsMode-2frah6h1.js +14 -0
- package/dist/chunks/workspace-zt81g1ms.js +4 -0
- package/dist/chunks/ws-client-n2bh4hkf.js +4 -0
- package/dist/iframe-entry.js +13 -13
- package/dist/manifest.json +62 -62
- package/dist/studio.js +87 -87
- package/dist/workers/ts.worker.js +3 -3
- package/package.json +10 -10
- package/src/canvas/canvas-render.ts +4 -1
- package/src/canvas/canvas-utils.ts +73 -15
- package/src/canvas/iframe-host.ts +67 -21
- package/src/canvas/iframe-inline-edit.ts +22 -4
- package/src/canvas/iframe-interaction.ts +7 -1
- package/src/canvas/iframe-protocol.ts +3 -0
- package/src/canvas/iframe-render.ts +5 -2
- package/src/commands/defaults.ts +4 -1
- package/src/commands/live-context.ts +3 -1
- package/src/editor/shortcuts.ts +55 -27
- package/src/files/media-paths.ts +3 -1
- package/src/new-project/add-repo-modal.ts +459 -59
- package/src/panels/editors.ts +3 -1
- package/src/panels/style-panel.ts +36 -11
- package/src/platforms/cloud.ts +95 -18
- package/src/platforms/devserver.ts +47 -12
- package/src/publish/pages-service.ts +7 -0
- package/src/publish/publish-panel.ts +16 -2
- package/src/studio.ts +14 -2
- package/src/surfaces/add-repo.json +721 -224
- package/src/surfaces/add-repo.ts +196 -31
- package/src/surfaces/library-pane.ts +6 -0
- package/src/surfaces/rail.json +126 -73
- package/src/surfaces/rail.ts +31 -5
- package/src/tabs/typing.ts +31 -0
- package/src/types.ts +42 -3
- package/src/utils/edit-display.ts +30 -1
- package/src/utils/project-dir.ts +24 -0
- package/src/workspace/workspace.ts +7 -0
- package/dist/chunks/2020-cs2sb9t1.js +0 -4
- package/dist/chunks/adapter-y5gdq19c.js +0 -4
- package/dist/chunks/cf-settings-20dznc3j.js +0 -4
- package/dist/chunks/components-q037dya7.js +0 -4
- package/dist/chunks/content-source-t0q184js.js +0 -4
- package/dist/chunks/editor-j5sq149q.js +0 -4
- package/dist/chunks/entry-commands-fxzgcc4k.js +0 -4
- package/dist/chunks/file-ops-2n77759h.js +0 -4
- package/dist/chunks/files-fjy7zpxt.js +0 -4
- package/dist/chunks/format-host-wj3mf3n8.js +0 -4
- package/dist/chunks/fs-events-7kcgn4e9.js +0 -4
- package/dist/chunks/git-panel-7z2mpt2d.js +0 -4
- package/dist/chunks/icons-qbh8akb1.js +0 -4
- package/dist/chunks/iframe-host-a0zjd6xp.js +0 -4
- package/dist/chunks/index-m0x5ztax.js +0 -4
- package/dist/chunks/jsonMode-800yvbem.js +0 -13
- package/dist/chunks/jx-validate-zc9zp77s.js +0 -4
- package/dist/chunks/layers-p677nrh5.js +0 -4
- package/dist/chunks/layers-panel-y4w1dq90.js +0 -4
- package/dist/chunks/lit-html-cfn61dn7.js +0 -4
- package/dist/chunks/media-picker-xg7yjpz9.js +0 -4
- package/dist/chunks/menu-fpekzj6w.js +0 -4
- package/dist/chunks/monaco-buffer-nn4dtx27.js +0 -4
- package/dist/chunks/monaco-setup-tj13ndtm.js +0 -4
- package/dist/chunks/notify-ahs57a2z.js +0 -4
- package/dist/chunks/platform-v9xyfkbx.js +0 -4
- package/dist/chunks/publish-panel-xtbfgg2r.js +0 -4
- package/dist/chunks/quick-search-d3a6m7j3.js +0 -4
- package/dist/chunks/references-99ws0b0m.js +0 -4
- package/dist/chunks/right-panel-31vbk5xp.js +0 -4
- package/dist/chunks/section-registry-b2yzp9sw.js +0 -4
- package/dist/chunks/studio-1a99w39s.js +0 -5
- package/dist/chunks/studio-3ck2a5c4.js +0 -10
- package/dist/chunks/studio-4fde4a0x.js +0 -5
- package/dist/chunks/studio-4jj9gme2.js +0 -6
- package/dist/chunks/studio-4m50vh74.js +0 -16
- package/dist/chunks/studio-4xppwncr.js +0 -4
- package/dist/chunks/studio-5d7wfmnp.js +0 -5
- package/dist/chunks/studio-5e9zsamh.js +0 -9
- package/dist/chunks/studio-5gs9th4b.js +0 -5
- package/dist/chunks/studio-7qvs7v0m.js +0 -5
- package/dist/chunks/studio-84rtc120.js +0 -5
- package/dist/chunks/studio-aekhcdzw.js +0 -7
- package/dist/chunks/studio-aqmph6k4.js +0 -5
- package/dist/chunks/studio-c6zdm25z.js +0 -9
- package/dist/chunks/studio-capk0583.js +0 -9
- package/dist/chunks/studio-chdmwwzw.js +0 -5
- package/dist/chunks/studio-cp1gbff9.js +0 -14
- package/dist/chunks/studio-cqmtbg2p.js +0 -5
- package/dist/chunks/studio-e47215hh.js +0 -5
- package/dist/chunks/studio-fbx6twps.js +0 -6
- package/dist/chunks/studio-fthwn4kf.js +0 -5
- package/dist/chunks/studio-g8dsj3d2.js +0 -6
- package/dist/chunks/studio-gjknvzam.js +0 -9
- package/dist/chunks/studio-hjbg8d5y.js +0 -12
- package/dist/chunks/studio-nbxqcgvf.js +0 -5
- package/dist/chunks/studio-t1eedsyx.js +0 -4
- package/dist/chunks/studio-vavmwshr.js +0 -5
- package/dist/chunks/studio-x5g1xk77.js +0 -5
- package/dist/chunks/studio-x92tgnee.js +0 -64
- package/dist/chunks/studio-xg1942as.js +0 -5
- package/dist/chunks/studio-zev8ssp1.js +0 -5
- package/dist/chunks/tsMode-k9s9fcag.js +0 -14
- package/dist/chunks/workspace-5vmgsagj.js +0 -4
- package/dist/chunks/ws-client-0ja08haa.js +0 -4
|
@@ -1471,13 +1471,32 @@ function buildEditor(
|
|
|
1471
1471
|
match, and a menu that offers unmatchable states is a menu people stop reading. What the
|
|
1472
1472
|
element already DECLARES and the active selector are unioned in after, so nothing an author has
|
|
1473
1473
|
written can drop out of it. */
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1474
|
+
/* In Project Styles the coordinate is a TAG PATH (`a`, `table th`) and its states nest under it
|
|
1475
|
+
(`a :hover`). The menu offered the ROOT's states instead, and picking one replaced the tag, so
|
|
1476
|
+
`:hover` styled the document element and a project had no way to write `a:hover` at all. Here
|
|
1477
|
+
the states are the tag's own, spelled as the path they write. */
|
|
1478
|
+
const stylebookPath =
|
|
1479
|
+
stylebookSelector && isTagPath(stylebookSelector) ? stylebookSelector : null;
|
|
1480
|
+
const stylebookLeaf = stylebookPath?.split(" ").at(-1) ?? null;
|
|
1481
|
+
const stylebookDeclared = stylebookPath
|
|
1482
|
+
? Object.keys(resolveNestedTagStyle(style, stylebookPath)).filter((k) => isNestedSelector(k))
|
|
1483
|
+
: [];
|
|
1484
|
+
const selectorOptions = stylebookPath
|
|
1485
|
+
? [
|
|
1486
|
+
...new Set([
|
|
1487
|
+
...[...selectorsForNode({ tagName: stylebookLeaf ?? "div" }), ...stylebookDeclared].map(
|
|
1488
|
+
(state) => `${stylebookPath} ${state}`,
|
|
1489
|
+
),
|
|
1490
|
+
...(activeSelector && activeSelector !== stylebookPath ? [activeSelector] : []),
|
|
1491
|
+
]),
|
|
1492
|
+
]
|
|
1493
|
+
: [
|
|
1494
|
+
...new Set([
|
|
1495
|
+
...selectorsForNode(node),
|
|
1496
|
+
...declaredSelectors,
|
|
1497
|
+
...(activeSelector ? [activeSelector] : []),
|
|
1498
|
+
]),
|
|
1499
|
+
];
|
|
1481
1500
|
|
|
1482
1501
|
// ── The Target Line (§6.2) ─────────────────────────────────────────────────
|
|
1483
1502
|
const stylebookTag = stylebookTagOf(stylebookSelector);
|
|
@@ -1486,15 +1505,19 @@ function buildEditor(
|
|
|
1486
1505
|
setTargetLine({
|
|
1487
1506
|
segments: targetSegments(elementLabel, mediaTab, mediaNames.length > 0, schemeLayer),
|
|
1488
1507
|
selector: {
|
|
1489
|
-
|
|
1508
|
+
// The bare tag path IS the base rule in Project Styles, so it reads as the base entry.
|
|
1509
|
+
value: stylebookPath && activeSelector === stylebookPath ? null : activeSelector,
|
|
1490
1510
|
options: selectorOptions,
|
|
1491
|
-
declared:
|
|
1511
|
+
declared: stylebookPath
|
|
1512
|
+
? new Set(stylebookDeclared.map((state) => `${stylebookPath} ${state}`))
|
|
1513
|
+
: declaredSelectors,
|
|
1492
1514
|
onSelect: (value) => {
|
|
1493
1515
|
const current = activeTab.value;
|
|
1494
1516
|
if (!current) {
|
|
1495
1517
|
return;
|
|
1496
1518
|
}
|
|
1497
|
-
|
|
1519
|
+
// Clearing the state in Project Styles returns to the tag, never to the document root.
|
|
1520
|
+
current.session.ui.activeSelector = value ?? stylebookPath;
|
|
1498
1521
|
/* §6.2's own rule: a control that selects a rendering context has to change the rendering,
|
|
1499
1522
|
or it is a control over a label. `:hover` gets away with not doing this because you can
|
|
1500
1523
|
hover the element; `:popover-open` cannot, because a closed popover is not on the screen
|
|
@@ -1523,7 +1546,9 @@ function buildEditor(
|
|
|
1523
1546
|
isNestedSelector(v.trim()) ? "" : 'A selector must start with ":", ".", "&" or "[".',
|
|
1524
1547
|
}).then((value) => {
|
|
1525
1548
|
if (value && activeTab.value) {
|
|
1526
|
-
activeTab.value.session.ui.activeSelector =
|
|
1549
|
+
activeTab.value.session.ui.activeSelector = stylebookPath
|
|
1550
|
+
? `${stylebookPath} ${value.trim()}`
|
|
1551
|
+
: value.trim();
|
|
1527
1552
|
}
|
|
1528
1553
|
});
|
|
1529
1554
|
},
|
package/src/platforms/cloud.ts
CHANGED
|
@@ -48,6 +48,7 @@ import type {
|
|
|
48
48
|
ReferencesResult,
|
|
49
49
|
RenameResult,
|
|
50
50
|
RepoInfo,
|
|
51
|
+
RepoProjects,
|
|
51
52
|
StarterInfo,
|
|
52
53
|
StudioPlatform,
|
|
53
54
|
} from "../types";
|
|
@@ -56,12 +57,21 @@ import type { ReadFilesRequest, ReadFilesResult, UploadResult } from "@jxsuite/p
|
|
|
56
57
|
import { reportActionRequired } from "../account/action-required";
|
|
57
58
|
import { offeredActions, retryHint } from "../platform-errors";
|
|
58
59
|
import { createReadBatcher } from "./read-batcher";
|
|
60
|
+
import { normalizeProjectDir } from "../utils/project-dir";
|
|
59
61
|
import type { BatchAnswer } from "./read-batcher";
|
|
60
62
|
|
|
63
|
+
// Re-exported for the cloud shell and its tests, which spell folders through this entry.
|
|
64
|
+
export { normalizeProjectDir } from "../utils/project-dir";
|
|
65
|
+
|
|
61
66
|
export interface CloudProject {
|
|
62
67
|
owner: string;
|
|
63
68
|
repo: string;
|
|
64
69
|
branch: string;
|
|
70
|
+
/**
|
|
71
|
+
* The folder holding the project's `project.json`, relative to the repository root and already
|
|
72
|
+
* normalized ({@link normalizeProjectDir}); absent or `""` when the project IS the repository.
|
|
73
|
+
*/
|
|
74
|
+
dir?: string;
|
|
65
75
|
}
|
|
66
76
|
|
|
67
77
|
interface ProjectInfoWire {
|
|
@@ -213,12 +223,45 @@ async function reportRefusal(res: Response): Promise<void> {
|
|
|
213
223
|
});
|
|
214
224
|
}
|
|
215
225
|
|
|
216
|
-
/**
|
|
226
|
+
/**
|
|
227
|
+
* The revision a session reads: the branch, then `:` and the project folder when there is one.
|
|
228
|
+
*
|
|
229
|
+
* Git's own `<rev>:<path>` notation, and unambiguous for the reason git can use it: `:` is one of
|
|
230
|
+
* the characters a ref name may never contain (git-check-ref-format), so the FIRST colon always
|
|
231
|
+
* ends the branch, however many slashes the branch or the folder holds.
|
|
232
|
+
*/
|
|
233
|
+
export function revisionSpec(project: CloudProject): string {
|
|
234
|
+
return project.dir ? `${project.branch}:${project.dir}` : project.branch;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Split a {@link revisionSpec} back into its branch and folder; null when either half is unusable. */
|
|
238
|
+
function parseRevisionSpec(spec: string): { branch: string; dir?: string } | null {
|
|
239
|
+
const colon = spec.indexOf(":");
|
|
240
|
+
const branch = colon === -1 ? spec : spec.slice(0, colon);
|
|
241
|
+
const dir = colon === -1 ? "" : normalizeProjectDir(spec.slice(colon + 1));
|
|
242
|
+
if (!branch || dir === null) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
return dir ? { branch, dir } : { branch };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Editor URL for a project session (mirrors the shell's route):
|
|
250
|
+
* `/edit/<owner>/<repo>@<branch>[:<folder>]`. The branch is one encoded component, so its slashes
|
|
251
|
+
* travel as `%2F`; the folder keeps its slashes, so a subfolder project's address reads as the path
|
|
252
|
+
* it is.
|
|
253
|
+
*/
|
|
217
254
|
export function editUrl(project: CloudProject): string {
|
|
218
|
-
|
|
255
|
+
const folder = project.dir
|
|
256
|
+
? `:${project.dir
|
|
257
|
+
.split("/")
|
|
258
|
+
.map((segment) => encodeURIComponent(segment))
|
|
259
|
+
.join("/")}`
|
|
260
|
+
: "";
|
|
261
|
+
return `/edit/${project.owner}/${project.repo}@${encodeURIComponent(project.branch)}${folder}`;
|
|
219
262
|
}
|
|
220
263
|
|
|
221
|
-
/** Parse an /edit/:owner/:repo@:branch path (editUrl's inverse); null when it is not one. */
|
|
264
|
+
/** Parse an /edit/:owner/:repo@:branch[:dir] path (editUrl's inverse); null when it is not one. */
|
|
222
265
|
export function parseEditPath(pathname: string): CloudProject | null {
|
|
223
266
|
/* Asset routers may normalize "@" to "%40", so decode the whole path first
|
|
224
267
|
(branch slashes survive: `.+` spans them). */
|
|
@@ -232,22 +275,27 @@ export function parseEditPath(pathname: string): CloudProject | null {
|
|
|
232
275
|
if (!match) {
|
|
233
276
|
return null;
|
|
234
277
|
}
|
|
235
|
-
const [, owner, repo,
|
|
236
|
-
|
|
278
|
+
const [, owner, repo, spec] = match;
|
|
279
|
+
const revision = spec ? parseRevisionSpec(spec) : null;
|
|
280
|
+
if (!owner || !repo || !revision) {
|
|
237
281
|
return null;
|
|
238
282
|
}
|
|
239
|
-
return { owner, repo,
|
|
283
|
+
return { owner, repo, ...revision };
|
|
240
284
|
}
|
|
241
285
|
|
|
242
|
-
/**
|
|
286
|
+
/**
|
|
287
|
+
* Gateway base path for a project session. The revision spec — branch and folder — is ONE encoded
|
|
288
|
+
* segment, so the gateway's route shape is the same for a subfolder project as for a repository,
|
|
289
|
+
* and the folder's slashes cannot be mistaken for the session's own sub-path.
|
|
290
|
+
*/
|
|
243
291
|
export function sessionBase(project: CloudProject): string {
|
|
244
|
-
const { owner, repo
|
|
245
|
-
return `/api/v1/p/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${encodeURIComponent(
|
|
292
|
+
const { owner, repo } = project;
|
|
293
|
+
return `/api/v1/p/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${encodeURIComponent(revisionSpec(project))}/studio`;
|
|
246
294
|
}
|
|
247
295
|
|
|
248
|
-
/** Catalogue/recents root key for a project: "owner/repo@branch". */
|
|
296
|
+
/** Catalogue/recents root key for a project: "owner/repo@branch", plus ":dir" for a subfolder. */
|
|
249
297
|
export function projectRootKey(project: CloudProject): string {
|
|
250
|
-
return `${project.owner}/${project.repo}@${project
|
|
298
|
+
return `${project.owner}/${project.repo}@${revisionSpec(project)}`;
|
|
251
299
|
}
|
|
252
300
|
|
|
253
301
|
/**
|
|
@@ -559,17 +607,18 @@ function awaitAction(popup: Window): Promise<ActionOutcome> {
|
|
|
559
607
|
});
|
|
560
608
|
}
|
|
561
609
|
|
|
562
|
-
/** Parse an "owner/repo@branch" root key; null when malformed. */
|
|
610
|
+
/** Parse an "owner/repo@branch[:dir]" root key; null when malformed. */
|
|
563
611
|
export function parseRootKey(root: string): CloudProject | null {
|
|
564
612
|
const match = /^([^/@]+)\/([^/@]+)@(.+)$/.exec(root);
|
|
565
613
|
if (!match) {
|
|
566
614
|
return null;
|
|
567
615
|
}
|
|
568
|
-
const [, owner, repo,
|
|
569
|
-
|
|
616
|
+
const [, owner, repo, spec] = match;
|
|
617
|
+
const revision = spec ? parseRevisionSpec(spec) : null;
|
|
618
|
+
if (!owner || !repo || !revision) {
|
|
570
619
|
return null;
|
|
571
620
|
}
|
|
572
|
-
return { owner, repo,
|
|
621
|
+
return { owner, repo, ...revision };
|
|
573
622
|
}
|
|
574
623
|
|
|
575
624
|
/** Every project this account can open, straight off the wire; [] when the catalogue is unreachable. */
|
|
@@ -1558,13 +1607,40 @@ export function createCloudPlatform(project: CloudProject | null): StudioPlatfor
|
|
|
1558
1607
|
}));
|
|
1559
1608
|
},
|
|
1560
1609
|
|
|
1561
|
-
/**
|
|
1562
|
-
|
|
1610
|
+
/**
|
|
1611
|
+
* The folders of a repository that hold a project.json, read from its default branch. The
|
|
1612
|
+
* platform drops vendored folders and other tools' `project.json` (Nx's), and says when the
|
|
1613
|
+
* repository was too large to list in full.
|
|
1614
|
+
*/
|
|
1615
|
+
async listRepoProjects(repo: { owner: string; name: string }): Promise<RepoProjects> {
|
|
1616
|
+
const res = await fetch(
|
|
1617
|
+
`/api/v1/repos/${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.name)}/projects`,
|
|
1618
|
+
{ credentials: "include" },
|
|
1619
|
+
);
|
|
1620
|
+
const found = await okJson<RepoProjects>(res, "Failed to look for projects");
|
|
1621
|
+
/* Canonical on the way in, so a location the reader picks is spelled the way the root key,
|
|
1622
|
+
the editor URL and the session will spell it; anything that climbs out is not offered. */
|
|
1623
|
+
const locations: RepoProjects["locations"] = [];
|
|
1624
|
+
for (const location of found.locations) {
|
|
1625
|
+
const dir = normalizeProjectDir(location.dir);
|
|
1626
|
+
if (dir !== null) {
|
|
1627
|
+
locations.push(location.name ? { dir, name: location.name } : { dir });
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
return { branch: found.branch, locations, truncated: found.truncated === true };
|
|
1631
|
+
},
|
|
1632
|
+
|
|
1633
|
+
/** Adopt an existing repo (or one folder of it) as a Jx project; resolves to its root key. */
|
|
1634
|
+
async importProject(opts: { owner: string; name: string; dir?: string }) {
|
|
1635
|
+
const dir = normalizeProjectDir(opts.dir ?? "");
|
|
1636
|
+
if (dir === null) {
|
|
1637
|
+
throw new Error("The project folder must stay inside the repository.");
|
|
1638
|
+
}
|
|
1563
1639
|
const res = await fetch("/api/v1/projects/import", {
|
|
1564
1640
|
method: "POST",
|
|
1565
1641
|
credentials: "include",
|
|
1566
1642
|
headers: { "Content-Type": "application/json" },
|
|
1567
|
-
body: JSON.stringify(opts),
|
|
1643
|
+
body: JSON.stringify({ owner: opts.owner, name: opts.name, ...(dir ? { dir } : {}) }),
|
|
1568
1644
|
});
|
|
1569
1645
|
const imported = await okJson<{ owner: string; name: string; defaultBranch: string }>(
|
|
1570
1646
|
res,
|
|
@@ -1575,6 +1651,7 @@ export function createCloudPlatform(project: CloudProject | null): StudioPlatfor
|
|
|
1575
1651
|
owner: imported.owner,
|
|
1576
1652
|
repo: imported.name,
|
|
1577
1653
|
branch: imported.defaultBranch,
|
|
1654
|
+
...(dir ? { dir } : {}),
|
|
1578
1655
|
}),
|
|
1579
1656
|
};
|
|
1580
1657
|
},
|
|
@@ -98,6 +98,50 @@ export function createDevServerPlatform() {
|
|
|
98
98
|
*/
|
|
99
99
|
let _collabConnection: Promise<WsCollabConnection> | null = null;
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Bind the server to `root` — the binding its rootless routes and its static fallback (every
|
|
103
|
+
* media thumbnail, every canvas image) resolve against.
|
|
104
|
+
*
|
|
105
|
+
* **The binding is one per SERVER, not per tab.** So an empty root posts nothing: a Studio tab
|
|
106
|
+
* opened with no project used to post `{ root: "" }` at boot, which unbound the server for every
|
|
107
|
+
* other tab and turned their images into 404s. And a tab re-claims it (see
|
|
108
|
+
* {@link reclaimBinding}) whenever it could have been taken.
|
|
109
|
+
*/
|
|
110
|
+
async function postActivate(root: string): Promise<void> {
|
|
111
|
+
if (!root) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const res = await fetch("/__studio/activate", {
|
|
115
|
+
body: JSON.stringify({ root }),
|
|
116
|
+
headers: { "Content-Type": "application/json" },
|
|
117
|
+
method: "POST",
|
|
118
|
+
});
|
|
119
|
+
if (!res.ok) {
|
|
120
|
+
const body = (await readJson<ErrorBody>(res).catch(() => ({}))) as ErrorBody;
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Could not open ${root}: ${problemDetail(body) ?? `activation failed (${res.status})`}`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Re-bind the server to this tab's project. Two events can take the binding away without this tab
|
|
129
|
+
* hearing of it: another Studio tab activating ITS project, and a restarted `bun run dev`, which
|
|
130
|
+
* comes back bound to nothing. So a tab re-claims it when the author comes back to it (focus) and
|
|
131
|
+
* when the reload stream reconnects (a restart). Only a tab with a project claims anything.
|
|
132
|
+
*/
|
|
133
|
+
function reclaimBinding(): void {
|
|
134
|
+
if (!_projectRoot) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
_activation = postActivate(_projectRoot).catch((error: unknown) => {
|
|
138
|
+
console.error("Project re-activation failed:", error);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (typeof window !== "undefined") {
|
|
142
|
+
window.addEventListener("focus", reclaimBinding);
|
|
143
|
+
}
|
|
144
|
+
|
|
101
145
|
/**
|
|
102
146
|
* Prefix a project-relative path with the active project root for server API calls.
|
|
103
147
|
*
|
|
@@ -162,18 +206,7 @@ export function createDevServerPlatform() {
|
|
|
162
206
|
* @param {string} [root]
|
|
163
207
|
*/
|
|
164
208
|
async activate(root?: string) {
|
|
165
|
-
|
|
166
|
-
const res = await fetch("/__studio/activate", {
|
|
167
|
-
body: JSON.stringify({ root: r }),
|
|
168
|
-
headers: { "Content-Type": "application/json" },
|
|
169
|
-
method: "POST",
|
|
170
|
-
});
|
|
171
|
-
if (!res.ok) {
|
|
172
|
-
const body = (await readJson<ErrorBody>(res).catch(() => ({}))) as ErrorBody;
|
|
173
|
-
throw new Error(
|
|
174
|
-
`Could not open ${r}: ${problemDetail(body) ?? `activation failed (${res.status})`}`,
|
|
175
|
-
);
|
|
176
|
-
}
|
|
209
|
+
await postActivate(root ?? _projectRoot);
|
|
177
210
|
},
|
|
178
211
|
|
|
179
212
|
// ─── Project opening ──────────────────────────────────────────────────
|
|
@@ -514,6 +547,8 @@ export function createDevServerPlatform() {
|
|
|
514
547
|
es.addEventListener("open", () => {
|
|
515
548
|
opens += 1;
|
|
516
549
|
if (opens > 1) {
|
|
550
|
+
// A reconnect is usually a restarted server, which comes back bound to no project.
|
|
551
|
+
reclaimBinding();
|
|
517
552
|
options?.onResync?.("reconnect");
|
|
518
553
|
}
|
|
519
554
|
});
|
|
@@ -36,6 +36,12 @@ export interface ConnectOptions {
|
|
|
36
36
|
productionBranch: string;
|
|
37
37
|
owner: string;
|
|
38
38
|
repo: string;
|
|
39
|
+
/**
|
|
40
|
+
* The folder of the repository the project lives in, when it is not the root (a cloud project
|
|
41
|
+
* opened from a subfolder). Pages builds there, so `bunx jx build` finds that project's
|
|
42
|
+
* `project.json` and `dist` is that project's output.
|
|
43
|
+
*/
|
|
44
|
+
rootDir?: string;
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
/** True when the active platform can reach the Cloudflare API. */
|
|
@@ -106,6 +112,7 @@ export async function createPagesProject(opts: ConnectOptions): Promise<PagesPro
|
|
|
106
112
|
build_config: {
|
|
107
113
|
build_command: "bunx jx build",
|
|
108
114
|
destination_dir: "dist",
|
|
115
|
+
...(opts.rootDir ? { root_dir: opts.rootDir } : {}),
|
|
109
116
|
},
|
|
110
117
|
source: {
|
|
111
118
|
type: "github",
|
|
@@ -19,12 +19,12 @@
|
|
|
19
19
|
* HTML, in the accessibility tree and in every DOM dump. It is gone, and the document is now the
|
|
20
20
|
* thing that makes it impossible rather than the thing that remembers not to — the field binds no
|
|
21
21
|
* `value` at all, and the draft lives in the adapter's closure until {@link saveToken} reads it. A
|
|
22
|
-
* stored token is reported as
|
|
22
|
+
* stored token is reported as _stored_, the field is only ever drawn for a REPLACEMENT the reader
|
|
23
23
|
* asked for, and revoking lives where every other credential's does — Preferences › Accounts
|
|
24
24
|
* (`studio.md` §15 rule 1: a surface never prints the secret it describes).
|
|
25
25
|
*
|
|
26
|
-
* @docs studio/publish/cloudflare
|
|
27
26
|
* @license MIT
|
|
27
|
+
* @docs studio/publish/cloudflare
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import type { DeployConfig, ProjectConfig } from "@jxsuite/schema/types";
|
|
@@ -36,6 +36,7 @@ import { resetModelCache } from "../services/ai-models";
|
|
|
36
36
|
import { projectState } from "../store";
|
|
37
37
|
import type { CfConnection } from "../types";
|
|
38
38
|
import { layerHost } from "../ui/layers";
|
|
39
|
+
import { normalizeProjectDir } from "../utils/project-dir";
|
|
39
40
|
import { openPublishSurface } from "../surfaces/publish";
|
|
40
41
|
import type { PublishForm, PublishSurfaceHandle, PublishView } from "../surfaces/publish";
|
|
41
42
|
import type { CfAccount, PagesDeploymentInfo } from "./pages-service";
|
|
@@ -82,6 +83,17 @@ function prefillRepo(): { owner: string; repo: string } {
|
|
|
82
83
|
return match ? { owner: match[1]!, repo: match[2]! } : { owner: "", repo: "" };
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
/**
|
|
87
|
+
* The repository folder a cloud project lives in ("owner/repo@branch:sites/blog"), or "" for one
|
|
88
|
+
* that is the whole repository and for every non-cloud root. Not a form field: the folder is the
|
|
89
|
+
* project's identity rather than a choice, and a Pages project that built anywhere else would build
|
|
90
|
+
* a different project, or none.
|
|
91
|
+
*/
|
|
92
|
+
function projectFolder(): string {
|
|
93
|
+
const spec = /^[\w.-]+\/[\w.-]+@[^:]+:(.+)$/.exec(getPlatform().projectRoot)?.[1] ?? "";
|
|
94
|
+
return normalizeProjectDir(spec) ?? "";
|
|
95
|
+
}
|
|
96
|
+
|
|
85
97
|
/** The connected Pages project, as the surface takes it: strings, never a config object. */
|
|
86
98
|
function deployView(): { projectName: string; productionUrl: string } | null {
|
|
87
99
|
const deploy: DeployConfig | undefined = currentDeploy();
|
|
@@ -257,12 +269,14 @@ async function submitConnect(): Promise<void> {
|
|
|
257
269
|
_error = "";
|
|
258
270
|
paint();
|
|
259
271
|
try {
|
|
272
|
+
const rootDir = projectFolder();
|
|
260
273
|
const deploy = await connectDeploy(config, {
|
|
261
274
|
accountId: _form.accountId,
|
|
262
275
|
owner: _form.owner,
|
|
263
276
|
productionBranch: _form.branch || "main",
|
|
264
277
|
projectName: _form.projectName,
|
|
265
278
|
repo: _form.repo,
|
|
279
|
+
...(rootDir ? { rootDir } : {}),
|
|
266
280
|
});
|
|
267
281
|
_deployment = await latestDeployment(deploy).catch(() => null);
|
|
268
282
|
} catch (error) {
|
package/src/studio.ts
CHANGED
|
@@ -200,7 +200,8 @@ import * as chatPanelMod from "./panels/chat-panel";
|
|
|
200
200
|
import { setProjectAdopter } from "./services/project-adoption";
|
|
201
201
|
import { setImportHandoff } from "./services/import-seed";
|
|
202
202
|
import { watchConnection } from "./services/connection";
|
|
203
|
-
import { tabBufferUnsaved } from "./services/monaco-buffer";
|
|
203
|
+
import { commitTabBuffers, tabBufferUnsaved } from "./services/monaco-buffer";
|
|
204
|
+
import { clearTyping } from "./tabs/typing";
|
|
204
205
|
import * as leftPanelMod from "./panels/left-panel";
|
|
205
206
|
import * as tabStrip from "./panels/tab-strip";
|
|
206
207
|
import * as paneContext from "./panels/pane-context";
|
|
@@ -1492,7 +1493,18 @@ registerStudioCommands(
|
|
|
1492
1493
|
// Whether the bytes landed, and a hook that neither supplies one nor reads the answer must not
|
|
1493
1494
|
// Silently forward its own first argument as the tab to save.
|
|
1494
1495
|
saveDocument: async () => {
|
|
1495
|
-
|
|
1496
|
+
const tab = activeTab.value;
|
|
1497
|
+
if (!tab) {
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
// What a source buffer holds goes into the document first, or ⌘S inside the commit's
|
|
1501
|
+
// Debounce would save the text from before the last keystrokes.
|
|
1502
|
+
await commitTabBuffers(tab);
|
|
1503
|
+
await saveFile(tab);
|
|
1504
|
+
// Text that would not parse is still only in the buffer, so it is still unsaved.
|
|
1505
|
+
if (!tabBufferUnsaved(tab)) {
|
|
1506
|
+
clearTyping(tab.id);
|
|
1507
|
+
}
|
|
1496
1508
|
},
|
|
1497
1509
|
},
|
|
1498
1510
|
stageContext,
|