Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ CI (`.github/workflows/ci.yml`) gates on: lint → format:check → typecheck
- **No `new Function()` / no `eval()`** — the expression parser uses a **tree-walking interpreter** deliberately. This avoids CSP violations and is part of the project's security posture. Don't "optimize" by generating code strings.
- **Digest TTL contract** — configurable via `Scope.create({ ttl: 20 })` (default 10). TTL breach throws with the watch function source in the error to help identify unstable watchers.
- **One-time bindings & constant watches** (spec 010) — the parser attaches `literal` / `constant` / `oneTime` flags on AST nodes. Scope wires `oneTimeWatchDelegate` (literal) or `oneTimeLiteralWatchDelegate` for `::expr` expressions; constant expressions use `constantWatchDelegate`. When modifying the watcher wiring, preserve those delegate selections.
- **Module boundary rule** — `parser/*` and `di/*` depend only on `@core` (prefer importing from `@core/index`, not `@core/utils` directly). `core/scope.ts` intentionally depends on `@parser/index` because scopes evaluate expression strings. `compiler/` has no dependencies yet.
- **Module boundary rule** — `parser/*` and `di/*` depend only on `@core` (prefer importing from `@core/index`, not `@core/utils` directly). `core/scope.ts` intentionally depends on `@parser/index` because scopes evaluate expression strings. Two documented exceptions: `di/module.ts` carries `import type`-only references to `@compiler`/`@controller`/`@filter` (erased at build — zero runtime dependency), and `parser/interpreter.ts` carries ONE runtime value import from `@filter` (`FilterLookupError`, spec 016 — the interpreter constructs the error when a filter lookup misses; its `FilterFn`/`FilterService` references are `import type`-only). Don't widen either exception without a spec.
- **Error handling in digest** — listener/watch exceptions are logged with `console.error('...', e)` and the digest continues. Don't swallow silently and don't abort the digest loop on a single listener failure.
- **Strict mode is frozen after the config phase** — `$sceProvider.enabled(false)` is the only way to disable SCE. Once the injector finishes config, `$sce.isEnabled()` is permanent. Strict-OFF turns both `trustAs*` and `getTrusted*` into total pass-throughs (no wrapper classes are constructed).
- **Trusted values are per-context nominal classes** — `TrustedResourceUrl extends TrustedUrl`, so a trusted resource URL is accepted where a trusted URL is expected (not vice-versa). Identity is checked via `instanceof`, not a string-based brand. Do NOT "optimize" to a single branded wrapper — the subtype rule matters for AngularJS parity.
Expand Down Expand Up @@ -156,12 +156,12 @@ CI (`.github/workflows/ci.yml`) gates on: lint → format:check → typecheck

## Coding conventions

- **TypeScript strict** (`strict: true` + `noUncheckedIndexedAccess`). No `any` — the single existing cast in `src/core/utils.ts:229` is the ceiling, not a precedent.
- **TypeScript strict** (`strict: true` + `noUncheckedIndexedAccess`). No `any` — the single existing cast in `src/core/utils.ts` (the TypedArray-copy `source.constructor as any` inside `copyRecursive`, ~line 277) is the ceiling, not a precedent.
- **Every `eslint-disable` comment must carry an inline justification** (`-- reason`). CI enforces lint.
- **No explicit return types** when TS inference handles them — let inference do the work. Annotate only on exported public-API boundaries where the declared shape is part of the contract.
- **Imports**: use path aliases (`@core`, `@parser`, `@di`, `@compiler`). `no-restricted-imports` blocks `../*` relative climbing.
- **File naming**: kebab-case (`scope-watch-delegates.ts`, `ast-flags.ts`). Tests under `src/<module>/__tests__/*.test.ts`.
- **File size target**: under 500 lines per source file. Refactor candidates today: `src/core/scope.ts` (827), `src/di/module.ts` (776), `src/di/injector.ts` (734).
- **File size target**: under 500 lines per source file. 17 source files currently exceed it (2026-07 audit); the standout refactor candidates: `src/compiler/compile.ts` (~2300 — 4.6× the target), `src/compiler/compile-provider.ts` (1281), `src/di/module.ts` (1269), `src/compiler/compile-error.ts` (1136), `src/compiler/directive-types.ts` (978), `src/core/scope.ts` (950), `src/compiler/attributes.ts` (846), `src/di/injector.ts` (750), `src/core/ng-module.ts` (676). A dedicated `compile.ts`-split spec is the highest-value refactor.

