Conversation
… tasks (spec 040) AWOS spec-driven planning for the Phase 4 Routing feature (ngRoute): - functional-spec.md — 14 requirements covering opt-in routing, route declaration + fallback, parameterized patterns, developer-selectable hash vs clean URLs, ng-view, inline + remote templates, per-route controllers, redirects, resolve pre-loading, navigation lifecycle events, current URL params/route info, manual reload. Full AngularJS ngRoute parity; ui-router / animations / angular namespace out of scope. - technical-considerations.md — new opt-in ngRoute module (src/route/) plus a genuinely-new $location service (src/location/, on core ngModule, full parity, behind injectable browser seams). ngView reuses ng-include mechanics; resolve via $injector.invoke + $q.all; no new EXCEPTION_HANDLER_CAUSES token. - tasks.md — 8 vertical slices, each green-at-completion, delegated across rollup-build / typescript-framework / vitest-testing / typedoc-docs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…registration (spec 040 slice 1)
- src/location/ new subpath: location-url.ts (pure hashbang parse/compose),
location.ts (createLocation pure factory behind locationRef/historyRef
seams), location-types.ts (LocationService contract), location-provider.ts
($LocationProvider with hashPrefix getter/setter frozen at $get), barrel.
- Registered .provider('$location', $LocationProvider) on core ngModule;
ng registry widened with $location, config registry with $locationProvider.
- Packaging: @location/* alias (tsconfig, vitest), rollup entry, ./location
package.json exports triple.
- 127 new tests (pure helpers, fake-seam factory, DI integration);
src/location at 100% line coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tate, replace(), $locationChange* events (spec 040 slice 2)
- absUrl/protocol/host/port (per-scheme default ports) + state() (HTML5-only
setter, exported STATE_REQUIRES_HTML5_MODE_MESSAGE throw in hashbang) +
fluent replace() with auto-clearing history-REPLACE flag.
- HTML5 mode: base-prefix parse/compose (parseServerUrl/normalizeBasePath/
parseHtml5Url/composeHtml5Url), pushState/replaceState writes with state,
foreign-URL → empty app URL.
- $LocationProvider.html5Mode() getter/setter (boolean | {enabled,
requireBase, rewriteLinks} — only enabled honored, documented), frozen
at $get alongside hashPrefix.
- Digest sync: $get installs a $rootScope side-effecting watch comparing
working vs committed absUrl/state — broadcasts cancelable
$locationChangeStart then browser write then $locationChangeSuccess;
initial fire newUrl === oldUrl with no write. Browser hashchange/popstate
via injectable addEventListenerRef seam, dispatched through a guarded
local dispatchGuarded → invokeExceptionHandler('eventListener').
Canceled app-driven change reverts state; canceled browser-driven change
force-replaces the old URL back. EXCEPTION_HANDLER_CAUSES stays 13.
- Standalone (non-DI) createLocation keeps synchronous writes; 3 DI tests
adjusted to digest-scheduled semantics.
- 93 new tests (location-html5.test.ts, location-events.test.ts) — full
suite 4463 passing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ange events (spec 040 slice 3)
- src/route/ new opt-in module: route-path.ts (upstream-ported pathRegExp
— :param / :param? slash-folding / :rest* greedy, ^…/?$ trailing-slash
tolerance, caseInsensitiveMatch; internal), route-types.ts (RouteDefinition
with custom-field index signature, Route, RouteService, RouteParams),
route-provider.ts ($RouteProvider.when chainable + otherwise with string
→ {redirectTo} shorthand), route.ts (createRoute — $locationChangeSuccess
listener, first-match-wins pipeline, cancelable $routeChangeStart →
current + in-place $routeParams → $routeChangeSuccess, upstream no-match
shapes, reload() via forceReload + $evalAsync), ng-route-module.ts
(createModule('ngRoute', []) + ModuleRegistry merge), barrel + root
re-exports (provider DI-only at root — sanitize precedent).
- Params merge: path params win over search (upstream extend order).
- Packaging: @route/* alias (tsconfig, vitest), rollup entry, ./route
exports triple.
- 79 new tests (route-path units, DI-driven $route, module wiring);
full suite 4542 passing.
- Known one-way trailing-slash gap for patterns AUTHORED with a trailing
slash (upstream covers via companion redirect) — addressed in slice 7.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ler (spec 040 slice 4)
- src/route/ng-view.ts: ngInclude-clone DDO (restrict ECA, priority 400,
terminal, transclude: 'element') keyed off $route.current +
$routeChangeSuccess. Render order pinned to upstream ngViewFillContent:
teardown → parseTemplate into wrapper <div> → child scope → $compile →
controller instantiate ($controller 3-arg form; 'Ctrl as vm' suffix
supported; stashed via stashController under 'ngController') → link →
insert after Comment placeholder → $viewContentLoaded $emit on the new
scope.
- Dual load-bearing cleanup (addElementCleanup on the placeholder +
scope $destroy listener); render errors route via
$exceptionHandler('$compile') — tuple stays 13.
- template string used verbatim (empty string still renders + runs
controller); fn form called with current.params; templateUrl-only
silently renders nothing (slice 5).
- Registered via .directive('ngView', …) module-DSL sugar on ngRoute
(DI-only, typed registry widened automatically).
- 21 new jsdom integration tests; full suite 4563 passing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…orm locals.$template (spec 040 slice 5) - route.ts pipeline: after the cancelable $routeChangeStart, templateUrl routes (string + fn-of-params forms) fetch via q.when($templateRequest(url)) → commit (current + $routeParams + $routeChangeSuccess) on resolve, $routeChangeError(next, previous, rejection) with NO commit on reject (previous view intact; error event is the channel — not $exceptionHandler). - Sync fast path preserved (documented divergence): inline template / no template commits same-digest — slice 3/4 contracts byte-unchanged. Inline wins over templateUrl (upstream getTemplateFor precedence). - Staleness token drops a superseded in-flight fetch silently (no events, no DOM); vetoed Start does not bump the token. - Uniform locals contract: committed routes always carry a locals object; locals.$template present whenever template text resolved (inline or fetched) — the upstream slot slice 6 merges resolve entries into. ngView renders locals.$template. route-template.ts pure helpers extracted. - $get deps widened (+$q, +$templateRequest); cache reuse free via the cache-first $templateRequest. - 9 new async tests (real createTemplateRequest cache-reuse, rejection, staleness race, fn forms) + stale slice-4 description reworded; full suite 4573 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… $q.all, $routeChangeError (spec 040 slice 6)
- route.ts: resolve map processed after the cancelable $routeChangeStart —
string entries via injector.get, invokables via injector.invoke (no
route-param locals, upstream parity; sync throws caught per-entry and
funneled as q.reject). Single object-form q.all({...entries, $template})
($template assigned last, wins collisions) — resolved object becomes
next.locals.
- Fast path narrowed to "no templateUrl AND no resolve"; either takes the
async arm under the existing staleness token (stale success AND failure
dropped silently).
- Any failure (reject, sync throw, template fetch) → $routeChangeError
(next, previous, rejection), NO commit, previous view stays mounted;
rejection kept handled — zero unhandled-$q reports.
- ng-view.ts: controller locals { ...current.locals, $scope: newScope }
($scope wins) — resolve results injectable by name (R9). Upstream
scope.$resolve publication deliberately not shipped.
- $get widened with $injector (6 deps).
- 11 new tests (success by-name injection, pending state, mixed
plain/$q/native aggregation, exact-rejection Error event, sync-throw
funneling, templateUrl+resolve compose, $template collision, stale
success/failure races, $scope precedence); full suite 4584 passing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oop guard (spec 040 slice 7)
- redirectTo: string form via the upstream interpolate() port
(route-redirect.ts — ':'-split, consumed keys deleted so .search carries
leftovers) → $location.path(...).search(params).replace(); fn form called
(pathParams, path, search) → $location.url(result).replace(); undefined
return → no redirect. Redirect pass emits NO route events and never
touches current (documented divergence — target's pass fires Start/
Success); same-URL self-redirect escape commits normally.
- RouteRedirectionLoopError (route-error.ts, brand-named) + exported
REDIRECT_LOOP_THRESHOLD (10): consecutive-redirect counter → one
$routeChangeError broadcast, $location not written, digest settles.
No new EXCEPTION_HANDLER_CAUSES token (stays 13).
- $routeUpdate branch (upstream boolean byte-equivalent): !forceReload &&
same $$route && (reloadOnUrl === false || (reloadOnSearch === false &&
isEqual(pathParams))) → params + $routeParams updated in place,
$broadcast('$routeUpdate', current), NO teardown (pathParams stale —
upstream parity). reload() bypasses via forceReload → fresh resolve +
fresh controller.
- updateParams(newParams): merged params → interpolated originalPath +
leftover search, normal history entry, pipeline honors reloadOnSearch;
no current route → synchronous Error.
- route-path.ts: authored trailing slash stripped pre-compile — '/list/'
now matches /list AND /list/ (closes the slice-3 one-way gap).
- One sanctioned test update (otherwise-shorthand now redirects); 17 new
tests; full suite 4601 passing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…agram, roadmap (spec 040 slice 8)
- Parity suites: route-parity.test.ts (41) + location-parity.test.ts (16)
porting upstream routeSpec/routeParamsSpec/ngViewSpec/locationSpec
scenario intent (chained-event ordering, incomplete-param fall-through,
optional-param matrices, redirect no-process-bits, mid-segment params,
ngView-in-ngInclude, Start-redirect semantics, no-infinite-digest
guards); documented divergences pinned with naming comments.
- Three parity bugs found by the suite and FIXED:
1. throwing redirectTo fn now broadcasts $routeChangeError (was escaping
to the digest watch-error path),
2. matchRoute no longer double-decodes captures (bare '%' path values
match instead of silent URIError no-match; upstream never decodes),
3. ngView inserts the container BEFORE linking (require: '^…' from route
templates now resolves across the view boundary — the spec-032
cloneAttachFn attach-before-link precedent), link-throw cleanup
removes the partially-mounted wrapper.
- Docs: src/location/README.md + src/route/README.md (contracts, flush
recipes, documented divergences), context/diagrams/routing.md + index
row + EXPECTED_DIAGRAMS pin, CLAUDE.md module rows + six invariants +
"Where to look when…" entries, roadmap Routing items ticked
(spec 040 — shipped).
- Final gates: typecheck / lint / test (225 files, 4664 passed, 26
skipped) / build (./location + ./route dist triples emitted) green;
coverage src/location 98.9% / src/route 96.0% lines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- All 45 acceptance criteria verified against the implementation + the ~430 routing/location tests; ticked. - R14 wording corrected to the approved AngularJS-parity behavior (default rebuilds on query-only change; reloadOnSearch/reloadOnUrl false opt out into $routeUpdate) — the original draft inverted the default; implementation was already upstream-correct. - functional-spec.md + technical-considerations.md Status → Completed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sses (spec 040) The $routeUpdate update-only branch and the no-match/no-previous silent bail returned from update() before the navigation-token bump, so an in-flight async navigation (templateUrl fetch / resolve) started by an earlier pass could still commit AFTER the URL had moved on — $route.current ended up on a route the browser URL no longer pointed at (repro: commit /x with reloadOnSearch:false, start an async nav to /a, change /x's query before the fetch settles → the stale /a commit landed under the /x URL). Both early-return branches now supersede the in-flight pass, matching the treatment runRedirect already applied; upstream avoids the hole by reassigning $route.current eagerly, which its staleness check reads — the token bump is this project's equivalent. Two regression tests pin the drop; route.ts file-header + CLAUDE.md staleness wording updated to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t note (spec 040 review follow-ups) createLocation always supported a basePath seam, but $LocationProvider.$get never threaded it, so an HTML5-mode app served under a subpath had no DI path to configure the base and every browser URL parsed against '/'. Adds the spec-034-idiom basePath() getter/setter (default '/', TypeError on non-string, frozen at $get) as the explicit stand-in for upstream's <base href> detection, with provider-surface + end-to-end DI tests (subpath parse, composed pushState writes, hashbang no-op) and README / CLAUDE.md documentation of the divergence. Also records src/route/route.ts (586 lines, ~40% file-header TSDoc) in CLAUDE.md's over-500-line audit list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uards (spec 040 review follow-up) route.ts's getResolveMap asserted the resolve map's value type (`as Record<string, string | Invokable>`) and ng-view.ts asserted the @di-Invokable → ControllerInvokable bridge on the route `controller` field — both narrowing consumer-authored values admitted by the RouteDefinition index signature. getResolveMap now narrows via @core's isObject and keeps values `unknown`; the resolve loop discriminates each entry with a new file-local isResolveInvokable guard (function-or-array — mirroring annotate's own top-level triage, full validation stays with $injector.invoke) and rejects any other shape with a TypeError through the same q.reject → $routeChangeError channel. ngView discriminates the controller field with a matching isControllerInvokable guard; garbage throws a TypeError into the existing render try/catch → '$compile' route. Two tests pin the new garbage paths; every previously-valid shape reaches the injector/$controller unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the Phase 4 Routing roadmap item via the AWOS spec-040 triad (functional spec → technical considerations → 8 vertical slices). Two new subpaths:
$location/$locationProvider— new corengservice (src/location/,./location)path/search/hash/url/absUrl/protocol/host/port/state/replace(), fluent setters#!prefix, configurable viahashPrefix), HTML5 mode opt-in (html5Mode) writing throughpushState/replaceState$rootScopewatch flushes app-driven changes and broadcasts cancelable$locationChangeStart→ browser write →$locationChangeSuccess; browserhashchange/popstatearrive through injectable seams with guarded dispatch ('eventListener'cause; canceled browser-driven changes force-replace the old URL back)locationRef/historyRef/addEventListenerRef) — fully unit-testable without touching jsdom navigationngRoute— new opt-in module (src/route/,./route)$routeProvider.when()/otherwise()with upstream-ported pattern compiler (:param,:param?,:rest*, per-routecaseInsensitiveMatch, trailing-slash tolerance both directions)$routeChangeStart→ sync fast path (inline template, no resolve) or async$qpath (templateUrlvia$templateRequest+resolvemap via$injectorunder a single object-form$q.all) with a staleness token → commit ($route.current, in-place$routeParams) →$routeChangeSuccess; any failure →$routeChangeError, previous view intactredirectTo(string interpolation + fn forms,.replace()history semantics) with a 10-hop redirect-loop guard (RouteRedirectionLoopError— hardening beyond upstream),reloadOnSearch/reloadOnUrl→$routeUpdate(no teardown),$route.reload(),$route.updateParams()ngViewdirective (ng-include-pattern: comment placeholder, wrapper div, dual cleanup) renderinglocals.$template, instantiating the per-route controller (controllerAs+'Ctrl as vm'forms) with resolve locals injectable by name, emitting$viewContentLoadedEXCEPTION_HANDLER_CAUSESstays 13 — no new cause tokenNotable
routeSpec/routeParamsSpec/ngViewSpec/locationSpecfound and fixed 3 real bugs (throwingredirectTofn not broadcasting$routeChangeError;matchRoutedouble-decodeURIError;ngViewlink-before-insert breakingrequire: '^'across the view boundary)$browser, standard URL encoding, redirect-pass event silence, wrapper-div insertion, sync fast path, noscope.$resolve/eagerInstantiation)src/location/README.md,src/route/README.md,context/diagrams/routing.md, CLAUDE.md module rows + invariants, roadmap tickedTest plan
pnpm typecheck/pnpm lintcleanpnpm test— 225 files, 4664 passed / 26 skipped (~430 new routing/location tests: pure-helper units, fake-seam factories, DI integration, jsdom ngView rendering, async templateUrl/resolve flows, redirect/reload semantics, upstream-ported parity scenarios)pnpm build—./location+./routeESM/CJS/.d.tstriples emittedsrc/location98.9% /src/route96.0% lines (≥90% gate)🤖 Generated with Claude Code