@jxsuite/studio 3.0.0 → 4.1.0
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/README.md +5 -5
- package/dist/chunks/{2020-xp8vnwma.js → 2020-n6tfwnda.js} +2 -2
- package/dist/chunks/Overlay.dev-etqjwyf9.js +23 -0
- package/dist/chunks/cf-account-picker-en86v7x7.js +163 -0
- package/dist/chunks/{cf-settings-zj2cj1ar.js → cf-settings-45fgyx8y.js} +6 -5
- package/dist/chunks/{components-z6cnkyy1.js → components-zmx10t62.js} +6 -5
- package/dist/chunks/content-source-48xg86wj.js +49 -0
- package/dist/chunks/{convert-to-repeater-6vvx96n2.js → convert-to-repeater-43tfn7cs.js} +33 -25
- package/dist/chunks/entry-commands-e63f1xc0.js +46 -0
- package/dist/chunks/{extension-sections-kwzvpemr.js → extension-sections-txv08xxg.js} +34 -26
- package/dist/chunks/file-ops-wbh4hhsx.js +48 -0
- package/dist/chunks/files-3kfkwhk4.js +93 -0
- package/dist/chunks/{focus-trap.esm-ggawf1hy.js → focus-trap.esm-9t2drp0g.js} +6 -5
- package/dist/chunks/{format-host-8fr3545e.js → format-host-w16q81tf.js} +3 -3
- package/dist/chunks/fs-events-hrrg328g.js +21 -0
- package/dist/chunks/{git-panel-g01d93vc.js → git-panel-532whr1y.js} +22 -16
- package/dist/chunks/{index-nj44jpzj.js → index-wcnt3r9g.js} +2 -2
- package/dist/chunks/{jx-validate-jj9rnt4n.js → jx-validate-8er6gbxy.js} +6 -4
- package/dist/chunks/layers-panel-07hm7g5j.js +67 -0
- package/dist/chunks/layers-q75tj2mf.js +45 -0
- package/dist/chunks/lit-html-j055mkh8.js +22 -0
- package/dist/chunks/monaco-buffer-dqw0xxpx.js +25 -0
- package/dist/chunks/{monaco-setup-a5wkmbgh.js → monaco-setup-vx9bhp1d.js} +4 -4
- package/dist/chunks/notify-yjm79n4s.js +30 -0
- package/dist/chunks/parse-24d0gay3.js +19 -0
- package/dist/chunks/platform-ba1j1gbh.js +16 -0
- package/dist/chunks/publish-panel-v9st96ja.js +29 -0
- package/dist/chunks/quick-search-zccjp6gv.js +66 -0
- package/dist/chunks/references-3qx8fkyp.js +29 -0
- package/dist/chunks/right-panel-vm3b6kwy.js +46 -0
- package/dist/chunks/sp-overlay.dev-qgvhnxf4.js +18 -0
- package/dist/chunks/sp-popover.dev-n2etmqdp.js +9 -0
- package/dist/chunks/sp-progress-circle.dev-hryhj0es.js +15 -0
- package/dist/chunks/sp-tooltip.dev-4sastdnw.js +15 -0
- package/dist/chunks/{sp-tray.dev-64v4pxw8.js → sp-tray.dev-tkbscm06.js} +5 -5
- package/dist/chunks/{studio-wdt9y92a.js → studio-0bq6eh7v.js} +2 -2
- package/dist/chunks/studio-1ngbejft.js +181 -0
- package/dist/chunks/studio-1wagb550.js +118 -0
- package/dist/chunks/{studio-j595e748.js → studio-2b946z26.js} +172 -95
- package/dist/chunks/{studio-pkp61mh8.js → studio-3bqpr0zh.js} +25 -17
- package/dist/chunks/{studio-k4jvjcfx.js → studio-4b5z1x43.js} +46 -12
- package/dist/chunks/{studio-ry1vz5sv.js → studio-4n2b5gzb.js} +15 -13
- package/dist/chunks/{studio-p5dja96s.js → studio-4r1pv6zf.js} +2 -2
- package/dist/chunks/{studio-kvfx9z2c.js → studio-4rzyrtjz.js} +70 -31
- package/dist/chunks/{studio-tzx3xmvx.js → studio-5fx3g9jg.js} +33 -52
- package/dist/chunks/{studio-rh1cnamv.js → studio-5myknm0a.js} +565 -135
- package/dist/chunks/studio-6yw4y3my.js +91 -0
- package/dist/chunks/{studio-0rqqhrdy.js → studio-7nte2hj4.js} +2 -2
- package/dist/chunks/studio-92ab2jqe.js +282 -0
- package/dist/chunks/{studio-6z5eyjwn.js → studio-950k0dx6.js} +72 -135
- package/dist/chunks/{studio-sas7mt3k.js → studio-9ab8gqsm.js} +2 -2
- package/dist/chunks/studio-9n4mrmnb.js +184 -0
- package/dist/chunks/{studio-hcxzyk3x.js → studio-ahn3pdgx.js} +6 -6
- package/dist/chunks/studio-b55y6ewx.js +498 -0
- package/dist/chunks/{studio-s519zc99.js → studio-bgxt93ga.js} +3 -3
- package/dist/chunks/studio-cft9r4r7.js +119 -0
- package/dist/chunks/{studio-dc8s8heb.js → studio-cmb03bg5.js} +6904 -8321
- package/dist/chunks/studio-dm19q43r.js +195 -0
- package/dist/chunks/{studio-2x9zbxmx.js → studio-ef49pnzx.js} +11 -9
- package/dist/chunks/{studio-ay8wcr2b.js → studio-eqq1w638.js} +7 -9
- package/dist/chunks/{studio-5efrtn1m.js → studio-ew0djmhg.js} +23 -19
- package/dist/chunks/{studio-tr20m8g8.js → studio-f3wqjtk5.js} +39 -3
- package/dist/chunks/{studio-xntfdy1x.js → studio-fbf92ckf.js} +36 -23
- package/dist/chunks/{studio-4q0wznzf.js → studio-gcye7qka.js} +442 -126
- package/dist/chunks/{studio-7p35254k.js → studio-gfgb8pe6.js} +36 -193
- package/dist/chunks/studio-jc994dnr.js +1066 -0
- package/dist/chunks/{studio-hj2se583.js → studio-jkftxyvz.js} +69 -3423
- package/dist/chunks/{studio-5x5774ak.js → studio-kryvsw6j.js} +3 -3
- package/dist/chunks/{studio-kfpek536.js → studio-kyh9avvv.js} +2 -2
- package/dist/chunks/{studio-twsafr1e.js → studio-m3hwn7y1.js} +3 -3
- package/dist/chunks/studio-msd847zn.js +1315 -0
- package/dist/chunks/{studio-g5gdqnyd.js → studio-mtfnaqs9.js} +19 -19
- package/dist/chunks/{studio-7ejemhej.js → studio-n6j9t10q.js} +6 -6
- package/dist/chunks/{studio-vhk5fmfv.js → studio-n8b9wqwy.js} +5 -5
- package/dist/chunks/{studio-cjm7fdjf.js → studio-p4ngc046.js} +8 -104
- package/dist/chunks/studio-q43a5y9h.js +394 -0
- package/dist/chunks/studio-q7v10cg2.js +106 -0
- package/dist/chunks/{studio-6kv3apa9.js → studio-q9jd1a60.js} +27 -11
- package/dist/chunks/studio-qcy05dp8.js +1743 -0
- package/dist/chunks/{studio-a0yp7syn.js → studio-qjy2z9bv.js} +29 -132
- package/dist/chunks/{studio-2w11grav.js → studio-qq6jym04.js} +3 -3
- package/dist/chunks/{studio-3fk1ac9z.js → studio-rdx8jcxy.js} +4 -4
- package/dist/chunks/{studio-4mqb9a0j.js → studio-rn9ta2mz.js} +4 -4
- package/dist/chunks/studio-t7qb9db8.js +665 -0
- package/dist/chunks/studio-tmc5emrw.js +8 -0
- package/dist/chunks/{studio-r5xbtctf.js → studio-trxrabqf.js} +292 -2297
- package/dist/chunks/studio-wcy8trfx.js +263 -0
- package/dist/chunks/{studio-3dkcd4c5.js → studio-x4qkdat5.js} +62 -57
- package/dist/chunks/studio-x813zp48.js +195 -0
- package/dist/chunks/{studio-qpfq7xhm.js → studio-xrapxwwc.js} +22 -285
- package/dist/chunks/{studio-xm3dbxvn.js → studio-zczps70q.js} +2 -2
- package/dist/chunks/{studio-8ezdegcx.js → studio-zqazsct2.js} +40 -36
- package/dist/chunks/workspace-fjx52ryv.js +89 -0
- package/dist/iframe-entry.js +141 -56
- package/dist/manifest.json +7 -0
- package/dist/studio.js +2605 -1950
- package/favicon.ico +0 -0
- package/index.html +1 -0
- package/package.json +20 -18
- package/src/about/about-modal.ts +4 -3
- package/src/browse/library-pane.ts +57 -3
- package/src/canvas/canvas-render.ts +23 -1
- package/src/canvas/iframe-patch.ts +47 -21
- package/src/canvas/iframe-render.ts +22 -6
- package/src/commands/app-commands.ts +4 -0
- package/src/commands/command-args.ts +29 -0
- package/src/commands/context.ts +15 -1
- package/src/commands/defaults.ts +29 -1
- package/src/commands/levels.ts +16 -0
- package/src/commands/live-context.ts +3 -0
- package/src/content/collection-match.ts +267 -0
- package/src/content/entry-commands.ts +38 -29
- package/src/content/entry-fields.ts +6 -0
- package/src/editor/shortcuts.ts +5 -0
- package/src/files/file-ops.ts +10 -42
- package/src/files/files.ts +487 -59
- package/src/files/fs-events.ts +9 -0
- package/src/files/gitignore.ts +432 -0
- package/src/files/media-paths.ts +13 -42
- package/src/files/media-usage.ts +41 -131
- package/src/files/serialize-document.ts +60 -0
- package/src/format/convert-file.ts +511 -0
- package/src/format/format-choices.ts +199 -0
- package/src/grid/grid-view.ts +16 -2
- package/src/hosting/document.ts +2 -0
- package/src/hosting/layout.ts +8 -0
- package/src/media/media-open.ts +58 -0
- package/src/media/media-pane.ts +310 -0
- package/src/new-project/import-tab.ts +357 -163
- package/src/new-project/location-fields.ts +8 -5
- package/src/new-project/new-project-modal.ts +27 -34
- package/src/packages/ensure-deps.ts +14 -7
- package/src/packages/jxsuite-update.ts +11 -11
- package/src/page-params.ts +10 -46
- package/src/panels/activity-bar.ts +45 -16
- package/src/panels/ai-chat/attached-context.ts +6 -1
- package/src/panels/ai-chat/chat-view.ts +255 -13
- package/src/panels/ai-chat/composer.ts +31 -97
- package/src/panels/ai-panel.ts +116 -2
- package/src/panels/navigator-panels.ts +7 -6
- package/src/panels/settings-menu.ts +799 -0
- package/src/panels/toolbar.ts +135 -15
- package/src/platforms/cloud.ts +261 -70
- package/src/platforms/devserver.ts +67 -33
- package/src/preview/preview-overlay.ts +226 -0
- package/src/publish/publish-panel.ts +62 -2
- package/src/services/ai-ask.ts +247 -0
- package/src/services/ai-import-tools.ts +532 -0
- package/src/services/ai-models.ts +108 -1
- package/src/services/ai-project-tools.ts +20 -26
- package/src/services/ai-system-prompt.ts +88 -2
- package/src/services/ai-turn-signal.ts +70 -0
- package/src/services/connection.ts +74 -0
- package/src/services/context-manager.ts +101 -1
- package/src/services/document-assistant.ts +45 -4
- package/src/services/import-client.ts +33 -6
- package/src/services/import-run.ts +158 -0
- package/src/services/import-seed.ts +120 -0
- package/src/services/jx-validate.ts +58 -6
- package/src/services/project-adoption.ts +71 -0
- package/src/services/references.ts +0 -0
- package/src/services/settings/definitions.ts +17 -0
- package/src/services/tool-executor.ts +45 -2
- package/src/settings/contexts-section.ts +45 -3
- package/src/settings/dependencies-editor.ts +43 -23
- package/src/settings/general-settings.ts +30 -12
- package/src/settings/preferences-accounts.ts +193 -12
- package/src/settings/preferences-dialog.ts +93 -59
- package/src/settings/preferences-sections.ts +57 -0
- package/src/settings/settings-document.ts +65 -9
- package/src/studio.ts +33 -6
- package/src/tabs/transact.ts +42 -19
- package/src/types.ts +129 -7
- package/src/ui/ai-managed-connect.ts +62 -13
- package/src/ui/ai-model-picker.ts +189 -0
- package/src/ui/cf-account-picker.ts +200 -0
- package/src/ui/choice-field.ts +72 -0
- package/src/ui/layers.ts +83 -5
- package/src/utils/studio-utils.ts +22 -52
- package/styles/forced-colors.css +7 -0
- package/styles/overlays.css +256 -0
- package/styles/panels.css +13 -1
- package/styles/shell.css +147 -0
- package/dist/chunks/Overlay.dev-d2ajy6sj.js +0 -23
- package/dist/chunks/content-source-7fy9qrm0.js +0 -40
- package/dist/chunks/files-xcybanxe.js +0 -70
- package/dist/chunks/layers-panel-27essgkc.js +0 -60
- package/dist/chunks/publish-panel-09sy2bab.js +0 -22
- package/dist/chunks/quick-search-sefb61v8.js +0 -55
- package/dist/chunks/right-panel-9r32jd5j.js +0 -39
- package/dist/chunks/sp-overlay.dev-s18hymn9.js +0 -18
- package/dist/chunks/sp-popover.dev-dj9x7e7a.js +0 -9
- package/dist/chunks/sp-progress-circle.dev-0zxcyz3r.js +0 -15
- package/dist/chunks/sp-tooltip.dev-5c6d1kjz.js +0 -15
- package/dist/chunks/studio-bw5dz6km.js +0 -608
- package/dist/chunks/studio-c31yb4t6.js +0 -55
- package/src/canvas/site-style-css.ts +0 -117
package/src/files/fs-events.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { getPlatform } from "../platform";
|
|
12
|
+
import { reloadIgnoreCache, touchesGitignore } from "./gitignore";
|
|
12
13
|
import { invalidateUsages } from "../services/references";
|
|
13
14
|
import { projectState } from "../store";
|
|
14
15
|
import type { DirEntry, FsEvent } from "../types";
|
|
@@ -174,6 +175,14 @@ export function startFsSync(ctx: FsSyncContext): () => void {
|
|
|
174
175
|
// Gone.
|
|
175
176
|
invalidateUsages();
|
|
176
177
|
ctx.invalidateDerivedCaches?.();
|
|
178
|
+
/* A `.gitignore` governs the whole tree beneath it, so it is handled HERE rather than in the
|
|
179
|
+
batch below: the echo filter drops the events Studio caused, and an author who just edited
|
|
180
|
+
their own `.gitignore` in Studio is precisely the one waiting to see the tree change. It also
|
|
181
|
+
never reaches `applyFsEvents` — a dotfile is in no listing, so no directory reads as changed
|
|
182
|
+
and no repaint would follow. Hence the explicit one, once the rules are back. */
|
|
183
|
+
if (touchesGitignore(events.map((event) => event.path))) {
|
|
184
|
+
void reloadIgnoreCache().then(() => ctx.renderLeftPanel());
|
|
185
|
+
}
|
|
177
186
|
pending.push(...events);
|
|
178
187
|
if (timer) {
|
|
179
188
|
clearTimeout(timer);
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gitignore matching for the Files sidebar.
|
|
3
|
+
*
|
|
4
|
+
* A project root is a working directory, and a working directory holds two populations: the files
|
|
5
|
+
* the author writes, and the files a tool wrote for them — `node_modules`, `dist`, `coverage`,
|
|
6
|
+
* `.next`. The sidebar listed both, so a project of forty documents opened as a tree of forty
|
|
7
|
+
* thousand rows. The author already told us which is which, in the one file every project of this
|
|
8
|
+
* shape carries: `.gitignore`.
|
|
9
|
+
*
|
|
10
|
+
* **This lives in Studio, not behind the PAL, on purpose.** `listDirectory` has three
|
|
11
|
+
* implementations — the dev server's route, the desktop session's `readdir`, and the cloud backend,
|
|
12
|
+
* which is not in this repository at all. Filtering there is the same rule written three times, and
|
|
13
|
+
* a rule written three times is a rule that disagrees with itself; the cloud copy could not even be
|
|
14
|
+
* changed in the same pull request. Filtering here is written once and is true on every host the
|
|
15
|
+
* moment it ships. It costs one `readFile` per directory (cached, negatives included), which is the
|
|
16
|
+
* same round trip the listing itself already makes.
|
|
17
|
+
*
|
|
18
|
+
* **The tree cache stays faithful.** {@link isIgnoredEntry} is consulted where rows are BUILT, not
|
|
19
|
+
* where entries are stored, so `projectState.dirs` keeps mirroring the filesystem and the show/hide
|
|
20
|
+
* toggle is a repaint rather than a refetch. It also means `applyFsEvents` needs to know nothing
|
|
21
|
+
* about any of this: a file arriving into an ignored directory lands in the cache and is simply not
|
|
22
|
+
* drawn.
|
|
23
|
+
*
|
|
24
|
+
* The matcher implements `gitignore(5)` — comments, negation, trailing-space and `#`/`!` escapes,
|
|
25
|
+
* directory-only patterns, anchoring, `**`, character classes, and last-match-wins across layers
|
|
26
|
+
* ordered shallow-to-deep. What it deliberately does NOT read is `.git/info/exclude` or
|
|
27
|
+
* `core.excludesFile`: both live outside the project, neither is reachable through the PAL's
|
|
28
|
+
* project-rooted `readFile`, and a rule the author cannot see in their own repository is a poor
|
|
29
|
+
* explanation for a missing row.
|
|
30
|
+
*
|
|
31
|
+
* @docs studio/interface
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { getPlatform } from "../platform";
|
|
35
|
+
|
|
36
|
+
// ─── The rule ─────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
/** One `.gitignore` line, compiled. */
|
|
39
|
+
export interface IgnoreRule {
|
|
40
|
+
/** The line as written. Kept so a hidden row can be explained by the rule that hid it. */
|
|
41
|
+
source: string;
|
|
42
|
+
/** `!pattern` — re-includes what an earlier rule excluded. */
|
|
43
|
+
negated: boolean;
|
|
44
|
+
/** `pattern/` — matches a directory and never a file. */
|
|
45
|
+
dirOnly: boolean;
|
|
46
|
+
/** Matches a path relative to the directory the rule's own `.gitignore` sits in. */
|
|
47
|
+
re: RegExp;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The rules of one `.gitignore`, with the directory they are relative to. */
|
|
51
|
+
export interface IgnoreLayer {
|
|
52
|
+
/** Project-relative, forward-slashed, no trailing slash; `""` is the project root. */
|
|
53
|
+
base: string;
|
|
54
|
+
rules: IgnoreRule[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Parsing ──────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/** Characters that mean something to a RegExp and nothing to a glob. */
|
|
60
|
+
const RE_SPECIAL = /[.*+?^${}()|[\]\\]/;
|
|
61
|
+
|
|
62
|
+
/** One glob character as a RegExp literal. */
|
|
63
|
+
function literal(ch: string): string {
|
|
64
|
+
return RE_SPECIAL.test(ch) ? `\\${ch}` : ch;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Drop trailing spaces, keeping any the author escaped.
|
|
69
|
+
*
|
|
70
|
+
* A space survives when the run of backslashes immediately before it is odd — the same rule that
|
|
71
|
+
* decides whether the backslash escaped the space or another backslash.
|
|
72
|
+
*/
|
|
73
|
+
function stripTrailingSpaces(line: string): string {
|
|
74
|
+
let end = line.length;
|
|
75
|
+
while (end > 0 && line[end - 1] === " ") {
|
|
76
|
+
let backslashes = 0;
|
|
77
|
+
let i = end - 2;
|
|
78
|
+
while (i >= 0 && line[i] === "\\") {
|
|
79
|
+
backslashes += 1;
|
|
80
|
+
i -= 1;
|
|
81
|
+
}
|
|
82
|
+
if (backslashes % 2 === 1) {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
end -= 1;
|
|
86
|
+
}
|
|
87
|
+
return line.slice(0, end);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The index of the `]` closing the class opened at `start`, or -1 when the line has none.
|
|
92
|
+
*
|
|
93
|
+
* A `]` in the first position (after an optional `!`) is a literal member rather than the close,
|
|
94
|
+
* which is POSIX's rule and git's.
|
|
95
|
+
*/
|
|
96
|
+
function charClassEnd(pattern: string, start: number): number {
|
|
97
|
+
let i = start + 1;
|
|
98
|
+
if (pattern[i] === "!") {
|
|
99
|
+
i += 1;
|
|
100
|
+
}
|
|
101
|
+
if (pattern[i] === "]") {
|
|
102
|
+
i += 1;
|
|
103
|
+
}
|
|
104
|
+
while (i < pattern.length) {
|
|
105
|
+
if (pattern[i] === "\\") {
|
|
106
|
+
i += 2;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (pattern[i] === "]") {
|
|
110
|
+
return i;
|
|
111
|
+
}
|
|
112
|
+
i += 1;
|
|
113
|
+
}
|
|
114
|
+
return -1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Compile a gitignore glob body to a RegExp source.
|
|
119
|
+
*
|
|
120
|
+
* `*` and `?` stop at a separator; `**` crosses them, but only where git says it does — as a whole
|
|
121
|
+
* path segment. `a**b` is git's "two stars that are not a segment", and it behaves as one star.
|
|
122
|
+
*/
|
|
123
|
+
function globToRegExpSource(pattern: string): string {
|
|
124
|
+
let out = "";
|
|
125
|
+
let i = 0;
|
|
126
|
+
while (i < pattern.length) {
|
|
127
|
+
const ch = pattern[i]!;
|
|
128
|
+
if (ch === "\\") {
|
|
129
|
+
const next = pattern[i + 1];
|
|
130
|
+
if (next === undefined) {
|
|
131
|
+
// A trailing backslash escapes nothing; match it literally.
|
|
132
|
+
out += String.raw`\\`;
|
|
133
|
+
i += 1;
|
|
134
|
+
} else {
|
|
135
|
+
out += literal(next);
|
|
136
|
+
i += 2;
|
|
137
|
+
}
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (ch === "*") {
|
|
141
|
+
let stars = 0;
|
|
142
|
+
while (pattern[i] === "*") {
|
|
143
|
+
stars += 1;
|
|
144
|
+
i += 1;
|
|
145
|
+
}
|
|
146
|
+
/* `out` ends in `/` only where a separator was just emitted — every compiled form above ends
|
|
147
|
+
in `*`, `)` or an escaped literal — so this is exactly "at the start of a segment". */
|
|
148
|
+
const atSegmentStart = out === "" || out.endsWith("/");
|
|
149
|
+
if (stars >= 2 && atSegmentStart && pattern[i] === "/") {
|
|
150
|
+
// `**/` — zero or more whole directories.
|
|
151
|
+
out += "(?:[^/]+/)*";
|
|
152
|
+
i += 1;
|
|
153
|
+
} else if (stars >= 2 && atSegmentStart && i >= pattern.length) {
|
|
154
|
+
// A trailing `/**` — everything inside, at any depth.
|
|
155
|
+
out += ".*";
|
|
156
|
+
} else {
|
|
157
|
+
out += "[^/]*";
|
|
158
|
+
}
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (ch === "?") {
|
|
162
|
+
out += "[^/]";
|
|
163
|
+
i += 1;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (ch === "[") {
|
|
167
|
+
const end = charClassEnd(pattern, i);
|
|
168
|
+
if (end === -1) {
|
|
169
|
+
out += String.raw`\[`;
|
|
170
|
+
i += 1;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const raw = pattern.slice(i + 1, end);
|
|
174
|
+
const negatedClass = raw.startsWith("!");
|
|
175
|
+
const members = negatedClass ? raw.slice(1) : raw;
|
|
176
|
+
/* A `]` in the first position is a MEMBER to git and POSIX, and {@link charClassEnd} finds
|
|
177
|
+
the real close accordingly — but JavaScript reads `[]` as an empty class that matches
|
|
178
|
+
nothing, and `[^]` as "any character". Emitting the member verbatim therefore inverts the
|
|
179
|
+
rule instead of translating it, so the leading `]` is escaped on the way out. */
|
|
180
|
+
out += `[${negatedClass ? "^" : ""}${members.startsWith("]") ? String.raw`\]${members.slice(1)}` : members}]`;
|
|
181
|
+
i = end + 1;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
out += literal(ch);
|
|
185
|
+
i += 1;
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Compile one line, or `null` when it is blank, a comment, or names nothing. */
|
|
191
|
+
function parseLine(raw: string): IgnoreRule | null {
|
|
192
|
+
let line = stripTrailingSpaces(raw);
|
|
193
|
+
if (line === "" || line.startsWith("#")) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
let negated = false;
|
|
197
|
+
if (line.startsWith("!")) {
|
|
198
|
+
negated = true;
|
|
199
|
+
line = line.slice(1);
|
|
200
|
+
}
|
|
201
|
+
/* `\#` and `\!` reach the compiler with their backslash intact and come out as literals, so the
|
|
202
|
+
two checks above are the only place the unescaped forms are read. */
|
|
203
|
+
let dirOnly = false;
|
|
204
|
+
if (line.endsWith("/")) {
|
|
205
|
+
dirOnly = true;
|
|
206
|
+
line = line.slice(0, -1);
|
|
207
|
+
}
|
|
208
|
+
/* A slash anywhere but the end anchors the pattern to the `.gitignore`'s own directory; a
|
|
209
|
+
slash-less pattern matches its name at any depth below it. */
|
|
210
|
+
let anchored = line.includes("/");
|
|
211
|
+
if (line.startsWith("/")) {
|
|
212
|
+
line = line.slice(1);
|
|
213
|
+
anchored = true;
|
|
214
|
+
}
|
|
215
|
+
if (line === "") {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
const body = globToRegExpSource(line);
|
|
219
|
+
return {
|
|
220
|
+
dirOnly,
|
|
221
|
+
negated,
|
|
222
|
+
re: new RegExp(`^${anchored ? "" : "(?:.*/)?"}${body}$`),
|
|
223
|
+
source: raw,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Compile a `.gitignore`'s text into rules, in file order. */
|
|
228
|
+
export function parseGitignore(text: string): IgnoreRule[] {
|
|
229
|
+
const rules: IgnoreRule[] = [];
|
|
230
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
231
|
+
const rule = parseLine(raw);
|
|
232
|
+
if (rule) {
|
|
233
|
+
rules.push(rule);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return rules;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ─── Paths ────────────────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
/** Project-relative, forward-slashed, and without the `.`/`./`/trailing-slash spellings of "here". */
|
|
242
|
+
export function normalizePath(path: string): string {
|
|
243
|
+
const p = path
|
|
244
|
+
.replaceAll("\\", "/")
|
|
245
|
+
.replace(/^\.\/+/, "")
|
|
246
|
+
.replace(/\/+$/, "");
|
|
247
|
+
return p === "." ? "" : p;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** `path` relative to `base`, or `null` when it is not underneath it. */
|
|
251
|
+
function relativeTo(base: string, path: string): string | null {
|
|
252
|
+
if (base === "") {
|
|
253
|
+
return path;
|
|
254
|
+
}
|
|
255
|
+
return path.startsWith(`${base}/`) ? path.slice(base.length + 1) : null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** The project root, then each directory down to `dir`, inclusive. */
|
|
259
|
+
export function ancestorDirs(dir: string): string[] {
|
|
260
|
+
const base = normalizePath(dir);
|
|
261
|
+
const chain = [""];
|
|
262
|
+
if (base === "") {
|
|
263
|
+
return chain;
|
|
264
|
+
}
|
|
265
|
+
let acc = "";
|
|
266
|
+
for (const segment of base.split("/")) {
|
|
267
|
+
acc = acc === "" ? segment : `${acc}/${segment}`;
|
|
268
|
+
chain.push(acc);
|
|
269
|
+
}
|
|
270
|
+
return chain;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ─── Matching ─────────────────────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The verdict on one path, judged on its own.
|
|
277
|
+
*
|
|
278
|
+
* Last match wins, and later layers are consulted after earlier ones — which is exactly git's two
|
|
279
|
+
* precedence rules (a later line beats an earlier one; a deeper `.gitignore` beats a shallower one)
|
|
280
|
+
* expressed as a single ordering.
|
|
281
|
+
*/
|
|
282
|
+
function verdictFor(layers: readonly IgnoreLayer[], path: string, isDir: boolean): boolean {
|
|
283
|
+
let ignored = false;
|
|
284
|
+
for (const layer of layers) {
|
|
285
|
+
const rel = relativeTo(layer.base, path);
|
|
286
|
+
if (rel === null || rel === "") {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
for (const rule of layer.rules) {
|
|
290
|
+
if (rule.dirOnly && !isDir) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (rule.re.test(rel)) {
|
|
294
|
+
ignored = !rule.negated;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return ignored;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Whether `path` is ignored by `layers`, which must be ordered shallow-to-deep.
|
|
303
|
+
*
|
|
304
|
+
* Every ancestor directory is judged before the path itself, and the first one that comes back
|
|
305
|
+
* ignored ends it. That is `gitignore(5)`'s "it is not possible to re-include a file if a parent
|
|
306
|
+
* directory of that file is excluded", and it is not decoration: `node_modules` and `dist/` are the
|
|
307
|
+
* two patterns every project has, and judging `node_modules/pkg/index.js` on its own says NOT
|
|
308
|
+
* ignored — the pattern names `node_modules`, and the path is not it. Checked against `git
|
|
309
|
+
* check-ignore` over a matrix of thirty-five paths, this rule is the difference between agreeing
|
|
310
|
+
* with git everywhere and disagreeing on the two cases that matter most.
|
|
311
|
+
*
|
|
312
|
+
* The sidebar happens not to depend on it — an ignored directory draws no row, so nothing ever asks
|
|
313
|
+
* about its children — but a predicate that is only correct for the caller that exists today is a
|
|
314
|
+
* trap for the next one.
|
|
315
|
+
*/
|
|
316
|
+
export function isIgnored(layers: readonly IgnoreLayer[], path: string, isDir: boolean): boolean {
|
|
317
|
+
const target = normalizePath(path);
|
|
318
|
+
if (target === "") {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
const segments = target.split("/");
|
|
322
|
+
let prefix = "";
|
|
323
|
+
for (const [index, segment] of segments.entries()) {
|
|
324
|
+
prefix = prefix === "" ? segment : `${prefix}/${segment}`;
|
|
325
|
+
const last = index === segments.length - 1;
|
|
326
|
+
// Every ancestor is a directory; only the final segment is what the caller said it is.
|
|
327
|
+
if (verdictFor(layers, prefix, last ? isDir : true)) {
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ─── The loaded layers ────────────────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
/** Directory → its own `.gitignore` compiled, or `null` for "looked, there is none". */
|
|
337
|
+
const layerCache = new Map<string, IgnoreLayer | null>();
|
|
338
|
+
/** Reads in progress, so a burst of sibling listings makes one request per directory. */
|
|
339
|
+
const inFlight = new Map<string, Promise<IgnoreLayer | null>>();
|
|
340
|
+
|
|
341
|
+
/** Forget every layer. Called when the project changes and when a `.gitignore` does. */
|
|
342
|
+
export function resetIgnoreCache(): void {
|
|
343
|
+
layerCache.clear();
|
|
344
|
+
inFlight.clear();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Read and compile one directory's `.gitignore`. Uncached, and never rejects.
|
|
349
|
+
*
|
|
350
|
+
* A directory without a `.gitignore` is the common case rather than an error, and a backend that
|
|
351
|
+
* fails the read for some other reason gets the same answer — "no rules" shows the file, and
|
|
352
|
+
* showing a file the author meant to hide is the only harmless way to be wrong here. The call is
|
|
353
|
+
* wrapped rather than awaited directly because a platform adapter is free to throw synchronously.
|
|
354
|
+
*/
|
|
355
|
+
async function readGitignore(dir: string): Promise<IgnoreLayer | null> {
|
|
356
|
+
try {
|
|
357
|
+
const text = await getPlatform().readFile(dir === "" ? ".gitignore" : `${dir}/.gitignore`);
|
|
358
|
+
return { base: dir, rules: parseGitignore(text) };
|
|
359
|
+
} catch {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** {@link readGitignore}, once per directory, with concurrent callers sharing the one read. */
|
|
365
|
+
function readLayer(dir: string): Promise<IgnoreLayer | null> {
|
|
366
|
+
const cached = layerCache.get(dir);
|
|
367
|
+
if (cached !== undefined) {
|
|
368
|
+
return Promise.resolve(cached);
|
|
369
|
+
}
|
|
370
|
+
const pending = inFlight.get(dir);
|
|
371
|
+
if (pending) {
|
|
372
|
+
return pending;
|
|
373
|
+
}
|
|
374
|
+
const read = readGitignore(dir).then((layer) => {
|
|
375
|
+
layerCache.set(dir, layer);
|
|
376
|
+
inFlight.delete(dir);
|
|
377
|
+
return layer;
|
|
378
|
+
});
|
|
379
|
+
inFlight.set(dir, read);
|
|
380
|
+
return read;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Re-read every `.gitignore` the tree has already consulted, and swap the results in together.
|
|
385
|
+
*
|
|
386
|
+
* Read-then-swap rather than clear-then-read, and that is the whole point of the function: an empty
|
|
387
|
+
* cache means "nothing is ignored", so a repaint landing between the two would draw a
|
|
388
|
+
* `node_modules` and then take it away again. Only directories already probed are re-read — the
|
|
389
|
+
* ones nobody has listed will read their rules when someone does.
|
|
390
|
+
*/
|
|
391
|
+
export async function reloadIgnoreCache(): Promise<void> {
|
|
392
|
+
const dirs = [...layerCache.keys()];
|
|
393
|
+
inFlight.clear();
|
|
394
|
+
const fresh = await Promise.all(dirs.map((dir) => readGitignore(dir)));
|
|
395
|
+
layerCache.clear();
|
|
396
|
+
for (const [index, dir] of dirs.entries()) {
|
|
397
|
+
layerCache.set(dir, fresh[index] ?? null);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Load every `.gitignore` governing `dir`, so {@link isIgnoredEntry} can answer synchronously. */
|
|
402
|
+
export async function ensureIgnoreLayers(dir: string): Promise<void> {
|
|
403
|
+
await Promise.all(ancestorDirs(dir).map((ancestor) => readLayer(ancestor)));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** The already-loaded layers governing `dir`, shallow-to-deep. */
|
|
407
|
+
export function loadedLayersFor(dir: string): IgnoreLayer[] {
|
|
408
|
+
const layers: IgnoreLayer[] = [];
|
|
409
|
+
for (const ancestor of ancestorDirs(dir)) {
|
|
410
|
+
const layer = layerCache.get(ancestor);
|
|
411
|
+
if (layer) {
|
|
412
|
+
layers.push(layer);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return layers;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Whether the entry at `path`, listed under `dir`, is masked by `.gitignore`.
|
|
420
|
+
*
|
|
421
|
+
* Synchronous, and answers `false` for anything whose layers have not been read yet — a row that
|
|
422
|
+
* appears and then disappears one frame later is a worse artefact than a row that stayed. The
|
|
423
|
+
* listing path awaits {@link ensureIgnoreLayers}, so that frame does not normally exist.
|
|
424
|
+
*/
|
|
425
|
+
export function isIgnoredEntry(dir: string, path: string, isDir: boolean): boolean {
|
|
426
|
+
return isIgnored(loadedLayersFor(dir), path, isDir);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Whether any of these paths is a `.gitignore`, i.e. whether the cache is now stale. */
|
|
430
|
+
export function touchesGitignore(paths: readonly string[]): boolean {
|
|
431
|
+
return paths.some((path) => normalizePath(path).split("/").pop() === ".gitignore");
|
|
432
|
+
}
|
package/src/files/media-paths.ts
CHANGED
|
@@ -15,14 +15,19 @@
|
|
|
15
15
|
* mount (specs/site-architecture.md §9.3), so the same file can also be named
|
|
16
16
|
* `/content/blog/images/hero.png` when the collection's `source` is not already `content/blog`.
|
|
17
17
|
*
|
|
18
|
-
* **Why this module exists at all.**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
18
|
+
* **Why this module exists at all.** A surface that has a FILE and needs a URL cannot get one by
|
|
19
|
+
* prefixing a slash. The library grid did exactly that, so every `public/` image asked for
|
|
20
|
+
* `/public/hero.jpg` — a URL the site does not publish — and every content-collection image skipped
|
|
21
|
+
* its mount entirely. {@link mediaSiteUrl} and {@link previewFileSrc} are the answer, and they take
|
|
22
|
+
* a FILE PATH: an authored reference is a different thing and resolves through
|
|
23
|
+
* `canvas/asset-refs`.
|
|
24
|
+
*
|
|
25
|
+
* This module used to carry a third function, `authoredRefTargets`, enumerating every path an
|
|
26
|
+
* authored reference to a file could resolve to, so a usage query could ask about all of them and
|
|
27
|
+
* union the answers. That existed because `findReferences` resolved a rooted reference against the
|
|
28
|
+
* project root alone; the engine now resolves every lane itself (`site-architecture.md` §9.3), on
|
|
29
|
+
* the write side as well as the read side, which a client-side union never could. Asking once is
|
|
30
|
+
* both simpler and more correct, so the helper is gone.
|
|
26
31
|
*
|
|
27
32
|
* Everything here is pure string math over project-relative, forward-slashed paths. The only input
|
|
28
33
|
* from outside is the open project's content sections, read the same way `canvas/asset-refs.ts`
|
|
@@ -101,40 +106,6 @@ export function mediaSiteUrl(path: string): string {
|
|
|
101
106
|
return mount ? `/${mountedPath(normalized, mount)}` : `/${normalized}`;
|
|
102
107
|
}
|
|
103
108
|
|
|
104
|
-
/**
|
|
105
|
-
* Every path an authored reference to this media file resolves to — the keys a usage query must ask
|
|
106
|
-
* about, most literal first.
|
|
107
|
-
*
|
|
108
|
-
* 1. The file itself. A sibling's `./images/hero.png`, a rooted `/content/blog/images/hero.png` and a
|
|
109
|
-
* bare `assets/logo.svg` all land here.
|
|
110
|
-
* 2. Its served path, for `public/` media. `/hero.jpg` resolves to `hero.jpg`, which is not a file on
|
|
111
|
-
* disk and does not need to be — the sweep compares resolved strings, and this is the string
|
|
112
|
-
* every reference to a public asset produces.
|
|
113
|
-
* 3. Its asset-mount path, when a collection's `source` differs from its mount prefix. Identical to
|
|
114
|
-
* (1) for the ordinary `content/<type>/` layout, and deduplicated when it is.
|
|
115
|
-
*
|
|
116
|
-
* The list is a UNION and is deliberately generous: over-counting a reference makes a delete dialog
|
|
117
|
-
* more alarming than it needs to be, while missing one makes it lie.
|
|
118
|
-
*/
|
|
119
|
-
export function authoredRefTargets(path: string): string[] {
|
|
120
|
-
const normalized = normalizeProjectPath(path);
|
|
121
|
-
if (normalized === "") {
|
|
122
|
-
return [];
|
|
123
|
-
}
|
|
124
|
-
const targets = [normalized];
|
|
125
|
-
if (normalized.startsWith(`${PUBLIC_DIR}/`)) {
|
|
126
|
-
targets.push(normalized.slice(PUBLIC_DIR.length + 1));
|
|
127
|
-
}
|
|
128
|
-
const mount = mountOf(normalized);
|
|
129
|
-
if (mount) {
|
|
130
|
-
const mounted = mountedPath(normalized, mount);
|
|
131
|
-
if (!targets.includes(mounted)) {
|
|
132
|
-
targets.push(mounted);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return targets;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
109
|
/**
|
|
139
110
|
* The src a PARENT-REALM preview should load for a PROJECT FILE PATH — a library thumbnail, a
|
|
140
111
|
* media-browser tile.
|