plan/react-rewrite #1
@@ -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.
|
||||
|
||||
**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.
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
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.)
|
||||
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.
|
||||
6. `src/` directory with:
|
||||
- `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 |
|
||||
| `pnpm-lock.yaml` | pnpm lockfile (generated) | 2 |
|
||||
| `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 |
|
||||
| `src/observability/logger/types.ts` | Type-only `Logger` surface | 6 |
|
||||
| `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:
|
||||
```bash
|
||||
mkdir -p src
|
||||
echo "export {};" > src/.typecheck-placeholder.ts
|
||||
echo "export {};" > src/typecheck-placeholder.ts
|
||||
pnpm typecheck
|
||||
```
|
||||
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**
|
||||
|
||||
```bash
|
||||
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"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4 — Create baseline `.eslintrc.cjs`
|
||||
## Task 4 — Create baseline `eslint.config.js` (ESLint 9 flat config)
|
||||
|
||||
**Files:**
|
||||
- Create: `.eslintrc.cjs`
|
||||
- Create: `.eslintignore`
|
||||
- Create: `eslint.config.js`
|
||||
|
||||
- [ ] **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
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaVersion: 2023,
|
||||
sourceType: "module",
|
||||
project: "./tsconfig.json",
|
||||
ecmaFeatures: { jsx: true },
|
||||
},
|
||||
plugins: ["@typescript-eslint", "unused-imports"],
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"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: "^_",
|
||||
},
|
||||
import js from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
import unusedImports from "eslint-plugin-unused-imports";
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: [
|
||||
"dist/**",
|
||||
"node_modules/**",
|
||||
"ClientApp/**",
|
||||
"wwwroot/**",
|
||||
"**/*.cjs",
|
||||
"pnpm-lock.yaml",
|
||||
],
|
||||
"@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"],
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write `.eslintignore`**
|
||||
|
||||
```
|
||||
dist/
|
||||
node_modules/
|
||||
ClientApp/
|
||||
wwwroot/
|
||||
*.cjs
|
||||
pnpm-lock.yaml
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
files: ["src/**/*.{ts,tsx}"],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
ecmaVersion: 2023,
|
||||
sourceType: "module",
|
||||
project: "./tsconfig.json",
|
||||
ecmaFeatures: { jsx: true },
|
||||
},
|
||||
},
|
||||
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**
|
||||
@@ -302,13 +315,29 @@ pnpm-lock.yaml
|
||||
```bash
|
||||
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
|
||||
git add .eslintrc.cjs .eslintignore
|
||||
git commit -m "Add baseline ESLint config (no boundary rules yet)"
|
||||
echo "const x: any = 1; export default x;" > src/lint-probe.ts
|
||||
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;
|
||||
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,
|
||||
BUILD_TARGET: raw.BUILD_TARGET,
|
||||
PROD_ORIGIN: raw.PROD_ORIGIN,
|
||||
API_BASE_URL: raw.API_BASE_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: {
|
||||
metrica: raw.ANALYTICS_METRICA,
|
||||
ctm: raw.ANALYTICS_CTM,
|
||||
@@ -732,7 +762,17 @@ export function getEnv(): Env {
|
||||
},
|
||||
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**
|
||||
|
||||
```bash
|
||||
rm -f src/.typecheck-placeholder.ts
|
||||
rm -f src/typecheck-placeholder.ts
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Typecheck and lint**
|
||||
@@ -842,7 +882,7 @@ Expected: both exit `0`.
|
||||
|
||||
```bash
|
||||
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"
|
||||
```
|
||||
|
||||
@@ -946,12 +986,17 @@ pnpm test
|
||||
```
|
||||
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
|
||||
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**
|
||||
|
||||
@@ -965,7 +1010,7 @@ Expected output shape:
|
||||
- `src/observability/analytics/types.ts`
|
||||
- Four feature directories each with `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**
|
||||
|
||||
@@ -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:
|
||||
- src/ layout (§1.3) → Task 5 (vitest.config), Task 6–10 (files created), Task 10 (barrels)
|
||||
- `tsconfig.json` → Task 3
|
||||
- `.eslintrc.cjs` base → Task 4
|
||||
- `eslint.config.js` flat config baseline → Task 4
|
||||
- `package.json` scripts → Task 1
|
||||
- `package.json` dep ownership (`zod`) → Task 2
|
||||
- `src/env/index.ts` Zod-validated → Task 9 (TDD)
|
||||
|
||||
Reference in New Issue
Block a user