@jxsuite/studio 2.0.1 → 2.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/canvas.html +21 -0
- package/dist/chunks/Overlay.dev-qfjzkms8.js +23 -0
- package/dist/chunks/{Overlay.dev-cszx7yn5.js.map → Overlay.dev-qfjzkms8.js.map} +1 -1
- package/dist/chunks/{adapter-z8fkmbmv.js → adapter-qfnesn5v.js} +2 -2
- package/dist/chunks/{cf-settings-mmyv436d.js → cf-settings-73sh5z4p.js} +3 -3
- package/dist/chunks/{components-799jpvhm.js → components-2bpqp4mq.js} +5 -7
- package/dist/chunks/{components-799jpvhm.js.map → components-2bpqp4mq.js.map} +1 -1
- package/dist/chunks/content-source-34gnkbnn.js +38 -0
- package/dist/chunks/content-source-34gnkbnn.js.map +9 -0
- package/dist/chunks/convert-to-repeater-t8kxre5s.js +258 -0
- package/dist/chunks/convert-to-repeater-t8kxre5s.js.map +10 -0
- package/dist/chunks/extension-sections-6mqydz35.js +301 -0
- package/dist/chunks/extension-sections-6mqydz35.js.map +11 -0
- package/dist/chunks/files-8kn321h1.js +70 -0
- package/dist/chunks/files-8kn321h1.js.map +9 -0
- package/dist/chunks/{format-host-8jxwts2p.js → format-host-mkwsmt8d.js} +3 -3
- package/dist/chunks/git-panel-tjbr191d.js +51 -0
- package/dist/chunks/git-panel-tjbr191d.js.map +9 -0
- package/dist/chunks/{index-baeg2x3y.js → index-7pxchcce.js} +4 -3
- package/dist/chunks/{index-baeg2x3y.js.map → index-7pxchcce.js.map} +3 -3
- package/dist/chunks/{jx-validate-qx8degj7.js → jx-validate-v6qgknrs.js} +3 -3
- package/dist/chunks/layers-panel-87pb25km.js +58 -0
- package/dist/chunks/layers-panel-87pb25km.js.map +9 -0
- package/dist/chunks/{monaco-setup-zjdp9zdw.js → monaco-setup-e0yh25j4.js} +4 -75
- package/dist/chunks/{monaco-setup-zjdp9zdw.js.map → monaco-setup-e0yh25j4.js.map} +3 -4
- package/dist/chunks/publish-panel-m0fcgdrt.js +21 -0
- package/dist/chunks/publish-panel-m0fcgdrt.js.map +9 -0
- package/dist/chunks/quick-search-rhs6cbsb.js +53 -0
- package/dist/chunks/quick-search-rhs6cbsb.js.map +9 -0
- package/dist/chunks/right-panel-zpdxd98t.js +37 -0
- package/dist/chunks/right-panel-zpdxd98t.js.map +9 -0
- package/dist/chunks/section-registry-zmckaxf4.js +30 -0
- package/dist/chunks/section-registry-zmckaxf4.js.map +9 -0
- package/dist/chunks/sp-overlay.dev-9ran0sfq.js +18 -0
- package/dist/chunks/sp-overlay.dev-9ran0sfq.js.map +9 -0
- package/dist/chunks/sp-popover.dev-rebmdncr.js +9 -0
- package/dist/chunks/sp-popover.dev-rebmdncr.js.map +9 -0
- package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js +15 -0
- package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js.map +9 -0
- package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js +15 -0
- package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js.map +9 -0
- package/dist/chunks/{sp-tray.dev-pb2fsd65.js → sp-tray.dev-ey59vfh7.js} +10 -8
- package/dist/chunks/{sp-tray.dev-pb2fsd65.js.map → sp-tray.dev-ey59vfh7.js.map} +2 -2
- package/dist/chunks/{studio-9qr6fq7p.js → studio-0rqqhrdy.js} +10 -378
- package/dist/chunks/{studio-9qr6fq7p.js.map → studio-0rqqhrdy.js.map} +4 -14
- package/dist/chunks/{studio-5x6tqqec.js → studio-2sj1jj3v.js} +10 -8
- package/dist/chunks/{studio-5x6tqqec.js.map → studio-2sj1jj3v.js.map} +2 -2
- package/dist/chunks/{studio-h3fw0wn8.js → studio-3fk1ac9z.js} +6 -4
- package/dist/chunks/{studio-h3fw0wn8.js.map → studio-3fk1ac9z.js.map} +2 -2
- package/dist/chunks/studio-41exgzhq.js +6690 -0
- package/dist/chunks/studio-41exgzhq.js.map +38 -0
- package/dist/chunks/{studio-7v2bpweq.js → studio-4mqb9a0j.js} +6 -4
- package/dist/chunks/{studio-7v2bpweq.js.map → studio-4mqb9a0j.js.map} +2 -2
- package/dist/chunks/studio-50k77cca.js +1787 -0
- package/dist/chunks/studio-50k77cca.js.map +18 -0
- package/dist/chunks/{studio-56v3zwk6.js → studio-6a0h7sne.js} +10 -8
- package/dist/chunks/{studio-56v3zwk6.js.map → studio-6a0h7sne.js.map} +2 -2
- package/dist/chunks/studio-6f0am8f2.js +53 -0
- package/dist/chunks/studio-6f0am8f2.js.map +10 -0
- package/dist/chunks/studio-6nmjc5qk.js +7002 -0
- package/dist/chunks/studio-6nmjc5qk.js.map +32 -0
- package/dist/chunks/studio-7afd21nw.js +67 -0
- package/dist/chunks/studio-7afd21nw.js.map +10 -0
- package/dist/chunks/{studio-xze4zrs1.js → studio-7bmdvzne.js} +2 -2
- package/dist/chunks/studio-7bmdvzne.js.map +10 -0
- package/dist/chunks/studio-8jpzqk5s.js +128 -0
- package/dist/chunks/studio-8jpzqk5s.js.map +10 -0
- package/dist/chunks/studio-94m77tgk.js +1482 -0
- package/dist/chunks/studio-94m77tgk.js.map +13 -0
- package/dist/chunks/studio-9je6w5sq.js +1000 -0
- package/dist/chunks/studio-9je6w5sq.js.map +16 -0
- package/dist/chunks/{studio-w7c6kvk7.js → studio-a7pzveq1.js} +2 -2
- package/dist/chunks/{studio-gs6kqdt1.js → studio-adn0z1k3.js} +14 -8
- package/dist/chunks/{studio-gs6kqdt1.js.map → studio-adn0z1k3.js.map} +3 -3
- package/dist/chunks/{studio-kr88ryr7.js → studio-bsxxx3x2.js} +17 -15
- package/dist/chunks/{studio-kr88ryr7.js.map → studio-bsxxx3x2.js.map} +7 -6
- package/dist/chunks/studio-bw5dz6km.js +608 -0
- package/dist/chunks/studio-bw5dz6km.js.map +21 -0
- package/dist/chunks/studio-c6s5getc.js +4232 -0
- package/dist/chunks/studio-c6s5getc.js.map +39 -0
- package/dist/chunks/studio-cjm7fdjf.js +451 -0
- package/dist/chunks/studio-cjm7fdjf.js.map +14 -0
- package/dist/chunks/{studio-cx91mnh8.js → studio-ckwz4d3x.js} +53 -53
- package/dist/chunks/{studio-cx91mnh8.js.map → studio-ckwz4d3x.js.map} +5 -5
- package/dist/chunks/studio-dg7zyt5x.js +28 -0
- package/dist/chunks/studio-dg7zyt5x.js.map +10 -0
- package/dist/chunks/studio-dzcyvvvz.js +166 -0
- package/dist/chunks/studio-dzcyvvvz.js.map +10 -0
- package/dist/chunks/{studio-7kqe41sa.js → studio-evkr5d3b.js} +15 -7
- package/dist/chunks/{studio-7kqe41sa.js.map → studio-evkr5d3b.js.map} +5 -4
- package/dist/chunks/studio-f12h6177.js +411 -0
- package/dist/chunks/studio-f12h6177.js.map +13 -0
- package/dist/chunks/studio-hbp1s8m9.js +1817 -0
- package/dist/chunks/studio-hbp1s8m9.js.map +14 -0
- package/dist/chunks/studio-k2sxjph5.js +15295 -0
- package/dist/chunks/studio-k2sxjph5.js.map +235 -0
- package/dist/chunks/studio-myje51t3.js +385 -0
- package/dist/chunks/studio-myje51t3.js.map +12 -0
- package/dist/chunks/studio-na6rwzhw.js +353 -0
- package/dist/chunks/studio-na6rwzhw.js.map +11 -0
- package/dist/chunks/{studio-4k0m4fmp.js → studio-p471gg5n.js} +126 -3
- package/dist/chunks/studio-p471gg5n.js.map +14 -0
- package/dist/chunks/{studio-9bjwf0d4.js → studio-rgwcpast.js} +5 -46
- package/dist/chunks/{studio-9bjwf0d4.js.map → studio-rgwcpast.js.map} +4 -5
- package/dist/chunks/{studio-esrewxap.js → studio-s519zc99.js} +3 -3
- package/dist/chunks/studio-sas7mt3k.js +75 -0
- package/dist/chunks/studio-sas7mt3k.js.map +10 -0
- package/dist/chunks/studio-t5wqwzdh.js +74 -0
- package/dist/chunks/studio-t5wqwzdh.js.map +10 -0
- package/dist/chunks/{studio-9qpspwfn.js → studio-temv5bw4.js} +5 -5
- package/dist/chunks/{studio-9qpspwfn.js.map → studio-temv5bw4.js.map} +1 -1
- package/dist/chunks/{studio-qkvt37c0.js → studio-tr20m8g8.js} +669 -18
- package/dist/chunks/studio-tr20m8g8.js.map +9 -0
- package/dist/chunks/{studio-v7jkjqng.js → studio-v96qfnww.js} +5514 -4291
- package/dist/chunks/studio-v96qfnww.js.map +54 -0
- package/dist/chunks/studio-vcetc91k.js +659 -0
- package/dist/chunks/studio-vcetc91k.js.map +10 -0
- package/dist/chunks/{studio-jm0z592w.js → studio-vmymjefp.js} +2972 -4937
- package/dist/chunks/studio-vmymjefp.js.map +98 -0
- package/dist/chunks/{studio-bghqfmea.js → studio-wwseqyw0.js} +2 -2
- package/dist/chunks/studio-x9hmy5kr.js +390 -0
- package/dist/chunks/studio-x9hmy5kr.js.map +10 -0
- package/dist/chunks/studio-xa0ce0n7.js +335 -0
- package/dist/chunks/studio-xa0ce0n7.js.map +10 -0
- package/dist/chunks/studio-xm3dbxvn.js +378 -0
- package/dist/chunks/studio-xm3dbxvn.js.map +19 -0
- package/dist/chunks/{studio-nqw433kv.js → studio-xmvbhg31.js} +2 -2
- package/dist/chunks/studio-zv37yrfa.js +1054 -0
- package/dist/chunks/studio-zv37yrfa.js.map +13 -0
- package/dist/chunks/{ws-client-q7hkz3j9.js → ws-client-654ts66j.js} +6 -4
- package/dist/chunks/ws-client-654ts66j.js.map +11 -0
- package/dist/iframe-entry.js +668 -230
- package/dist/studio.js +21080 -32585
- package/index.html +35 -4685
- package/package.json +17 -15
- package/src/about/about-modal.ts +42 -11
- package/src/browse/library-commands.ts +240 -0
- package/src/browse/library-layouts.ts +369 -0
- package/src/browse/library-model.ts +396 -0
- package/src/browse/library-pane.ts +1049 -0
- package/src/browse/library-preview.ts +252 -0
- package/src/browse/library-source.ts +150 -0
- package/src/canvas/canvas-helpers.ts +33 -9
- package/src/canvas/canvas-live-render.ts +45 -29
- package/src/canvas/canvas-patcher.ts +151 -31
- package/src/canvas/canvas-perf.ts +46 -7
- package/src/canvas/canvas-render.ts +1092 -220
- package/src/canvas/canvas-surface.ts +240 -0
- package/src/canvas/canvas-utils.ts +1166 -124
- package/src/canvas/iframe-drop.ts +2 -1
- package/src/canvas/iframe-entry.ts +241 -4
- package/src/canvas/iframe-host.ts +1226 -143
- package/src/canvas/iframe-inline-edit.ts +31 -0
- package/src/canvas/iframe-interaction.ts +76 -16
- package/src/canvas/iframe-keys.ts +107 -45
- package/src/canvas/iframe-overlay.ts +38 -1
- package/src/canvas/iframe-protocol.ts +137 -2
- package/src/canvas/iframe-render.ts +70 -2
- package/src/canvas/iframe-slash.ts +6 -0
- package/src/canvas/path-mapping.ts +56 -6
- package/src/canvas/surface-registry.ts +292 -0
- package/src/collab/collab-commands.ts +244 -0
- package/src/collab/collab-session.ts +198 -32
- package/src/collab/collab-state.ts +28 -2
- package/src/collab/presence-chips.ts +100 -9
- package/src/commands/active-registry.ts +42 -0
- package/src/commands/app-commands.ts +176 -0
- package/src/commands/budget.ts +158 -0
- package/src/commands/command-args.ts +253 -0
- package/src/commands/context.ts +387 -0
- package/src/commands/defaults.ts +580 -0
- package/src/commands/keymap.ts +571 -0
- package/src/commands/levels.ts +345 -0
- package/src/commands/live-context.ts +193 -0
- package/src/commands/reference.ts +227 -0
- package/src/commands/registry.ts +293 -0
- package/src/component-props.ts +3 -2
- package/src/content/draft-state.ts +106 -0
- package/src/content/entry-commands.ts +282 -0
- package/src/content/entry-editor.ts +327 -0
- package/src/content/entry-fields.ts +116 -0
- package/src/content/entry-model.ts +175 -0
- package/src/editor/canvas-context-menu.ts +17 -11
- package/src/editor/canvas-slash-bridge.ts +11 -4
- package/src/editor/context-menu.ts +804 -204
- package/src/editor/convert-to-component.ts +31 -10
- package/src/editor/convert-to-repeater.ts +3 -2
- package/src/editor/inline-edit-apply.ts +5 -5
- package/src/editor/inline-edit.ts +86 -50
- package/src/editor/insert-zone-action.ts +1 -1
- package/src/editor/shortcuts.ts +1043 -329
- package/src/editor/slash-menu.ts +1 -0
- package/src/files/elements.ts +166 -0
- package/src/files/file-ops.ts +178 -31
- package/src/files/files.ts +898 -268
- package/src/files/fs-events.ts +21 -0
- package/src/files/media-meta.ts +203 -0
- package/src/files/media-paths.ts +141 -0
- package/src/files/media-upload.ts +7 -3
- package/src/files/media-usage.ts +189 -0
- package/src/github/github-auth.ts +235 -33
- package/src/github/github-publish.ts +80 -21
- package/src/grid/grid-controller.ts +152 -12
- package/src/grid/grid-layout.ts +225 -20
- package/src/grid/grid-open.ts +120 -0
- package/src/grid/grid-panel.ts +554 -39
- package/src/grid/grid-source.ts +27 -1
- package/src/grid/redirects-grid.ts +599 -0
- package/src/grid/redirects.ts +398 -0
- package/src/grid/sources/content-source.ts +108 -2
- package/src/i18n/i18n-commands.ts +322 -0
- package/src/new-project/add-repo-modal.ts +8 -10
- package/src/new-project/import-tab.ts +7 -0
- package/src/new-project/location-fields.ts +9 -1
- package/src/new-project/new-project-modal.ts +221 -245
- package/src/packages/jxsuite-update.ts +97 -48
- package/src/packages/pull-package-sync.ts +18 -9
- package/src/page-params.ts +2 -1
- package/src/panels/activity-bar.ts +152 -91
- package/src/panels/activity-panel.ts +481 -0
- package/src/panels/ai-chat/chat-markdown.ts +8 -3
- package/src/panels/ai-chat/chat-view.ts +245 -18
- package/src/panels/ai-chat/composer.ts +31 -5
- package/src/panels/ai-chat/sessions-view.ts +17 -10
- package/src/panels/ai-panel.ts +279 -53
- package/src/panels/block-action-bar.ts +1115 -224
- package/src/panels/bottom-dock.ts +337 -0
- package/src/panels/canvas-dnd-bridge.ts +37 -8
- package/src/panels/chat-panel.ts +29 -12
- package/src/panels/data-explorer.ts +254 -112
- package/src/panels/data-grid.ts +10 -4
- package/src/panels/dnd.ts +28 -51
- package/src/panels/editors.ts +434 -107
- package/src/panels/elements-panel.ts +66 -2
- package/src/panels/empty-state.ts +112 -0
- package/src/panels/events-panel.ts +596 -82
- package/src/panels/formula-workspace.ts +408 -218
- package/src/panels/frontmatter-fields.ts +51 -10
- package/src/panels/frontmatter-panel.ts +357 -79
- package/src/panels/git-panel.ts +375 -164
- package/src/panels/head-panel.ts +791 -85
- package/src/panels/i18n-panel.ts +592 -0
- package/src/panels/imports-panel.ts +95 -85
- package/src/panels/jump-bar.ts +584 -0
- package/src/panels/layers-panel.ts +1106 -328
- package/src/panels/left-panel.ts +109 -204
- package/src/panels/navigator-panels.ts +131 -0
- package/src/panels/overlays.ts +7 -2
- package/src/panels/pane-context.ts +1477 -0
- package/src/panels/pane-grid.ts +406 -0
- package/src/panels/panel-registry.ts +340 -0
- package/src/panels/panel-scheduler.ts +75 -0
- package/src/panels/problems-panel.ts +234 -0
- package/src/panels/properties-panel.ts +946 -1139
- package/src/panels/provenance.ts +258 -0
- package/src/panels/quick-search.ts +791 -122
- package/src/panels/right-panel.ts +228 -102
- package/src/panels/seo-modal.ts +395 -0
- package/src/panels/settings-pane.ts +203 -0
- package/src/panels/signals-panel.ts +354 -137
- package/src/panels/statement-editor.ts +41 -25
- package/src/panels/statusbar.ts +342 -106
- package/src/panels/style-panel.ts +974 -463
- package/src/panels/style-utils.ts +3 -3
- package/src/panels/stylebook-doc.ts +4 -2
- package/src/panels/stylebook-layers-panel.ts +3 -2
- package/src/panels/stylebook-panel.ts +51 -26
- package/src/panels/tab-strip.ts +1058 -78
- package/src/panels/target-line.ts +274 -0
- package/src/panels/toolbar.ts +631 -484
- package/src/panels/welcome-screen.ts +301 -198
- package/src/platform-errors.ts +26 -3
- package/src/platform.ts +96 -2
- package/src/platforms/cloud.ts +71 -8
- package/src/platforms/devserver.ts +92 -29
- package/src/publish/deploy-checklist.ts +383 -0
- package/src/publish/pages-service.ts +10 -1
- package/src/publish/publish-commands.ts +218 -0
- package/src/publish/publish-panel.ts +110 -16
- package/src/recent-projects.ts +3 -2
- package/src/services/a11y-report.ts +505 -0
- package/src/services/ai-models.ts +30 -1
- package/src/services/ai-project-tools.ts +29 -10
- package/src/services/ai-system-prompt.ts +53 -14
- package/src/services/ai-tools.ts +39 -3
- package/src/services/ai-writes.ts +154 -0
- package/src/services/announce.ts +97 -0
- package/src/services/automation.ts +424 -175
- package/src/services/cem-export.ts +9 -4
- package/src/services/clock.ts +61 -0
- package/src/services/document-assistant.ts +36 -8
- package/src/services/idle.ts +179 -0
- package/src/services/import-client.ts +26 -0
- package/src/services/monaco-buffer.ts +627 -0
- package/src/services/monaco-lazy.ts +36 -0
- package/src/services/notify.ts +271 -0
- package/src/services/profile.ts +155 -0
- package/src/services/references.ts +0 -0
- package/src/services/token-lint.ts +3 -2
- package/src/services/tool-executor.ts +53 -7
- package/src/services/trusted-types.ts +123 -0
- package/src/settings/contexts-section.ts +427 -0
- package/src/settings/contributed-section.ts +149 -11
- package/src/settings/css-vars-editor.ts +306 -349
- package/src/settings/defs-editor.ts +93 -34
- package/src/settings/dependencies-editor.ts +7 -7
- package/src/settings/extension-sections.ts +37 -5
- package/src/settings/general-settings.ts +204 -121
- package/src/settings/locales-section.ts +347 -0
- package/src/settings/preferences-accounts.ts +140 -0
- package/src/settings/preferences-dialog.ts +620 -0
- package/src/settings/preferences-keymap.ts +270 -0
- package/src/settings/project-sections.ts +260 -0
- package/src/settings/section-registry.ts +169 -0
- package/src/settings/settings-document.ts +385 -0
- package/src/shell.ts +1549 -0
- package/src/site-context.ts +45 -20
- package/src/state.ts +14 -275
- package/src/store.ts +88 -43
- package/src/studio.ts +790 -346
- package/src/style/live-preview.ts +36 -0
- package/src/style/project-styles.ts +349 -0
- package/src/style/token-ref.ts +123 -0
- package/src/tabs/patch-ops.ts +16 -4
- package/src/tabs/project-config.ts +416 -0
- package/src/tabs/selection.ts +323 -0
- package/src/tabs/tab.ts +182 -47
- package/src/tabs/transact.ts +209 -28
- package/src/types.ts +68 -13
- package/src/ui/ai-credentials-form.ts +39 -35
- package/src/ui/ai-managed-connect.ts +7 -1
- package/src/ui/dynamic-slot.ts +224 -81
- package/src/ui/expression-editor.ts +71 -63
- package/src/ui/field-input.ts +4 -6
- package/src/ui/field-row.ts +215 -38
- package/src/ui/form-controls.ts +194 -7
- package/src/ui/layers.ts +492 -69
- package/src/ui/media-picker.ts +115 -25
- package/src/ui/panel-resize.ts +109 -91
- package/src/ui/progress-modal.ts +131 -50
- package/src/ui/regions.ts +462 -0
- package/src/ui/schema-form.ts +229 -45
- package/src/ui/spectrum.ts +24 -8
- package/src/ui/timing.ts +49 -0
- package/src/ui/value-selector.ts +20 -0
- package/src/ui/value-source.ts +348 -0
- package/src/ui/virtual-window.ts +400 -0
- package/src/utils/edit-display.ts +3 -2
- package/src/utils/inherited-style.ts +67 -37
- package/src/view.ts +64 -78
- package/src/workspace/pane-derive.ts +1258 -0
- package/src/workspace/session.ts +317 -0
- package/src/workspace/workspace.ts +1345 -23
- package/styles/canvas.css +653 -0
- package/styles/forced-colors.css +106 -0
- package/styles/inspector.css +1454 -0
- package/styles/overlays.css +2704 -0
- package/styles/panels.css +1701 -0
- package/styles/shell.css +1423 -0
- package/styles/tokens.css +151 -0
- package/dist/chunks/Overlay.dev-cszx7yn5.js +0 -22
- package/dist/chunks/extension-sections-qr3drkkw.js +0 -531
- package/dist/chunks/extension-sections-qr3drkkw.js.map +0 -12
- package/dist/chunks/files-j8a1t06k.js +0 -50
- package/dist/chunks/files-j8a1t06k.js.map +0 -9
- package/dist/chunks/git-panel-xtq5z81x.js +0 -28
- package/dist/chunks/git-panel-xtq5z81x.js.map +0 -9
- package/dist/chunks/layers-panel-ancn2wpq.js +0 -26
- package/dist/chunks/layers-panel-ancn2wpq.js.map +0 -9
- package/dist/chunks/settings-modal-17ttr96g.js +0 -29
- package/dist/chunks/settings-modal-17ttr96g.js.map +0 -9
- package/dist/chunks/sp-overlay.dev-earn5as8.js +0 -17
- package/dist/chunks/sp-overlay.dev-earn5as8.js.map +0 -9
- package/dist/chunks/sp-popover.dev-frxtfejn.js +0 -8
- package/dist/chunks/sp-popover.dev-frxtfejn.js.map +0 -9
- package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js +0 -14
- package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js.map +0 -9
- package/dist/chunks/sp-tooltip.dev-a9fywesm.js +0 -21
- package/dist/chunks/sp-tooltip.dev-a9fywesm.js.map +0 -10
- package/dist/chunks/studio-0xgs7xrs.js +0 -429
- package/dist/chunks/studio-0xgs7xrs.js.map +0 -13
- package/dist/chunks/studio-4d8hzcz5.js +0 -3427
- package/dist/chunks/studio-4d8hzcz5.js.map +0 -26
- package/dist/chunks/studio-4k0m4fmp.js.map +0 -13
- package/dist/chunks/studio-9smfzqhc.js +0 -19
- package/dist/chunks/studio-9smfzqhc.js.map +0 -10
- package/dist/chunks/studio-jm0z592w.js.map +0 -115
- package/dist/chunks/studio-m2t4mdg5.js +0 -1854
- package/dist/chunks/studio-m2t4mdg5.js.map +0 -18
- package/dist/chunks/studio-n4p11dfy.js +0 -853
- package/dist/chunks/studio-n4p11dfy.js.map +0 -12
- package/dist/chunks/studio-qkvt37c0.js.map +0 -9
- package/dist/chunks/studio-sjh1wt1a.js +0 -3744
- package/dist/chunks/studio-sjh1wt1a.js.map +0 -22
- package/dist/chunks/studio-v7jkjqng.js.map +0 -46
- package/dist/chunks/studio-xq0t7n5t.js +0 -140
- package/dist/chunks/studio-xq0t7n5t.js.map +0 -10
- package/dist/chunks/studio-xze4zrs1.js.map +0 -10
- package/dist/chunks/studio-yfcstdzy.js +0 -399
- package/dist/chunks/studio-yfcstdzy.js.map +0 -12
- package/dist/chunks/studio-zp7jncs8.js +0 -469
- package/dist/chunks/studio-zp7jncs8.js.map +0 -14
- package/dist/chunks/ws-client-q7hkz3j9.js.map +0 -11
- package/src/browse/browse-modal.ts +0 -69
- package/src/browse/browse.ts +0 -904
- package/src/canvas/canvas-diff.ts +0 -216
- package/src/canvas/nested-site-style.ts +0 -58
- package/src/new-project/design-fields.ts +0 -264
- package/src/new-project/templates.ts +0 -61
- package/src/panels/tab-bar.ts +0 -548
- package/src/settings/settings-modal.ts +0 -203
- /package/dist/chunks/{adapter-z8fkmbmv.js.map → adapter-qfnesn5v.js.map} +0 -0
- /package/dist/chunks/{cf-settings-mmyv436d.js.map → cf-settings-73sh5z4p.js.map} +0 -0
- /package/dist/chunks/{format-host-8jxwts2p.js.map → format-host-mkwsmt8d.js.map} +0 -0
- /package/dist/chunks/{jx-validate-qx8degj7.js.map → jx-validate-v6qgknrs.js.map} +0 -0
- /package/dist/chunks/{studio-w7c6kvk7.js.map → studio-a7pzveq1.js.map} +0 -0
- /package/dist/chunks/{studio-esrewxap.js.map → studio-s519zc99.js.map} +0 -0
- /package/dist/chunks/{studio-bghqfmea.js.map → studio-wwseqyw0.js.map} +0 -0
- /package/dist/chunks/{studio-nqw433kv.js.map → studio-xmvbhg31.js.map} +0 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redirect rules — the model, the three validations, and the two import formats.
|
|
3
|
+
*
|
|
4
|
+
* Pure: no platform, no DOM, no project state. `redirects-grid.ts` is what puts this in front of an
|
|
5
|
+
* author; everything decidable about a redirect is decided here, where it can be tested with a
|
|
6
|
+
* literal array instead of a project.
|
|
7
|
+
*
|
|
8
|
+
* **Why three validations and not a lint pass.** `project.json`'s `redirects` map is the one part
|
|
9
|
+
* of a Jx project that is invisible in the thing it configures: nothing in the site renders it, and
|
|
10
|
+
* a rule that is wrong looks exactly like a rule that is right (specs/site-architecture.md §11.4
|
|
11
|
+
* asks for precisely these three). Each has a different cost and a different fix:
|
|
12
|
+
*
|
|
13
|
+
* - **Chain** — `/a → /b` where `/b` is itself a rule. The visitor pays a second round trip, and
|
|
14
|
+
* every hop is separately cacheable, so the last one to be corrected wins for a while. Fix: point
|
|
15
|
+
* the first rule at the final destination.
|
|
16
|
+
* - **Loop** — a cycle. The page is unreachable. This is the only one filed as an error.
|
|
17
|
+
* - **Shadow** — the project has a real route the rule's source covers. On a host that serves static
|
|
18
|
+
* assets before consulting `_redirects` (Netlify, Cloudflare Pages) the page wins and the rule is
|
|
19
|
+
* dead config; in `dist/` the compiler's own meta-refresh file overwrites the page and warns
|
|
20
|
+
* (§11.1). Two hosts, two answers, and neither is what the author intended by writing both.
|
|
21
|
+
*
|
|
22
|
+
* **What is deliberately NOT decided.** A destination containing `:param` or `*` is not followed:
|
|
23
|
+
* its concrete target depends on the request, so calling it a chain would be a guess. Nothing is
|
|
24
|
+
* reported about it either way — see {@link validateRedirects}.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { REDIRECT_STATUSES } from "@jxsuite/schema/defs";
|
|
28
|
+
import { parseCsv } from "./csv-codec";
|
|
29
|
+
|
|
30
|
+
/** One redirect rule, flattened out of `project.json`'s two accepted spellings. */
|
|
31
|
+
/**
|
|
32
|
+
* A rule's target: an RFC 9110 §15.4 redirection status, or a rewrite.
|
|
33
|
+
*
|
|
34
|
+
* A rewrite is NOT status 200. It serves the destination's content at the source URL, and the 200
|
|
35
|
+
* that reaches `_redirects` is the host's convention for saying so. Modelling it as a status is
|
|
36
|
+
* what let the compiler emit a meta-refresh page for one — see site-architecture.md §11.3.
|
|
37
|
+
*/
|
|
38
|
+
export const REWRITE = "rewrite";
|
|
39
|
+
export type RedirectTarget = (typeof REDIRECT_STATUSES)[number] | typeof REWRITE;
|
|
40
|
+
|
|
41
|
+
export interface RedirectRule {
|
|
42
|
+
source: string;
|
|
43
|
+
destination: string;
|
|
44
|
+
status: RedirectTarget;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** What the status column offers, in the order it offers them. */
|
|
48
|
+
export const REDIRECT_TARGETS: readonly RedirectTarget[] = [...REDIRECT_STATUSES, REWRITE];
|
|
49
|
+
|
|
50
|
+
/** The status `project.json` means when a rule is written as a bare string. */
|
|
51
|
+
export const DEFAULT_REDIRECT_STATUS = 301;
|
|
52
|
+
|
|
53
|
+
/** The status a rewrite is written as in `_redirects`, and read back from an import. */
|
|
54
|
+
export const REWRITE_WIRE_STATUS = 200;
|
|
55
|
+
|
|
56
|
+
/** `project.json`'s redirects map, as the schema types it. */
|
|
57
|
+
export type RedirectConfig = Record<
|
|
58
|
+
string,
|
|
59
|
+
string | { destination: string; status?: number } | { destination: string; rewrite: true }
|
|
60
|
+
>;
|
|
61
|
+
|
|
62
|
+
// ─── Paths and patterns ───────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
/** Trailing-slash-insensitive form, matching the compiler's own `normalizeUrl` in `site-build.ts`. */
|
|
65
|
+
export function normalizePath(path: string): string {
|
|
66
|
+
const trimmed = path.trim();
|
|
67
|
+
return trimmed.length > 1 && trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Whether a path is a URLPattern rather than a literal — `:param` or `*` (§11.2). */
|
|
71
|
+
export function isPattern(path: string): boolean {
|
|
72
|
+
return path.includes(":") || path.includes("*");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Whether a path leaves the site: a scheme, a protocol-relative URL, or a mailto/tel. */
|
|
76
|
+
export function isExternal(path: string): boolean {
|
|
77
|
+
return /^[a-z][\d+.a-z-]*:/i.test(path) || path.startsWith("//");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Whether `path` is covered by `pattern`.
|
|
82
|
+
*
|
|
83
|
+
* URLPattern-lite over pathname segments, which is all §11.2 uses: `:name` matches exactly one
|
|
84
|
+
* non-empty segment and `*` matches the rest, including nothing. Enough to answer "does this rule
|
|
85
|
+
* ever fire for that request", which is the only question chain, loop and shadow ask.
|
|
86
|
+
*/
|
|
87
|
+
export function matchesPattern(pattern: string, path: string): boolean {
|
|
88
|
+
const p = normalizePath(pattern).split("/");
|
|
89
|
+
const s = normalizePath(path).split("/");
|
|
90
|
+
for (const [i, segment] of p.entries()) {
|
|
91
|
+
if (segment === "*") {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
if (i >= s.length) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
if (segment.startsWith(":")) {
|
|
98
|
+
if (s[i] === "") {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (segment !== s[i]) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return s.length === p.length;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A page route as a pattern: `/blog/[slug]` → `/blog/:slug`.
|
|
112
|
+
*
|
|
113
|
+
* File-based routing spells a dynamic segment with brackets and a redirect spells it with a colon.
|
|
114
|
+
* The shadow check compares the two, so one of them has to be translated, and translating the route
|
|
115
|
+
* leaves the author's own rule text untouched in every message.
|
|
116
|
+
*/
|
|
117
|
+
export function routePattern(route: string): string {
|
|
118
|
+
return route.replaceAll(/\[([^\]]+)]/g, ":$1");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── project.json ↔ rules ─────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
/** Flatten `project.json`'s redirects map into rules, in declaration order. */
|
|
124
|
+
export function rulesFromConfig(config: RedirectConfig | undefined): RedirectRule[] {
|
|
125
|
+
return Object.entries(config ?? {}).map(([source, target]) => {
|
|
126
|
+
if (typeof target === "string") {
|
|
127
|
+
return { destination: target, source, status: DEFAULT_REDIRECT_STATUS };
|
|
128
|
+
}
|
|
129
|
+
if ("rewrite" in target) {
|
|
130
|
+
return { destination: target.destination, source, status: REWRITE };
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
destination: target.destination,
|
|
134
|
+
source,
|
|
135
|
+
status: (target.status ?? DEFAULT_REDIRECT_STATUS) as RedirectTarget,
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Rules back to the map `project.json` holds.
|
|
142
|
+
*
|
|
143
|
+
* A 301 collapses to the string spelling, because that is what the file already says for every rule
|
|
144
|
+
* an author wrote by hand and expanding them all to objects would rewrite the whole block on the
|
|
145
|
+
* first edit — the same "a no-op edit writes nothing" rule `tabs/project-config.ts` enforces one
|
|
146
|
+
* level down. A later duplicate source wins, which is what a JSON object means anyway; the grid
|
|
147
|
+
* refuses duplicates before they get here.
|
|
148
|
+
*/
|
|
149
|
+
export function configFromRules(rules: readonly RedirectRule[]): RedirectConfig {
|
|
150
|
+
const config: RedirectConfig = {};
|
|
151
|
+
for (const rule of rules) {
|
|
152
|
+
if (rule.status === REWRITE) {
|
|
153
|
+
config[rule.source] = { destination: rule.destination, rewrite: true };
|
|
154
|
+
} else if (rule.status === DEFAULT_REDIRECT_STATUS) {
|
|
155
|
+
config[rule.source] = rule.destination;
|
|
156
|
+
} else {
|
|
157
|
+
config[rule.source] = { destination: rule.destination, status: rule.status };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return config;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ─── Validation ───────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
/** Which of the three rules a finding names. Printed in the Problem, so it is part of the contract. */
|
|
166
|
+
export type RedirectRuleName = "chain" | "loop" | "shadow";
|
|
167
|
+
|
|
168
|
+
export interface RedirectProblem {
|
|
169
|
+
rule: RedirectRuleName;
|
|
170
|
+
/** The rule's source path — the row the author has to look at. */
|
|
171
|
+
source: string;
|
|
172
|
+
/** One line, naming the rule and the path it walks. */
|
|
173
|
+
message: string;
|
|
174
|
+
/** Why it costs something, and what to do instead. */
|
|
175
|
+
detail: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The rule a request for `path` would hit: a literal source first, then the first matching pattern. */
|
|
179
|
+
function ruleFor(rules: readonly RedirectRule[], path: string): RedirectRule | undefined {
|
|
180
|
+
const target = normalizePath(path);
|
|
181
|
+
return (
|
|
182
|
+
rules.find((rule) => !isPattern(rule.source) && normalizePath(rule.source) === target) ??
|
|
183
|
+
rules.find((rule) => isPattern(rule.source) && matchesPattern(rule.source, target))
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Follow destinations from a rule until they stop resolving. Returns the path walked. */
|
|
188
|
+
function walk(
|
|
189
|
+
rules: readonly RedirectRule[],
|
|
190
|
+
start: RedirectRule,
|
|
191
|
+
): { path: string[]; looped: boolean } {
|
|
192
|
+
const path = [start.source];
|
|
193
|
+
const seen = new Set([normalizePath(start.source)]);
|
|
194
|
+
let current = start;
|
|
195
|
+
for (;;) {
|
|
196
|
+
if (isPattern(current.destination) || isExternal(current.destination)) {
|
|
197
|
+
return { looped: false, path };
|
|
198
|
+
}
|
|
199
|
+
const next = ruleFor(rules, current.destination);
|
|
200
|
+
path.push(current.destination);
|
|
201
|
+
if (!next) {
|
|
202
|
+
return { looped: false, path };
|
|
203
|
+
}
|
|
204
|
+
const key = normalizePath(next.source);
|
|
205
|
+
if (seen.has(key)) {
|
|
206
|
+
return { looped: true, path };
|
|
207
|
+
}
|
|
208
|
+
seen.add(key);
|
|
209
|
+
current = next;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Every chain, loop and shadow in a rule set.
|
|
215
|
+
*
|
|
216
|
+
* `routes` are the site's real routes with their file-based spelling (`/blog/[slug]`); they are
|
|
217
|
+
* translated by {@link routePattern} here so callers hand over what the project actually has rather
|
|
218
|
+
* than a pre-massaged list. An empty `routes` produces no shadow findings — which is honest only
|
|
219
|
+
* because the caller knows whether it enumerated them; a caller that could not read `pages/` must
|
|
220
|
+
* say so rather than pass `[]` and let this report a clean bill.
|
|
221
|
+
*/
|
|
222
|
+
export function validateRedirects(
|
|
223
|
+
rules: readonly RedirectRule[],
|
|
224
|
+
routes: readonly string[],
|
|
225
|
+
): RedirectProblem[] {
|
|
226
|
+
const problems: RedirectProblem[] = [];
|
|
227
|
+
const reportedLoops = new Set<string>();
|
|
228
|
+
|
|
229
|
+
for (const rule of rules) {
|
|
230
|
+
const { looped, path } = walk(rules, rule);
|
|
231
|
+
if (looped) {
|
|
232
|
+
// One cycle, one Problem: every rule on it walks the same ring, and three copies of the same
|
|
233
|
+
// Finding is three things to fix where there is one.
|
|
234
|
+
const ring = [...new Set(path.map((step) => normalizePath(step)))].toSorted().join(">");
|
|
235
|
+
if (!reportedLoops.has(ring)) {
|
|
236
|
+
reportedLoops.add(ring);
|
|
237
|
+
problems.push({
|
|
238
|
+
detail:
|
|
239
|
+
`Following the rules from ${rule.source} returns to a source already visited, so a ` +
|
|
240
|
+
`request for it never reaches a page. Break the cycle by pointing one of these rules ` +
|
|
241
|
+
`at a real route.`,
|
|
242
|
+
// The walk already closes the ring — its last step IS the source it returned to.
|
|
243
|
+
message: `Redirect loop: ${path.join(" → ")}`,
|
|
244
|
+
rule: "loop",
|
|
245
|
+
source: rule.source,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
} else if (path.length > 2) {
|
|
249
|
+
problems.push({
|
|
250
|
+
detail:
|
|
251
|
+
`Each hop is a separate request and is cached separately, so a visitor pays for the ` +
|
|
252
|
+
`whole chain and a correction to the last hop can be masked by a cached earlier one. ` +
|
|
253
|
+
`Point ${rule.source} straight at ${path.at(-1)}.`,
|
|
254
|
+
message: `Redirect chain: ${path.join(" → ")}`,
|
|
255
|
+
rule: "chain",
|
|
256
|
+
source: rule.source,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
for (const rule of rules) {
|
|
262
|
+
const covered = routes.filter((route) =>
|
|
263
|
+
isPattern(rule.source)
|
|
264
|
+
? matchesPattern(rule.source, routePattern(route))
|
|
265
|
+
: matchesPattern(routePattern(route), rule.source),
|
|
266
|
+
);
|
|
267
|
+
const [first] = covered;
|
|
268
|
+
if (first === undefined) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
problems.push({
|
|
272
|
+
detail:
|
|
273
|
+
`This project has a page at ${first}${covered.length > 1 ? ` (and ${covered.length - 1} more)` : ""}. ` +
|
|
274
|
+
`A host that serves static files before consulting _redirects answers with the page and ` +
|
|
275
|
+
`this rule never fires; the build meanwhile writes its meta-refresh file over that page in ` +
|
|
276
|
+
`dist/ and warns (site-architecture.md §11.1). Remove the rule or the page.`,
|
|
277
|
+
message: `Redirect shadowed by a page: ${rule.source}`,
|
|
278
|
+
rule: "shadow",
|
|
279
|
+
source: rule.source,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return problems;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ─── Import ───────────────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
export interface RedirectImport {
|
|
289
|
+
/** Which reader ran. Reported to the author, because the sniff can be wrong. */
|
|
290
|
+
format: "_redirects" | "csv";
|
|
291
|
+
rules: RedirectRule[];
|
|
292
|
+
/** One line per input line that could not be read. Never silently dropped. */
|
|
293
|
+
errors: string[];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** A status token, tolerating Netlify's forcing `!` suffix. Null when it is not a status at all. */
|
|
297
|
+
function parseStatus(token: string | undefined): RedirectTarget | null {
|
|
298
|
+
if (token === undefined || token === "") {
|
|
299
|
+
return DEFAULT_REDIRECT_STATUS;
|
|
300
|
+
}
|
|
301
|
+
const raw = token.trim().replace(/!$/, "");
|
|
302
|
+
// An imported `_redirects` writes a rewrite as 200, and a pasted CSV may say so in words.
|
|
303
|
+
if (raw.toLowerCase() === REWRITE || Number(raw) === REWRITE_WIRE_STATUS) {
|
|
304
|
+
return REWRITE;
|
|
305
|
+
}
|
|
306
|
+
const value = Number(raw);
|
|
307
|
+
return (REDIRECT_STATUSES as readonly number[]).includes(value)
|
|
308
|
+
? (value as RedirectTarget)
|
|
309
|
+
: null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Netlify/Cloudflare `_redirects`: `source destination [status]`, `#` comments, blank lines. */
|
|
313
|
+
export function parseRedirectsFile(text: string): RedirectImport {
|
|
314
|
+
const rules: RedirectRule[] = [];
|
|
315
|
+
const errors: string[] = [];
|
|
316
|
+
for (const [index, raw] of text.split(/\r?\n/).entries()) {
|
|
317
|
+
const line = raw.trim();
|
|
318
|
+
if (line === "" || line.startsWith("#")) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const [source, destination, status] = line.split(/\s+/);
|
|
322
|
+
if (!source || !destination) {
|
|
323
|
+
errors.push(`Line ${index + 1}: "${line}" — expected "source destination [status]".`);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const code = parseStatus(status);
|
|
327
|
+
if (code === null) {
|
|
328
|
+
errors.push(`Line ${index + 1}: "${status}" is not an HTTP status.`);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
rules.push({ destination, source, status: code });
|
|
332
|
+
}
|
|
333
|
+
return { errors, format: "_redirects", rules };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Header spellings each column answers to. First match wins, left to right. */
|
|
337
|
+
const CSV_HEADERS: Readonly<Record<"source" | "destination" | "status", readonly string[]>> = {
|
|
338
|
+
destination: ["destination", "to", "new", "target"],
|
|
339
|
+
source: ["source", "from", "old", "path"],
|
|
340
|
+
status: ["status", "code"],
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
/** CSV with a `source,destination[,status]` header, or the same three columns positionally. */
|
|
344
|
+
export function parseRedirectsCsv(text: string): RedirectImport {
|
|
345
|
+
const doc = parseCsv(text);
|
|
346
|
+
const errors: string[] = [];
|
|
347
|
+
const rules: RedirectRule[] = [];
|
|
348
|
+
const headers = doc.headers.map((header) => header.trim().toLowerCase());
|
|
349
|
+
const indexOf = (names: readonly string[]) => headers.findIndex((h) => names.includes(h));
|
|
350
|
+
|
|
351
|
+
const sourceAt = indexOf(CSV_HEADERS.source);
|
|
352
|
+
const destinationAt = indexOf(CSV_HEADERS.destination);
|
|
353
|
+
const headed = sourceAt !== -1 && destinationAt !== -1;
|
|
354
|
+
// No recognizable header means the first record is data, not names — the two-column paste.
|
|
355
|
+
const records = headed ? doc.rows : [doc.headers, ...doc.rows];
|
|
356
|
+
const columns = headed
|
|
357
|
+
? { destination: destinationAt, source: sourceAt, status: indexOf(CSV_HEADERS.status) }
|
|
358
|
+
: { destination: 1, source: 0, status: 2 };
|
|
359
|
+
|
|
360
|
+
for (const [index, record] of records.entries()) {
|
|
361
|
+
const source = (record[columns.source] ?? "").trim();
|
|
362
|
+
const destination = (record[columns.destination] ?? "").trim();
|
|
363
|
+
if (source === "" && destination === "") {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const line = headed ? index + 2 : index + 1;
|
|
367
|
+
if (source === "" || destination === "") {
|
|
368
|
+
errors.push(`Row ${line}: both a source and a destination are required.`);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const code = parseStatus(
|
|
372
|
+
columns.status === -1 ? undefined : (record[columns.status] ?? "").trim(),
|
|
373
|
+
);
|
|
374
|
+
if (code === null) {
|
|
375
|
+
errors.push(
|
|
376
|
+
`Row ${line}: "${record[columns.status]}" is not one of ${REDIRECT_TARGETS.join(", ")}.`,
|
|
377
|
+
);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
rules.push({ destination, source, status: code });
|
|
381
|
+
}
|
|
382
|
+
return { errors, format: "csv", rules };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Read pasted text as whichever format it looks like.
|
|
387
|
+
*
|
|
388
|
+
* The sniff is one rule — a comma on the first content line means CSV — and the result SAYS which
|
|
389
|
+
* reader ran, because a `_redirects` line with a comma in a query string would be read as CSV and
|
|
390
|
+
* the author is the only one who can tell that the answer is wrong.
|
|
391
|
+
*/
|
|
392
|
+
export function parseRedirectImport(text: string): RedirectImport {
|
|
393
|
+
const first = text
|
|
394
|
+
.split(/\r?\n/)
|
|
395
|
+
.map((line) => line.trim())
|
|
396
|
+
.find((line) => line !== "" && !line.startsWith("#"));
|
|
397
|
+
return first?.includes(",") ? parseRedirectsCsv(text) : parseRedirectsFile(text);
|
|
398
|
+
}
|
|
@@ -11,10 +11,24 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Collections type their columns from the content-type schema (plus inferred extras found in the
|
|
13
13
|
* data); the pages grid is fully inference-driven.
|
|
14
|
+
*
|
|
15
|
+
* **A collection listing obeys the draft perspective (§7.6).** `content/draft-state.ts` owns both
|
|
16
|
+
* the one flag and the one definition of what a draft is, and this source calls it rather than
|
|
17
|
+
* testing `draft === true` itself — which is what makes "including drafts" mean the same thing here
|
|
18
|
+
* as it does on the tab chip. It can, because that module imports nothing but `reactivity`: the
|
|
19
|
+
* richer `content/entry-model.ts` asks THIS file where a collection's entries live, so an edge back
|
|
20
|
+
* to it would be the cycle `import/no-cycle` rejects.
|
|
14
21
|
*/
|
|
15
22
|
import { getPlatform } from "../../platform";
|
|
16
23
|
import { projectState } from "../../store";
|
|
17
24
|
import { workspace } from "../../workspace/workspace";
|
|
25
|
+
import {
|
|
26
|
+
DRAFT_FIELD,
|
|
27
|
+
applyDraftFilter,
|
|
28
|
+
hasDraftAxis,
|
|
29
|
+
includingDrafts,
|
|
30
|
+
} from "../../content/draft-state";
|
|
31
|
+
import { getGridController } from "../grid-controller";
|
|
18
32
|
import { isRecentLocal, markLocalMutation } from "../../files/fs-events";
|
|
19
33
|
import {
|
|
20
34
|
defaultContentFormat,
|
|
@@ -192,10 +206,55 @@ interface EntryFileSourceOptions {
|
|
|
192
206
|
insertFormatName: (path: string) => string | null;
|
|
193
207
|
/** Inferred columns to front-load, in order (e.g. title/description for pages). */
|
|
194
208
|
priorityFields?: string[];
|
|
209
|
+
/**
|
|
210
|
+
* Whether this listing is subject to the draft perspective (§7.6) — collections are, the pages
|
|
211
|
+
* tree is not. A draft is a property of a content ENTRY: a page carrying `draft: true` would
|
|
212
|
+
* vanish from the only tree that lists it, with no collection schema to explain why.
|
|
213
|
+
*/
|
|
214
|
+
drafts?: boolean;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The live sources that obey the draft perspective.
|
|
219
|
+
*
|
|
220
|
+
* A `WeakSet` of the source objects rather than a list of tab-id prefixes: whether a listing is
|
|
221
|
+
* draft filtered is decided by one `drafts: true` in this file, and re-deriving it from
|
|
222
|
+
* `"grid://collection/"` in {@link reloadDraftAwareGrids} would be a second answer free to drift.
|
|
223
|
+
* Membership is added by the engine off that same option, so the two cannot disagree. Weak, so a
|
|
224
|
+
* closed tab's source is collectable.
|
|
225
|
+
*/
|
|
226
|
+
const draftAwareSources = new WeakSet<GridSource>();
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Reload every open grid whose rows depend on the draft perspective.
|
|
230
|
+
*
|
|
231
|
+
* The perspective is a flag, and a grid holds its rows in controller state that was loaded once —
|
|
232
|
+
* so `content.setIncludeDrafts` changing the flag has to say so. `load()` (not `refresh()`) is the
|
|
233
|
+
* right verb: it re-pulls columns and rows and leaves the edit buffer alone, whereas `refresh()`
|
|
234
|
+
* would stop to ask an author with pending edits whether to discard them, which is not a question a
|
|
235
|
+
* change of perspective should provoke.
|
|
236
|
+
*/
|
|
237
|
+
export async function reloadDraftAwareGrids(): Promise<void> {
|
|
238
|
+
const reloads: Promise<void>[] = [];
|
|
239
|
+
for (const tab of workspace.tabs.values()) {
|
|
240
|
+
const controller = getGridController(tab);
|
|
241
|
+
if (controller && draftAwareSources.has(controller.source)) {
|
|
242
|
+
reloads.push(controller.load());
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
await Promise.all(reloads);
|
|
195
246
|
}
|
|
196
247
|
|
|
197
248
|
/** Shared engine for frontmatter-file sources (collections, pages). */
|
|
198
249
|
function createEntryFileSource(opts: EntryFileSourceOptions): GridSource {
|
|
250
|
+
const source = buildEntryFileSource(opts);
|
|
251
|
+
if (opts.drafts) {
|
|
252
|
+
draftAwareSources.add(source);
|
|
253
|
+
}
|
|
254
|
+
return source;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function buildEntryFileSource(opts: EntryFileSourceOptions): GridSource {
|
|
199
258
|
const entries = new Map<string, EntryRecord>();
|
|
200
259
|
let loadPromise: Promise<void> | null = null;
|
|
201
260
|
let columnsCache: GridColumn[] | null = null;
|
|
@@ -268,10 +327,46 @@ function createEntryFileSource(opts: EntryFileSourceOptions): GridSource {
|
|
|
268
327
|
extraColumns.sort((a, b) => rank(a) - rank(b));
|
|
269
328
|
}
|
|
270
329
|
|
|
271
|
-
columnsCache = [pathColumn, ...schemaColumns, ...extraColumns];
|
|
330
|
+
columnsCache = withDraftColumn([pathColumn, ...schemaColumns, ...extraColumns]);
|
|
272
331
|
return columnsCache;
|
|
273
332
|
};
|
|
274
333
|
|
|
334
|
+
/** Whether this listing has a draft axis at all — the schema declares one, or an entry claims one. */
|
|
335
|
+
const draftAxis = (): boolean => {
|
|
336
|
+
if (!opts.drafts) {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
const schema = opts.schema();
|
|
340
|
+
return (
|
|
341
|
+
hasDraftAxis(schema) ||
|
|
342
|
+
[...entries.values()].some((entry) => hasDraftAxis(schema, entry.frontmatter))
|
|
343
|
+
);
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Put the Draft column immediately after Path, when this collection has a draft axis.
|
|
348
|
+
*
|
|
349
|
+
* A MOVE, and deliberately not a synthesis: wherever there is an axis there is already a column.
|
|
350
|
+
* A schema that declares `draft` gave `columnsFromSchema` one, and an entry that carries the
|
|
351
|
+
* field without the schema declaring it gave `inferColumnsFromRows` one — the two conditions of
|
|
352
|
+
* {@link hasDraftAxis} are exactly the two conditions that produce a column. Building a third here
|
|
353
|
+
* would be an unreachable branch pretending to be a safety net.
|
|
354
|
+
*
|
|
355
|
+
* What the move buys is that the one field deciding whether an entry appears in its own list is
|
|
356
|
+
* not sitting out at column nine, where declaration order or first-sight order happened to put
|
|
357
|
+
* it. A collection whose entries only ever say `draft: false` has NO axis and is left alone: the
|
|
358
|
+
* column is ordinary data there, and promoting it would invent a workflow the project never
|
|
359
|
+
* declared.
|
|
360
|
+
*/
|
|
361
|
+
const withDraftColumn = (columns: GridColumn[]): GridColumn[] => {
|
|
362
|
+
const index = columns.findIndex((column) => column.field === DRAFT_FIELD);
|
|
363
|
+
if (index < 1 || !draftAxis()) {
|
|
364
|
+
return columns;
|
|
365
|
+
}
|
|
366
|
+
const rest = columns.filter((column) => column.field !== DRAFT_FIELD);
|
|
367
|
+
return [rest[0]!, columns[index]!, ...rest.slice(1)];
|
|
368
|
+
};
|
|
369
|
+
|
|
275
370
|
const rowFor = (path: string, entry: EntryRecord): GridRow => {
|
|
276
371
|
const cells: Record<string, GridCellValue> = { [PATH_FIELD]: path };
|
|
277
372
|
for (const column of buildColumns()) {
|
|
@@ -468,7 +563,17 @@ function createEntryFileSource(opts: EntryFileSourceOptions): GridSource {
|
|
|
468
563
|
|
|
469
564
|
async rows(): Promise<GridRowsResult> {
|
|
470
565
|
await load();
|
|
471
|
-
|
|
566
|
+
// The perspective is applied to the LISTING, not the load: `entries` keeps every parsed file,
|
|
567
|
+
// So the Draft column still knows the collection has an axis while its drafts are hidden, and
|
|
568
|
+
// Turning the perspective back on is a re-list rather than a re-read of the directory.
|
|
569
|
+
const listed = opts.drafts
|
|
570
|
+
? applyDraftFilter(
|
|
571
|
+
[...entries.entries()],
|
|
572
|
+
([, entry]) => entry.frontmatter,
|
|
573
|
+
includingDrafts(),
|
|
574
|
+
)
|
|
575
|
+
: [...entries.entries()];
|
|
576
|
+
const rows = listed.map(([path, entry]) => rowFor(path, entry));
|
|
472
577
|
return { rows, total: rows.length };
|
|
473
578
|
},
|
|
474
579
|
};
|
|
@@ -485,6 +590,7 @@ export function createCollectionSource(typeName: string): GridSource {
|
|
|
485
590
|
};
|
|
486
591
|
|
|
487
592
|
return createEntryFileSource({
|
|
593
|
+
drafts: true,
|
|
488
594
|
id: makeGridTabId({ kind: "collection", name: typeName }),
|
|
489
595
|
insertFormatName: (path) => {
|
|
490
596
|
const info = requireInfo();
|