Correct 1A-1 plan for 4 bugs found during execution
- Task 3: dotfile placeholder (src/.typecheck-placeholder.ts) is ignored by TypeScript's glob; use non-dotfile name. - Task 4: replace legacy .eslintrc.cjs with ESLint 9 flat config. ESLint 9 requires flat config natively; the legacy format triggers deprecation warnings and needs a FlatCompat shim. - Task 9: env impl cannot unconditionally assign undefined to optional fields under exactOptionalPropertyTypes; build base object then conditionally assign the three optional URL/header fields. - Task 12 Step 5: defer coverage check to 1B, which owns the @vitest/coverage-v8 dep per the master plan shared-files table.
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
**Goal:** Bootstrap the empty Modern.js React project skeleton at the repo root with a typechecked, lintable, testable `src/` tree, a Zod-validated env module, a frozen-public-barrel rule for the four feature modules, and the `HostContract` type — producing a baseline that every downstream Phase 1 sub-plan builds on.
|
**Goal:** Bootstrap the empty Modern.js React project skeleton at the repo root with a typechecked, lintable, testable `src/` tree, a Zod-validated env module, a frozen-public-barrel rule for the four feature modules, and the `HostContract` type — producing a baseline that every downstream Phase 1 sub-plan builds on.
|
||||||
|
|
||||||
**Architecture:** The new React project lives at the **repo root**, alongside the existing ASP.NET files and the Angular `ClientApp/` (both untouched). The root gains a `package.json`, `tsconfig.json`, `.eslintrc.cjs`, `vitest.config.ts`, and an empty `src/` tree. Fate of the ASP.NET host is Phase 0 assumption **A5** (hard blocker resolved before 1A-1 starts); regardless of A5, 1A-1 never deletes or modifies ASP.NET or `ClientApp/` files.
|
**Architecture:** The new React project lives at the **repo root**, alongside the existing ASP.NET files and the Angular `ClientApp/` (both untouched). The root gains a `package.json`, `tsconfig.json`, `eslint.config.js` (flat config, ESLint 9), `vitest.config.ts`, and an empty `src/` tree. Fate of the ASP.NET host is Phase 0 assumption **A5** (hard blocker resolved before 1A-1 starts); regardless of A5, 1A-1 never deletes or modifies ASP.NET or `ClientApp/` files.
|
||||||
|
|
||||||
**Tech Stack:** Node 24, pnpm, TypeScript 5 strict, Vitest, ESLint 9 (flat config compatible), Zod for runtime env validation.
|
**Tech Stack:** Node 24, pnpm, TypeScript 5 strict, Vitest, ESLint 9 (flat config compatible), Zod for runtime env validation.
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
1. `.nvmrc` pinned to Node 24.
|
1. `.nvmrc` pinned to Node 24.
|
||||||
2. Root `package.json` with scripts `dev`, `build:standalone`, `build:remote`, `build:both`, `test`, `test:coverage`, `lint`, `typecheck`. (Non-build scripts are stubs until 1A-2 wires Modern.js.)
|
2. Root `package.json` with scripts `dev`, `build:standalone`, `build:remote`, `build:both`, `test`, `test:coverage`, `lint`, `typecheck`. (Non-build scripts are stubs until 1A-2 wires Modern.js.)
|
||||||
3. `tsconfig.json` — strict, `noUncheckedIndexedAccess`, `isolatedModules`, path aliases `@/*` → `src/*`, `@phase0/*` → `scripts/phase-0/*`.
|
3. `tsconfig.json` — strict, `noUncheckedIndexedAccess`, `isolatedModules`, path aliases `@/*` → `src/*`, `@phase0/*` → `scripts/phase-0/*`.
|
||||||
4. `.eslintrc.cjs` baseline (no boundary rules yet).
|
4. `eslint.config.js` baseline flat config (no boundary rules yet). ESLint 9 requires flat config natively.
|
||||||
5. `vitest.config.ts` wired to the `@/` alias.
|
5. `vitest.config.ts` wired to the `@/` alias.
|
||||||
6. `src/` directory with:
|
6. `src/` directory with:
|
||||||
- `src/env/index.ts` — Zod-validated `Env` reader (and its test).
|
- `src/env/index.ts` — Zod-validated `Env` reader (and its test).
|
||||||
@@ -47,7 +47,7 @@ Files created or modified by this plan, in order of appearance:
|
|||||||
| `package.json` | Root workspace manifest + scripts + deps | 1, 2 |
|
| `package.json` | Root workspace manifest + scripts + deps | 1, 2 |
|
||||||
| `pnpm-lock.yaml` | pnpm lockfile (generated) | 2 |
|
| `pnpm-lock.yaml` | pnpm lockfile (generated) | 2 |
|
||||||
| `tsconfig.json` | TypeScript strict config + path aliases | 3 |
|
| `tsconfig.json` | TypeScript strict config + path aliases | 3 |
|
||||||
| `.eslintrc.cjs` | Baseline ESLint config (no boundaries yet) | 4 |
|
| `eslint.config.js` | Baseline ESLint 9 flat config (no boundaries yet) | 4 |
|
||||||
| `vitest.config.ts` | Vitest runner config with `@/` alias | 5 |
|
| `vitest.config.ts` | Vitest runner config with `@/` alias | 5 |
|
||||||
| `src/observability/logger/types.ts` | Type-only `Logger` surface | 6 |
|
| `src/observability/logger/types.ts` | Type-only `Logger` surface | 6 |
|
||||||
| `src/observability/analytics/types.ts` | Type-only `AnalyticsProviders` + friends | 7 |
|
| `src/observability/analytics/types.ts` | Type-only `AnalyticsProviders` + friends | 7 |
|
||||||
@@ -224,77 +224,90 @@ Expected: exits `0` with no output, OR errors `error TS18003: No inputs were fou
|
|||||||
If TS18003 fires, create an empty placeholder so it goes away:
|
If TS18003 fires, create an empty placeholder so it goes away:
|
||||||
```bash
|
```bash
|
||||||
mkdir -p src
|
mkdir -p src
|
||||||
echo "export {};" > src/.typecheck-placeholder.ts
|
echo "export {};" > src/typecheck-placeholder.ts
|
||||||
pnpm typecheck
|
pnpm typecheck
|
||||||
```
|
```
|
||||||
Expected: exits `0` silently.
|
Expected: exits `0` silently.
|
||||||
|
|
||||||
|
**Note on the filename.** Use `src/typecheck-placeholder.ts`, NOT `src/.typecheck-placeholder.ts` — TypeScript's glob matcher skips dotfiles, so a dot-prefixed name won't be picked up by the `include: ["src/**/*.ts"]` pattern and the placeholder won't suppress TS18003.
|
||||||
|
|
||||||
- [ ] **Step 3: Commit**
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add tsconfig.json
|
git add tsconfig.json
|
||||||
git add src/.typecheck-placeholder.ts 2>/dev/null || true
|
git add src/typecheck-placeholder.ts 2>/dev/null || true
|
||||||
git commit -m "Add strict TypeScript config with @/ and @phase0/ aliases"
|
git commit -m "Add strict TypeScript config with @/ and @phase0/ aliases"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Task 4 — Create baseline `.eslintrc.cjs`
|
## Task 4 — Create baseline `eslint.config.js` (ESLint 9 flat config)
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Create: `.eslintrc.cjs`
|
- Create: `eslint.config.js`
|
||||||
- Create: `.eslintignore`
|
|
||||||
|
|
||||||
- [ ] **Step 1: Write `.eslintrc.cjs`**
|
**Note on format.** ESLint 9 requires flat config natively — legacy `.eslintrc.cjs` is not supported without a compat shim, and shims produce deprecation warnings. This task ships flat config directly. Boundary rules (`eslint-plugin-boundaries`) and `no-restricted-imports` land in **1A-3**.
|
||||||
|
|
||||||
Baseline only. Layered dependency rules (`eslint-plugin-boundaries`) and `no-restricted-imports` come in **1A-3**.
|
- [ ] **Step 1: Install flat-config helper packages**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add -D @eslint/js@^9.0.0 typescript-eslint@^8.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
The `typescript-eslint` meta-package is the recommended flat-config entrypoint — it re-exports the parser and plugin as flat-config-ready objects. `@eslint/js` provides `js.configs.recommended`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Write `eslint.config.js`**
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
/** @type {import('eslint').Linter.Config} */
|
import js from "@eslint/js";
|
||||||
module.exports = {
|
import tseslint from "typescript-eslint";
|
||||||
root: true,
|
import unusedImports from "eslint-plugin-unused-imports";
|
||||||
parser: "@typescript-eslint/parser",
|
|
||||||
parserOptions: {
|
export default [
|
||||||
ecmaVersion: 2023,
|
{
|
||||||
sourceType: "module",
|
ignores: [
|
||||||
project: "./tsconfig.json",
|
"dist/**",
|
||||||
ecmaFeatures: { jsx: true },
|
"node_modules/**",
|
||||||
},
|
"ClientApp/**",
|
||||||
plugins: ["@typescript-eslint", "unused-imports"],
|
"wwwroot/**",
|
||||||
extends: [
|
"**/*.cjs",
|
||||||
"eslint:recommended",
|
"pnpm-lock.yaml",
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
|
||||||
"unused-imports/no-unused-imports": "error",
|
|
||||||
"unused-imports/no-unused-vars": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
vars: "all",
|
|
||||||
varsIgnorePattern: "^_",
|
|
||||||
args: "after-used",
|
|
||||||
argsIgnorePattern: "^_",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
"@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }],
|
|
||||||
"@typescript-eslint/no-explicit-any": "warn",
|
|
||||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
|
||||||
"no-console": ["warn", { allow: ["warn", "error"] }],
|
|
||||||
},
|
},
|
||||||
ignorePatterns: ["dist/", "node_modules/", "ClientApp/", "wwwroot/", "*.cjs"],
|
js.configs.recommended,
|
||||||
};
|
...tseslint.configs.recommended,
|
||||||
```
|
{
|
||||||
|
files: ["src/**/*.{ts,tsx}"],
|
||||||
- [ ] **Step 2: Write `.eslintignore`**
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
```
|
ecmaVersion: 2023,
|
||||||
dist/
|
sourceType: "module",
|
||||||
node_modules/
|
project: "./tsconfig.json",
|
||||||
ClientApp/
|
ecmaFeatures: { jsx: true },
|
||||||
wwwroot/
|
},
|
||||||
*.cjs
|
},
|
||||||
pnpm-lock.yaml
|
plugins: {
|
||||||
|
"unused-imports": unusedImports,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
|
"unused-imports/no-unused-imports": "error",
|
||||||
|
"unused-imports/no-unused-vars": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
vars: "all",
|
||||||
|
varsIgnorePattern: "^_",
|
||||||
|
args: "after-used",
|
||||||
|
argsIgnorePattern: "^_",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }],
|
||||||
|
"@typescript-eslint/no-explicit-any": "warn",
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||||
|
"no-console": ["warn", { allow: ["warn", "error"] }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 3: Run lint**
|
- [ ] **Step 3: Run lint**
|
||||||
@@ -302,13 +315,29 @@ pnpm-lock.yaml
|
|||||||
```bash
|
```bash
|
||||||
pnpm lint
|
pnpm lint
|
||||||
```
|
```
|
||||||
Expected: either `0` errors / `0` warnings with no files to lint, OR a message like "No files matching the pattern 'src/**/*.{ts,tsx}' were found." Both are acceptable — the point is that the config parses and the rule set loads without error.
|
Expected: exit `0` with no errors, no warnings, and no deprecation notices. If ESLint reports "no files matching the pattern" that's also acceptable — the point is the config parses and rules load without error.
|
||||||
|
|
||||||
- [ ] **Step 4: Commit**
|
- [ ] **Step 4: Smoke-test that rules actually fire**
|
||||||
|
|
||||||
|
Create a temporary probe file to confirm `no-explicit-any` fires:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add .eslintrc.cjs .eslintignore
|
echo "const x: any = 1; export default x;" > src/lint-probe.ts
|
||||||
git commit -m "Add baseline ESLint config (no boundary rules yet)"
|
pnpm lint
|
||||||
|
```
|
||||||
|
Expected: a warning on `any`. Delete the probe and re-run lint:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm src/lint-probe.ts
|
||||||
|
pnpm lint
|
||||||
|
```
|
||||||
|
Expected: exit `0`.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add eslint.config.js package.json pnpm-lock.yaml
|
||||||
|
git commit -m "Add baseline ESLint 9 flat config (no boundary rules yet)"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -715,15 +744,16 @@ export function getEnv(): Env {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const raw = parsed.data;
|
const raw = parsed.data;
|
||||||
cached = {
|
// Build with required fields first; the three optional URL/header
|
||||||
|
// fields are assigned only when defined. Under the tsconfig's
|
||||||
|
// `exactOptionalPropertyTypes: true`, assigning `undefined` to an
|
||||||
|
// optional property is a type error — hence the conditional shape.
|
||||||
|
const result: Env = {
|
||||||
NODE_ENV: raw.NODE_ENV,
|
NODE_ENV: raw.NODE_ENV,
|
||||||
BUILD_TARGET: raw.BUILD_TARGET,
|
BUILD_TARGET: raw.BUILD_TARGET,
|
||||||
PROD_ORIGIN: raw.PROD_ORIGIN,
|
PROD_ORIGIN: raw.PROD_ORIGIN,
|
||||||
API_BASE_URL: raw.API_BASE_URL,
|
API_BASE_URL: raw.API_BASE_URL,
|
||||||
SIGNALR_HUB_URL: raw.SIGNALR_HUB_URL,
|
SIGNALR_HUB_URL: raw.SIGNALR_HUB_URL,
|
||||||
OTEL_EXPORTER_OTLP_ENDPOINT: raw.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
||||||
OTEL_EXPORTER_OTLP_HEADERS: raw.OTEL_EXPORTER_OTLP_HEADERS,
|
|
||||||
LOGS_ENDPOINT: raw.LOGS_ENDPOINT,
|
|
||||||
ANALYTICS_ENABLED: {
|
ANALYTICS_ENABLED: {
|
||||||
metrica: raw.ANALYTICS_METRICA,
|
metrica: raw.ANALYTICS_METRICA,
|
||||||
ctm: raw.ANALYTICS_CTM,
|
ctm: raw.ANALYTICS_CTM,
|
||||||
@@ -732,7 +762,17 @@ export function getEnv(): Env {
|
|||||||
},
|
},
|
||||||
VERSION: raw.VERSION,
|
VERSION: raw.VERSION,
|
||||||
};
|
};
|
||||||
return cached;
|
if (raw.OTEL_EXPORTER_OTLP_ENDPOINT !== undefined) {
|
||||||
|
result.OTEL_EXPORTER_OTLP_ENDPOINT = raw.OTEL_EXPORTER_OTLP_ENDPOINT;
|
||||||
|
}
|
||||||
|
if (raw.OTEL_EXPORTER_OTLP_HEADERS !== undefined) {
|
||||||
|
result.OTEL_EXPORTER_OTLP_HEADERS = raw.OTEL_EXPORTER_OTLP_HEADERS;
|
||||||
|
}
|
||||||
|
if (raw.LOGS_ENDPOINT !== undefined) {
|
||||||
|
result.LOGS_ENDPOINT = raw.LOGS_ENDPOINT;
|
||||||
|
}
|
||||||
|
cached = result;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -828,7 +868,7 @@ export {};
|
|||||||
- [ ] **Step 2: Delete the placeholder from Task 3 if it exists**
|
- [ ] **Step 2: Delete the placeholder from Task 3 if it exists**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rm -f src/.typecheck-placeholder.ts
|
rm -f src/typecheck-placeholder.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 3: Typecheck and lint**
|
- [ ] **Step 3: Typecheck and lint**
|
||||||
@@ -842,7 +882,7 @@ Expected: both exit `0`.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add src/features/ src/ui/
|
git add src/features/ src/ui/
|
||||||
git rm -f src/.typecheck-placeholder.ts 2>/dev/null || true
|
git rm -f src/typecheck-placeholder.ts 2>/dev/null || true
|
||||||
git commit -m "Seed frozen public barrels for 4 features + UI adapter"
|
git commit -m "Seed frozen public barrels for 4 features + UI adapter"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -946,12 +986,17 @@ pnpm test
|
|||||||
```
|
```
|
||||||
Expected: 7 env tests pass, no other test files.
|
Expected: 7 env tests pass, no other test files.
|
||||||
|
|
||||||
- [ ] **Step 5: Coverage sanity check**
|
- [ ] **Step 5: Coverage sanity check — DEFERRED to 1B**
|
||||||
|
|
||||||
|
`pnpm test:coverage` requires `@vitest/coverage-v8`, which is owned by **1B** per the master plan's shared-files table. Do NOT install it here — 1B lands the dep and validates coverage end-to-end. Skip this step during 1A-1 execution; the exit gate still holds because the next sub-plan (1B) covers it.
|
||||||
|
|
||||||
|
If you want a smoke-check anyway, install the dep in a throwaway fashion, run, then remove — but this is optional and adds no value over 1B:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm test:coverage
|
pnpm add -D @vitest/coverage-v8@^3.0.0 # temporary
|
||||||
|
pnpm test:coverage # should show src/env near 100%
|
||||||
|
pnpm remove @vitest/coverage-v8 # revert so 1B owns it
|
||||||
```
|
```
|
||||||
Expected: exit `0`. `src/env/index.ts` reports near-100% coverage. Type-only files (`logger/types.ts`, `analytics/types.ts`, `host-contract.ts`) are excluded per `vitest.config.ts`, so they do not appear in the coverage report.
|
|
||||||
|
|
||||||
- [ ] **Step 6: Verify the sub-plan directory listing matches the deliverables**
|
- [ ] **Step 6: Verify the sub-plan directory listing matches the deliverables**
|
||||||
|
|
||||||
@@ -965,7 +1010,7 @@ Expected output shape:
|
|||||||
- `src/observability/analytics/types.ts`
|
- `src/observability/analytics/types.ts`
|
||||||
- Four feature directories each with `index.ts`
|
- Four feature directories each with `index.ts`
|
||||||
- `src/ui/index.ts`
|
- `src/ui/index.ts`
|
||||||
- No stray files (`.typecheck-placeholder.ts` must be gone).
|
- No stray files (`typecheck-placeholder.ts` must be gone).
|
||||||
|
|
||||||
- [ ] **Step 7: Verify Phase 0 and other sub-plans are unaffected**
|
- [ ] **Step 7: Verify Phase 0 and other sub-plans are unaffected**
|
||||||
|
|
||||||
@@ -989,7 +1034,7 @@ git status --porcelain | grep -q . && git commit -am "1A-1 exit gate verificatio
|
|||||||
**Spec coverage.** Every item in the master plan §1A-1 "Exports" bullet list maps to a task:
|
**Spec coverage.** Every item in the master plan §1A-1 "Exports" bullet list maps to a task:
|
||||||
- src/ layout (§1.3) → Task 5 (vitest.config), Task 6–10 (files created), Task 10 (barrels)
|
- src/ layout (§1.3) → Task 5 (vitest.config), Task 6–10 (files created), Task 10 (barrels)
|
||||||
- `tsconfig.json` → Task 3
|
- `tsconfig.json` → Task 3
|
||||||
- `.eslintrc.cjs` base → Task 4
|
- `eslint.config.js` flat config baseline → Task 4
|
||||||
- `package.json` scripts → Task 1
|
- `package.json` scripts → Task 1
|
||||||
- `package.json` dep ownership (`zod`) → Task 2
|
- `package.json` dep ownership (`zod`) → Task 2
|
||||||
- `src/env/index.ts` Zod-validated → Task 9 (TDD)
|
- `src/env/index.ts` Zod-validated → Task 9 (TDD)
|
||||||
|
|||||||
Reference in New Issue
Block a user