pixelweaver 0.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/.env.development +1 -0
- package/.github/workflows/ci.yml +22 -0
- package/.github/workflows/publish.yml +18 -0
- package/.prettierignore +5 -0
- package/.prettierrc +16 -0
- package/.python-version +1 -0
- package/.rlsbl/bases/.github/workflows/ci.yml +21 -0
- package/.rlsbl/bases/.github/workflows/publish.yml +18 -0
- package/.rlsbl/bases/.rlsbl/changes/unreleased.jsonl +0 -0
- package/.rlsbl/bases/.rlsbl/hooks/post-release.sh +8 -0
- package/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +5 -0
- package/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +8 -0
- package/.rlsbl/bases/.rlsbl/lint/go.toml +17 -0
- package/.rlsbl/bases/.rlsbl/lint/npm.toml +19 -0
- package/.rlsbl/bases/.rlsbl/lint/python.toml +25 -0
- package/.rlsbl/bases/CHANGELOG.md +5 -0
- package/.rlsbl/bases/LICENSE +21 -0
- package/.rlsbl/changes/.validated +1 -0
- package/.rlsbl/changes/0.1.0.jsonl +85 -0
- package/.rlsbl/changes/0.1.0.md +13 -0
- package/.rlsbl/changes/unreleased.jsonl +0 -0
- package/.rlsbl/config.json +6 -0
- package/.rlsbl/hashes.json +14 -0
- package/.rlsbl/hooks/post-release.sh +8 -0
- package/.rlsbl/hooks/pre-checks.sh +5 -0
- package/.rlsbl/hooks/pre-release.sh +8 -0
- package/.rlsbl/lint/go.toml +17 -0
- package/.rlsbl/lint/npm.toml +19 -0
- package/.rlsbl/lint/python.toml +25 -0
- package/.rlsbl/releases/unreleased.toml +0 -0
- package/.rlsbl/releases/v0.1.0.toml +3 -0
- package/.rlsbl/version +1 -0
- package/.selfdoc/hashes/hashes.json +146 -0
- package/.strictcli/schema.json +227 -0
- package/CHANGELOG.md +17 -0
- package/CLAUDE.md +100 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/assets/icon.png +0 -0
- package/docs/_README.md +117 -0
- package/docs/cli-config.md +35 -0
- package/docs/cli-dev.md +21 -0
- package/docs/cli-diagnose.md +12 -0
- package/docs/cli-index.md +30 -0
- package/docs/cli-list.md +18 -0
- package/docs/cli-mcp.md +18 -0
- package/docs/cli-new.md +26 -0
- package/docs/cli-open.md +21 -0
- package/docs/cli-serve.md +21 -0
- package/docs/cli-stop.md +12 -0
- package/docs/gen-index.md +36 -0
- package/docs/index.md +13 -0
- package/docs/server-src-pixelweaver-__main__.md +12 -0
- package/docs/server-src-pixelweaver-autosave.md +12 -0
- package/docs/server-src-pixelweaver-bridge.md +12 -0
- package/docs/server-src-pixelweaver-cli.md +12 -0
- package/docs/server-src-pixelweaver-config.md +12 -0
- package/docs/server-src-pixelweaver-connections.md +12 -0
- package/docs/server-src-pixelweaver-main.md +12 -0
- package/docs/server-src-pixelweaver-mcp_bridge.md +12 -0
- package/docs/server-src-pixelweaver-mcp_drawing_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_export_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_frame_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_history_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_layer_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_lock.md +12 -0
- package/docs/server-src-pixelweaver-mcp_project_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_read_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_registry.md +12 -0
- package/docs/server-src-pixelweaver-mcp_resources.md +12 -0
- package/docs/server-src-pixelweaver-mcp_server.md +12 -0
- package/docs/server-src-pixelweaver-protocol.md +12 -0
- package/docs/server-src-pixelweaver-state.md +12 -0
- package/docs/server-src-pixelweaver-storage.md +12 -0
- package/docs/server-src-pixelweaver-websocket.md +12 -0
- package/docs/server-src-pixelweaver.md +12 -0
- package/e2e/app-launch.test.ts +35 -0
- package/e2e/menus.test.ts +26 -0
- package/e2e/tools.test.ts +27 -0
- package/e2e/undo-redo.test.ts +11 -0
- package/eslint.config.js +62 -0
- package/index.html +13 -0
- package/package.json +48 -0
- package/playwright.config.ts +19 -0
- package/plugins/builtin/.gitkeep +0 -0
- package/plugins/builtin/advanced-fill-tool.ts +146 -0
- package/plugins/builtin/circle-tool.ts +186 -0
- package/plugins/builtin/diamond-tool.ts +182 -0
- package/plugins/builtin/dither-tool.ts +186 -0
- package/plugins/builtin/drawing-primitives-plugin.ts +362 -0
- package/plugins/builtin/drawing-utils.test.ts +495 -0
- package/plugins/builtin/drawing-utils.ts +431 -0
- package/plugins/builtin/effects/blur.ts +97 -0
- package/plugins/builtin/effects/color-effects.ts +278 -0
- package/plugins/builtin/effects/flip.ts +83 -0
- package/plugins/builtin/effects/glow.ts +118 -0
- package/plugins/builtin/effects/outline.ts +110 -0
- package/plugins/builtin/effects/rotate.ts +357 -0
- package/plugins/builtin/effects/scale.ts +258 -0
- package/plugins/builtin/effects/shadow.ts +111 -0
- package/plugins/builtin/effects/sharpen.ts +102 -0
- package/plugins/builtin/effects.test.ts +715 -0
- package/plugins/builtin/eraser-tool.ts +23 -0
- package/plugins/builtin/eyedropper-tool.ts +83 -0
- package/plugins/builtin/fill-tool.ts +93 -0
- package/plugins/builtin/gradient-tool.ts +204 -0
- package/plugins/builtin/gradient.test.ts +142 -0
- package/plugins/builtin/importers/aseprite-importer-plugin.ts +174 -0
- package/plugins/builtin/importers/aseprite-parser.ts +497 -0
- package/plugins/builtin/importers/piskel-importer-plugin.ts +222 -0
- package/plugins/builtin/importers/sky-spec-plugin.ts +409 -0
- package/plugins/builtin/line-tool.ts +267 -0
- package/plugins/builtin/make-stroke-tool.ts +271 -0
- package/plugins/builtin/noise-dither.test.ts +151 -0
- package/plugins/builtin/noise-tool.ts +131 -0
- package/plugins/builtin/pattern-stamp-tool.ts +162 -0
- package/plugins/builtin/pencil-tool.ts +25 -0
- package/plugins/builtin/rect-tool.ts +179 -0
- package/plugins/builtin/selection-tool.ts +388 -0
- package/plugins/builtin/selection.test.ts +195 -0
- package/plugins/builtin/tool-plugins.test.ts +529 -0
- package/public/favicon.svg +7 -0
- package/public/sw.js +91 -0
- package/pyproject.toml +49 -0
- package/scripts/eslint-wave-a-list.sh +24 -0
- package/scripts/eslint-wave-a-status.sh +25 -0
- package/scripts/fix-index-signature-access.py +127 -0
- package/scripts/fix-unchecked-index.py +128 -0
- package/scripts/fix-unchecked-vars.py +127 -0
- package/scripts/fix-wave-a-bangs.py +36 -0
- package/scripts/fix-wave-a-templates.py +108 -0
- package/scripts/fix-wave-b-void-expr.py +167 -0
- package/scripts/generate-command-params.py +540 -0
- package/scripts/migrate-single-frame-to-multi.py +171 -0
- package/scripts/smoke-test.sh +77 -0
- package/selfdoc.json +10 -0
- package/server/README.md +0 -0
- package/server/src/pixelweaver/__init__.py +1 -0
- package/server/src/pixelweaver/__main__.py +4 -0
- package/server/src/pixelweaver/autosave.py +114 -0
- package/server/src/pixelweaver/bridge.py +127 -0
- package/server/src/pixelweaver/cli.py +199 -0
- package/server/src/pixelweaver/config.py +24 -0
- package/server/src/pixelweaver/connections.py +54 -0
- package/server/src/pixelweaver/main.py +271 -0
- package/server/src/pixelweaver/mcp_bridge.py +189 -0
- package/server/src/pixelweaver/mcp_drawing_tools.py +178 -0
- package/server/src/pixelweaver/mcp_export_tools.py +291 -0
- package/server/src/pixelweaver/mcp_frame_tools.py +423 -0
- package/server/src/pixelweaver/mcp_history_tools.py +106 -0
- package/server/src/pixelweaver/mcp_layer_tools.py +64 -0
- package/server/src/pixelweaver/mcp_lock.py +37 -0
- package/server/src/pixelweaver/mcp_project_tools.py +302 -0
- package/server/src/pixelweaver/mcp_read_tools.py +163 -0
- package/server/src/pixelweaver/mcp_registry.py +247 -0
- package/server/src/pixelweaver/mcp_resources.py +312 -0
- package/server/src/pixelweaver/mcp_server.py +234 -0
- package/server/src/pixelweaver/protocol.py +219 -0
- package/server/src/pixelweaver/state.py +267 -0
- package/server/src/pixelweaver/storage.py +293 -0
- package/server/src/pixelweaver/websocket.py +282 -0
- package/server/tests/__init__.py +0 -0
- package/server/tests/conftest.py +17 -0
- package/server/tests/test_api.py +96 -0
- package/server/tests/test_bridge.py +161 -0
- package/server/tests/test_health.py +9 -0
- package/server/tests/test_integration.py +86 -0
- package/server/tests/test_mcp_bridge.py +293 -0
- package/server/tests/test_mcp_lock.py +34 -0
- package/server/tests/test_mcp_registry.py +679 -0
- package/server/tests/test_mcp_resources.py +648 -0
- package/server/tests/test_protocol.py +125 -0
- package/server/tests/test_state.py +87 -0
- package/server/tests/test_storage.py +306 -0
- package/server/tests/test_websocket.py +275 -0
- package/src/App.svelte +107 -0
- package/src/app.css +215 -0
- package/src/lib/animation/AnimationPreviewPanel.svelte +667 -0
- package/src/lib/animation/animation-commands.test.ts +228 -0
- package/src/lib/animation/animation-commands.ts +540 -0
- package/src/lib/animation/animation-preview-panel-plugin.ts +25 -0
- package/src/lib/animation/animation-preview.svelte.ts +151 -0
- package/src/lib/animation/clipboard.ts +134 -0
- package/src/lib/animation/frame-model.svelte.ts +437 -0
- package/src/lib/animation/frame-model.test.ts +314 -0
- package/src/lib/animation/frame-selection.svelte.ts +77 -0
- package/src/lib/animation/frame-tags.svelte.ts +238 -0
- package/src/lib/animation/frame-tags.test.ts +136 -0
- package/src/lib/animation/import.test.ts +141 -0
- package/src/lib/animation/import.ts +112 -0
- package/src/lib/animation/spritesheet-export.test.ts +239 -0
- package/src/lib/animation/spritesheet-export.ts +314 -0
- package/src/lib/canvas/CanvasViewport.svelte +216 -0
- package/src/lib/canvas/canvas-init-plugin.ts +178 -0
- package/src/lib/canvas/canvas-renderer.ts +408 -0
- package/src/lib/canvas/canvas-state.svelte.ts +232 -0
- package/src/lib/canvas/canvas-state.test.ts +139 -0
- package/src/lib/canvas/input-handler.ts +221 -0
- package/src/lib/canvas/input-plugin.ts +150 -0
- package/src/lib/canvas/onion-skin.ts +94 -0
- package/src/lib/canvas/pixel-buffer.test.ts +249 -0
- package/src/lib/canvas/pixel-buffer.ts +151 -0
- package/src/lib/canvas/render-state.svelte.ts +18 -0
- package/src/lib/canvas/shape-preview-state.svelte.ts +36 -0
- package/src/lib/canvas/tile-mode.test.ts +53 -0
- package/src/lib/canvas/tile-mode.ts +92 -0
- package/src/lib/canvas/viewport-utils.ts +24 -0
- package/src/lib/canvas/zoom-utils.ts +31 -0
- package/src/lib/color/.gitkeep +0 -0
- package/src/lib/color/color-commands.ts +87 -0
- package/src/lib/color/color-state.svelte.ts +98 -0
- package/src/lib/color/color-state.test.ts +91 -0
- package/src/lib/color/color-utils.test.ts +220 -0
- package/src/lib/color/color-utils.ts +243 -0
- package/src/lib/color/palette-state.svelte.ts +127 -0
- package/src/lib/color/palette-state.test.ts +154 -0
- package/src/lib/color/palette.ts +79 -0
- package/src/lib/core/bootstrap.ts +66 -0
- package/src/lib/core/command-params.generated.ts +1549 -0
- package/src/lib/core/command-params.ts +20 -0
- package/src/lib/core/command-runner.ts +79 -0
- package/src/lib/core/commands.ts +134 -0
- package/src/lib/core/dispatcher.test.ts +548 -0
- package/src/lib/core/dispatcher.ts +361 -0
- package/src/lib/core/index.test.ts +7 -0
- package/src/lib/core/notification-state.svelte.ts +119 -0
- package/src/lib/core/plugin-api.ts +210 -0
- package/src/lib/core/plugin-discovery.test.ts +53 -0
- package/src/lib/core/plugin-discovery.ts +65 -0
- package/src/lib/core/plugin-loader.test.ts +159 -0
- package/src/lib/core/plugin-loader.ts +240 -0
- package/src/lib/core/plugin-types.ts +286 -0
- package/src/lib/core/registries.svelte.ts +74 -0
- package/src/lib/core/tool-options-state.svelte.ts +61 -0
- package/src/lib/dock/DockLayout.svelte +375 -0
- package/src/lib/dock/TabAddMenu.svelte +90 -0
- package/src/lib/dock/dock-persistence.ts +46 -0
- package/src/lib/dock/dock-plugin.ts +49 -0
- package/src/lib/dock/dock-presets.ts +156 -0
- package/src/lib/dock/dock-state.svelte.ts +77 -0
- package/src/lib/dock/dock-types.ts +2 -0
- package/src/lib/dock/dockview-theme.css +226 -0
- package/src/lib/dock/dockview-theme.ts +17 -0
- package/src/lib/dock/header-action-renderer.ts +77 -0
- package/src/lib/edit/clipboard-state.ts +34 -0
- package/src/lib/export/download.ts +201 -0
- package/src/lib/export/png-metadata.ts +181 -0
- package/src/lib/history/ActionLogPanel.svelte +418 -0
- package/src/lib/history/action-log-panel-plugin.ts +24 -0
- package/src/lib/history/action-log.svelte.ts +172 -0
- package/src/lib/history/action-log.test.ts +168 -0
- package/src/lib/history/history-commands.ts +403 -0
- package/src/lib/history/macros.svelte.ts +320 -0
- package/src/lib/history/macros.test.ts +224 -0
- package/src/lib/history/replay-engine.test.ts +241 -0
- package/src/lib/history/replay-engine.ts +149 -0
- package/src/lib/history/spec-format.test.ts +250 -0
- package/src/lib/history/spec-format.ts +210 -0
- package/src/lib/iso/SeamCheckerPanel.svelte +251 -0
- package/src/lib/iso/iso-math.test.ts +77 -0
- package/src/lib/iso/iso-math.ts +77 -0
- package/src/lib/iso/seam-checker-panel-plugin.ts +25 -0
- package/src/lib/iso/seam-checker.test.ts +126 -0
- package/src/lib/iso/seam-checker.ts +93 -0
- package/src/lib/iso/tessellation.ts +67 -0
- package/src/lib/layers/compositor.test.ts +193 -0
- package/src/lib/layers/compositor.ts +175 -0
- package/src/lib/layers/layer-commands.test.ts +263 -0
- package/src/lib/layers/layer-commands.ts +429 -0
- package/src/lib/layers/layer-tree.svelte.ts +516 -0
- package/src/lib/layers/layer-tree.test.ts +383 -0
- package/src/lib/layers/layer-types.ts +56 -0
- package/src/lib/leveleditor/LevelEditorViewport.svelte +1808 -0
- package/src/lib/leveleditor/MapPropertiesPanel.svelte +266 -0
- package/src/lib/leveleditor/TilePickerPanel.svelte +324 -0
- package/src/lib/leveleditor/depth-sort.test.ts +70 -0
- package/src/lib/leveleditor/depth-sort.ts +39 -0
- package/src/lib/leveleditor/level-editor-commands.ts +353 -0
- package/src/lib/leveleditor/level-editor-viewport-plugin.ts +25 -0
- package/src/lib/leveleditor/map-properties-panel-plugin.ts +25 -0
- package/src/lib/leveleditor/map-state.svelte.ts +372 -0
- package/src/lib/leveleditor/map-state.test.ts +243 -0
- package/src/lib/leveleditor/tile-picker-panel-plugin.ts +25 -0
- package/src/lib/leveleditor/tile-source-registry.svelte.ts +91 -0
- package/src/lib/leveleditor/tiled-export.test.ts +144 -0
- package/src/lib/leveleditor/tiled-export.ts +374 -0
- package/src/lib/pywebview.d.ts +15 -0
- package/src/lib/recovery/.gitkeep +0 -0
- package/src/lib/recovery/idb-store.ts +118 -0
- package/src/lib/recovery/recovery-manager.test.ts +321 -0
- package/src/lib/recovery/recovery-manager.ts +115 -0
- package/src/lib/recovery/recovery-plugin.ts +55 -0
- package/src/lib/recovery/recovery-state.svelte.ts +21 -0
- package/src/lib/recovery/sw-plugin.ts +18 -0
- package/src/lib/recovery/sw-register.ts +17 -0
- package/src/lib/save/directory-format.ts +42 -0
- package/src/lib/save/project-snapshot.ts +139 -0
- package/src/lib/save/recent-projects.ts +56 -0
- package/src/lib/save/save-state.svelte.ts +35 -0
- package/src/lib/save/storage.ts +167 -0
- package/src/lib/save/zip-format.ts +45 -0
- package/src/lib/shortcuts/.gitkeep +0 -0
- package/src/lib/shortcuts/ShortcutEditorPanel.svelte +690 -0
- package/src/lib/shortcuts/default-bindings.ts +61 -0
- package/src/lib/shortcuts/shortcut-editor-panel-plugin.ts +25 -0
- package/src/lib/shortcuts/shortcut-init.ts +380 -0
- package/src/lib/shortcuts/shortcut-manager.test.ts +466 -0
- package/src/lib/shortcuts/shortcut-manager.ts +281 -0
- package/src/lib/shortcuts/shortcut-state.svelte.ts +78 -0
- package/src/lib/shortcuts/shortcuts-plugin.ts +17 -0
- package/src/lib/sync/patch-applicator.ts +300 -0
- package/src/lib/sync/patch-types.ts +65 -0
- package/src/lib/sync/project-manager.ts +108 -0
- package/src/lib/sync/sync-init.ts +152 -0
- package/src/lib/sync/sync-plugin.ts +19 -0
- package/src/lib/sync/sync-state.svelte.ts +56 -0
- package/src/lib/sync/ws-client.test.ts +604 -0
- package/src/lib/sync/ws-client.ts +574 -0
- package/src/lib/tools/.gitkeep +0 -0
- package/src/lib/ui/.gitkeep +0 -0
- package/src/lib/ui/AboutDialog.svelte +113 -0
- package/src/lib/ui/ColorPicker.svelte +761 -0
- package/src/lib/ui/ContextMenu.svelte +216 -0
- package/src/lib/ui/ExportDialog.svelte +747 -0
- package/src/lib/ui/FrameStrip.svelte +854 -0
- package/src/lib/ui/LayerPanel.svelte +810 -0
- package/src/lib/ui/MenuBar.svelte +590 -0
- package/src/lib/ui/NewProjectDialog.svelte +803 -0
- package/src/lib/ui/PluginManagerPanel.svelte +475 -0
- package/src/lib/ui/PromptDialog.svelte +252 -0
- package/src/lib/ui/RecoveryDialog.svelte +295 -0
- package/src/lib/ui/ResizeDialog.svelte +416 -0
- package/src/lib/ui/StatusBar.svelte +145 -0
- package/src/lib/ui/ToolbarPanel.svelte +488 -0
- package/src/lib/ui/animation-menu-commands.ts +194 -0
- package/src/lib/ui/command-palette/CommandPalette.svelte +232 -0
- package/src/lib/ui/command-palette/command-palette-plugin.ts +30 -0
- package/src/lib/ui/command-palette/command-palette-state.svelte.ts +190 -0
- package/src/lib/ui/command-palette/command-palette.test.ts +129 -0
- package/src/lib/ui/dialog-state.svelte.ts +70 -0
- package/src/lib/ui/edit-commands.ts +271 -0
- package/src/lib/ui/file-commands.ts +275 -0
- package/src/lib/ui/file-open.ts +99 -0
- package/src/lib/ui/help-commands.ts +93 -0
- package/src/lib/ui/image-commands.ts +181 -0
- package/src/lib/ui/layer-menu-commands.ts +420 -0
- package/src/lib/ui/menu-builder.ts +224 -0
- package/src/lib/ui/notifications/NotificationBanner.svelte +137 -0
- package/src/lib/ui/notifications/notification-plugin.ts +29 -0
- package/src/lib/ui/notifications/notification-state.svelte.ts +9 -0
- package/src/lib/ui/plugin-manager-panel-plugin.ts +26 -0
- package/src/lib/ui/plugin-state.svelte.ts +62 -0
- package/src/lib/ui/select-commands.ts +75 -0
- package/src/lib/ui/theme-plugin.ts +18 -0
- package/src/lib/ui/theme.svelte.ts +45 -0
- package/src/lib/ui/theme.test.ts +51 -0
- package/src/lib/ui/toolbar-config.ts +90 -0
- package/src/lib/ui/toolbar-plugin.ts +39 -0
- package/src/lib/ui/view-commands.ts +629 -0
- package/src/lib/variants/BisectionExportDialog.svelte +500 -0
- package/src/lib/variants/VariantPanel.svelte +822 -0
- package/src/lib/variants/bisection-export.test.ts +113 -0
- package/src/lib/variants/bisection-export.ts +148 -0
- package/src/lib/variants/palette-extraction.test.ts +111 -0
- package/src/lib/variants/palette-extraction.ts +84 -0
- package/src/lib/variants/palette-interpolation.test.ts +113 -0
- package/src/lib/variants/palette-interpolation.ts +87 -0
- package/src/lib/variants/palette-swap.test.ts +101 -0
- package/src/lib/variants/palette-swap.ts +114 -0
- package/src/lib/variants/variant-commands.ts +594 -0
- package/src/lib/variants/variant-panel-plugin.ts +27 -0
- package/src/lib/variants/variant-randomizer.ts +101 -0
- package/src/lib/variants/variant-state.svelte.ts +166 -0
- package/src/lib/variants/variant-state.test.ts +138 -0
- package/src/main.ts +14 -0
- package/src/vite-env.d.ts +3 -0
- package/svelte.config.js +2 -0
- package/todo/.done/audit-design-decisions.md +812 -0
- package/todo/.done/audit-implementation-plan.md +1235 -0
- package/todo/.done/happy-path-polish.md +177 -0
- package/todo/.done/pixelweaver-full-build.md +937 -0
- package/todo/.done/server-multi-frame-design.md +405 -0
- package/todo/.done/typed-dispatcher-design.md +435 -0
- package/todo/.done/unified-toolbar-and-action-system.md +323 -0
- package/todo/.obsolete/comprehensive-audit-obsolete-items.md +33 -0
- package/todo/.obsolete/tauri-desktop-bundle.md +424 -0
- package/todo/comprehensive-audit.md +1085 -0
- package/tsconfig.app.json +26 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +20 -0
- package/uv.lock +1167 -0
- package/vite.config.ts +32 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Counts Wave A target rule violations (no-non-null-assertion + restrict-template-expressions)
|
|
3
|
+
# Usage: ./scripts/eslint-wave-a-status.sh [path...]
|
|
4
|
+
# Without args, checks src/ plugins/
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
PATHS=("$@")
|
|
7
|
+
if [ ${#PATHS[@]} -eq 0 ]; then
|
|
8
|
+
PATHS=(src/ plugins/)
|
|
9
|
+
fi
|
|
10
|
+
npx eslint "${PATHS[@]}" -f json 2>/dev/null | node -e '
|
|
11
|
+
let input = "";
|
|
12
|
+
process.stdin.on("data", d => input += d);
|
|
13
|
+
process.stdin.on("end", () => {
|
|
14
|
+
const data = JSON.parse(input);
|
|
15
|
+
let non = 0, tpl = 0, other = 0, total = 0;
|
|
16
|
+
for (const f of data) for (const m of f.messages) {
|
|
17
|
+
if (m.severity !== 2) continue;
|
|
18
|
+
total++;
|
|
19
|
+
if (m.ruleId === "@typescript-eslint/no-non-null-assertion") non++;
|
|
20
|
+
else if (m.ruleId === "@typescript-eslint/restrict-template-expressions") tpl++;
|
|
21
|
+
else other++;
|
|
22
|
+
}
|
|
23
|
+
console.log(`total=${total} non-null=${non} template=${tpl} other=${other}`);
|
|
24
|
+
});
|
|
25
|
+
'
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Apply targeted fixes for noPropertyAccessFromIndexSignature (TS4111) errors.
|
|
4
|
+
|
|
5
|
+
TS4111 format:
|
|
6
|
+
path(line,col): error TS4111: Property 'foo' comes from an index
|
|
7
|
+
signature, so it must be accessed with ['foo'].
|
|
8
|
+
|
|
9
|
+
At the reported column in the source line, we expect either:
|
|
10
|
+
- `.foo` -> replace with `["foo"]`
|
|
11
|
+
- `?.foo` -> replace with `?.["foo"]` (optional chaining form)
|
|
12
|
+
|
|
13
|
+
The column from tsc points to the start of the property name `foo`, so we
|
|
14
|
+
look back to see if the preceding character is `.`. If so we rewrite it.
|
|
15
|
+
We process columns right-to-left on the same line so earlier edits don't
|
|
16
|
+
shift later positions.
|
|
17
|
+
|
|
18
|
+
Usage: scripts/fix-index-signature-access.py <tsc-errors.log> [--apply]
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from collections import defaultdict
|
|
26
|
+
|
|
27
|
+
ERROR_LINE_RE = re.compile(
|
|
28
|
+
r"^(?P<path>[^(]+)\((?P<line>\d+),(?P<col>\d+)\): error TS4111: "
|
|
29
|
+
r"Property '(?P<prop>[^']+)' comes from an index signature"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def parse_errors(log_path: Path) -> dict[Path, list[tuple[int, int, str]]]:
|
|
34
|
+
errs: dict[Path, list[tuple[int, int, str]]] = defaultdict(list)
|
|
35
|
+
for raw in log_path.read_text().splitlines():
|
|
36
|
+
m = ERROR_LINE_RE.match(raw)
|
|
37
|
+
if not m:
|
|
38
|
+
continue
|
|
39
|
+
path = Path(m.group("path"))
|
|
40
|
+
errs[path].append((int(m.group("line")), int(m.group("col")), m.group("prop")))
|
|
41
|
+
return errs
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def fix_line(line: str, col: int, prop: str) -> str | None:
|
|
45
|
+
"""
|
|
46
|
+
col is 1-based and points to the first character of `prop`.
|
|
47
|
+
We look at line[col-2] which should be `.`.
|
|
48
|
+
If the char before that is also `?` (optional chain), handle `?.foo`.
|
|
49
|
+
Replace with `["prop"]` or `?.["prop"]` (keep the ? chain).
|
|
50
|
+
"""
|
|
51
|
+
# Convert to 0-based
|
|
52
|
+
start = col - 1
|
|
53
|
+
end = start + len(prop)
|
|
54
|
+
if end > len(line):
|
|
55
|
+
return None
|
|
56
|
+
if line[start:end] != prop:
|
|
57
|
+
# The column may be slightly off; try a nearby match
|
|
58
|
+
return None
|
|
59
|
+
if start == 0 or line[start - 1] != ".":
|
|
60
|
+
return None
|
|
61
|
+
# Check for optional chaining ?.
|
|
62
|
+
if start >= 2 and line[start - 2] == "?":
|
|
63
|
+
# `?.foo` -> `?.["foo"]`
|
|
64
|
+
prefix = line[: start - 2]
|
|
65
|
+
suffix = line[end:]
|
|
66
|
+
return prefix + '?.["' + prop + '"]' + suffix
|
|
67
|
+
else:
|
|
68
|
+
# `.foo` -> `["foo"]`
|
|
69
|
+
prefix = line[: start - 1]
|
|
70
|
+
suffix = line[end:]
|
|
71
|
+
return prefix + '["' + prop + '"]' + suffix
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def main() -> int:
|
|
75
|
+
if len(sys.argv) < 2:
|
|
76
|
+
print(__doc__)
|
|
77
|
+
return 1
|
|
78
|
+
log = Path(sys.argv[1])
|
|
79
|
+
apply = "--apply" in sys.argv[2:]
|
|
80
|
+
errs = parse_errors(log)
|
|
81
|
+
total_fixed = 0
|
|
82
|
+
total_files = 0
|
|
83
|
+
total_skipped = 0
|
|
84
|
+
for path, positions in errs.items():
|
|
85
|
+
if not path.exists():
|
|
86
|
+
continue
|
|
87
|
+
lines = path.read_text().splitlines(keepends=True)
|
|
88
|
+
# Sort positions: by line ascending, then col DESCENDING so that
|
|
89
|
+
# replacements on the same line don't shift earlier columns.
|
|
90
|
+
positions.sort(key=lambda p: (p[0], -p[1]))
|
|
91
|
+
file_fixed = 0
|
|
92
|
+
file_skipped = 0
|
|
93
|
+
for lineno, col, prop in positions:
|
|
94
|
+
idx = lineno - 1
|
|
95
|
+
if idx >= len(lines):
|
|
96
|
+
continue
|
|
97
|
+
original = lines[idx]
|
|
98
|
+
eol = ""
|
|
99
|
+
if original.endswith("\r\n"):
|
|
100
|
+
eol = "\r\n"
|
|
101
|
+
content = original[:-2]
|
|
102
|
+
elif original.endswith("\n"):
|
|
103
|
+
eol = "\n"
|
|
104
|
+
content = original[:-1]
|
|
105
|
+
else:
|
|
106
|
+
content = original
|
|
107
|
+
new = fix_line(content, col, prop)
|
|
108
|
+
if new is not None and new != content:
|
|
109
|
+
lines[idx] = new + eol
|
|
110
|
+
file_fixed += 1
|
|
111
|
+
else:
|
|
112
|
+
file_skipped += 1
|
|
113
|
+
if file_fixed:
|
|
114
|
+
total_fixed += file_fixed
|
|
115
|
+
total_files += 1
|
|
116
|
+
if apply:
|
|
117
|
+
path.write_text("".join(lines))
|
|
118
|
+
print(f"{'FIX' if apply else 'DRY'} {path}: {file_fixed} line(s), {file_skipped} skipped")
|
|
119
|
+
elif file_skipped:
|
|
120
|
+
print(f"SKIP {path}: {file_skipped} skipped")
|
|
121
|
+
total_skipped += file_skipped
|
|
122
|
+
print(f"Total: {total_fixed} fix(es) across {total_files} file(s), {total_skipped} skipped")
|
|
123
|
+
return 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
sys.exit(main())
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Apply targeted fixes for noUncheckedIndexedAccess errors in test files.
|
|
4
|
+
|
|
5
|
+
For each line reported by tsc, if the pattern is a simple `arr[N].foo`
|
|
6
|
+
or `arr[key].foo`, it rewrites to `arr[N]!.foo`. It's meant to be run
|
|
7
|
+
iteratively; rerun tsc after each pass.
|
|
8
|
+
|
|
9
|
+
Usage: scripts/fix-unchecked-index.py <tsc-errors.log> [--apply]
|
|
10
|
+
|
|
11
|
+
Without --apply, it only prints what it would do. Operates only on .test.ts
|
|
12
|
+
files by default.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from collections import defaultdict
|
|
20
|
+
|
|
21
|
+
ERROR_LINE_RE = re.compile(
|
|
22
|
+
r"^(?P<path>[^(]+)\((?P<line>\d+),(?P<col>\d+)\): error TS(?P<code>\d+): (?P<msg>.*)$"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Match identifier/call chain followed by [index] followed by .prop.
|
|
26
|
+
# Captures: group 3 is the `.` after `[...]`. Handles identifiers, member
|
|
27
|
+
# accesses (`a.b.c`), and calls (`a.b()`), including multiple chained calls.
|
|
28
|
+
BRACKET_DOT_RE = re.compile(
|
|
29
|
+
r"([A-Za-z_$][\w$]*(?:\??\.[A-Za-z_$][\w$]*|\([^()]*\))*)\[([^\]\[]+)\](\.)"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def parse_errors(log_path: Path, only_tests: bool) -> dict[Path, list[tuple[int, int]]]:
|
|
34
|
+
errs: dict[Path, list[tuple[int, int]]] = defaultdict(list)
|
|
35
|
+
for raw in log_path.read_text().splitlines():
|
|
36
|
+
m = ERROR_LINE_RE.match(raw)
|
|
37
|
+
if not m:
|
|
38
|
+
continue
|
|
39
|
+
code = m.group("code")
|
|
40
|
+
if code not in ("2532", "18048"):
|
|
41
|
+
continue
|
|
42
|
+
path = Path(m.group("path"))
|
|
43
|
+
if only_tests and not path.name.endswith(".test.ts"):
|
|
44
|
+
continue
|
|
45
|
+
errs[path].append((int(m.group("line")), int(m.group("col"))))
|
|
46
|
+
return errs
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def fix_line(line: str, col: int) -> str | None:
|
|
50
|
+
"""
|
|
51
|
+
Look for `identifier[idx].` where the `.` (or at least the `]`) is near `col`.
|
|
52
|
+
Add `!` before the `.` so it becomes `identifier[idx]!.`.
|
|
53
|
+
Prefer the match whose `]` is closest to (but before or at) col-1.
|
|
54
|
+
Returns new line or None if no change.
|
|
55
|
+
"""
|
|
56
|
+
matches = list(BRACKET_DOT_RE.finditer(line))
|
|
57
|
+
if not matches:
|
|
58
|
+
return None
|
|
59
|
+
# Column from tsc is 1-based and usually points to start of the whole expr
|
|
60
|
+
# or the `.` position; in practice it's the start of the object. Pick the
|
|
61
|
+
# first match at-or-after col-1 if any, else the last one on the line.
|
|
62
|
+
chosen = None
|
|
63
|
+
target = col - 1
|
|
64
|
+
for m in matches:
|
|
65
|
+
if m.start() >= target:
|
|
66
|
+
chosen = m
|
|
67
|
+
break
|
|
68
|
+
if chosen is None:
|
|
69
|
+
chosen = matches[0]
|
|
70
|
+
# Insert ! before the dot. `chosen.end(3)` is index after the dot; the dot
|
|
71
|
+
# is group(3). Position of dot: chosen.start(3).
|
|
72
|
+
dot_pos = chosen.start(3)
|
|
73
|
+
# Ensure there is no ! right before the dot already
|
|
74
|
+
if dot_pos > 0 and line[dot_pos - 1] == "!":
|
|
75
|
+
return None
|
|
76
|
+
return line[:dot_pos] + "!" + line[dot_pos:]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def main() -> int:
|
|
80
|
+
if len(sys.argv) < 2:
|
|
81
|
+
print(__doc__)
|
|
82
|
+
return 1
|
|
83
|
+
log = Path(sys.argv[1])
|
|
84
|
+
apply = "--apply" in sys.argv[2:]
|
|
85
|
+
only_tests = "--all" not in sys.argv[2:]
|
|
86
|
+
errs = parse_errors(log, only_tests=only_tests)
|
|
87
|
+
total_fixed = 0
|
|
88
|
+
total_files = 0
|
|
89
|
+
for path, positions in errs.items():
|
|
90
|
+
if not path.exists():
|
|
91
|
+
continue
|
|
92
|
+
lines = path.read_text().splitlines(keepends=True)
|
|
93
|
+
# Sort positions descending so we fix later cols first on same line.
|
|
94
|
+
# Actually sort by (line, -col) so we process multiple cols on a line
|
|
95
|
+
# from right to left, preserving indices.
|
|
96
|
+
positions.sort(key=lambda p: (p[0], -p[1]))
|
|
97
|
+
file_fixed = 0
|
|
98
|
+
for lineno, col in positions:
|
|
99
|
+
idx = lineno - 1
|
|
100
|
+
if idx >= len(lines):
|
|
101
|
+
continue
|
|
102
|
+
original = lines[idx]
|
|
103
|
+
# Strip trailing newline for regex, reattach after
|
|
104
|
+
eol = ""
|
|
105
|
+
if original.endswith("\r\n"):
|
|
106
|
+
eol = "\r\n"
|
|
107
|
+
content = original[:-2]
|
|
108
|
+
elif original.endswith("\n"):
|
|
109
|
+
eol = "\n"
|
|
110
|
+
content = original[:-1]
|
|
111
|
+
else:
|
|
112
|
+
content = original
|
|
113
|
+
new = fix_line(content, col)
|
|
114
|
+
if new is not None and new != content:
|
|
115
|
+
lines[idx] = new + eol
|
|
116
|
+
file_fixed += 1
|
|
117
|
+
if file_fixed:
|
|
118
|
+
total_fixed += file_fixed
|
|
119
|
+
total_files += 1
|
|
120
|
+
if apply:
|
|
121
|
+
path.write_text("".join(lines))
|
|
122
|
+
print(f"{'FIX' if apply else 'DRY'} {path}: {file_fixed} line(s)")
|
|
123
|
+
print(f"Total: {total_fixed} fix(es) across {total_files} file(s)")
|
|
124
|
+
return 0
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == "__main__":
|
|
128
|
+
sys.exit(main())
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Fix TS18048/TS2532 by chasing "X is possibly undefined" errors back to a
|
|
4
|
+
variable declaration of the form `const X = <expr>[key];` and appending `!`.
|
|
5
|
+
|
|
6
|
+
Usage: scripts/fix-unchecked-vars.py <tsc-errors.log> [--apply] [--all]
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from collections import defaultdict
|
|
14
|
+
|
|
15
|
+
ERROR_LINE_RE = re.compile(
|
|
16
|
+
r"^(?P<path>[^(]+)\((?P<line>\d+),(?P<col>\d+)\): error TS(?P<code>\d+): (?P<msg>.*)$"
|
|
17
|
+
)
|
|
18
|
+
POSSIBLY_UNDEF_RE = re.compile(r"'([A-Za-z_$][\w$]*)' is possibly 'undefined'")
|
|
19
|
+
|
|
20
|
+
# const NAME = <expr>[<key>]; or const NAME = <expr>.get(...);
|
|
21
|
+
# Append ! before the trailing ; (or end of statement) if not already present
|
|
22
|
+
DECL_INDEX_RE = re.compile(
|
|
23
|
+
r"^(?P<pre>\s*(?:const|let|var)\s+(?P<name>[A-Za-z_$][\w$]*)\s*=\s*[^;]*?\])(?P<post>\s*;?\s*)$"
|
|
24
|
+
)
|
|
25
|
+
DECL_GET_RE = re.compile(
|
|
26
|
+
r"^(?P<pre>\s*(?:const|let|var)\s+(?P<name>[A-Za-z_$][\w$]*)\s*=\s*[^;]*\.get\([^;]*\))(?P<post>\s*;?\s*)$"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def parse_errors(log_path: Path, only_tests: bool) -> dict[Path, list[tuple[int, str]]]:
|
|
31
|
+
errs: dict[Path, set[tuple[int, str]]] = defaultdict(set)
|
|
32
|
+
for raw in log_path.read_text().splitlines():
|
|
33
|
+
m = ERROR_LINE_RE.match(raw)
|
|
34
|
+
if not m:
|
|
35
|
+
continue
|
|
36
|
+
code = m.group("code")
|
|
37
|
+
if code not in ("2532", "18048"):
|
|
38
|
+
continue
|
|
39
|
+
msg = m.group("msg")
|
|
40
|
+
mm = POSSIBLY_UNDEF_RE.match(msg)
|
|
41
|
+
if not mm:
|
|
42
|
+
continue
|
|
43
|
+
name = mm.group(1)
|
|
44
|
+
path = Path(m.group("path"))
|
|
45
|
+
if only_tests and not path.name.endswith(".test.ts"):
|
|
46
|
+
continue
|
|
47
|
+
errs[path].add((int(m.group("line")), name))
|
|
48
|
+
return {p: sorted(s) for p, s in errs.items()}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def find_decl(lines: list[str], use_line_idx: int, name: str) -> int | None:
|
|
52
|
+
"""Search backwards for `const|let|var NAME =` declaration."""
|
|
53
|
+
pattern = re.compile(rf"^\s*(?:const|let|var)\s+{re.escape(name)}\s*=")
|
|
54
|
+
for i in range(use_line_idx, -1, -1):
|
|
55
|
+
if pattern.match(lines[i]):
|
|
56
|
+
return i
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def try_fix_decl_line(content: str) -> str | None:
|
|
61
|
+
# index form
|
|
62
|
+
m = DECL_INDEX_RE.match(content)
|
|
63
|
+
if m:
|
|
64
|
+
pre = m.group("pre")
|
|
65
|
+
post = m.group("post")
|
|
66
|
+
if pre.endswith("!"):
|
|
67
|
+
return None
|
|
68
|
+
return pre + "!" + post
|
|
69
|
+
m = DECL_GET_RE.match(content)
|
|
70
|
+
if m:
|
|
71
|
+
pre = m.group("pre")
|
|
72
|
+
post = m.group("post")
|
|
73
|
+
if pre.endswith("!"):
|
|
74
|
+
return None
|
|
75
|
+
return pre + "!" + post
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def main() -> int:
|
|
80
|
+
if len(sys.argv) < 2:
|
|
81
|
+
print(__doc__)
|
|
82
|
+
return 1
|
|
83
|
+
log = Path(sys.argv[1])
|
|
84
|
+
apply = "--apply" in sys.argv[2:]
|
|
85
|
+
only_tests = "--all" not in sys.argv[2:]
|
|
86
|
+
errs = parse_errors(log, only_tests=only_tests)
|
|
87
|
+
total_fixed = 0
|
|
88
|
+
total_files = 0
|
|
89
|
+
for path, positions in errs.items():
|
|
90
|
+
if not path.exists():
|
|
91
|
+
continue
|
|
92
|
+
lines = path.read_text().splitlines(keepends=True)
|
|
93
|
+
fixed_decls: set[int] = set()
|
|
94
|
+
file_fixed = 0
|
|
95
|
+
for lineno, name in positions:
|
|
96
|
+
idx = lineno - 1
|
|
97
|
+
if idx >= len(lines):
|
|
98
|
+
continue
|
|
99
|
+
decl_idx = find_decl(lines, idx, name)
|
|
100
|
+
if decl_idx is None or decl_idx in fixed_decls:
|
|
101
|
+
continue
|
|
102
|
+
# Strip EOL
|
|
103
|
+
original = lines[decl_idx]
|
|
104
|
+
eol = ""
|
|
105
|
+
if original.endswith("\r\n"):
|
|
106
|
+
eol, content = "\r\n", original[:-2]
|
|
107
|
+
elif original.endswith("\n"):
|
|
108
|
+
eol, content = "\n", original[:-1]
|
|
109
|
+
else:
|
|
110
|
+
content = original
|
|
111
|
+
new = try_fix_decl_line(content)
|
|
112
|
+
if new and new != content:
|
|
113
|
+
lines[decl_idx] = new + eol
|
|
114
|
+
fixed_decls.add(decl_idx)
|
|
115
|
+
file_fixed += 1
|
|
116
|
+
if file_fixed:
|
|
117
|
+
total_fixed += file_fixed
|
|
118
|
+
total_files += 1
|
|
119
|
+
if apply:
|
|
120
|
+
path.write_text("".join(lines))
|
|
121
|
+
print(f"{'FIX' if apply else 'DRY'} {path}: {file_fixed} decl(s)")
|
|
122
|
+
print(f"Total: {total_fixed} fix(es) across {total_files} file(s)")
|
|
123
|
+
return 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
sys.exit(main())
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Mechanically replace `x!.prop` with `x?.prop` in expect() contexts in test files.
|
|
4
|
+
Safe when the result is immediately chained with .toBe/.toEqual/etc, because
|
|
5
|
+
`expect(undefined).toBe(expected)` still fails the assertion cleanly.
|
|
6
|
+
|
|
7
|
+
Usage: fix-wave-a-bangs.py <file>...
|
|
8
|
+
"""
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
# Pattern: something like "arr[0]!" or "foo.bar!" or "foo()!" followed by .prop
|
|
13
|
+
# We only rewrite when the `!.` is followed by an identifier (method/prop access).
|
|
14
|
+
# This avoids touching `x!` standalone statements.
|
|
15
|
+
|
|
16
|
+
BANG_DOT = re.compile(r'!\.')
|
|
17
|
+
|
|
18
|
+
def fix_file(path: str) -> int:
|
|
19
|
+
with open(path, 'r') as f:
|
|
20
|
+
content = f.read()
|
|
21
|
+
# Only rewrite lines that start with whitespace + "expect(" to be safe for tests.
|
|
22
|
+
# Actually also handle const x = foo[0]!.bar patterns. Let's be more aggressive
|
|
23
|
+
# and replace ALL `!.` occurrences, then let tsc verify.
|
|
24
|
+
new_content, n = BANG_DOT.subn('?.', content)
|
|
25
|
+
if n > 0:
|
|
26
|
+
with open(path, 'w') as f:
|
|
27
|
+
f.write(new_content)
|
|
28
|
+
return n
|
|
29
|
+
|
|
30
|
+
if __name__ == '__main__':
|
|
31
|
+
total = 0
|
|
32
|
+
for p in sys.argv[1:]:
|
|
33
|
+
n = fix_file(p)
|
|
34
|
+
print(f'{p}: {n} replacements')
|
|
35
|
+
total += n
|
|
36
|
+
print(f'TOTAL: {total}')
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Wrap template-literal interpolations in String(...) ONLY at locations flagged
|
|
4
|
+
by eslint's restrict-template-expressions rule. Reads JSON ESLint output from
|
|
5
|
+
stdin or generates it itself.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
./scripts/fix-wave-a-templates.py <path> [<path>...]
|
|
9
|
+
|
|
10
|
+
The script:
|
|
11
|
+
1. Runs `npx eslint <paths> -f json` to discover restrict-template-expressions
|
|
12
|
+
errors with their line:column locations.
|
|
13
|
+
2. For each file, reads its content and at each flagged column finds the
|
|
14
|
+
enclosing ${...} expression and wraps the inner expression in String(...).
|
|
15
|
+
3. Writes the file back if any changes were made.
|
|
16
|
+
|
|
17
|
+
Skips expressions that are already inside String(), are string literals, or
|
|
18
|
+
contain `instanceof Error` (likely error-handling already correct).
|
|
19
|
+
"""
|
|
20
|
+
import json
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
from collections import defaultdict
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def run_eslint(paths: list[str]) -> dict:
|
|
27
|
+
res = subprocess.run(
|
|
28
|
+
['npx', 'eslint', *paths, '-f', 'json'],
|
|
29
|
+
capture_output=True, text=True
|
|
30
|
+
)
|
|
31
|
+
return json.loads(res.stdout)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def collect_errors(eslint_data) -> dict:
|
|
35
|
+
by_file = defaultdict(list)
|
|
36
|
+
for f in eslint_data:
|
|
37
|
+
for m in f['messages']:
|
|
38
|
+
if m.get('severity') != 2: continue
|
|
39
|
+
if m.get('ruleId') != '@typescript-eslint/restrict-template-expressions': continue
|
|
40
|
+
by_file[f['filePath']].append((m['line'], m['column']))
|
|
41
|
+
return by_file
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def fix_file(path: str, locations: list[tuple[int, int]]) -> int:
|
|
45
|
+
with open(path, 'r') as f:
|
|
46
|
+
lines = f.readlines()
|
|
47
|
+
# Process from bottom to top so column offsets stay valid for earlier edits.
|
|
48
|
+
locations = sorted(set(locations), key=lambda lc: (-lc[0], -lc[1]))
|
|
49
|
+
fixed = 0
|
|
50
|
+
for line_no, col in locations:
|
|
51
|
+
idx = line_no - 1
|
|
52
|
+
if idx >= len(lines): continue
|
|
53
|
+
line = lines[idx]
|
|
54
|
+
# The reported column is 1-based and points at the start of the
|
|
55
|
+
# offending expression *inside* the ${...}. Walk left to find the '$'
|
|
56
|
+
# that opened it, then find the matching '}'.
|
|
57
|
+
c = col - 1 # 0-based
|
|
58
|
+
# find '${' just before c, looking back at most 200 chars
|
|
59
|
+
start = -1
|
|
60
|
+
for k in range(c - 1, max(0, c - 200) - 1, -1):
|
|
61
|
+
if line[k] == '{' and k > 0 and line[k-1] == '$':
|
|
62
|
+
start = k - 1 # index of '$'
|
|
63
|
+
break
|
|
64
|
+
if start == -1:
|
|
65
|
+
continue
|
|
66
|
+
# find matching '}' from c onward
|
|
67
|
+
depth = 1
|
|
68
|
+
j = start + 2
|
|
69
|
+
while j < len(line) and depth > 0:
|
|
70
|
+
if line[j] == '{': depth += 1
|
|
71
|
+
elif line[j] == '}':
|
|
72
|
+
depth -= 1
|
|
73
|
+
if depth == 0: break
|
|
74
|
+
j += 1
|
|
75
|
+
if depth != 0:
|
|
76
|
+
continue
|
|
77
|
+
expr = line[start+2:j]
|
|
78
|
+
stripped = expr.strip()
|
|
79
|
+
if stripped.startswith('String(') and stripped.endswith(')'):
|
|
80
|
+
continue
|
|
81
|
+
if stripped.startswith(("'", '"', '`')):
|
|
82
|
+
continue
|
|
83
|
+
if 'instanceof Error' in stripped:
|
|
84
|
+
continue
|
|
85
|
+
# Wrap
|
|
86
|
+
new_segment = '${String(' + stripped + ')}'
|
|
87
|
+
new_line = line[:start] + new_segment + line[j+1:]
|
|
88
|
+
lines[idx] = new_line
|
|
89
|
+
fixed += 1
|
|
90
|
+
if fixed > 0:
|
|
91
|
+
with open(path, 'w') as f:
|
|
92
|
+
f.writelines(lines)
|
|
93
|
+
return fixed
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == '__main__':
|
|
97
|
+
if len(sys.argv) < 2:
|
|
98
|
+
print("Usage: fix-wave-a-templates.py <path> [<path>...]", file=sys.stderr)
|
|
99
|
+
sys.exit(2)
|
|
100
|
+
paths = sys.argv[1:]
|
|
101
|
+
data = run_eslint(paths)
|
|
102
|
+
by_file = collect_errors(data)
|
|
103
|
+
total = 0
|
|
104
|
+
for fpath, locs in by_file.items():
|
|
105
|
+
n = fix_file(fpath, locs)
|
|
106
|
+
print(f'{fpath}: {n} wrappings')
|
|
107
|
+
total += n
|
|
108
|
+
print(f'TOTAL: {total}')
|