## Git + spec workflow

Expand Down
6 changes: 3 additions & 3 deletions context/product/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ _Complete the essential building blocks that everything else depends on._

_The layer that connects the runtime to templates and the DOM._

- [ ] **Expressions & Parser**
- [x] **Expressions & Parser**
- [x] **Expression Parser:** Implement a full expression parser supporting property access, method calls, operators, literals, and assignments and all the supported features of AngularJS 1.x, integration with scope.
- [x] **One-Time Bindings:** Support `::` prefix for expressions that unwatch after stabilization.
- [x] **Interpolation:** Implement `$interpolate` service for `{{expression}}` resolution in strings and templates.
Expand All @@ -59,15 +59,15 @@ _The layer that connects the runtime to templates and the DOM._
- [x] **$interpolate Integration:** Wire the `trustedContext` parameter on `$interpolate` to `$sce.getTrusted(...)` — resolves the `TODO(spec-$sce)` marker in `src/interpolate/interpolate.ts` left by spec 011.
- [x] **$sceProvider:** Support config-phase `enabled(value?)` to toggle strict mode.

- [ ] **HTML Sanitization ($sanitize / ngSanitize)**
- [x] **HTML Sanitization ($sanitize / ngSanitize)**
- [x] **Separate `ngSanitize` module:** Ship as a dedicated module, NOT part of core `ng`. Mirrors AngularJS 1.x `angular-sanitize.js` packaging so apps that don't need sanitization don't pay for its parser tables or attack surface. New `src/sanitize/` subpath + `@sanitize/*` alias + `./sanitize` in `package.json` exports and `rollup.config.mjs` entries, following the `./sce` / `./interpolate` layout.
- [x] **ESM-first `createSanitize` / `sanitize` factory:** Pure `(untrustedHtml: string) => string` pipeline with no DI dependency — usable standalone and via `$sanitize` DI registration. Follows the `createSce` / `sce` precedent.
- [x] **`$sanitize` service + `$SanitizeProvider`:** DI-layer thin shim registered on `ngSanitize`; provider owns only the allow-list extensions (see below). `$get` depends on the ESM factory — zero duplicate logic.
- [x] **HTML parser + tag allow-list:** Token-walker with a fixed whitelist of safe block/inline tags (`div`, `span`, `p`, `h1`–`h6`, `ul`, `ol`, `li`, `a`, `b`, `i`, `em`, `strong`, `br`, `img`, `table`/`tr`/`td`, etc.). Disallowed tags (`script`, `iframe`, `object`, `embed`, `style`, `svg` by default, …) and their contents are dropped; text content is preserved and entity-escaped.
- [x] **Attribute allow-list per tag:** Fixed whitelist (`href`, `src`, `alt`, `title`, `class`, `id`, …) with tag-specific constraints (e.g. `target` only on `<a>`). Disallowed attributes (including all `on*` event handlers) are stripped. *(Implementation note: ships as a single global allow-list (`VALID_ATTRS`) rather than per-tag — AngularJS 1.x parity. Per-tag scoping is deferred.)*
- [x] **URL-protocol safe-list for `href` / `src`:** Same allow-list regex used by `$compileProvider.aHrefSanitizationTrustedUrlList` — defaults to `/^\s*(https?|s?ftp|mailto|tel|file):/` plus relative URLs. `javascript:` and dangerous `data:` URIs are stripped. Configurable via `$sanitizeProvider.uriPattern(RegExp)`.
- [x] **`$sce.getTrustedHtml` fallback integration:** When a value reaches `$sce.getTrustedHtml(...)` WITHOUT being wrapped AND `$sanitize` is available on the injector, delegate to `$sanitize(value)` instead of throwing. Keeps the spec-012 strict-mode contract intact (plain strings still throw when `$sanitize` isn't loaded) and matches AngularJS 1.x `ng-bind-html` behavior. Small coordination edit in `src/sce/sce.ts` gated behind an optional dependency lookup.
- [ ] **`ng-bind-html` directive integration:** Lands with the Directives & DOM Compilation roadmap item below — `ng-bind-html="expr"` evaluates `expr`, runs through `$sce.getTrustedHtml` (which now routes to `$sanitize` when appropriate), and sets `innerHTML`. *(Deferred — depends on `$compile`.)*
- [x] **`ng-bind-html` directive integration:** Lands with the Directives & DOM Compilation roadmap item below — `ng-bind-html="expr"` evaluates `expr`, runs through `$sce.getTrustedHtml` (which now routes to `$sanitize` when appropriate), and sets `innerHTML`. _(spec 023 — shipped as part of the visibility & binding built-ins.)_
- [x] **AngularJS parity tests + documented CVE regressions:** Port test vectors from `angular/angular.js/test/ngSanitize/sanitizeSpec.js`. Include a dedicated mXSS-regression suite covering each historical `ngSanitize` CVE (tag confusion, attribute-context breaks, etc.) so future edits can't regress.
- [x] **DOMPurify-compat escape hatch:** Document how to swap the built-in implementation for DOMPurify via a decorator (`.decorator('$sanitize', () => domPurifyBackedImpl)`). No hard dependency; purely a documented pattern so teams with stricter security posture can opt in. *(Documented in `src/sanitize/README.md`.)*

Expand Down
4 changes: 2 additions & 2 deletions src/bootstrap/bootstrap-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
* are part of the public contract and locked by tests.
*
* These are PROGRAMMER errors surfaced directly to the caller — they are NOT
* routed through `$exceptionHandler` (the `EXCEPTION_HANDLER_CAUSES` tuple stays
* at 10). Unregistered string-name modules reuse `getModule`'s existing
* routed through `$exceptionHandler` (the `EXCEPTION_HANDLER_CAUSES` tuple gains no
* bootstrap token). Unregistered string-name modules reuse `getModule`'s existing
* `Module not found: <name>` throw rather than a new class.
*/

Expand Down
2 changes: 1 addition & 1 deletion src/compiler/__tests__/component.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* {@link InvalidComponentDefinitionError}; downstream directive
* normalization runs lazily at `<name>Directive` provider `$get` time
* and routes via `$exceptionHandler('$compile')` through the existing
* factory `try/catch`. `EXCEPTION_HANDLER_CAUSES` stays at 10.
* factory `try/catch`. `EXCEPTION_HANDLER_CAUSES` is unchanged.
*
* **Controller spelling.** Tests use the canonical array-style
* annotation with a trailing function expression that stashes the
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/__tests__/require.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* BEFORE `$onInit` runs.
*
* Resolution failure (`MissingRequiredControllerError`) routes via
* `$exceptionHandler('$compile')`; the tuple stays at 10.
* `$exceptionHandler('$compile')`; the tuple is unchanged.
*
* Both link sites are exercised: the inline (synchronous) link path
* AND the `templateUrl` post-template-install link path — same
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/__tests__/spec023-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
*
* Mirrors the structural precedent set by
* `src/compiler/__tests__/spec022-parity.test.ts` (and the
* `EXCEPTION_HANDLER_CAUSES.length === 10` regression guard pattern
* `EXCEPTION_HANDLER_CAUSES.length` regression guard pattern
* from there).
*
* @see context/spec/023-visibility-and-binding-directives/functional-spec.md
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/__tests__/spec024-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
*
* Mirrors the structural precedent set by
* `src/compiler/__tests__/spec023-parity.test.ts` (and the
* `EXCEPTION_HANDLER_CAUSES.length === 10` regression guard pattern
* `EXCEPTION_HANDLER_CAUSES.length` regression guard pattern
* from there).
*
* @see context/spec/024-class-and-style-directives/functional-spec.md
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/__tests__/spec025-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
*
* Mirrors the structural precedent set by
* `src/compiler/__tests__/spec024-parity.test.ts` (and the
* `EXCEPTION_HANDLER_CAUSES.length === 10` regression guard pattern
* `EXCEPTION_HANDLER_CAUSES.length` regression guard pattern
* from there).
*
* @see context/spec/025-attribute-helper-directives/functional-spec.md
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/__tests__/spec026-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
*
* Mirrors the structural precedent set by
* `src/compiler/__tests__/spec025-parity.test.ts` (and the
* `EXCEPTION_HANDLER_CAUSES.length === 10` regression guard pattern
* `EXCEPTION_HANDLER_CAUSES.length` regression guard pattern
* from there).
*
* @see context/spec/026-event-directives/functional-spec.md
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/__tests__/spec027-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
*
* Mirrors the structural precedent set by
* `src/compiler/__tests__/spec026-parity.test.ts` (and the
* `EXCEPTION_HANDLER_CAUSES.length === 10` regression guard pattern
* `EXCEPTION_HANDLER_CAUSES.length` regression guard pattern
* from there).
*
* @see context/spec/027-structural-flow-control-directives/functional-spec.md
Expand Down
4 changes: 2 additions & 2 deletions src/compiler/__tests__/spec028-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@
* own catch routes via the existing `'$compile'` cause token, NOT
* `'watchListener'`.
*
* Plus the `EXCEPTION_HANDLER_CAUSES.length === 10` regression guard —
* Plus the `EXCEPTION_HANDLER_CAUSES.length` regression guard —
* spec 028 introduces FOUR new error classes
* (`NgRepeatBadIteratorExpressionError`, `NgRepeatBadIdentifierError`,
* `NgRepeatBadAliasError`, `NgRepeatDuplicateKeyError`) but ZERO new
* cause tokens; every error site reuses the existing `'$compile'` token.
*
* Mirrors the structural precedent set by
* `src/compiler/__tests__/spec027-parity.test.ts` (and the
* `EXCEPTION_HANDLER_CAUSES.length === 10` regression-guard pattern
* `EXCEPTION_HANDLER_CAUSES.length` regression-guard pattern
* established by spec 023 → spec 027).
*
* @see context/spec/028-ng-repeat/functional-spec.md
Expand Down
4 changes: 2 additions & 2 deletions src/compiler/__tests__/spec029-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@
* down cleanly inside an `ng-repeat` row and on an `ng-if` subtree
* without tripping the spec-027 same-element structural gap.
*
* Plus the `EXCEPTION_HANDLER_CAUSES.length === 10` regression guard —
* Plus the `EXCEPTION_HANDLER_CAUSES.length` regression guard —
* spec 029 introduces TWO new error classes
* (`NgPluralizeNoRuleDefinedError`, `NgPluralizeBadOffsetError`) but
* ZERO new cause tokens; both route via the existing `'$compile'`
* token.
*
* Mirrors the structural precedent set by
* `src/compiler/__tests__/spec028-parity.test.ts` (and the
* `EXCEPTION_HANDLER_CAUSES.length === 10` regression-guard pattern
* `EXCEPTION_HANDLER_CAUSES.length` regression-guard pattern
* established by spec 023 → spec 028).
*
* @see context/spec/029-ng-pluralize/functional-spec.md
Expand Down
4 changes: 2 additions & 2 deletions src/compiler/__tests__/spec030-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@
* several rows, each carrying an `ng-ref`, compile / digest / render
* the expected row count without errors.
*
* Plus the `EXCEPTION_HANDLER_CAUSES.length === 10` regression guard —
* Plus the `EXCEPTION_HANDLER_CAUSES.length` regression guard —
* spec 030 introduces new error classes (`NgRefBadExpressionError`,
* `NgRefNoControllerError`) but ZERO new cause tokens; both route via
* the existing `'$compile'` token.
*
* Mirrors the structural precedent set by
* `src/compiler/__tests__/spec029-parity.test.ts` (and the
* `EXCEPTION_HANDLER_CAUSES.length === 10` regression-guard pattern
* `EXCEPTION_HANDLER_CAUSES.length` regression-guard pattern
* established by spec 023 → spec 029).
*
* @see context/spec/030-csp-template-cache-element-overrides/functional-spec.md
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/__tests__/spec031-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
* - **Composite** — a page mixing text `{{ }}`, attribute `{{ }}`,
* `ng-if`, and `ng-repeat` renders end-to-end and updates on digest.
*
* Plus the `EXCEPTION_HANDLER_CAUSES.length === 10` regression guard —
* Plus the `EXCEPTION_HANDLER_CAUSES.length` regression guard —
* spec 031 introduces ZERO new cause tokens (the eager-pass catch reuses
* the existing `'$compile'` token).
*
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/__tests__/structural-conflict.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
* - The same error for `ng-if` + `ng-include` and
* `ng-repeat` + `ng-switch-when`.
* - The canonical nested workaround renders correctly with no error.
* - `EXCEPTION_HANDLER_CAUSES.length === 10` (no new token).
* - `EXCEPTION_HANDLER_CAUSES.length` unchanged (no new token).
*/

import { afterEach, describe, expect, it } from 'vitest';
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/__tests__/template-errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* `invokeExceptionHandler` recursion guard catches the handler's
* throw; template loading does NOT crash; falls back to
* `console.error`.
* 2. `EXCEPTION_HANDLER_CAUSES.length === 10` regression — no new cause
* 2. `EXCEPTION_HANDLER_CAUSES.length` regression — no new cause
* token introduced by spec 019; `'$compile'` covers every template
* error site.
* 3. `'$compile' satisfies ExceptionHandlerCause` compile-time check.
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/__tests__/transclude-errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* the same clone still link; other clones produce normally.
* 4. Custom `$exceptionHandler` that itself throws → spec-014 recursion
* guard catches; transclusion does not crash.
* 5. `EXCEPTION_HANDLER_CAUSES` length unchanged at 10; `'$compile'`
* 5. `EXCEPTION_HANDLER_CAUSES` length unchanged (no transclude token); `'$compile'`
* still included.
* 6. `'$compile' satisfies ExceptionHandlerCause` at compile time.
*/
Expand Down
55 changes: 55 additions & 0 deletions src/compiler/apply-phase-guarded.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Shared `$$phase`-guarded `$apply` / `$evalAsync` dispatch.
*
* A native event (or any framework callback firing outside the digest
* machinery) must NOT call `scope.$apply` while a digest is already in
* flight — `$apply` would throw `'$digest already in progress'`. When
* `scope.$$phase` is non-null the runner is queued through
* `scope.$evalAsync` instead; a throw from the drained runner is then
* covered by the digest's standard `'$evalAsync'` catch path. On the
* common no-phase path the runner goes through `scope.$apply`, and
* because this project's `$apply` is `try/finally`-only (no internal
* `try/catch` — see `src/core/scope.ts`), the synchronous throw is
* caught HERE and routed via `$exceptionHandler` under the caller's
* cause token.
*
* Callers and their cause tokens (the asymmetry is test-pinned — see
* `spec026-parity.test.ts`):
*
* - the spec-026 event directives (`ng-event-directives.ts`) pass
* `'eventListener'`;
* - the forms surfaces (`input-types.ts` via `applyDuringEvent`,
* `form.ts` submit, `select.ts` change) pass `'$compile'`.
*
* The `$timeout` / `$interval` services implement the same idea through
* injected `apply` / `rootPhase` seams (pure factories with no `Scope`
* dependency) — deliberately NOT unified with this helper.
*
* Compiler-internal shared helper (the `expression-assign.ts` /
* `element-slots.ts` precedent) — not exported from `@compiler/index`.
*/

import type { Scope } from '@core/index';

import { invokeExceptionHandler, type ExceptionHandler, type ExceptionHandlerCause } from '@exception-handler/index';

/**
* Dispatch `run` through the `$$phase`-guarded `$apply` / `$evalAsync`
* seam, routing any synchronous throw via `$exceptionHandler(cause)`.
*/
export function applyPhaseGuarded(
scope: Scope,
exceptionHandler: ExceptionHandler,
cause: ExceptionHandlerCause,
run: () => void,
): void {
try {
if (scope.$$phase !== null) {
scope.$evalAsync(run);
} else {
scope.$apply(run);
}
} catch (err) {
invokeExceptionHandler(exceptionHandler, err, cause);
}
}
Loading