@wazir-dev/cli 1.0.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/AGENTS.md +111 -0
- package/CHANGELOG.md +14 -0
- package/CONTRIBUTING.md +101 -0
- package/LICENSE +21 -0
- package/README.md +314 -0
- package/assets/composition-engine.mmd +34 -0
- package/assets/demo-script.sh +17 -0
- package/assets/logo-dark.svg +14 -0
- package/assets/logo.svg +14 -0
- package/assets/pipeline.mmd +39 -0
- package/assets/record-demo.sh +51 -0
- package/docs/README.md +51 -0
- package/docs/adapters/context-mode.md +60 -0
- package/docs/concepts/architecture.md +87 -0
- package/docs/concepts/artifact-model.md +60 -0
- package/docs/concepts/composition-engine.md +36 -0
- package/docs/concepts/indexing-and-recall.md +160 -0
- package/docs/concepts/observability.md +41 -0
- package/docs/concepts/roles-and-workflows.md +59 -0
- package/docs/concepts/terminology-policy.md +27 -0
- package/docs/getting-started/01-installation.md +78 -0
- package/docs/getting-started/02-first-run.md +102 -0
- package/docs/getting-started/03-adding-to-project.md +15 -0
- package/docs/getting-started/04-host-setup.md +15 -0
- package/docs/guides/ci-integration.md +15 -0
- package/docs/guides/creating-skills.md +15 -0
- package/docs/guides/expertise-module-authoring.md +15 -0
- package/docs/guides/hook-development.md +15 -0
- package/docs/guides/memory-and-learnings.md +34 -0
- package/docs/guides/multi-host-export.md +15 -0
- package/docs/guides/troubleshooting.md +101 -0
- package/docs/guides/writing-custom-roles.md +15 -0
- package/docs/plans/2026-03-15-cli-pipeline-integration-design.md +592 -0
- package/docs/plans/2026-03-15-cli-pipeline-integration-plan.md +598 -0
- package/docs/plans/2026-03-15-docs-enforcement-plan.md +238 -0
- package/docs/readmes/INDEX.md +99 -0
- package/docs/readmes/features/expertise/README.md +171 -0
- package/docs/readmes/features/exports/README.md +222 -0
- package/docs/readmes/features/hooks/README.md +103 -0
- package/docs/readmes/features/hooks/loop-cap-guard.md +133 -0
- package/docs/readmes/features/hooks/post-tool-capture.md +121 -0
- package/docs/readmes/features/hooks/post-tool-lint.md +130 -0
- package/docs/readmes/features/hooks/pre-compact-summary.md +122 -0
- package/docs/readmes/features/hooks/pre-tool-capture-route.md +100 -0
- package/docs/readmes/features/hooks/protected-path-write-guard.md +128 -0
- package/docs/readmes/features/hooks/session-start.md +119 -0
- package/docs/readmes/features/hooks/stop-handoff-harvest.md +125 -0
- package/docs/readmes/features/roles/README.md +157 -0
- package/docs/readmes/features/roles/clarifier.md +152 -0
- package/docs/readmes/features/roles/content-author.md +190 -0
- package/docs/readmes/features/roles/designer.md +193 -0
- package/docs/readmes/features/roles/executor.md +184 -0
- package/docs/readmes/features/roles/learner.md +210 -0
- package/docs/readmes/features/roles/planner.md +182 -0
- package/docs/readmes/features/roles/researcher.md +164 -0
- package/docs/readmes/features/roles/reviewer.md +184 -0
- package/docs/readmes/features/roles/specifier.md +162 -0
- package/docs/readmes/features/roles/verifier.md +215 -0
- package/docs/readmes/features/schemas/README.md +178 -0
- package/docs/readmes/features/skills/README.md +63 -0
- package/docs/readmes/features/skills/brainstorming.md +96 -0
- package/docs/readmes/features/skills/debugging.md +148 -0
- package/docs/readmes/features/skills/design.md +120 -0
- package/docs/readmes/features/skills/prepare-next.md +109 -0
- package/docs/readmes/features/skills/run-audit.md +159 -0
- package/docs/readmes/features/skills/scan-project.md +109 -0
- package/docs/readmes/features/skills/self-audit.md +176 -0
- package/docs/readmes/features/skills/tdd.md +137 -0
- package/docs/readmes/features/skills/using-skills.md +92 -0
- package/docs/readmes/features/skills/verification.md +120 -0
- package/docs/readmes/features/skills/writing-plans.md +104 -0
- package/docs/readmes/features/tooling/README.md +320 -0
- package/docs/readmes/features/workflows/README.md +186 -0
- package/docs/readmes/features/workflows/author.md +181 -0
- package/docs/readmes/features/workflows/clarify.md +154 -0
- package/docs/readmes/features/workflows/design-review.md +171 -0
- package/docs/readmes/features/workflows/design.md +169 -0
- package/docs/readmes/features/workflows/discover.md +162 -0
- package/docs/readmes/features/workflows/execute.md +173 -0
- package/docs/readmes/features/workflows/learn.md +167 -0
- package/docs/readmes/features/workflows/plan-review.md +165 -0
- package/docs/readmes/features/workflows/plan.md +170 -0
- package/docs/readmes/features/workflows/prepare-next.md +167 -0
- package/docs/readmes/features/workflows/review.md +169 -0
- package/docs/readmes/features/workflows/run-audit.md +191 -0
- package/docs/readmes/features/workflows/spec-challenge.md +159 -0
- package/docs/readmes/features/workflows/specify.md +160 -0
- package/docs/readmes/features/workflows/verify.md +177 -0
- package/docs/readmes/packages/README.md +50 -0
- package/docs/readmes/packages/ajv.md +117 -0
- package/docs/readmes/packages/context-mode.md +118 -0
- package/docs/readmes/packages/gray-matter.md +116 -0
- package/docs/readmes/packages/node-test.md +137 -0
- package/docs/readmes/packages/yaml.md +112 -0
- package/docs/reference/configuration-reference.md +159 -0
- package/docs/reference/expertise-index.md +52 -0
- package/docs/reference/git-flow.md +43 -0
- package/docs/reference/hooks.md +87 -0
- package/docs/reference/host-exports.md +50 -0
- package/docs/reference/launch-checklist.md +172 -0
- package/docs/reference/marketplace-listings.md +76 -0
- package/docs/reference/release-process.md +34 -0
- package/docs/reference/roles-reference.md +77 -0
- package/docs/reference/skills.md +33 -0
- package/docs/reference/templates.md +29 -0
- package/docs/reference/tooling-cli.md +94 -0
- package/docs/truth-claims.yaml +222 -0
- package/expertise/PROGRESS.md +63 -0
- package/expertise/README.md +18 -0
- package/expertise/antipatterns/PROGRESS.md +56 -0
- package/expertise/antipatterns/backend/api-design-antipatterns.md +1271 -0
- package/expertise/antipatterns/backend/auth-antipatterns.md +1195 -0
- package/expertise/antipatterns/backend/caching-antipatterns.md +622 -0
- package/expertise/antipatterns/backend/database-antipatterns.md +1038 -0
- package/expertise/antipatterns/backend/index.md +24 -0
- package/expertise/antipatterns/backend/microservices-antipatterns.md +850 -0
- package/expertise/antipatterns/code/architecture-antipatterns.md +919 -0
- package/expertise/antipatterns/code/async-antipatterns.md +622 -0
- package/expertise/antipatterns/code/code-smells.md +1186 -0
- package/expertise/antipatterns/code/dependency-antipatterns.md +1209 -0
- package/expertise/antipatterns/code/error-handling-antipatterns.md +1360 -0
- package/expertise/antipatterns/code/index.md +27 -0
- package/expertise/antipatterns/code/naming-and-abstraction.md +1118 -0
- package/expertise/antipatterns/code/state-management-antipatterns.md +1076 -0
- package/expertise/antipatterns/code/testing-antipatterns.md +1053 -0
- package/expertise/antipatterns/design/accessibility-antipatterns.md +1136 -0
- package/expertise/antipatterns/design/dark-patterns.md +1121 -0
- package/expertise/antipatterns/design/index.md +22 -0
- package/expertise/antipatterns/design/ui-antipatterns.md +1202 -0
- package/expertise/antipatterns/design/ux-antipatterns.md +680 -0
- package/expertise/antipatterns/frontend/css-layout-antipatterns.md +691 -0
- package/expertise/antipatterns/frontend/flutter-antipatterns.md +1827 -0
- package/expertise/antipatterns/frontend/index.md +23 -0
- package/expertise/antipatterns/frontend/mobile-antipatterns.md +573 -0
- package/expertise/antipatterns/frontend/react-antipatterns.md +1128 -0
- package/expertise/antipatterns/frontend/spa-antipatterns.md +1235 -0
- package/expertise/antipatterns/index.md +31 -0
- package/expertise/antipatterns/performance/index.md +20 -0
- package/expertise/antipatterns/performance/performance-antipatterns.md +1013 -0
- package/expertise/antipatterns/performance/premature-optimization.md +623 -0
- package/expertise/antipatterns/performance/scaling-antipatterns.md +785 -0
- package/expertise/antipatterns/process/ai-coding-antipatterns.md +853 -0
- package/expertise/antipatterns/process/code-review-antipatterns.md +656 -0
- package/expertise/antipatterns/process/deployment-antipatterns.md +920 -0
- package/expertise/antipatterns/process/index.md +23 -0
- package/expertise/antipatterns/process/technical-debt-antipatterns.md +647 -0
- package/expertise/antipatterns/security/index.md +20 -0
- package/expertise/antipatterns/security/secrets-antipatterns.md +849 -0
- package/expertise/antipatterns/security/security-theater.md +843 -0
- package/expertise/antipatterns/security/vulnerability-patterns.md +801 -0
- package/expertise/architecture/PROGRESS.md +70 -0
- package/expertise/architecture/data/caching-architecture.md +671 -0
- package/expertise/architecture/data/data-consistency.md +574 -0
- package/expertise/architecture/data/data-modeling.md +536 -0
- package/expertise/architecture/data/event-streams-and-queues.md +634 -0
- package/expertise/architecture/data/index.md +25 -0
- package/expertise/architecture/data/search-architecture.md +663 -0
- package/expertise/architecture/data/sql-vs-nosql.md +708 -0
- package/expertise/architecture/decisions/architecture-decision-records.md +640 -0
- package/expertise/architecture/decisions/build-vs-buy.md +616 -0
- package/expertise/architecture/decisions/index.md +23 -0
- package/expertise/architecture/decisions/monolith-to-microservices.md +790 -0
- package/expertise/architecture/decisions/technology-selection.md +616 -0
- package/expertise/architecture/distributed/cap-theorem-and-tradeoffs.md +800 -0
- package/expertise/architecture/distributed/circuit-breaker-bulkhead.md +741 -0
- package/expertise/architecture/distributed/consensus-and-coordination.md +796 -0
- package/expertise/architecture/distributed/distributed-systems-fundamentals.md +564 -0
- package/expertise/architecture/distributed/idempotency-and-retry.md +796 -0
- package/expertise/architecture/distributed/index.md +25 -0
- package/expertise/architecture/distributed/saga-pattern.md +797 -0
- package/expertise/architecture/foundations/architectural-thinking.md +460 -0
- package/expertise/architecture/foundations/coupling-and-cohesion.md +770 -0
- package/expertise/architecture/foundations/design-principles-solid.md +649 -0
- package/expertise/architecture/foundations/domain-driven-design.md +719 -0
- package/expertise/architecture/foundations/index.md +25 -0
- package/expertise/architecture/foundations/separation-of-concerns.md +472 -0
- package/expertise/architecture/foundations/twelve-factor-app.md +797 -0
- package/expertise/architecture/index.md +34 -0
- package/expertise/architecture/integration/api-design-graphql.md +638 -0
- package/expertise/architecture/integration/api-design-grpc.md +804 -0
- package/expertise/architecture/integration/api-design-rest.md +892 -0
- package/expertise/architecture/integration/index.md +25 -0
- package/expertise/architecture/integration/third-party-integration.md +795 -0
- package/expertise/architecture/integration/webhooks-and-callbacks.md +1152 -0
- package/expertise/architecture/integration/websockets-realtime.md +791 -0
- package/expertise/architecture/mobile-architecture/index.md +22 -0
- package/expertise/architecture/mobile-architecture/mobile-app-architecture.md +780 -0
- package/expertise/architecture/mobile-architecture/mobile-backend-for-frontend.md +670 -0
- package/expertise/architecture/mobile-architecture/offline-first.md +719 -0
- package/expertise/architecture/mobile-architecture/push-and-sync.md +782 -0
- package/expertise/architecture/patterns/cqrs-event-sourcing.md +717 -0
- package/expertise/architecture/patterns/event-driven.md +797 -0
- package/expertise/architecture/patterns/hexagonal-clean-architecture.md +870 -0
- package/expertise/architecture/patterns/index.md +27 -0
- package/expertise/architecture/patterns/layered-architecture.md +736 -0
- package/expertise/architecture/patterns/microservices.md +753 -0
- package/expertise/architecture/patterns/modular-monolith.md +692 -0
- package/expertise/architecture/patterns/monolith.md +626 -0
- package/expertise/architecture/patterns/plugin-architecture.md +735 -0
- package/expertise/architecture/patterns/serverless.md +780 -0
- package/expertise/architecture/scaling/database-scaling.md +615 -0
- package/expertise/architecture/scaling/feature-flags-and-rollouts.md +757 -0
- package/expertise/architecture/scaling/horizontal-vs-vertical.md +606 -0
- package/expertise/architecture/scaling/index.md +24 -0
- package/expertise/architecture/scaling/multi-tenancy.md +800 -0
- package/expertise/architecture/scaling/stateless-design.md +787 -0
- package/expertise/backend/embedded-firmware.md +625 -0
- package/expertise/backend/go.md +853 -0
- package/expertise/backend/index.md +24 -0
- package/expertise/backend/java-spring.md +448 -0
- package/expertise/backend/node-typescript.md +625 -0
- package/expertise/backend/python-fastapi.md +724 -0
- package/expertise/backend/rust.md +458 -0
- package/expertise/backend/solidity.md +711 -0
- package/expertise/composition-map.yaml +443 -0
- package/expertise/content/foundations/content-modeling.md +395 -0
- package/expertise/content/foundations/editorial-standards.md +449 -0
- package/expertise/content/foundations/index.md +24 -0
- package/expertise/content/foundations/microcopy.md +455 -0
- package/expertise/content/foundations/terminology-governance.md +509 -0
- package/expertise/content/index.md +34 -0
- package/expertise/content/patterns/accessibility-copy.md +518 -0
- package/expertise/content/patterns/index.md +24 -0
- package/expertise/content/patterns/notification-content.md +433 -0
- package/expertise/content/patterns/sample-content.md +486 -0
- package/expertise/content/patterns/state-copy.md +439 -0
- package/expertise/design/PROGRESS.md +58 -0
- package/expertise/design/disciplines/dark-mode-theming.md +577 -0
- package/expertise/design/disciplines/design-systems.md +595 -0
- package/expertise/design/disciplines/index.md +25 -0
- package/expertise/design/disciplines/information-architecture.md +800 -0
- package/expertise/design/disciplines/interaction-design.md +788 -0
- package/expertise/design/disciplines/responsive-design.md +552 -0
- package/expertise/design/disciplines/usability-testing.md +516 -0
- package/expertise/design/disciplines/user-research.md +792 -0
- package/expertise/design/foundations/accessibility-design.md +796 -0
- package/expertise/design/foundations/color-theory.md +797 -0
- package/expertise/design/foundations/iconography.md +795 -0
- package/expertise/design/foundations/index.md +26 -0
- package/expertise/design/foundations/motion-and-animation.md +653 -0
- package/expertise/design/foundations/rtl-design.md +585 -0
- package/expertise/design/foundations/spacing-and-layout.md +607 -0
- package/expertise/design/foundations/typography.md +800 -0
- package/expertise/design/foundations/visual-hierarchy.md +761 -0
- package/expertise/design/index.md +32 -0
- package/expertise/design/patterns/authentication-flows.md +474 -0
- package/expertise/design/patterns/content-consumption.md +789 -0
- package/expertise/design/patterns/data-display.md +618 -0
- package/expertise/design/patterns/e-commerce.md +1494 -0
- package/expertise/design/patterns/feedback-and-states.md +642 -0
- package/expertise/design/patterns/forms-and-input.md +819 -0
- package/expertise/design/patterns/gamification.md +801 -0
- package/expertise/design/patterns/index.md +31 -0
- package/expertise/design/patterns/microinteractions.md +449 -0
- package/expertise/design/patterns/navigation.md +800 -0
- package/expertise/design/patterns/notifications.md +705 -0
- package/expertise/design/patterns/onboarding.md +700 -0
- package/expertise/design/patterns/search-and-filter.md +601 -0
- package/expertise/design/patterns/settings-and-preferences.md +768 -0
- package/expertise/design/patterns/social-and-community.md +748 -0
- package/expertise/design/platforms/desktop-native.md +612 -0
- package/expertise/design/platforms/index.md +25 -0
- package/expertise/design/platforms/mobile-android.md +825 -0
- package/expertise/design/platforms/mobile-cross-platform.md +983 -0
- package/expertise/design/platforms/mobile-ios.md +699 -0
- package/expertise/design/platforms/tablet.md +794 -0
- package/expertise/design/platforms/web-dashboard.md +790 -0
- package/expertise/design/platforms/web-responsive.md +550 -0
- package/expertise/design/psychology/behavioral-nudges.md +449 -0
- package/expertise/design/psychology/cognitive-load.md +1191 -0
- package/expertise/design/psychology/error-psychology.md +778 -0
- package/expertise/design/psychology/index.md +22 -0
- package/expertise/design/psychology/persuasive-design.md +736 -0
- package/expertise/design/psychology/user-mental-models.md +623 -0
- package/expertise/design/tooling/open-pencil.md +266 -0
- package/expertise/frontend/angular.md +1073 -0
- package/expertise/frontend/desktop-electron.md +546 -0
- package/expertise/frontend/flutter.md +782 -0
- package/expertise/frontend/index.md +27 -0
- package/expertise/frontend/native-android.md +409 -0
- package/expertise/frontend/native-ios.md +490 -0
- package/expertise/frontend/react-native.md +1160 -0
- package/expertise/frontend/react.md +808 -0
- package/expertise/frontend/vue.md +1089 -0
- package/expertise/humanize/domain-rules-code.md +79 -0
- package/expertise/humanize/domain-rules-content.md +67 -0
- package/expertise/humanize/domain-rules-technical-docs.md +56 -0
- package/expertise/humanize/index.md +35 -0
- package/expertise/humanize/self-audit-checklist.md +87 -0
- package/expertise/humanize/sentence-patterns.md +218 -0
- package/expertise/humanize/vocabulary-blacklist.md +105 -0
- package/expertise/i18n/PROGRESS.md +65 -0
- package/expertise/i18n/advanced/accessibility-and-i18n.md +28 -0
- package/expertise/i18n/advanced/bidirectional-text-algorithm.md +38 -0
- package/expertise/i18n/advanced/complex-scripts.md +30 -0
- package/expertise/i18n/advanced/performance-and-i18n.md +27 -0
- package/expertise/i18n/advanced/testing-i18n.md +28 -0
- package/expertise/i18n/content/content-adaptation.md +23 -0
- package/expertise/i18n/content/locale-specific-formatting.md +23 -0
- package/expertise/i18n/content/machine-translation-integration.md +28 -0
- package/expertise/i18n/content/translation-management.md +29 -0
- package/expertise/i18n/foundations/date-time-calendars.md +67 -0
- package/expertise/i18n/foundations/i18n-architecture.md +272 -0
- package/expertise/i18n/foundations/locale-and-language-tags.md +79 -0
- package/expertise/i18n/foundations/numbers-currency-units.md +61 -0
- package/expertise/i18n/foundations/pluralization-and-gender.md +109 -0
- package/expertise/i18n/foundations/string-externalization.md +236 -0
- package/expertise/i18n/foundations/text-direction-bidi.md +241 -0
- package/expertise/i18n/foundations/unicode-and-encoding.md +86 -0
- package/expertise/i18n/index.md +38 -0
- package/expertise/i18n/platform/backend-i18n.md +31 -0
- package/expertise/i18n/platform/flutter-i18n.md +148 -0
- package/expertise/i18n/platform/native-android-i18n.md +36 -0
- package/expertise/i18n/platform/native-ios-i18n.md +36 -0
- package/expertise/i18n/platform/react-i18n.md +103 -0
- package/expertise/i18n/platform/web-css-i18n.md +81 -0
- package/expertise/i18n/rtl/arabic-specific.md +175 -0
- package/expertise/i18n/rtl/hebrew-specific.md +149 -0
- package/expertise/i18n/rtl/rtl-animations-and-transitions.md +111 -0
- package/expertise/i18n/rtl/rtl-forms-and-input.md +161 -0
- package/expertise/i18n/rtl/rtl-fundamentals.md +211 -0
- package/expertise/i18n/rtl/rtl-icons-and-images.md +181 -0
- package/expertise/i18n/rtl/rtl-layout-mirroring.md +252 -0
- package/expertise/i18n/rtl/rtl-navigation-and-gestures.md +107 -0
- package/expertise/i18n/rtl/rtl-testing-and-qa.md +147 -0
- package/expertise/i18n/rtl/rtl-typography.md +160 -0
- package/expertise/index.md +113 -0
- package/expertise/index.yaml +216 -0
- package/expertise/infrastructure/cloud-aws.md +597 -0
- package/expertise/infrastructure/cloud-gcp.md +599 -0
- package/expertise/infrastructure/cybersecurity.md +816 -0
- package/expertise/infrastructure/database-mongodb.md +447 -0
- package/expertise/infrastructure/database-postgres.md +400 -0
- package/expertise/infrastructure/devops-cicd.md +787 -0
- package/expertise/infrastructure/index.md +27 -0
- package/expertise/performance/PROGRESS.md +50 -0
- package/expertise/performance/backend/api-latency.md +1204 -0
- package/expertise/performance/backend/background-jobs.md +506 -0
- package/expertise/performance/backend/connection-pooling.md +1209 -0
- package/expertise/performance/backend/database-query-optimization.md +515 -0
- package/expertise/performance/backend/index.md +23 -0
- package/expertise/performance/backend/rate-limiting-and-throttling.md +971 -0
- package/expertise/performance/foundations/algorithmic-complexity.md +954 -0
- package/expertise/performance/foundations/caching-strategies.md +489 -0
- package/expertise/performance/foundations/concurrency-and-parallelism.md +847 -0
- package/expertise/performance/foundations/index.md +24 -0
- package/expertise/performance/foundations/measuring-and-profiling.md +440 -0
- package/expertise/performance/foundations/memory-management.md +964 -0
- package/expertise/performance/foundations/performance-budgets.md +1314 -0
- package/expertise/performance/index.md +31 -0
- package/expertise/performance/infrastructure/auto-scaling.md +1059 -0
- package/expertise/performance/infrastructure/cdn-and-edge.md +1081 -0
- package/expertise/performance/infrastructure/index.md +22 -0
- package/expertise/performance/infrastructure/load-balancing.md +1081 -0
- package/expertise/performance/infrastructure/observability.md +1079 -0
- package/expertise/performance/mobile/index.md +23 -0
- package/expertise/performance/mobile/mobile-animations.md +544 -0
- package/expertise/performance/mobile/mobile-memory-battery.md +416 -0
- package/expertise/performance/mobile/mobile-network.md +452 -0
- package/expertise/performance/mobile/mobile-rendering.md +599 -0
- package/expertise/performance/mobile/mobile-startup-time.md +505 -0
- package/expertise/performance/platform-specific/flutter-performance.md +647 -0
- package/expertise/performance/platform-specific/index.md +22 -0
- package/expertise/performance/platform-specific/node-performance.md +1307 -0
- package/expertise/performance/platform-specific/postgres-performance.md +1366 -0
- package/expertise/performance/platform-specific/react-performance.md +1403 -0
- package/expertise/performance/web/bundle-optimization.md +1239 -0
- package/expertise/performance/web/image-and-media.md +636 -0
- package/expertise/performance/web/index.md +24 -0
- package/expertise/performance/web/network-optimization.md +1133 -0
- package/expertise/performance/web/rendering-performance.md +1098 -0
- package/expertise/performance/web/ssr-and-hydration.md +918 -0
- package/expertise/performance/web/web-vitals.md +1374 -0
- package/expertise/quality/accessibility.md +985 -0
- package/expertise/quality/evidence-based-verification.md +499 -0
- package/expertise/quality/index.md +24 -0
- package/expertise/quality/ml-model-audit.md +614 -0
- package/expertise/quality/performance.md +600 -0
- package/expertise/quality/testing-api.md +891 -0
- package/expertise/quality/testing-mobile.md +496 -0
- package/expertise/quality/testing-web.md +849 -0
- package/expertise/security/PROGRESS.md +54 -0
- package/expertise/security/agentic-identity.md +540 -0
- package/expertise/security/compliance-frameworks.md +601 -0
- package/expertise/security/data/data-encryption.md +364 -0
- package/expertise/security/data/data-privacy-gdpr.md +692 -0
- package/expertise/security/data/database-security.md +1171 -0
- package/expertise/security/data/index.md +22 -0
- package/expertise/security/data/pii-handling.md +531 -0
- package/expertise/security/foundations/authentication.md +1041 -0
- package/expertise/security/foundations/authorization.md +603 -0
- package/expertise/security/foundations/cryptography.md +1001 -0
- package/expertise/security/foundations/index.md +25 -0
- package/expertise/security/foundations/owasp-top-10.md +1354 -0
- package/expertise/security/foundations/secrets-management.md +1217 -0
- package/expertise/security/foundations/secure-sdlc.md +700 -0
- package/expertise/security/foundations/supply-chain-security.md +698 -0
- package/expertise/security/index.md +31 -0
- package/expertise/security/infrastructure/cloud-security-aws.md +1296 -0
- package/expertise/security/infrastructure/cloud-security-gcp.md +1376 -0
- package/expertise/security/infrastructure/container-security.md +721 -0
- package/expertise/security/infrastructure/incident-response.md +1295 -0
- package/expertise/security/infrastructure/index.md +24 -0
- package/expertise/security/infrastructure/logging-and-monitoring.md +1618 -0
- package/expertise/security/infrastructure/network-security.md +1337 -0
- package/expertise/security/mobile/index.md +23 -0
- package/expertise/security/mobile/mobile-android-security.md +1218 -0
- package/expertise/security/mobile/mobile-binary-protection.md +1229 -0
- package/expertise/security/mobile/mobile-data-storage.md +1265 -0
- package/expertise/security/mobile/mobile-ios-security.md +1401 -0
- package/expertise/security/mobile/mobile-network-security.md +1520 -0
- package/expertise/security/smart-contract-security.md +594 -0
- package/expertise/security/testing/index.md +22 -0
- package/expertise/security/testing/penetration-testing.md +1258 -0
- package/expertise/security/testing/security-code-review.md +1765 -0
- package/expertise/security/testing/threat-modeling.md +1074 -0
- package/expertise/security/testing/vulnerability-scanning.md +1062 -0
- package/expertise/security/web/api-security.md +586 -0
- package/expertise/security/web/cors-and-headers.md +433 -0
- package/expertise/security/web/csrf.md +562 -0
- package/expertise/security/web/file-upload.md +1477 -0
- package/expertise/security/web/index.md +25 -0
- package/expertise/security/web/injection.md +1375 -0
- package/expertise/security/web/session-management.md +1101 -0
- package/expertise/security/web/xss.md +1158 -0
- package/exports/README.md +17 -0
- package/exports/hosts/claude/.claude/agents/clarifier.md +42 -0
- package/exports/hosts/claude/.claude/agents/content-author.md +63 -0
- package/exports/hosts/claude/.claude/agents/designer.md +55 -0
- package/exports/hosts/claude/.claude/agents/executor.md +55 -0
- package/exports/hosts/claude/.claude/agents/learner.md +51 -0
- package/exports/hosts/claude/.claude/agents/planner.md +53 -0
- package/exports/hosts/claude/.claude/agents/researcher.md +43 -0
- package/exports/hosts/claude/.claude/agents/reviewer.md +54 -0
- package/exports/hosts/claude/.claude/agents/specifier.md +47 -0
- package/exports/hosts/claude/.claude/agents/verifier.md +71 -0
- package/exports/hosts/claude/.claude/commands/author.md +42 -0
- package/exports/hosts/claude/.claude/commands/clarify.md +38 -0
- package/exports/hosts/claude/.claude/commands/design-review.md +46 -0
- package/exports/hosts/claude/.claude/commands/design.md +44 -0
- package/exports/hosts/claude/.claude/commands/discover.md +37 -0
- package/exports/hosts/claude/.claude/commands/execute.md +48 -0
- package/exports/hosts/claude/.claude/commands/learn.md +38 -0
- package/exports/hosts/claude/.claude/commands/plan-review.md +42 -0
- package/exports/hosts/claude/.claude/commands/plan.md +39 -0
- package/exports/hosts/claude/.claude/commands/prepare-next.md +37 -0
- package/exports/hosts/claude/.claude/commands/review.md +40 -0
- package/exports/hosts/claude/.claude/commands/run-audit.md +41 -0
- package/exports/hosts/claude/.claude/commands/spec-challenge.md +41 -0
- package/exports/hosts/claude/.claude/commands/specify.md +38 -0
- package/exports/hosts/claude/.claude/commands/verify.md +37 -0
- package/exports/hosts/claude/.claude/settings.json +34 -0
- package/exports/hosts/claude/CLAUDE.md +19 -0
- package/exports/hosts/claude/export.manifest.json +38 -0
- package/exports/hosts/claude/host-package.json +67 -0
- package/exports/hosts/codex/AGENTS.md +19 -0
- package/exports/hosts/codex/export.manifest.json +38 -0
- package/exports/hosts/codex/host-package.json +41 -0
- package/exports/hosts/cursor/.cursor/hooks.json +16 -0
- package/exports/hosts/cursor/.cursor/rules/wazir-core.mdc +19 -0
- package/exports/hosts/cursor/export.manifest.json +38 -0
- package/exports/hosts/cursor/host-package.json +42 -0
- package/exports/hosts/gemini/GEMINI.md +19 -0
- package/exports/hosts/gemini/export.manifest.json +38 -0
- package/exports/hosts/gemini/host-package.json +41 -0
- package/hooks/README.md +18 -0
- package/hooks/definitions/loop_cap_guard.yaml +21 -0
- package/hooks/definitions/post_tool_capture.yaml +24 -0
- package/hooks/definitions/pre_compact_summary.yaml +19 -0
- package/hooks/definitions/pre_tool_capture_route.yaml +19 -0
- package/hooks/definitions/protected_path_write_guard.yaml +19 -0
- package/hooks/definitions/session_start.yaml +19 -0
- package/hooks/definitions/stop_handoff_harvest.yaml +20 -0
- package/hooks/loop-cap-guard +17 -0
- package/hooks/post-tool-lint +36 -0
- package/hooks/protected-path-write-guard +17 -0
- package/hooks/session-start +41 -0
- package/llms-full.txt +2355 -0
- package/llms.txt +43 -0
- package/package.json +79 -0
- package/roles/README.md +20 -0
- package/roles/clarifier.md +42 -0
- package/roles/content-author.md +63 -0
- package/roles/designer.md +55 -0
- package/roles/executor.md +55 -0
- package/roles/learner.md +51 -0
- package/roles/planner.md +53 -0
- package/roles/researcher.md +43 -0
- package/roles/reviewer.md +54 -0
- package/roles/specifier.md +47 -0
- package/roles/verifier.md +71 -0
- package/schemas/README.md +24 -0
- package/schemas/accepted-learning.schema.json +20 -0
- package/schemas/author-artifact.schema.json +156 -0
- package/schemas/clarification.schema.json +19 -0
- package/schemas/design-artifact.schema.json +80 -0
- package/schemas/docs-claim.schema.json +18 -0
- package/schemas/export-manifest.schema.json +20 -0
- package/schemas/hook.schema.json +67 -0
- package/schemas/host-export-package.schema.json +18 -0
- package/schemas/implementation-plan.schema.json +19 -0
- package/schemas/proposed-learning.schema.json +19 -0
- package/schemas/research.schema.json +18 -0
- package/schemas/review.schema.json +29 -0
- package/schemas/run-manifest.schema.json +18 -0
- package/schemas/spec-challenge.schema.json +18 -0
- package/schemas/spec.schema.json +20 -0
- package/schemas/usage.schema.json +102 -0
- package/schemas/verification-proof.schema.json +29 -0
- package/schemas/wazir-manifest.schema.json +173 -0
- package/skills/README.md +40 -0
- package/skills/brainstorming/SKILL.md +77 -0
- package/skills/debugging/SKILL.md +50 -0
- package/skills/design/SKILL.md +61 -0
- package/skills/dispatching-parallel-agents/SKILL.md +128 -0
- package/skills/executing-plans/SKILL.md +70 -0
- package/skills/finishing-a-development-branch/SKILL.md +169 -0
- package/skills/humanize/SKILL.md +123 -0
- package/skills/init-pipeline/SKILL.md +124 -0
- package/skills/prepare-next/SKILL.md +20 -0
- package/skills/receiving-code-review/SKILL.md +123 -0
- package/skills/requesting-code-review/SKILL.md +105 -0
- package/skills/requesting-code-review/code-reviewer.md +108 -0
- package/skills/run-audit/SKILL.md +197 -0
- package/skills/scan-project/SKILL.md +41 -0
- package/skills/self-audit/SKILL.md +153 -0
- package/skills/subagent-driven-development/SKILL.md +154 -0
- package/skills/subagent-driven-development/code-quality-reviewer-prompt.md +26 -0
- package/skills/subagent-driven-development/implementer-prompt.md +102 -0
- package/skills/subagent-driven-development/spec-reviewer-prompt.md +61 -0
- package/skills/tdd/SKILL.md +23 -0
- package/skills/using-git-worktrees/SKILL.md +163 -0
- package/skills/using-skills/SKILL.md +95 -0
- package/skills/verification/SKILL.md +22 -0
- package/skills/wazir/SKILL.md +463 -0
- package/skills/writing-plans/SKILL.md +30 -0
- package/skills/writing-skills/SKILL.md +157 -0
- package/skills/writing-skills/anthropic-best-practices.md +122 -0
- package/skills/writing-skills/persuasion-principles.md +50 -0
- package/templates/README.md +20 -0
- package/templates/artifacts/README.md +10 -0
- package/templates/artifacts/accepted-learning.md +19 -0
- package/templates/artifacts/accepted-learning.template.json +12 -0
- package/templates/artifacts/author.md +74 -0
- package/templates/artifacts/author.template.json +19 -0
- package/templates/artifacts/clarification.md +21 -0
- package/templates/artifacts/clarification.template.json +12 -0
- package/templates/artifacts/execute-notes.md +19 -0
- package/templates/artifacts/implementation-plan.md +21 -0
- package/templates/artifacts/implementation-plan.template.json +11 -0
- package/templates/artifacts/learning-proposal.md +19 -0
- package/templates/artifacts/next-run-handoff.md +21 -0
- package/templates/artifacts/plan-review.md +19 -0
- package/templates/artifacts/proposed-learning.template.json +12 -0
- package/templates/artifacts/research.md +21 -0
- package/templates/artifacts/research.template.json +12 -0
- package/templates/artifacts/review-findings.md +19 -0
- package/templates/artifacts/review.template.json +11 -0
- package/templates/artifacts/run-manifest.template.json +8 -0
- package/templates/artifacts/spec-challenge.md +19 -0
- package/templates/artifacts/spec-challenge.template.json +11 -0
- package/templates/artifacts/spec.md +21 -0
- package/templates/artifacts/spec.template.json +12 -0
- package/templates/artifacts/verification-proof.md +19 -0
- package/templates/artifacts/verification-proof.template.json +11 -0
- package/templates/examples/accepted-learning.example.json +14 -0
- package/templates/examples/author.example.json +152 -0
- package/templates/examples/clarification.example.json +15 -0
- package/templates/examples/docs-claim.example.json +8 -0
- package/templates/examples/export-manifest.example.json +7 -0
- package/templates/examples/host-export-package.example.json +11 -0
- package/templates/examples/implementation-plan.example.json +17 -0
- package/templates/examples/proposed-learning.example.json +13 -0
- package/templates/examples/research.example.json +15 -0
- package/templates/examples/research.example.md +6 -0
- package/templates/examples/review.example.json +17 -0
- package/templates/examples/run-manifest.example.json +9 -0
- package/templates/examples/spec-challenge.example.json +14 -0
- package/templates/examples/spec.example.json +21 -0
- package/templates/examples/verification-proof.example.json +21 -0
- package/templates/examples/wazir-manifest.example.yaml +65 -0
- package/templates/task-definition-schema.md +99 -0
- package/tooling/README.md +20 -0
- package/tooling/src/adapters/context-mode.js +50 -0
- package/tooling/src/capture/command.js +376 -0
- package/tooling/src/capture/store.js +99 -0
- package/tooling/src/capture/usage.js +270 -0
- package/tooling/src/checks/branches.js +50 -0
- package/tooling/src/checks/brand-truth.js +110 -0
- package/tooling/src/checks/changelog.js +231 -0
- package/tooling/src/checks/command-registry.js +36 -0
- package/tooling/src/checks/commits.js +102 -0
- package/tooling/src/checks/docs-drift.js +103 -0
- package/tooling/src/checks/docs-truth.js +201 -0
- package/tooling/src/checks/runtime-surface.js +156 -0
- package/tooling/src/cli.js +116 -0
- package/tooling/src/command-options.js +56 -0
- package/tooling/src/commands/validate.js +320 -0
- package/tooling/src/doctor/command.js +91 -0
- package/tooling/src/export/command.js +77 -0
- package/tooling/src/export/compiler.js +498 -0
- package/tooling/src/guards/loop-cap-guard.js +52 -0
- package/tooling/src/guards/protected-path-write-guard.js +67 -0
- package/tooling/src/index/command.js +152 -0
- package/tooling/src/index/storage.js +1061 -0
- package/tooling/src/index/summarizers.js +261 -0
- package/tooling/src/loaders.js +18 -0
- package/tooling/src/project-root.js +22 -0
- package/tooling/src/recall/command.js +225 -0
- package/tooling/src/schema-validator.js +30 -0
- package/tooling/src/state-root.js +40 -0
- package/tooling/src/status/command.js +71 -0
- package/wazir.manifest.yaml +135 -0
- package/workflows/README.md +19 -0
- package/workflows/author.md +42 -0
- package/workflows/clarify.md +38 -0
- package/workflows/design-review.md +46 -0
- package/workflows/design.md +44 -0
- package/workflows/discover.md +37 -0
- package/workflows/execute.md +48 -0
- package/workflows/learn.md +38 -0
- package/workflows/plan-review.md +42 -0
- package/workflows/plan.md +39 -0
- package/workflows/prepare-next.md +37 -0
- package/workflows/review.md +40 -0
- package/workflows/run-audit.md +41 -0
- package/workflows/spec-challenge.md +41 -0
- package/workflows/specify.md +38 -0
- package/workflows/verify.md +37 -0
|
@@ -0,0 +1,1235 @@
|
|
|
1
|
+
# SPA Anti-Patterns
|
|
2
|
+
|
|
3
|
+
> **Domain:** Frontend
|
|
4
|
+
> **Anti-patterns covered:** 20
|
|
5
|
+
> **Highest severity:** Critical
|
|
6
|
+
|
|
7
|
+
Single Page Applications promised desktop-class responsiveness in the browser. Delivered carelessly, they produce the opposite: bloated bundles, broken navigation, invisible content, inaccessible transitions, and memory that grows until the tab crashes. The core tension is that SPAs must reimplement behaviors the browser provides for free -- history, scroll restoration, focus management, resource loading, error pages -- and every reimplementation is a surface for failure.
|
|
8
|
+
|
|
9
|
+
This module catalogs the 20 most damaging SPA anti-patterns drawn from production incidents, framework issue trackers, and empirical research across 500+ repositories. Each entry is classified by frequency, severity, and detection difficulty.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Anti-Pattern Entries
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
### AP-01: SPA for Content / SEO-Dependent Sites
|
|
18
|
+
|
|
19
|
+
**Also known as:** Hammer looking for a nail, SPA-everything, client-render-all
|
|
20
|
+
**Frequency:** Very Common
|
|
21
|
+
**Severity:** Critical
|
|
22
|
+
**Detection difficulty:** Easy
|
|
23
|
+
|
|
24
|
+
**What it looks like:**
|
|
25
|
+
|
|
26
|
+
A marketing site, blog, or e-commerce catalog is built as a pure client-rendered SPA. The server delivers a near-empty HTML shell with a single `<div id="root"></div>` and a large JavaScript bundle. Search engine crawlers see no content.
|
|
27
|
+
|
|
28
|
+
```html
|
|
29
|
+
<!-- What Googlebot receives -->
|
|
30
|
+
<!DOCTYPE html>
|
|
31
|
+
<html>
|
|
32
|
+
<head><title>My Store</title></head>
|
|
33
|
+
<body>
|
|
34
|
+
<div id="root"></div>
|
|
35
|
+
<script src="/bundle.js" defer></script> <!-- 1.8 MB -->
|
|
36
|
+
</body>
|
|
37
|
+
</html>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Why developers do it:**
|
|
41
|
+
|
|
42
|
+
The team knows React/Vue/Angular and applies it uniformly. SPA frameworks are the default starting point in tutorials and bootcamps, and nobody asks "does this page need client-side rendering?" before scaffolding. Management sees a competitor's interactive dashboard and wants the same technology for their content site.
|
|
43
|
+
|
|
44
|
+
**What goes wrong:**
|
|
45
|
+
|
|
46
|
+
Google can render JavaScript but queues it in a "second wave" of indexing that can take days to weeks. Other search engines (Bing, Baidu, Yandex) have limited or no JS rendering. A pure SPA serving an empty shell can see zero pages indexed for weeks after launch. Even when eventually indexed, Core Web Vitals suffer: Largest Contentful Paint balloons to 4-8 seconds on mobile, causing ranking penalties. Social media link previews (Open Graph, Twitter Cards) show blank content because preview crawlers never execute JavaScript.
|
|
47
|
+
|
|
48
|
+
**The fix:**
|
|
49
|
+
|
|
50
|
+
Use SSR, SSG, or a hybrid framework (Next.js, Nuxt, SvelteKit, Remix) for content-heavy pages. Reserve pure client-side SPA architecture for authenticated dashboard-style applications where SEO is irrelevant.
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
Content/SEO pages --> SSR or SSG (server-rendered HTML)
|
|
54
|
+
Authenticated app --> SPA with client-side rendering (fine)
|
|
55
|
+
Hybrid --> Framework with per-route rendering strategy
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Detection rule:**
|
|
59
|
+
|
|
60
|
+
Flag any project where `robots.txt` allows crawling but the index HTML contains fewer than 50 characters of visible text content outside of `<script>` tags.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
### AP-02: No Code Splitting
|
|
65
|
+
|
|
66
|
+
**Also known as:** Monolithic bundle, one-big-chunk, webpack-ball
|
|
67
|
+
**Frequency:** Very Common
|
|
68
|
+
**Severity:** High
|
|
69
|
+
**Detection difficulty:** Easy
|
|
70
|
+
|
|
71
|
+
**What it looks like:**
|
|
72
|
+
|
|
73
|
+
The entire application -- every route, every feature, every library -- is compiled into a single JavaScript bundle. Users downloading the login page also download the admin dashboard, the report generator, and the PDF viewer.
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
// webpack.config.js -- no dynamic imports, no splitChunks
|
|
77
|
+
module.exports = {
|
|
78
|
+
entry: './src/index.js',
|
|
79
|
+
output: { filename: 'bundle.js' }
|
|
80
|
+
// splitChunks: absent
|
|
81
|
+
};
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Why developers do it:**
|
|
85
|
+
|
|
86
|
+
Code splitting adds configuration complexity. Dynamic `import()` introduces asynchronous boundaries that complicate component loading. Teams with small apps never notice the problem until the bundle silently crosses 1 MB, then 2 MB, then 5 MB.
|
|
87
|
+
|
|
88
|
+
**What goes wrong:**
|
|
89
|
+
|
|
90
|
+
Research from Google shows that 53% of mobile users abandon sites that take longer than 3 seconds to load. A 2 MB JavaScript bundle takes approximately 6 seconds to download on a 3G connection and another 2-4 seconds to parse and execute. Every user pays the cost for every feature, even features they will never use in that session. Cache invalidation becomes all-or-nothing: any code change busts the entire bundle cache.
|
|
91
|
+
|
|
92
|
+
**The fix:**
|
|
93
|
+
|
|
94
|
+
```javascript
|
|
95
|
+
// Route-based code splitting with React.lazy
|
|
96
|
+
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
|
|
97
|
+
const Settings = React.lazy(() => import('./pages/Settings'));
|
|
98
|
+
const Reports = React.lazy(() => import('./pages/Reports'));
|
|
99
|
+
|
|
100
|
+
function App() {
|
|
101
|
+
return (
|
|
102
|
+
<Suspense fallback={<LoadingSkeleton />}>
|
|
103
|
+
<Routes>
|
|
104
|
+
<Route path="/dashboard" element={<Dashboard />} />
|
|
105
|
+
<Route path="/settings" element={<Settings />} />
|
|
106
|
+
<Route path="/reports" element={<Reports />} />
|
|
107
|
+
</Routes>
|
|
108
|
+
</Suspense>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Configure `splitChunks` for vendor separation. Use `webpack-bundle-analyzer` or `source-map-explorer` to visualize what ships.
|
|
114
|
+
|
|
115
|
+
**Detection rule:**
|
|
116
|
+
|
|
117
|
+
Flag when a single JS bundle exceeds 250 KB gzipped, or when `import()` / `React.lazy` / `defineAsyncComponent` appear zero times in a project with more than 5 routes.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
### AP-03: Loading All Routes Upfront
|
|
122
|
+
|
|
123
|
+
**Also known as:** Eager route loading, route pre-registration bloat
|
|
124
|
+
**Frequency:** Common
|
|
125
|
+
**Severity:** High
|
|
126
|
+
**Detection difficulty:** Easy
|
|
127
|
+
|
|
128
|
+
**What it looks like:**
|
|
129
|
+
|
|
130
|
+
All route components are statically imported at the top of the router file, forcing the bundler to include every route in the initial chunk even when code splitting is nominally configured elsewhere.
|
|
131
|
+
|
|
132
|
+
```javascript
|
|
133
|
+
// All routes eagerly imported -- defeats code splitting
|
|
134
|
+
import Home from './pages/Home';
|
|
135
|
+
import Dashboard from './pages/Dashboard';
|
|
136
|
+
import Analytics from './pages/Analytics';
|
|
137
|
+
import Admin from './pages/Admin';
|
|
138
|
+
import UserProfile from './pages/UserProfile';
|
|
139
|
+
import Settings from './pages/Settings';
|
|
140
|
+
// ... 20 more imports
|
|
141
|
+
|
|
142
|
+
const routes = [
|
|
143
|
+
{ path: '/', component: Home },
|
|
144
|
+
{ path: '/dashboard', component: Dashboard },
|
|
145
|
+
// ...
|
|
146
|
+
];
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Why developers do it:**
|
|
150
|
+
|
|
151
|
+
Static imports are simpler and avoid the async boundary complexity of lazy loading. IDE autocomplete and type checking work better with static imports. Developers copy route configuration patterns from tutorials that use small demo apps where the distinction is irrelevant.
|
|
152
|
+
|
|
153
|
+
**What goes wrong:**
|
|
154
|
+
|
|
155
|
+
Initial load includes JavaScript for every route in the application. An admin panel that 2% of users ever visit adds its weight to every single page load. As the application grows, initial Time to Interactive regresses linearly with route count. Build analysis shows all page code in a single chunk despite having a router.
|
|
156
|
+
|
|
157
|
+
**The fix:**
|
|
158
|
+
|
|
159
|
+
```javascript
|
|
160
|
+
// Vue Router with lazy loading
|
|
161
|
+
const routes = [
|
|
162
|
+
{ path: '/', component: () => import('./pages/Home.vue') },
|
|
163
|
+
{ path: '/dashboard', component: () => import('./pages/Dashboard.vue') },
|
|
164
|
+
{ path: '/admin', component: () => import('./pages/Admin.vue') },
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
// Angular with loadChildren
|
|
168
|
+
const routes: Routes = [
|
|
169
|
+
{ path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) }
|
|
170
|
+
];
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Detection rule:**
|
|
174
|
+
|
|
175
|
+
In the router configuration file, count static `import` statements for page/view components. Flag if more than 3 route components are statically imported.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
### AP-04: No Server Fallback Routing
|
|
180
|
+
|
|
181
|
+
**Also known as:** 404 on refresh, direct-link death, missing catch-all
|
|
182
|
+
**Frequency:** Very Common
|
|
183
|
+
**Severity:** Critical
|
|
184
|
+
**Detection difficulty:** Easy
|
|
185
|
+
|
|
186
|
+
**What it looks like:**
|
|
187
|
+
|
|
188
|
+
A user navigates to `/dashboard/settings` within the SPA, bookmarks it, and later opens the bookmark directly. The server returns a 404 because no physical file exists at that path. The SPA router never gets a chance to handle it.
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
User requests: GET /dashboard/settings
|
|
192
|
+
Server looks for: /dashboard/settings/index.html --> does not exist
|
|
193
|
+
Server returns: 404 Not Found
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Why developers do it:**
|
|
197
|
+
|
|
198
|
+
Development servers (Vite, webpack-dev-server) automatically fall back to `index.html` for all routes, masking the problem entirely. Developers never encounter it until the first production deployment, and sometimes not until a user reports it.
|
|
199
|
+
|
|
200
|
+
**What goes wrong:**
|
|
201
|
+
|
|
202
|
+
Every deep link, bookmark, and shared URL returns a 404 in production. Browser refresh on any non-root route fails. Search engine crawlers get 404s for every interior URL they discover. The application appears to work perfectly in development and breaks immediately in production.
|
|
203
|
+
|
|
204
|
+
**The fix:**
|
|
205
|
+
|
|
206
|
+
Configure the server to serve `index.html` for all routes that do not match a static asset:
|
|
207
|
+
|
|
208
|
+
```nginx
|
|
209
|
+
# Nginx
|
|
210
|
+
location / {
|
|
211
|
+
try_files $uri $uri/ /index.html;
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
```apache
|
|
216
|
+
# Apache .htaccess
|
|
217
|
+
RewriteEngine On
|
|
218
|
+
RewriteBase /
|
|
219
|
+
RewriteRule ^index\.html$ - [L]
|
|
220
|
+
RewriteCond %{REQUEST_FILENAME} !-f
|
|
221
|
+
RewriteCond %{REQUEST_FILENAME} !-d
|
|
222
|
+
RewriteRule . /index.html [L]
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
```javascript
|
|
226
|
+
// Express
|
|
227
|
+
app.get('*', (req, res) => {
|
|
228
|
+
res.sendFile(path.join(__dirname, 'build', 'index.html'));
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**Detection rule:**
|
|
233
|
+
|
|
234
|
+
After deployment, request any known SPA route directly via `curl`. Flag if the HTTP status code is anything other than 200.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
### AP-05: SPA Memory Leaks
|
|
239
|
+
|
|
240
|
+
**Also known as:** Heap creep, tab-killer, navigation leak
|
|
241
|
+
**Frequency:** Very Common
|
|
242
|
+
**Severity:** Critical
|
|
243
|
+
**Detection difficulty:** Hard
|
|
244
|
+
|
|
245
|
+
**What it looks like:**
|
|
246
|
+
|
|
247
|
+
Memory usage grows with every route navigation and never reclaims. After 20-30 navigations, the browser tab becomes sluggish. After 100+, it crashes. A study across 500 repositories found 22,384 instances of `setTimeout` inside component code without a corresponding `clearTimeout` on unmount, and 10,616 instances of `addEventListener` without `removeEventListener`.
|
|
248
|
+
|
|
249
|
+
```javascript
|
|
250
|
+
// React component that leaks on every mount
|
|
251
|
+
function LiveDashboard() {
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
const ws = new WebSocket('wss://api.example.com/stream');
|
|
254
|
+
const interval = setInterval(fetchMetrics, 5000);
|
|
255
|
+
window.addEventListener('resize', handleResize);
|
|
256
|
+
|
|
257
|
+
// NO cleanup function returned -- all three leak on unmount
|
|
258
|
+
}, []);
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Why developers do it:**
|
|
263
|
+
|
|
264
|
+
The component works perfectly on first render. Developers test by loading the page once, verifying it works, and moving on. Memory leaks are invisible until cumulative, and most testing workflows never navigate back and forth enough to trigger symptoms. The cleanup return in `useEffect` feels optional because omitting it produces no error or warning.
|
|
265
|
+
|
|
266
|
+
**What goes wrong:**
|
|
267
|
+
|
|
268
|
+
Each route change mounts and unmounts components. Without cleanup, every mount adds event listeners, timers, WebSocket connections, and closure-captured DOM references that persist after unmount. Facebook's engineering team built MemLab specifically to detect this pattern after finding it pervasive in their own SPA. Heap grows linearly with user navigation. In long-lived sessions (dashboards, internal tools), this degrades to tab crashes within hours.
|
|
269
|
+
|
|
270
|
+
**The fix:**
|
|
271
|
+
|
|
272
|
+
```javascript
|
|
273
|
+
function LiveDashboard() {
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
const ws = new WebSocket('wss://api.example.com/stream');
|
|
276
|
+
const interval = setInterval(fetchMetrics, 5000);
|
|
277
|
+
window.addEventListener('resize', handleResize);
|
|
278
|
+
|
|
279
|
+
return () => { // Cleanup: runs on unmount
|
|
280
|
+
ws.close();
|
|
281
|
+
clearInterval(interval);
|
|
282
|
+
window.removeEventListener('resize', handleResize);
|
|
283
|
+
};
|
|
284
|
+
}, []);
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Use Chrome DevTools Memory tab to take heap snapshots before and after navigation cycles. Automate with MemLab for CI.
|
|
289
|
+
|
|
290
|
+
**Detection rule:**
|
|
291
|
+
|
|
292
|
+
In component lifecycle code, flag every `addEventListener`, `setInterval`, `setTimeout`, `new WebSocket`, `new EventSource`, or `.subscribe()` that lacks a corresponding cleanup in the same scope. ESLint rule: `react-hooks/exhaustive-deps` catches some cases; custom rules or MemLab catch the rest.
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
### AP-06: Breaking the Back Button
|
|
297
|
+
|
|
298
|
+
**Also known as:** History hijacking, navigation trap, phantom history entries
|
|
299
|
+
**Frequency:** Common
|
|
300
|
+
**Severity:** High
|
|
301
|
+
**Detection difficulty:** Moderate
|
|
302
|
+
|
|
303
|
+
**What it looks like:**
|
|
304
|
+
|
|
305
|
+
The user clicks the browser back button and nothing happens -- or worse, the app navigates to an unexpected state. Common variants: modals that push history entries (back closes modal instead of navigating), multi-step wizards that do not push entries (back skips multiple steps), and `history.replaceState` used where `pushState` is appropriate.
|
|
306
|
+
|
|
307
|
+
```javascript
|
|
308
|
+
// Modal pushes a history entry -- back button closes modal instead of navigating
|
|
309
|
+
function openModal() {
|
|
310
|
+
setModalOpen(true);
|
|
311
|
+
window.history.pushState({ modal: true }, ''); // Pollutes history
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Wizard steps don't push entries -- back button skips entire wizard
|
|
315
|
+
function goToStep(n) {
|
|
316
|
+
setCurrentStep(n); // No history.pushState -- browser knows nothing about steps
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**Why developers do it:**
|
|
321
|
+
|
|
322
|
+
The History API is subtle. `pushState` vs. `replaceState`, when to add entries, what constitutes a "navigation" vs. a "state change" -- these are UX design decisions that often fall to developers with no UX guidance. Mobile web adds complexity because the back button is a hardware/OS control that users reach for reflexively.
|
|
323
|
+
|
|
324
|
+
**What goes wrong:**
|
|
325
|
+
|
|
326
|
+
Users become trapped in modal loops, unable to navigate backward. Or they lose multi-step form progress because back skips past the wizard entirely. On mobile, where the back button is the primary navigation affordance, broken back-button behavior drives 15-20% higher bounce rates according to UX research by the Baymard Institute.
|
|
327
|
+
|
|
328
|
+
**The fix:**
|
|
329
|
+
|
|
330
|
+
Establish a clear policy: any state change that the user perceives as "I went somewhere" should push a history entry. Modal opens and tab switches generally should not. Multi-step flows should push one entry per meaningful step.
|
|
331
|
+
|
|
332
|
+
```javascript
|
|
333
|
+
// Wizard: push state for each step
|
|
334
|
+
function goToStep(n) {
|
|
335
|
+
setCurrentStep(n);
|
|
336
|
+
window.history.pushState({ step: n }, '', `?step=${n}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
window.addEventListener('popstate', (e) => {
|
|
340
|
+
if (e.state?.step != null) setCurrentStep(e.state.step);
|
|
341
|
+
});
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**Detection rule:**
|
|
345
|
+
|
|
346
|
+
Audit all calls to `pushState` and `replaceState`. Flag modals and dialogs that call `pushState`. Flag multi-step flows that never call `pushState`.
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
### AP-07: Deep Linking Failures
|
|
351
|
+
|
|
352
|
+
**Also known as:** State-not-in-URL, un-shareable state, ephemeral views
|
|
353
|
+
**Frequency:** Common
|
|
354
|
+
**Severity:** High
|
|
355
|
+
**Detection difficulty:** Moderate
|
|
356
|
+
|
|
357
|
+
**What it looks like:**
|
|
358
|
+
|
|
359
|
+
Application state that a user would want to share or bookmark is stored only in component state or a global store, not reflected in the URL. Filters, search queries, pagination, tab selections, and modal contents are invisible to the URL bar.
|
|
360
|
+
|
|
361
|
+
```javascript
|
|
362
|
+
// Filters exist only in React state -- URL is always just /products
|
|
363
|
+
function ProductList() {
|
|
364
|
+
const [category, setCategory] = useState('all');
|
|
365
|
+
const [sort, setSort] = useState('price-asc');
|
|
366
|
+
const [page, setPage] = useState(1);
|
|
367
|
+
// URL never changes: always /products
|
|
368
|
+
// Sharing the link shares the default state, not the user's view
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**Why developers do it:**
|
|
373
|
+
|
|
374
|
+
URL synchronization adds complexity: encoding/decoding query params, handling invalid values, maintaining two-way binding between URL and state. Component state is simpler and faster to implement. The developer tests by clicking through the UI and never tries sharing a link.
|
|
375
|
+
|
|
376
|
+
**What goes wrong:**
|
|
377
|
+
|
|
378
|
+
Users cannot share filtered views with colleagues. Bookmarking captures only the base URL. Browser refresh loses all applied filters, selections, and pagination. Customer support cannot reproduce user-reported issues because the state cannot be communicated via URL. Analytics cannot distinguish between different filtered views.
|
|
379
|
+
|
|
380
|
+
**The fix:**
|
|
381
|
+
|
|
382
|
+
```javascript
|
|
383
|
+
// Sync meaningful UI state to URL search params
|
|
384
|
+
function ProductList() {
|
|
385
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
386
|
+
const category = searchParams.get('category') || 'all';
|
|
387
|
+
const sort = searchParams.get('sort') || 'price-asc';
|
|
388
|
+
const page = Number(searchParams.get('page')) || 1;
|
|
389
|
+
|
|
390
|
+
function updateFilters(updates) {
|
|
391
|
+
setSearchParams(prev => {
|
|
392
|
+
const next = new URLSearchParams(prev);
|
|
393
|
+
Object.entries(updates).forEach(([k, v]) => next.set(k, v));
|
|
394
|
+
return next;
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
// URL: /products?category=shoes&sort=price-asc&page=3 -- shareable, bookmarkable
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
**Detection rule:**
|
|
402
|
+
|
|
403
|
+
Identify filter/search/pagination state in components. Flag when this state uses `useState` or a store but no corresponding URL parameter synchronization exists.
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
### AP-08: No Scroll Position Preservation
|
|
408
|
+
|
|
409
|
+
**Also known as:** Scroll amnesia, scroll-to-top-on-back, lost position
|
|
410
|
+
**Frequency:** Common
|
|
411
|
+
**Severity:** Medium
|
|
412
|
+
**Detection difficulty:** Moderate
|
|
413
|
+
|
|
414
|
+
**What it looks like:**
|
|
415
|
+
|
|
416
|
+
A user scrolls down a long list, clicks an item to view details, then presses back. Instead of returning to their scroll position in the list, the page resets to the top. The user must scroll through the entire list again to find where they were.
|
|
417
|
+
|
|
418
|
+
**Why developers do it:**
|
|
419
|
+
|
|
420
|
+
Multi-page websites get scroll restoration from the browser for free via the `bfcache`. SPAs bypass this mechanism entirely by using `history.pushState` for navigation. The browser does not know the page "changed" in a way that warrants scroll storage. Most SPA routers default to scroll-to-top on navigation (which is correct for forward navigation) but do not differentiate between forward and back navigation.
|
|
421
|
+
|
|
422
|
+
**What goes wrong:**
|
|
423
|
+
|
|
424
|
+
Users on long lists (search results, feeds, catalogs) lose their place on every back navigation. This is especially punishing on mobile where scrolling is the primary interaction. Users learn to open links in new tabs as a workaround, defeating the SPA's navigation model entirely. Next.js had a long-standing issue (#3303) about scroll restoration firing before content rendered, causing incorrect positions.
|
|
425
|
+
|
|
426
|
+
**The fix:**
|
|
427
|
+
|
|
428
|
+
```javascript
|
|
429
|
+
// Manual scroll restoration
|
|
430
|
+
const scrollPositions = useRef({});
|
|
431
|
+
|
|
432
|
+
useEffect(() => {
|
|
433
|
+
// Save scroll position before leaving
|
|
434
|
+
const saveScroll = () => {
|
|
435
|
+
scrollPositions.current[location.key] = window.scrollY;
|
|
436
|
+
};
|
|
437
|
+
window.addEventListener('beforeunload', saveScroll);
|
|
438
|
+
|
|
439
|
+
// Restore on popstate (back/forward)
|
|
440
|
+
const restoreScroll = () => {
|
|
441
|
+
const saved = scrollPositions.current[location.key];
|
|
442
|
+
if (saved != null) {
|
|
443
|
+
requestAnimationFrame(() => window.scrollTo(0, saved));
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
return () => window.removeEventListener('beforeunload', saveScroll);
|
|
448
|
+
}, [location.key]);
|
|
449
|
+
|
|
450
|
+
// Or use router-level support:
|
|
451
|
+
// React Router: <ScrollRestoration />
|
|
452
|
+
// Vue Router: scrollBehavior option
|
|
453
|
+
// Next.js: experimental.scrollRestoration: true
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
**Detection rule:**
|
|
457
|
+
|
|
458
|
+
Navigate to a scrollable list, scroll down, click a detail link, press back. If scroll position resets to top, the anti-pattern is present.
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
### AP-09: Client-Side Overload
|
|
463
|
+
|
|
464
|
+
**Also known as:** Browser-as-server, client-does-everything, fat client
|
|
465
|
+
**Frequency:** Common
|
|
466
|
+
**Severity:** High
|
|
467
|
+
**Detection difficulty:** Moderate
|
|
468
|
+
|
|
469
|
+
**What it looks like:**
|
|
470
|
+
|
|
471
|
+
Business logic that belongs on the server -- data aggregation, complex filtering, sorting large datasets, PDF generation, image processing -- runs in the browser. The client fetches raw data and processes it locally.
|
|
472
|
+
|
|
473
|
+
```javascript
|
|
474
|
+
// Fetching ALL products to filter and sort on the client
|
|
475
|
+
async function getFilteredProducts(category, sortBy) {
|
|
476
|
+
const response = await fetch('/api/products'); // Returns 50,000 products
|
|
477
|
+
const allProducts = await response.json(); // 12 MB JSON payload
|
|
478
|
+
const filtered = allProducts.filter(p => p.category === category);
|
|
479
|
+
const sorted = filtered.sort((a, b) => a[sortBy] - b[sortBy]);
|
|
480
|
+
return sorted.slice(0, 20); // User sees 20 items
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
**Why developers do it:**
|
|
485
|
+
|
|
486
|
+
It is faster to write a client-side filter than to add a query parameter to an API endpoint. In early development, datasets are small and everything is fast. The API might be owned by another team with a long lead time for changes.
|
|
487
|
+
|
|
488
|
+
**What goes wrong:**
|
|
489
|
+
|
|
490
|
+
Mobile devices with limited memory and CPU struggle with large datasets. A 12 MB JSON response on 3G takes 40+ seconds to download. Parsing and processing blocks the main thread, freezing the UI. Battery drain accelerates. Users on lower-end devices experience crashes. The server sends far more data than needed, wasting bandwidth and increasing costs.
|
|
491
|
+
|
|
492
|
+
**The fix:**
|
|
493
|
+
|
|
494
|
+
Move filtering, sorting, and pagination to the server. The client sends parameters; the server returns only the matching page.
|
|
495
|
+
|
|
496
|
+
```javascript
|
|
497
|
+
async function getFilteredProducts(category, sortBy, page = 1) {
|
|
498
|
+
const params = new URLSearchParams({ category, sortBy, page, limit: 20 });
|
|
499
|
+
const response = await fetch(`/api/products?${params}`);
|
|
500
|
+
return response.json(); // Returns only the 20 items needed
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
**Detection rule:**
|
|
505
|
+
|
|
506
|
+
Flag API calls that return collections without pagination parameters. Flag client-side `.filter()` or `.sort()` on arrays that originate from API responses and exceed 100 items.
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
### AP-10: Not Using SSR When You Should
|
|
511
|
+
|
|
512
|
+
**Also known as:** CSR-only, missing server render, blank-page-until-JS
|
|
513
|
+
**Frequency:** Common
|
|
514
|
+
**Severity:** High
|
|
515
|
+
**Detection difficulty:** Easy
|
|
516
|
+
|
|
517
|
+
**What it looks like:**
|
|
518
|
+
|
|
519
|
+
Pages that need to be fast, crawlable, or accessible on slow devices are rendered entirely on the client. The user sees a blank white screen (or a spinner) until the JavaScript bundle downloads, parses, and executes.
|
|
520
|
+
|
|
521
|
+
**Why developers do it:**
|
|
522
|
+
|
|
523
|
+
SSR adds architectural complexity: server runtime, hydration mismatches, state serialization, deployment constraints. Pure client-side rendering is simpler to deploy (static file hosting). Teams that started with Create React App or vanilla Vite setups have no SSR infrastructure and adding it later is a significant migration.
|
|
524
|
+
|
|
525
|
+
**What goes wrong:**
|
|
526
|
+
|
|
527
|
+
First Contentful Paint on a CSR-only page is gated by bundle download + parse + execute. On a median mobile device over 4G, this can be 3-5 seconds of blank screen. Users on slow connections see nothing for 8-15 seconds. Search engines may not index content. Accessibility tools that rely on initial HTML structure find nothing to parse. Social media previews show blank cards.
|
|
528
|
+
|
|
529
|
+
**The fix:**
|
|
530
|
+
|
|
531
|
+
Adopt a framework with built-in SSR/SSG capabilities. Apply SSR selectively to routes that need it:
|
|
532
|
+
|
|
533
|
+
```
|
|
534
|
+
Public pages (landing, blog, product) --> SSR or SSG
|
|
535
|
+
Authenticated dashboard --> CSR (fine)
|
|
536
|
+
Marketing pages --> SSG with revalidation
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
Use streaming SSR (React 18 `renderToPipeableStream`, Nuxt `renderToStream`) to send HTML progressively and reduce Time to First Byte.
|
|
540
|
+
|
|
541
|
+
**Detection rule:**
|
|
542
|
+
|
|
543
|
+
Fetch any public-facing page with JavaScript disabled (`curl` or browser with JS off). If the page is blank or shows only a loading indicator, SSR is missing where it is likely needed.
|
|
544
|
+
|
|
545
|
+
---
|
|
546
|
+
|
|
547
|
+
### AP-11: Auth State Only in Memory
|
|
548
|
+
|
|
549
|
+
**Also known as:** Volatile auth, refresh-kills-session, RAM-only tokens
|
|
550
|
+
**Frequency:** Common
|
|
551
|
+
**Severity:** Critical
|
|
552
|
+
**Detection difficulty:** Moderate
|
|
553
|
+
|
|
554
|
+
**What it looks like:**
|
|
555
|
+
|
|
556
|
+
Authentication tokens (JWT, session ID) are stored only in JavaScript variables or React/Vue state. A page refresh clears the token, logging the user out. Opening a new tab starts an unauthenticated session.
|
|
557
|
+
|
|
558
|
+
```javascript
|
|
559
|
+
// Token lives only in React state -- refresh kills it
|
|
560
|
+
function AuthProvider({ children }) {
|
|
561
|
+
const [token, setToken] = useState(null); // Gone on refresh
|
|
562
|
+
|
|
563
|
+
async function login(credentials) {
|
|
564
|
+
const { accessToken } = await api.login(credentials);
|
|
565
|
+
setToken(accessToken); // Stored in RAM only
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
**Why developers do it:**
|
|
571
|
+
|
|
572
|
+
Storing tokens in memory is the simplest approach and avoids the security considerations of `localStorage` (XSS-accessible) and cookies (CSRF, configuration complexity). Blog posts warning about `localStorage` XSS risks push developers toward memory-only storage without explaining the UX consequences.
|
|
573
|
+
|
|
574
|
+
**What goes wrong:**
|
|
575
|
+
|
|
576
|
+
Every page refresh requires re-authentication. Opening the app in a new tab requires re-login. Users lose in-progress work when they accidentally refresh. Redirects during OAuth flows clear the token. If the token is also used for WebSocket connections, those break on any page lifecycle event.
|
|
577
|
+
|
|
578
|
+
**The fix:**
|
|
579
|
+
|
|
580
|
+
Use HTTP-only, Secure, SameSite cookies for session tokens (immune to XSS, persist across refreshes). For SPAs that must use JWTs, implement the Token Handler Pattern: a lightweight backend-for-frontend (BFF) proxy that stores tokens in secure cookies and relays them to APIs.
|
|
581
|
+
|
|
582
|
+
```javascript
|
|
583
|
+
// BFF pattern: cookie holds the session, BFF forwards to API
|
|
584
|
+
// Browser <-> BFF (cookie-based session) <-> API (Bearer token)
|
|
585
|
+
|
|
586
|
+
// If cookies are not possible, use sessionStorage as a middle ground:
|
|
587
|
+
function AuthProvider({ children }) {
|
|
588
|
+
const [token, setToken] = useState(() => sessionStorage.getItem('token'));
|
|
589
|
+
|
|
590
|
+
function login(credentials) {
|
|
591
|
+
const { accessToken } = await api.login(credentials);
|
|
592
|
+
sessionStorage.setItem('token', accessToken);
|
|
593
|
+
setToken(accessToken);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
// sessionStorage: survives refresh, scoped to tab, cleared on tab close
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
**Detection rule:**
|
|
600
|
+
|
|
601
|
+
Search for auth token storage. Flag tokens stored only in `useState`, `useRef`, module-level variables, or Redux/Vuex/Pinia state without persistence to `sessionStorage`, `localStorage`, or cookies.
|
|
602
|
+
|
|
603
|
+
---
|
|
604
|
+
|
|
605
|
+
### AP-12: No Offline Handling
|
|
606
|
+
|
|
607
|
+
**Also known as:** Online-only SPA, network-or-nothing, airplane mode crash
|
|
608
|
+
**Frequency:** Common
|
|
609
|
+
**Severity:** Medium
|
|
610
|
+
**Detection difficulty:** Easy
|
|
611
|
+
|
|
612
|
+
**What it looks like:**
|
|
613
|
+
|
|
614
|
+
The application makes no provision for network loss. API calls fail silently or show uncaught promise rejection errors. The UI freezes, shows a white screen, or displays cryptic error messages. Previously loaded content disappears.
|
|
615
|
+
|
|
616
|
+
```javascript
|
|
617
|
+
// No error handling, no offline detection, no caching
|
|
618
|
+
async function loadDashboard() {
|
|
619
|
+
const data = await fetch('/api/dashboard'); // Throws on network failure
|
|
620
|
+
return data.json(); // Uncaught if fetch throws
|
|
621
|
+
}
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
**Why developers do it:**
|
|
625
|
+
|
|
626
|
+
Developers work on stable office/home networks and rarely test with network toggled off. Offline handling is seen as a progressive enhancement, not a baseline requirement. Service Worker configuration is complex and error-prone.
|
|
627
|
+
|
|
628
|
+
**What goes wrong:**
|
|
629
|
+
|
|
630
|
+
Mobile users lose connectivity constantly: elevators, tunnels, spotty cell coverage. A commuter using the app on a train sees the app break repeatedly. Form data entered during an offline moment is lost. Previously displayed content vanishes when a re-fetch fails, even though the data was already in memory. Users have no indication whether the app is offline or the server is down.
|
|
631
|
+
|
|
632
|
+
**The fix:**
|
|
633
|
+
|
|
634
|
+
```javascript
|
|
635
|
+
// 1. Detect network status and show indicator
|
|
636
|
+
function useOnlineStatus() {
|
|
637
|
+
const [isOnline, setIsOnline] = useState(navigator.onLine);
|
|
638
|
+
useEffect(() => {
|
|
639
|
+
const goOnline = () => setIsOnline(true);
|
|
640
|
+
const goOffline = () => setIsOnline(false);
|
|
641
|
+
window.addEventListener('online', goOnline);
|
|
642
|
+
window.addEventListener('offline', goOffline);
|
|
643
|
+
return () => {
|
|
644
|
+
window.removeEventListener('online', goOnline);
|
|
645
|
+
window.removeEventListener('offline', goOffline);
|
|
646
|
+
};
|
|
647
|
+
}, []);
|
|
648
|
+
return isOnline;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// 2. Cache API responses (SWR, React Query, or Service Worker)
|
|
652
|
+
// 3. Queue mutations during offline (IndexedDB + background sync)
|
|
653
|
+
// 4. Show stale data with "offline" badge rather than blank screen
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
**Detection rule:**
|
|
657
|
+
|
|
658
|
+
Toggle network off in DevTools. If the application shows an unhandled error, blank screen, or loses previously displayed content, offline handling is absent.
|
|
659
|
+
|
|
660
|
+
---
|
|
661
|
+
|
|
662
|
+
### AP-13: Huge Initial Bundles
|
|
663
|
+
|
|
664
|
+
**Also known as:** Kitchen-sink bundle, dependency bloat, node_modules-in-browser
|
|
665
|
+
**Frequency:** Very Common
|
|
666
|
+
**Severity:** High
|
|
667
|
+
**Detection difficulty:** Easy
|
|
668
|
+
|
|
669
|
+
**What it looks like:**
|
|
670
|
+
|
|
671
|
+
The application ships massive dependencies that dwarf the application code itself. `moment.js` (300 KB) imported for a single date format. `lodash` (70 KB) imported for one utility function. Full icon libraries imported for 5 icons. Multiple charting libraries included when one suffices.
|
|
672
|
+
|
|
673
|
+
```javascript
|
|
674
|
+
import moment from 'moment'; // 300 KB for format()
|
|
675
|
+
import _ from 'lodash'; // 70 KB for _.debounce
|
|
676
|
+
import * as Icons from 'heroicons'; // All 800 icons for 5 used
|
|
677
|
+
import Chart from 'chart.js/auto'; // Full Chart.js for one bar chart
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
**Why developers do it:**
|
|
681
|
+
|
|
682
|
+
Default imports are the simplest syntax. Tree-shakeable imports require knowing the library's module structure. Package.json dependencies accumulate over time as features are added but bundles are never audited. Nobody is assigned ownership of bundle size.
|
|
683
|
+
|
|
684
|
+
**What goes wrong:**
|
|
685
|
+
|
|
686
|
+
A study of production SPAs found that JavaScript bundles averaging 2 MB+ are common, with some exceeding 5 MB. On mobile 3G, each additional 100 KB of JavaScript adds roughly 350ms of load time. Parse and compile time on low-end devices is even worse: V8 on a budget Android phone processes JavaScript at roughly 1 MB/second. Users on emerging-market devices wait 5-10 seconds staring at a blank screen.
|
|
687
|
+
|
|
688
|
+
**The fix:**
|
|
689
|
+
|
|
690
|
+
```javascript
|
|
691
|
+
// Tree-shakeable imports
|
|
692
|
+
import { format } from 'date-fns'; // 2 KB vs 300 KB
|
|
693
|
+
import debounce from 'lodash/debounce'; // 1 KB vs 70 KB
|
|
694
|
+
import { ArrowLeft } from '@heroicons/react/24/outline'; // Single icon
|
|
695
|
+
import { Bar } from 'react-chartjs-2'; // Just bar chart
|
|
696
|
+
|
|
697
|
+
// Add bundle analysis to CI
|
|
698
|
+
// package.json
|
|
699
|
+
{
|
|
700
|
+
"scripts": {
|
|
701
|
+
"analyze": "webpack-bundle-analyzer dist/stats.json"
|
|
702
|
+
},
|
|
703
|
+
"bundlesize": [
|
|
704
|
+
{ "path": "dist/*.js", "maxSize": "250 kB" }
|
|
705
|
+
]
|
|
706
|
+
}
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
**Detection rule:**
|
|
710
|
+
|
|
711
|
+
Run `npx webpack-bundle-analyzer` or `npx source-map-explorer`. Flag any single dependency exceeding 50 KB gzipped, or total initial JS exceeding 250 KB gzipped.
|
|
712
|
+
|
|
713
|
+
---
|
|
714
|
+
|
|
715
|
+
### AP-14: No 404 / Error Route Handling
|
|
716
|
+
|
|
717
|
+
**Also known as:** Silent navigation failure, blank unknown routes, catch-all void
|
|
718
|
+
**Frequency:** Common
|
|
719
|
+
**Severity:** Medium
|
|
720
|
+
**Detection difficulty:** Easy
|
|
721
|
+
|
|
722
|
+
**What it looks like:**
|
|
723
|
+
|
|
724
|
+
A user navigates to a URL that does not match any defined route. Instead of seeing a helpful 404 page, they see a blank screen, the home page (confusingly), or the app crashes.
|
|
725
|
+
|
|
726
|
+
```javascript
|
|
727
|
+
// No catch-all route
|
|
728
|
+
<Routes>
|
|
729
|
+
<Route path="/" element={<Home />} />
|
|
730
|
+
<Route path="/dashboard" element={<Dashboard />} />
|
|
731
|
+
<Route path="/settings" element={<Settings />} />
|
|
732
|
+
{/* /typo-in-url renders... nothing */}
|
|
733
|
+
</Routes>
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
**Why developers do it:**
|
|
737
|
+
|
|
738
|
+
404 pages are not part of feature specs. They are invisible during normal development because developers navigate via the UI, not by typing URLs. The router silently renders nothing for unmatched routes instead of throwing an error, so nobody notices.
|
|
739
|
+
|
|
740
|
+
**What goes wrong:**
|
|
741
|
+
|
|
742
|
+
Users who mistype a URL or follow a stale link see a blank screen with no guidance. They assume the app is broken. Search engine crawlers encountering dead internal links get no 404 signal, polluting the crawl budget. Support teams cannot distinguish between "page doesn't exist" and "app crashed" reports.
|
|
743
|
+
|
|
744
|
+
**The fix:**
|
|
745
|
+
|
|
746
|
+
```javascript
|
|
747
|
+
<Routes>
|
|
748
|
+
<Route path="/" element={<Home />} />
|
|
749
|
+
<Route path="/dashboard" element={<Dashboard />} />
|
|
750
|
+
<Route path="/settings" element={<Settings />} />
|
|
751
|
+
<Route path="*" element={<NotFound />} /> {/* Catch-all */}
|
|
752
|
+
</Routes>
|
|
753
|
+
|
|
754
|
+
function NotFound() {
|
|
755
|
+
return (
|
|
756
|
+
<div role="alert">
|
|
757
|
+
<h1>Page not found</h1>
|
|
758
|
+
<p>The page you requested does not exist.</p>
|
|
759
|
+
<a href="/">Return to home</a>
|
|
760
|
+
</div>
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
For SSR apps, also set the HTTP status code to 404 so crawlers handle it correctly.
|
|
766
|
+
|
|
767
|
+
**Detection rule:**
|
|
768
|
+
|
|
769
|
+
Search router configuration for a catch-all/wildcard route (`path="*"` or `path: '*'`). Flag if absent. Manually test by navigating to a random URL path.
|
|
770
|
+
|
|
771
|
+
---
|
|
772
|
+
|
|
773
|
+
### AP-15: Everything in Client-Side Global State
|
|
774
|
+
|
|
775
|
+
**Also known as:** Global state dumping ground, Redux-for-everything, store bloat
|
|
776
|
+
**Frequency:** Common
|
|
777
|
+
**Severity:** Medium
|
|
778
|
+
**Detection difficulty:** Moderate
|
|
779
|
+
|
|
780
|
+
**What it looks like:**
|
|
781
|
+
|
|
782
|
+
Every piece of data -- server cache, form inputs, UI toggles, modal visibility, hover states -- lives in a global store (Redux, Vuex, MobX). The store has hundreds of keys. Every component connects to the store even for ephemeral local concerns.
|
|
783
|
+
|
|
784
|
+
```javascript
|
|
785
|
+
// Redux store shaped like this:
|
|
786
|
+
{
|
|
787
|
+
user: { ... },
|
|
788
|
+
products: { items: [...], loading: true },
|
|
789
|
+
ui: {
|
|
790
|
+
sidebarOpen: true,
|
|
791
|
+
modalVisible: false,
|
|
792
|
+
tooltipTarget: null,
|
|
793
|
+
dropdownExpanded: 'nav-menu',
|
|
794
|
+
hoverItem: 42,
|
|
795
|
+
formDirty: true,
|
|
796
|
+
},
|
|
797
|
+
forms: {
|
|
798
|
+
loginForm: { email: '', password: '' },
|
|
799
|
+
searchQuery: 'shoes',
|
|
800
|
+
checkoutStep: 2,
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
**Why developers do it:**
|
|
806
|
+
|
|
807
|
+
Early Redux tutorials taught "put everything in the store." Teams establish the pattern with a few legitimate use cases and then extend it by inertia. Developers find it easier to `dispatch` than to lift state or use local state because the plumbing already exists.
|
|
808
|
+
|
|
809
|
+
**What goes wrong:**
|
|
810
|
+
|
|
811
|
+
Every state change triggers the entire subscription diffing machinery. Components re-render for state changes they do not care about. Debugging becomes difficult: a "why did this re-render?" investigation requires tracing through unrelated store slices. Boilerplate explodes: actions, reducers, selectors, and types for a boolean toggle. The store becomes a second database with its own stale-data problems.
|
|
812
|
+
|
|
813
|
+
**The fix:**
|
|
814
|
+
|
|
815
|
+
Apply the state placement hierarchy: local state first, then context, then global store as a last resort. Use server-cache libraries (TanStack Query, SWR, Apollo) for API data. Reserve the global store for genuinely cross-cutting concerns (auth, theme, feature flags).
|
|
816
|
+
|
|
817
|
+
```
|
|
818
|
+
Ephemeral UI state (hover, focus, toggle) --> useState / local state
|
|
819
|
+
Form state --> Form library or useState
|
|
820
|
+
Server data --> TanStack Query / SWR
|
|
821
|
+
Cross-cutting app state --> Global store (Redux, Zustand)
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
**Detection rule:**
|
|
825
|
+
|
|
826
|
+
Count keys in the root state shape. Flag if more than 15 top-level keys exist, or if UI-only concerns (modal visibility, hover targets, dropdown states) appear in the global store.
|
|
827
|
+
|
|
828
|
+
---
|
|
829
|
+
|
|
830
|
+
### AP-16: No Prefetch or Preload
|
|
831
|
+
|
|
832
|
+
**Also known as:** Lazy-everything, fetch-on-render waterfall, no anticipation
|
|
833
|
+
**Frequency:** Common
|
|
834
|
+
**Severity:** Medium
|
|
835
|
+
**Detection difficulty:** Moderate
|
|
836
|
+
|
|
837
|
+
**What it looks like:**
|
|
838
|
+
|
|
839
|
+
Every resource -- code chunks, data, images -- is fetched only when the user explicitly navigates. Hovering over a link does nothing. Idle time is wasted. Every navigation feels slow because the fetch starts at click time.
|
|
840
|
+
|
|
841
|
+
```javascript
|
|
842
|
+
// Navigation flow:
|
|
843
|
+
// 1. User clicks link (0ms)
|
|
844
|
+
// 2. Route chunk download begins (0ms)
|
|
845
|
+
// 3. Route chunk arrives (800ms)
|
|
846
|
+
// 4. Component mounts, data fetch (800ms)
|
|
847
|
+
// 5. Data arrives (1600ms)
|
|
848
|
+
// 6. Content renders (1650ms)
|
|
849
|
+
// Total: 1650ms of staring at a spinner
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
**Why developers do it:**
|
|
853
|
+
|
|
854
|
+
Prefetching is seen as premature optimization. It adds complexity: which routes to prefetch, when to trigger it, how to avoid wasting bandwidth. Code splitting tutorials show lazy loading but rarely cover the complementary prefetch step.
|
|
855
|
+
|
|
856
|
+
**What goes wrong:**
|
|
857
|
+
|
|
858
|
+
Every navigation incurs the full serial waterfall: chunk download, then data fetch, then render. On slow connections, transitions feel sluggish. Users perceive the SPA as slower than a traditional multi-page site because the browser's built-in prefetch heuristics do not apply to client-side navigations.
|
|
859
|
+
|
|
860
|
+
**The fix:**
|
|
861
|
+
|
|
862
|
+
```javascript
|
|
863
|
+
// Prefetch route chunk on hover/focus
|
|
864
|
+
function NavLink({ to, children }) {
|
|
865
|
+
const prefetch = () => {
|
|
866
|
+
// Trigger dynamic import to preload the chunk
|
|
867
|
+
if (to === '/dashboard') import('./pages/Dashboard');
|
|
868
|
+
if (to === '/reports') import('./pages/Reports');
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
return (
|
|
872
|
+
<Link
|
|
873
|
+
to={to}
|
|
874
|
+
onMouseEnter={prefetch}
|
|
875
|
+
onFocus={prefetch}
|
|
876
|
+
>
|
|
877
|
+
{children}
|
|
878
|
+
</Link>
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Use Speculation Rules API for modern browsers
|
|
883
|
+
<script type="speculationrules">
|
|
884
|
+
{
|
|
885
|
+
"prerender": [
|
|
886
|
+
{ "where": { "href_matches": "/dashboard/*" }, "eagerness": "moderate" }
|
|
887
|
+
]
|
|
888
|
+
}
|
|
889
|
+
</script>
|
|
890
|
+
|
|
891
|
+
// Prefetch data alongside route chunk (parallel, not serial)
|
|
892
|
+
// TanStack Router, Remix, and SvelteKit support loader-based prefetching natively
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
**Detection rule:**
|
|
896
|
+
|
|
897
|
+
Inspect network tab during hover over navigation links. If no requests fire until click, prefetching is absent. Measure time from click to content-rendered for key navigations; flag if consistently above 500ms on fast connections.
|
|
898
|
+
|
|
899
|
+
---
|
|
900
|
+
|
|
901
|
+
### AP-17: Analytics Tracking Failures
|
|
902
|
+
|
|
903
|
+
**Also known as:** Missing pageviews, SPA tracking gap, single-pageview analytics
|
|
904
|
+
**Frequency:** Very Common
|
|
905
|
+
**Severity:** High
|
|
906
|
+
**Detection difficulty:** Moderate
|
|
907
|
+
|
|
908
|
+
**What it looks like:**
|
|
909
|
+
|
|
910
|
+
Google Analytics (or any analytics tool) records only the initial page load. All subsequent SPA navigations are invisible. The analytics dashboard shows 100% of sessions viewing a single page with 100% bounce rate -- despite users actively navigating the application.
|
|
911
|
+
|
|
912
|
+
```javascript
|
|
913
|
+
// GA4 snippet in index.html -- fires once on initial load
|
|
914
|
+
// All subsequent route changes are untracked
|
|
915
|
+
gtag('config', 'G-XXXXXX');
|
|
916
|
+
|
|
917
|
+
// Missing: no route change listener
|
|
918
|
+
// router.afterEach() / useEffect on location / history.listen -- absent
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
**Why developers do it:**
|
|
922
|
+
|
|
923
|
+
Standard analytics snippets are designed for multi-page sites. The default setup fires on page load, which in an SPA happens only once. Framework-specific analytics integration is a separate task that often falls through the cracks. Developers verify "analytics is installed" by checking the initial page load and stop there.
|
|
924
|
+
|
|
925
|
+
**What goes wrong:**
|
|
926
|
+
|
|
927
|
+
Product decisions are made on fundamentally wrong data. A feature that 80% of users engage with shows as 0% in analytics. Conversion funnels are blind past the landing page. Marketing cannot attribute traffic to interior pages. A/B test results are invalid because only the variant shown on initial load is tracked.
|
|
928
|
+
|
|
929
|
+
**The fix:**
|
|
930
|
+
|
|
931
|
+
```javascript
|
|
932
|
+
// React: track route changes
|
|
933
|
+
function AnalyticsTracker() {
|
|
934
|
+
const location = useLocation();
|
|
935
|
+
|
|
936
|
+
useEffect(() => {
|
|
937
|
+
gtag('event', 'page_view', {
|
|
938
|
+
page_path: location.pathname + location.search,
|
|
939
|
+
page_title: document.title,
|
|
940
|
+
});
|
|
941
|
+
}, [location]);
|
|
942
|
+
|
|
943
|
+
return null;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Vue Router
|
|
947
|
+
router.afterEach((to) => {
|
|
948
|
+
gtag('event', 'page_view', {
|
|
949
|
+
page_path: to.fullPath,
|
|
950
|
+
page_title: to.meta.title || document.title,
|
|
951
|
+
});
|
|
952
|
+
});
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
Verify in GA4 Real-Time view by navigating through multiple pages and confirming each appears.
|
|
956
|
+
|
|
957
|
+
**Detection rule:**
|
|
958
|
+
|
|
959
|
+
Open the application, navigate through 5+ routes, then check the analytics Real-Time report. If only 1 pageview appears, SPA tracking is broken.
|
|
960
|
+
|
|
961
|
+
---
|
|
962
|
+
|
|
963
|
+
### AP-18: Form State Lost on Navigation
|
|
964
|
+
|
|
965
|
+
**Also known as:** Disappearing forms, navigation data loss, unguarded routes
|
|
966
|
+
**Frequency:** Common
|
|
967
|
+
**Severity:** High
|
|
968
|
+
**Detection difficulty:** Moderate
|
|
969
|
+
|
|
970
|
+
**What it looks like:**
|
|
971
|
+
|
|
972
|
+
A user fills out a long form (profile, application, order), accidentally clicks a navigation link or presses back, and all entered data is gone. There is no confirmation prompt, no draft saving, no recovery mechanism.
|
|
973
|
+
|
|
974
|
+
```javascript
|
|
975
|
+
// User fills 15 fields over 5 minutes, clicks "Products" in nav
|
|
976
|
+
// Component unmounts, useState resets, data is gone forever
|
|
977
|
+
function ApplicationForm() {
|
|
978
|
+
const [formData, setFormData] = useState({});
|
|
979
|
+
// No beforeunload listener
|
|
980
|
+
// No navigation guard
|
|
981
|
+
// No draft persistence
|
|
982
|
+
}
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
**Why developers do it:**
|
|
986
|
+
|
|
987
|
+
Navigation guards add complexity and interrupt the "everything is a smooth transition" SPA mental model. Saving drafts requires choosing a persistence mechanism (localStorage, server, IndexedDB) and handling serialization edge cases. It is an easy feature to defer and a hard one to prioritize because it only matters when something goes wrong.
|
|
988
|
+
|
|
989
|
+
**What goes wrong:**
|
|
990
|
+
|
|
991
|
+
Users lose minutes of work. In contexts where the form data is difficult to reproduce (job applications, insurance claims, detailed orders), this causes real harm and abandonment. Users become anxious and stop trusting the application. Completion rates for multi-field forms drop significantly without draft protection.
|
|
992
|
+
|
|
993
|
+
**The fix:**
|
|
994
|
+
|
|
995
|
+
```javascript
|
|
996
|
+
function ApplicationForm() {
|
|
997
|
+
// 1. Persist draft to localStorage
|
|
998
|
+
const [formData, setFormData] = useState(() => {
|
|
999
|
+
const saved = localStorage.getItem('application-draft');
|
|
1000
|
+
return saved ? JSON.parse(saved) : {};
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
useEffect(() => {
|
|
1004
|
+
localStorage.setItem('application-draft', JSON.stringify(formData));
|
|
1005
|
+
}, [formData]);
|
|
1006
|
+
|
|
1007
|
+
// 2. Warn on navigation away
|
|
1008
|
+
const blocker = useBlocker(
|
|
1009
|
+
({ currentLocation, nextLocation }) =>
|
|
1010
|
+
isDirty && currentLocation.pathname !== nextLocation.pathname
|
|
1011
|
+
);
|
|
1012
|
+
|
|
1013
|
+
// 3. Warn on browser close/refresh
|
|
1014
|
+
useEffect(() => {
|
|
1015
|
+
const handler = (e) => {
|
|
1016
|
+
if (isDirty) { e.preventDefault(); e.returnValue = ''; }
|
|
1017
|
+
};
|
|
1018
|
+
window.addEventListener('beforeunload', handler);
|
|
1019
|
+
return () => window.removeEventListener('beforeunload', handler);
|
|
1020
|
+
}, [isDirty]);
|
|
1021
|
+
}
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
**Detection rule:**
|
|
1025
|
+
|
|
1026
|
+
Fill a form partially, then click a navigation link. If no confirmation dialog appears and data is lost on returning, the anti-pattern is present.
|
|
1027
|
+
|
|
1028
|
+
---
|
|
1029
|
+
|
|
1030
|
+
### AP-19: No Loading Skeletons / Transition Feedback
|
|
1031
|
+
|
|
1032
|
+
**Also known as:** Blank flash, spinner-only, layout shift on load
|
|
1033
|
+
**Frequency:** Common
|
|
1034
|
+
**Severity:** Medium
|
|
1035
|
+
**Detection difficulty:** Easy
|
|
1036
|
+
|
|
1037
|
+
**What it looks like:**
|
|
1038
|
+
|
|
1039
|
+
Route transitions show either nothing (blank white space) or a generic centered spinner while content loads. When content arrives, the entire layout shifts as elements pop into existence. There is no visual hint of what the page will look like.
|
|
1040
|
+
|
|
1041
|
+
```javascript
|
|
1042
|
+
// The entire page is either a spinner or the full content -- nothing in between
|
|
1043
|
+
function Dashboard() {
|
|
1044
|
+
const { data, isLoading } = useQuery('dashboard', fetchDashboard);
|
|
1045
|
+
|
|
1046
|
+
if (isLoading) return <Spinner />; // Blank page with centered spinner
|
|
1047
|
+
|
|
1048
|
+
return <DashboardContent data={data} />;
|
|
1049
|
+
}
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
**Why developers do it:**
|
|
1053
|
+
|
|
1054
|
+
A spinner is the simplest loading indicator to implement -- one component, one conditional. Skeleton screens require designing placeholder layouts that match the actual content structure. It feels like building the UI twice.
|
|
1055
|
+
|
|
1056
|
+
**What goes wrong:**
|
|
1057
|
+
|
|
1058
|
+
Perceived performance degrades sharply. Research shows that skeleton screens make users perceive load times as 15-30% shorter than spinners for the same actual duration. Without skeletons, every navigation feels like a full page load. Cumulative Layout Shift (CLS) spikes when content suddenly appears, hurting Core Web Vitals scores. Users are uncertain whether the app is working or stuck.
|
|
1059
|
+
|
|
1060
|
+
**The fix:**
|
|
1061
|
+
|
|
1062
|
+
```javascript
|
|
1063
|
+
function Dashboard() {
|
|
1064
|
+
const { data, isLoading } = useQuery('dashboard', fetchDashboard);
|
|
1065
|
+
|
|
1066
|
+
if (isLoading) return <DashboardSkeleton />;
|
|
1067
|
+
|
|
1068
|
+
return <DashboardContent data={data} />;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function DashboardSkeleton() {
|
|
1072
|
+
return (
|
|
1073
|
+
<div className="dashboard-layout">
|
|
1074
|
+
<div className="skeleton-header" aria-busy="true" />
|
|
1075
|
+
<div className="skeleton-chart" />
|
|
1076
|
+
<div className="skeleton-table">
|
|
1077
|
+
{Array.from({ length: 5 }, (_, i) => (
|
|
1078
|
+
<div key={i} className="skeleton-row" />
|
|
1079
|
+
))}
|
|
1080
|
+
</div>
|
|
1081
|
+
</div>
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
// Skeleton matches the layout of DashboardContent -- no layout shift
|
|
1085
|
+
```
|
|
1086
|
+
|
|
1087
|
+
**Detection rule:**
|
|
1088
|
+
|
|
1089
|
+
Throttle network to Slow 3G in DevTools and navigate between routes. If any route shows a blank screen or generic spinner lasting more than 200ms without a skeleton that matches the final layout, the anti-pattern is present.
|
|
1090
|
+
|
|
1091
|
+
---
|
|
1092
|
+
|
|
1093
|
+
### AP-20: Ignoring Accessibility in Route Transitions
|
|
1094
|
+
|
|
1095
|
+
**Also known as:** Silent navigation, screen-reader blindspot, focus void
|
|
1096
|
+
**Frequency:** Very Common
|
|
1097
|
+
**Severity:** Critical
|
|
1098
|
+
**Detection difficulty:** Hard
|
|
1099
|
+
|
|
1100
|
+
**What it looks like:**
|
|
1101
|
+
|
|
1102
|
+
A screen reader user clicks a navigation link. The URL changes, the DOM updates, but the screen reader announces nothing. Focus remains on the now-stale navigation link. The user has no idea that content changed. There is no page title update, no ARIA live region announcement, and no focus management.
|
|
1103
|
+
|
|
1104
|
+
```javascript
|
|
1105
|
+
// Router changes content but does nothing for assistive technology
|
|
1106
|
+
<Route path="/dashboard" element={<Dashboard />} />
|
|
1107
|
+
// Screen reader user experience:
|
|
1108
|
+
// 1. Clicks "Dashboard" link
|
|
1109
|
+
// 2. Hears: nothing
|
|
1110
|
+
// 3. Focus: still on "Dashboard" link (which is now the active page)
|
|
1111
|
+
// 4. User presses Tab: lands on... who knows?
|
|
1112
|
+
```
|
|
1113
|
+
|
|
1114
|
+
**Why developers do it:**
|
|
1115
|
+
|
|
1116
|
+
Accessibility testing is not part of the default development workflow. Most developers do not use screen readers and cannot observe the problem. SPA frameworks do not handle focus management or announcements out of the box -- unlike the browser, which announces the new page title on every full page load. The WCAG requirements for SPAs are not well understood.
|
|
1117
|
+
|
|
1118
|
+
**What goes wrong:**
|
|
1119
|
+
|
|
1120
|
+
Screen reader users -- approximately 8 million in the US alone -- cannot use the application. They hear nothing on navigation, lose track of their position in the page, and must manually explore from the top of the DOM to find new content. Tab order becomes nonsensical after a route change. This violates WCAG 2.1 Success Criteria 2.4.3 (Focus Order), 4.1.3 (Status Messages), and arguably 1.3.1 (Info and Relationships). Legal liability under ADA and equivalent laws.
|
|
1121
|
+
|
|
1122
|
+
**The fix:**
|
|
1123
|
+
|
|
1124
|
+
```javascript
|
|
1125
|
+
// 1. Update document.title on every route change
|
|
1126
|
+
useEffect(() => {
|
|
1127
|
+
document.title = `${pageTitle} | MyApp`;
|
|
1128
|
+
}, [pageTitle]);
|
|
1129
|
+
|
|
1130
|
+
// 2. Announce navigation via ARIA live region
|
|
1131
|
+
function RouteAnnouncer() {
|
|
1132
|
+
const [announcement, setAnnouncement] = useState('');
|
|
1133
|
+
const location = useLocation();
|
|
1134
|
+
|
|
1135
|
+
useEffect(() => {
|
|
1136
|
+
setAnnouncement(`Navigated to ${document.title}`);
|
|
1137
|
+
}, [location]);
|
|
1138
|
+
|
|
1139
|
+
return (
|
|
1140
|
+
<div
|
|
1141
|
+
role="status"
|
|
1142
|
+
aria-live="assertive"
|
|
1143
|
+
aria-atomic="true"
|
|
1144
|
+
className="sr-only"
|
|
1145
|
+
>
|
|
1146
|
+
{announcement}
|
|
1147
|
+
</div>
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// 3. Move focus to main content heading after navigation
|
|
1152
|
+
useEffect(() => {
|
|
1153
|
+
const heading = document.querySelector('main h1');
|
|
1154
|
+
if (heading) {
|
|
1155
|
+
heading.setAttribute('tabindex', '-1');
|
|
1156
|
+
heading.focus();
|
|
1157
|
+
}
|
|
1158
|
+
}, [location]);
|
|
1159
|
+
```
|
|
1160
|
+
|
|
1161
|
+
**Detection rule:**
|
|
1162
|
+
|
|
1163
|
+
Enable a screen reader (VoiceOver, NVDA, JAWS). Navigate between routes. If the screen reader does not announce the new page or content area, the anti-pattern is present. Automated: check that `document.title` changes on route change and that an element with `aria-live` exists in the DOM.
|
|
1164
|
+
|
|
1165
|
+
---
|
|
1166
|
+
|
|
1167
|
+
## Root Cause Analysis
|
|
1168
|
+
|
|
1169
|
+
Most SPA anti-patterns trace back to a small set of root causes. Understanding these causes helps teams prevent entire categories of mistakes rather than chasing individual symptoms.
|
|
1170
|
+
|
|
1171
|
+
| Root Cause | Anti-Patterns It Produces | Prevention Strategy |
|
|
1172
|
+
|---|---|---|
|
|
1173
|
+
| **Browser reimplementation blindness** -- Not recognizing that SPAs must reimplement browser-native features | AP-04, AP-06, AP-08, AP-14, AP-20 | Maintain a checklist of browser behaviors that SPAs bypass: history, scroll, focus, error pages, page title, loading indicators |
|
|
1174
|
+
| **Dev-server masking** -- Development tools hide production problems | AP-04, AP-07, AP-12, AP-14 | Test against production builds weekly; run `curl` and Lighthouse against staging |
|
|
1175
|
+
| **Incremental bundle growth** -- No one monitors bundle size as features accumulate | AP-02, AP-03, AP-13 | Enforce bundle budgets in CI; fail the build if thresholds are exceeded |
|
|
1176
|
+
| **Single-device testing** -- Developers test only on fast machines with fast networks | AP-02, AP-09, AP-12, AP-13, AP-19 | Mandate testing on throttled networks and low-end device emulation |
|
|
1177
|
+
| **Architecture cargo-culting** -- Using SPA architecture because it is familiar, not because it fits | AP-01, AP-10, AP-15 | Require a rendering strategy decision document per project before scaffolding |
|
|
1178
|
+
| **Cleanup amnesia** -- Setup code written without corresponding teardown | AP-05 | Lint rules requiring cleanup returns in useEffect; MemLab in CI |
|
|
1179
|
+
| **URL-as-afterthought** -- Treating the URL bar as decoration rather than application state | AP-06, AP-07, AP-08, AP-17 | Design URL schema before building features; treat URL as the canonical source of navigation state |
|
|
1180
|
+
| **Accessibility as bolt-on** -- Treating a11y as a final polish step rather than a structural requirement | AP-20, AP-19 | Include screen reader testing in the definition of done; use axe-core in CI |
|
|
1181
|
+
| **Security shortcut** -- Choosing simplest auth implementation without considering persistence | AP-11 | Require threat model review for auth flow; use BFF pattern by default |
|
|
1182
|
+
| **Missing observability** -- No monitoring of real user behavior | AP-17, AP-05, AP-12 | Verify analytics captures all routes; add RUM (Real User Monitoring); set memory alerts |
|
|
1183
|
+
|
|
1184
|
+
---
|
|
1185
|
+
|
|
1186
|
+
## Self-Check Questions
|
|
1187
|
+
|
|
1188
|
+
Before shipping an SPA, walk through these questions. A "no" to any item flags a likely anti-pattern.
|
|
1189
|
+
|
|
1190
|
+
1. **Does this project genuinely need to be a SPA?** Could SSR, SSG, or a multi-page architecture serve users better? (AP-01)
|
|
1191
|
+
2. **Is every route lazy-loaded?** Does the initial bundle contain only the code for the landing route? (AP-02, AP-03)
|
|
1192
|
+
3. **Does a direct URL request to any interior route return 200 with the app shell?** Have you tested this on the production server, not just the dev server? (AP-04)
|
|
1193
|
+
4. **Do all `useEffect` / `onMounted` / `ngOnInit` hooks with subscriptions, timers, or event listeners have corresponding cleanup?** (AP-05)
|
|
1194
|
+
5. **Does the back button behave as a user would expect from every screen in the app?** Have you tested modals, wizards, and tab views? (AP-06)
|
|
1195
|
+
6. **Can a user share a URL that reproduces their current view, including filters, search, pagination, and selected tabs?** (AP-07)
|
|
1196
|
+
7. **Does pressing back after scrolling a long list restore the scroll position?** (AP-08)
|
|
1197
|
+
8. **Is the JavaScript bundle under 250 KB gzipped for the initial route?** Have you run a bundle analyzer in the last month? (AP-13)
|
|
1198
|
+
9. **Does navigating to a nonexistent URL show a helpful 404 page?** (AP-14)
|
|
1199
|
+
10. **Are authentication tokens persisted across page refresh without requiring re-login?** (AP-11)
|
|
1200
|
+
11. **Does the app degrade gracefully when the network drops?** Does it show cached content and an offline indicator? (AP-12)
|
|
1201
|
+
12. **Does your analytics tool record a pageview for every SPA route change?** Have you verified in the Real-Time report? (AP-17)
|
|
1202
|
+
13. **If a user partially fills a long form and accidentally navigates away, is there a confirmation prompt and/or draft recovery?** (AP-18)
|
|
1203
|
+
14. **Do route transitions show skeleton screens that match the final layout, rather than generic spinners or blank screens?** (AP-19)
|
|
1204
|
+
15. **Can a screen reader user navigate between routes and hear announcements of the new page content?** (AP-20)
|
|
1205
|
+
|
|
1206
|
+
---
|
|
1207
|
+
|
|
1208
|
+
## Code Smell Quick Reference
|
|
1209
|
+
|
|
1210
|
+
| Code Smell | Likely Anti-Pattern | Severity | One-Line Detection |
|
|
1211
|
+
|---|---|---|---|
|
|
1212
|
+
| `<div id="root"></div>` is the only HTML content in `index.html` | AP-01: SPA for SEO sites | Critical | `grep -c '<div id="root">' index.html` and check for no other content |
|
|
1213
|
+
| Zero `import()` calls in a project with 5+ routes | AP-02: No code splitting | High | `grep -r 'import(' src/ --include='*.js' --include='*.ts' \| wc -l` returns 0 |
|
|
1214
|
+
| All route components are static imports in the router file | AP-03: All routes upfront | High | Count `import ... from './pages'` in router file |
|
|
1215
|
+
| No `try_files` or catch-all in server config | AP-04: No server fallback | Critical | `curl -o /dev/null -s -w '%{http_code}' https://app.com/any/deep/route` returns 404 |
|
|
1216
|
+
| `addEventListener` without `removeEventListener` in component | AP-05: Memory leak | Critical | Lint for unpaired `addEventListener` in component lifecycle code |
|
|
1217
|
+
| `history.pushState` inside modal/dialog open handler | AP-06: Back button broken | High | Search for `pushState` near `modal` or `dialog` |
|
|
1218
|
+
| Filter/sort/page state in `useState` but not in URL params | AP-07: Deep linking failure | High | Check if `useSearchParams` or equivalent is used alongside filter state |
|
|
1219
|
+
| No `scrollRestoration` or manual scroll save/restore logic | AP-08: Scroll amnesia | Medium | `grep -r 'scrollRestoration\|scrollTo\|scrollPositions' src/` |
|
|
1220
|
+
| `fetch('/api/items')` returning unbounded collections filtered client-side | AP-09: Client overload | High | Flag `.filter()` or `.sort()` on API response data > 100 items |
|
|
1221
|
+
| Public pages return blank HTML when fetched without JS | AP-10: Missing SSR | High | `curl -s https://app.com/ \| grep -c '<h1>'` returns 0 |
|
|
1222
|
+
| Auth token stored only in `useState` or module variable | AP-11: Volatile auth | Critical | Search for token assignment to `useState` or `let token =` |
|
|
1223
|
+
| No `navigator.onLine` check or `offline`/`online` event listeners | AP-12: No offline handling | Medium | `grep -r 'navigator.onLine\|offline\|online' src/` returns 0 |
|
|
1224
|
+
| `import moment from 'moment'` or `import _ from 'lodash'` (full imports) | AP-13: Huge bundles | High | `grep -r "from 'moment'\|from 'lodash'" src/` |
|
|
1225
|
+
| No `path="*"` or wildcard route in router config | AP-14: No 404 handling | Medium | Search router file for `*` or `404` route |
|
|
1226
|
+
| Redux/Vuex store has keys like `modalOpen`, `hoverItem`, `tooltipTarget` | AP-15: Store bloat | Medium | Inspect root state shape for UI-only keys |
|
|
1227
|
+
| No `onMouseEnter` prefetch or `<link rel="prefetch">` for routes | AP-16: No prefetch | Medium | `grep -r 'prefetch\|preload\|speculationrules' src/ public/` |
|
|
1228
|
+
| `gtag` or analytics call only in `index.html`, no route-change listener | AP-17: Analytics gap | High | Search for `page_view` event dispatch on `location` change |
|
|
1229
|
+
| Long forms with no `beforeunload` listener or `useBlocker` | AP-18: Form state loss | High | `grep -r 'beforeunload\|useBlocker\|Prompt' src/` near form components |
|
|
1230
|
+
| `if (isLoading) return <Spinner />` with no skeleton alternative | AP-19: No skeletons | Medium | `grep -r 'Spinner\|Loading...' src/` without corresponding `Skeleton` |
|
|
1231
|
+
| No `aria-live` region and no `document.title` update on route change | AP-20: Silent navigation | Critical | `grep -r 'aria-live\|document.title' src/` in route-change handlers |
|
|
1232
|
+
|
|
1233
|
+
---
|
|
1234
|
+
|
|
1235
|
+
*Researched: 2026-03-08 | Sources: Nolan Lawson (SPA accessibility & memory leaks), Facebook MemLab research, StackInsight 500-repository memory leak study, Google Web Fundamentals (code splitting & Core Web Vitals), Deque Systems (SPA accessibility), Prerender.io (SPA SEO challenges), Auth0 (JavaScript memory leaks), Baymard Institute (UX research), WICG Navigation API (focus management), Curity (SPA OAuth best practices), DebugBear (SPA performance monitoring), Google Cloud Blog (SPA security vulnerabilities), WeWeb (SPA SEO 2026 guide), Adam Silver (problems with SPAs)*
|