diff --git a/CLAUDE.md b/CLAUDE.md index d9a27b8..fc48bbb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,15 +1,21 @@ -# React Native Application Template +# React Native App Template Instructions for contributors (and AI agents) working in this repository. These take precedence over generic assumptions about how a React Native project is laid out. -## What this repository is +## What this is -A production-oriented mobile app project template built with **React Native** and -**TypeScript** (Expo, with Continuous Native Generation). It ships a layered -architecture, MVVM-style state management, dependency injection through a composition -root, a set of cross-cutting concerns wired end to end, and a runnable sample feature -so a new app can start from a working, tested baseline. +A production-oriented mobile app built with **React Native** and **TypeScript** +(Expo, with Continuous Native Generation). It ships a layered architecture, +MVVM-style state management, dependency injection through a composition root, a set +of cross-cutting concerns wired end to end, and a runnable sample feature so the app +starts from a working, tested baseline. + + +This repository is the **template** those apps are generated from. Run `yarn generate` +to stamp out a new app — identifiers substituted, template-only scaffolding removed; +see [doc/ProjectGenerator.md](doc/ProjectGenerator.md). + ## Architecture @@ -27,7 +33,7 @@ The app is organized into layers, and within each layer code is grouped **by fea - **Dependency injection** wires the layers together: services are declared as interfaces and constructed in a single composition root (`src/framework/composition/createServices.ts`) — there is no DI container. -- **Cross-cutting concerns** the template ships: environments/configuration, +- **Cross-cutting concerns** the app ships: environments/configuration, logging, a diagnostics overlay, forced update, kill switch, localization, HTTP, local storage, serialization, navigation, and automated testing. - **A runnable sample feature** — the "Dad Jokes" app — demonstrates a full vertical @@ -185,7 +191,7 @@ Operational conventions (see the matching `doc/` pages): Platform-integration conventions (see the matching `doc/` pages): - **Opt-in vendor SDKs.** Firebase Remote Config and Bugsee are **not** default - dependencies — the base template ships (and prebuilds) without them. Each SDK is + dependencies — the app ships (and prebuilds) without them. Each SDK is loaded by a **literal guarded `require`** inside a single native gateway (`FirebaseRemoteConfigGateway`, `NativeBugseeGateway`), reachable **only** from `src/framework/composition/platformIntegrations.ts`, which the app entry imports diff --git a/cli/generate.test.ts b/cli/generate.test.ts index 992d43f..d9612e6 100644 --- a/cli/generate.test.ts +++ b/cli/generate.test.ts @@ -15,6 +15,7 @@ import { renderAppReadme, resolveIdentifiers, run, + sanitizeIosScheme, slugify, stripTemplateOnlyBlocks, TEMPLATE_IDENTIFIERS, @@ -68,6 +69,21 @@ describe('slugify', () => { }); }); +describe('sanitizeIosScheme', () => { + it("mirrors Expo's sanitizedName: strips spaces/punctuation and diacritics", () => { + // The template's own name and the value hard-coded as `iosScheme`. + expect(sanitizeIosScheme('React Native App Template')).toBe('ReactNativeAppTemplate'); + expect(sanitizeIosScheme('Acme App')).toBe('AcmeApp'); + // Faithful to Expo: precomposed accented letters are non-word chars and are + // dropped by the first strip, so they vanish rather than de-accenting. + expect(sanitizeIosScheme('Café Münster')).toBe('CafMnster'); + }); + + it('falls back to `app` when nothing alphanumeric survives', () => { + expect(sanitizeIosScheme('!!!')).toBe('app'); + }); +}); + describe('deriveInternalBundleId', () => { it('inserts an internal segment before the last', () => { expect(deriveInternalBundleId('com.acme.acmeapp')).toBe('com.acme.internal.acmeapp'); @@ -124,6 +140,17 @@ describe('buildReplacements', () => { froms.indexOf(TEMPLATE_IDENTIFIERS.baseBundleId), ); }); + + it('includes a rule for the derived iOS scheme name', () => { + const rule = buildReplacements(ACME).find((r) => r.from === 'ReactNativeAppTemplate'); + expect(rule).toEqual({ from: 'ReactNativeAppTemplate', to: 'AcmeApp' }); + }); + + it('omits the scheme rule when the sanitized name is unchanged', () => { + const sameName: Identifiers = { ...ACME, appName: TEMPLATE_IDENTIFIERS.appName }; + const froms = buildReplacements(sameName).map((r) => r.from); + expect(froms).not.toContain('ReactNativeAppTemplate'); + }); }); describe('applyReplacements', () => { @@ -253,6 +280,13 @@ describe('generate (filesystem)', () => { join(root, 'build', 'pipeline.yml'), `staging: ${TEMPLATE_IDENTIFIERS.internalBundleId}.jks\nprod: ${TEMPLATE_IDENTIFIERS.baseBundleId}.jks\n`, ); + // The iOS scheme/workspace name expo derives from the app name — hard-coded + // here, and NOT one of the four identifier strings, so a dedicated rule must + // rewrite it (see sanitizeIosScheme). + writeFileSync( + join(root, 'build', 'variables.yml'), + `iosScheme: '${sanitizeIosScheme(TEMPLATE_IDENTIFIERS.appName)}'\n`, + ); // Template-only CI steps template: removed, but the rest of build/ is kept. writeFileSync(join(root, 'build', 'templates', 'template-validation.yml'), 'steps: []\n'); // The main pipeline carries a Template_Validation stage bracketed by markers; @@ -304,6 +338,42 @@ describe('generate (filesystem)', () => { join(root, 'cli', 'generate.ts'), `const from = '${TEMPLATE_IDENTIFIERS.appName}';`, ); + // CLAUDE.md and the doc index self-identify as a template. The title uses the + // Expo `name` (renamed by substitution) and the template framing sits in a + // `template-only` block (scrubbed in place), so a generated app reads as an app. + writeFileSync( + join(root, 'CLAUDE.md'), + [ + `# ${TEMPLATE_IDENTIFIERS.appName}`, + '', + 'Contributor and AI-agent instructions.', + '', + '', + 'This repository is the **template** new apps are generated from; run `yarn generate`.', + '', + '', + '## Architecture', + '', + 'Layer boundaries that every generated app keeps.', + '', + ].join('\n'), + ); + writeFileSync( + join(root, 'doc', 'README.md'), + [ + '# Documentation', + '', + `Per-concern documentation for ${TEMPLATE_IDENTIFIERS.appName}.`, + '', + '| Topic | Page |', + '| ----- | ---- |', + '| CI pipeline | [AzurePipelines.md](AzurePipelines.md) |', + '', + '| Project generator CLI | [ProjectGenerator.md](ProjectGenerator.md) |', + '', + '', + ].join('\n'), + ); writeFileSync(join(root, 'README.md'), '# Template'); }); @@ -326,6 +396,10 @@ describe('generate (filesystem)', () => { expect(readFileSync(join(root, 'build', 'pipeline.yml'), 'utf8')).toBe( 'staging: com.acme.internal.acmeapp.jks\nprod: com.acme.acmeapp.jks\n', ); + // The iOS scheme is renamed too — `Acme App` sanitizes to `AcmeApp`. + expect(readFileSync(join(root, 'build', 'variables.yml'), 'utf8')).toBe( + "iosScheme: 'AcmeApp'\n", + ); // Untouched binary. expect(readFileSync(join(root, 'icon.png'), 'utf8')).toBe(TEMPLATE_IDENTIFIERS.appName); @@ -340,6 +414,9 @@ describe('generate (filesystem)', () => { const content = readFileSync(file, 'utf8'); expect(content).not.toContain('reactnativeapptemplate'); expect(content).not.toContain('React Native App Template'); + // The sanitized iOS scheme name (case-sensitive; not caught by the + // lowercase check above) must not survive either. + expect(content).not.toContain('ReactNativeAppTemplate'); } }); @@ -391,6 +468,28 @@ describe('generate (filesystem)', () => { ); }); + it('rewrites CLAUDE.md and the doc index so the app no longer reads as a template', () => { + const result = generate(root, ACME); + + const claude = readFileSync(join(root, 'CLAUDE.md'), 'utf8'); + // The Expo-name title is renamed, and the template framing is gone with its markers. + expect(claude).toContain('# Acme App'); + expect(claude).not.toContain('template-only'); + expect(claude).not.toContain('generated from'); + // The architecture guidance every app keeps survives the scrub. + expect(claude).toContain('## Architecture'); + expect(claude).toContain('Layer boundaries that every generated app keeps.'); + + const docIndex = readFileSync(join(root, 'doc', 'README.md'), 'utf8'); + expect(docIndex).toContain('Per-concern documentation for Acme App.'); + // The link to the deleted generator page is gone; the real rows remain. + expect(docIndex).not.toContain('ProjectGenerator'); + expect(docIndex).not.toContain('template-only'); + expect(docIndex).toContain('[AzurePipelines.md](AzurePipelines.md)'); + + expect(result.unwired).toEqual(expect.arrayContaining(['CLAUDE.md', 'doc/README.md'])); + }); + it('un-wires the generator from package.json and jest.config.js when cli is removed', () => { const result = generate(root, ACME); expect(result.unwired).toEqual(expect.arrayContaining(['package.json', 'jest.config.js'])); diff --git a/cli/generate.ts b/cli/generate.ts index f9b63fd..f6149f5 100644 --- a/cli/generate.ts +++ b/cli/generate.ts @@ -185,6 +185,34 @@ export function slugify(value: string): string { .replace(/^-+|-+$/g, ''); } +/** + * The iOS Xcode scheme/workspace/project name that `expo prebuild` derives from + * the app `name`. Mirrors Expo's `sanitizedName` + * (`@expo/config-plugins` → `ios/utils/Xcodeproj`): strip every non-word + * character and underscore, then drop combining diacritics. So + * `React Native App Template` -> `ReactNativeAppTemplate` and `Acme App` -> + * `AcmeApp`. + * + * The generated `ios/` project isn't committed (Continuous Native Generation), + * but this derived name IS hard-coded in committed files — the `iosScheme` + * pipeline variable in `build/variables.yml` and the `xcodebuild` examples in + * the docs — so a rename must rewrite it too. Miss it and the iOS build runs + * `xcodebuild -workspace ios/.xcworkspace`, which prebuild never + * generated under that name ("… .xcworkspace does not exist"). + * + * Faithful to Expo for any name with latin alphanumerics (the realistic case). + * A name that sanitizes to empty (e.g. all non-ASCII) falls back to `app` here, + * where Expo would first try a transliterated slug — an edge case a rename this + * tool targets does not hit. + */ +export function sanitizeIosScheme(name: string): string { + const sanitized = name + .replace(/[\W_]+/g, '') + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, ''); + return sanitized || 'app'; +} + const BUNDLE_ID_PATTERN = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/; const SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/; @@ -262,12 +290,29 @@ export interface Replacement { * can never partially consume a longer one. */ export function buildReplacements(identifiers: Identifiers): Replacement[] { - return [ + const replacements: Replacement[] = [ { from: TEMPLATE_IDENTIFIERS.internalBundleId, to: identifiers.internalBundleId }, { from: TEMPLATE_IDENTIFIERS.baseBundleId, to: identifiers.baseBundleId }, { from: TEMPLATE_IDENTIFIERS.slug, to: identifiers.slug }, { from: TEMPLATE_IDENTIFIERS.appName, to: identifiers.appName }, - ].sort((a, b) => b.from.length - a.from.length); + ]; + + // The iOS Xcode scheme/workspace name `expo prebuild` derives from the app + // `name` (see sanitizeIosScheme) is a *fifth* identity string, distinct from + // the spaced display name and the bundle ids: `React Native App Template` + // sanitizes to `ReactNativeAppTemplate`, which none of the four rules above + // match. It is hard-coded in committed files (the `iosScheme` pipeline + // variable, the docs' `xcodebuild` examples), so rewrite it too — otherwise + // the generated app's iOS build points at a workspace prebuild never makes. + // Skip when the sanitized name is unchanged (a rename touching only the + // bundle id), so no no-op rule is emitted. + const fromScheme = sanitizeIosScheme(TEMPLATE_IDENTIFIERS.appName); + const toScheme = sanitizeIosScheme(identifiers.appName); + if (fromScheme !== toScheme) { + replacements.push({ from: fromScheme, to: toScheme }); + } + + return replacements.sort((a, b) => b.from.length - a.from.length); } /** Applies replacements to a string, returning the result and a match count. */ @@ -435,14 +480,18 @@ export function stripTemplateOnlyBlocks(content: string): string { /** * Files (relative to the project root) that carry `template-only` regions the - * generated app must not keep: the CI stage that runs this generator and its - * documentation. Deleting the generator's `cli` folder handles the code; these - * files instead have a marked region scrubbed in place so the rest of the file - * (the real pipeline, the rest of the doc) survives. + * generated app must not keep — e.g. the CI stage that runs this generator, the + * "this repository is a template" framing in `CLAUDE.md`, and the doc-index link + * to the (deleted) generator page. Deleting the generator's `cli` folder handles + * the code; these files instead have a marked region scrubbed in place so the + * rest of the file (the real pipeline, the architecture guidance, the rest of the + * index) survives. */ export const FILES_WITH_TEMPLATE_ONLY_BLOCKS: readonly string[] = [ 'build/azure-pipelines.yml', 'doc/AzurePipelines.md', + 'CLAUDE.md', + 'doc/README.md', ]; /** @@ -450,7 +499,7 @@ export const FILES_WITH_TEMPLATE_ONLY_BLOCKS: readonly string[] = [ * in place. Returns the files it changed. Idempotent — safe to run when the * regions are already gone. */ -export function stripTemplateOnlyPipelineStages(root: string): string[] { +export function stripTemplateOnlyBlocksInFiles(root: string): string[] { const changed: string[] = []; for (const relativePath of FILES_WITH_TEMPLATE_ONLY_BLOCKS) { const target = join(root, relativePath); @@ -529,10 +578,12 @@ export function generate( } // Once the cli folder is removed, its references in the tooling would break // the quality gates; strip them so the generated project stays green. Also - // scrub the template-only CI stage (and its doc) that drive this generator — - // they reference the now-deleted cli/ and are meaningless in a generated app. + // scrub every `template-only` region — the CI stage that drives this + // generator, the doc-index link to the deleted generator page, and the + // "this repository is a template" framing in CLAUDE.md / the doc index — so a + // generated app reads as an app, not the template it was stamped from. if (!dryRun) { - unwired = [...unwireGeneratorTooling(root), ...stripTemplateOnlyPipelineStages(root)]; + unwired = [...unwireGeneratorTooling(root), ...stripTemplateOnlyBlocksInFiles(root)]; } } diff --git a/doc/AppReviews.md b/doc/AppReviews.md index 25212aa..53f4fb4 100644 --- a/doc/AppReviews.md +++ b/doc/AppReviews.md @@ -43,7 +43,7 @@ The call never throws — a review prompt must not break the flow that triggered From Presentation, read the thin [`useAppReview`](../src/presentation/appReview/useAppReview.ts) hook and call it at -a genuinely positive moment. The template wires it to the **feedback form's +a genuinely positive moment. The app wires it to the **feedback form's successful submit** (a completed flow) — see [Forms.md](Forms.md): ```ts diff --git a/doc/Architecture.md b/doc/Architecture.md index e397ab2..78453c1 100644 --- a/doc/Architecture.md +++ b/doc/Architecture.md @@ -19,7 +19,7 @@ Each layer's `README.md` states its rules; wiring is described in ## State management (MVVM) -Rather than ViewModel classes with observable properties, this template keeps the +Rather than ViewModel classes with observable properties, this app keeps the **principle** — business state observable by the UI, logic testable without a renderer — with two explicit data paths: diff --git a/doc/AzurePipelines.md b/doc/AzurePipelines.md index 1e4bcd8..aa256b8 100644 --- a/doc/AzurePipelines.md +++ b/doc/AzurePipelines.md @@ -1,6 +1,6 @@ # Azure Pipelines -CI/CD for this template runs on **Azure Pipelines**. **EAS Build is not used**: +CI/CD for this app runs on **Azure Pipelines**. **EAS Build is not used**: native builds run `expo prebuild` to generate the `android/` and `ios/` projects, then build with **Gradle** and **Xcode** directly on the build agents. @@ -174,7 +174,7 @@ one-time setup. **Firebase is optional and off by default.** In `build/azure-pipelines.yml` the `firebaseAndroidSecureFile` / `firebaseIosSecureFile` lines are **commented out**, so the pipeline builds Firebase-free (no `FIREBASE_ENABLED`, no -`googleServicesFile`, a clean `expo prebuild`) — matching the base template, where +`googleServicesFile`, a clean `expo prebuild`) — matching the app default, where Firebase is opt-in. The **presence of the secure-file name is the only switch**: uncomment the line (and upload the file) to turn Firebase on for that build; there is no separate flag to keep in sync. See [Enabling Firebase](#enabling-firebase). @@ -408,7 +408,7 @@ The public repo commits **no vendor keys** — CI injects them at build time: ## Enabling Firebase Firebase is **off by default** so a fresh clone (with no Firebase project yet) -builds and signs without it — matching the base template, where Firebase is +builds and signs without it — matching the app default, where Firebase is opt-in. The build templates key entirely off whether a Firebase secure-file name was passed: no name ⇒ no download, no `FIREBASE_ENABLED`, and `expo prebuild` produces no Firebase native footprint. @@ -422,7 +422,7 @@ To turn it on for a lane: (e.g. `google-services-reactnative-internal.json`). 3. In `build/azure-pipelines.yml`, **uncomment** the matching `firebaseAndroidSecureFile` / `firebaseIosSecureFile` line in that lane's job. - That single line is the switch — the template derives `FIREBASE_ENABLED` from + That single line is the switch — the build derives `FIREBASE_ENABLED` from its presence, so there is no separate flag to set. To turn it back off, re-comment the line (and optionally remove the secure file). diff --git a/doc/CrashReporting.md b/doc/CrashReporting.md index 5c50747..10c90f4 100644 --- a/doc/CrashReporting.md +++ b/doc/CrashReporting.md @@ -1,6 +1,6 @@ # Crash & session reporting -Crash and session reporting through a single Access seam. The template ships +Crash and session reporting through a single Access seam. The app ships **Bugsee for internal builds only** — it is billed per user, so it is only worth running on internal distribution builds, never production. diff --git a/doc/DadJokes.md b/doc/DadJokes.md index 579c577..1b988e1 100644 --- a/doc/DadJokes.md +++ b/doc/DadJokes.md @@ -1,6 +1,6 @@ # The Dad Jokes sample feature -Dad Jokes is the template's **canonical vertical slice** — the reference every +Dad Jokes is the app's **canonical vertical slice** — the reference every new feature is cloned from, following the recipe in [CLAUDE.md](../CLAUDE.md#adding-a-feature-the-canonical-recipe). It exercises every pattern the architecture names: interface + real + mock at the Access diff --git a/doc/DesignSystem.md b/doc/DesignSystem.md index d96eb1d..c6a54c3 100644 --- a/doc/DesignSystem.md +++ b/doc/DesignSystem.md @@ -1,6 +1,6 @@ # Design system & theming -The template's visual language: design tokens (colors, spacing, radii, +The app's visual language: design tokens (colors, spacing, radii, typography), light/dark support, and a small set of themed base components every screen builds from. diff --git a/doc/ErrorHandling.md b/doc/ErrorHandling.md index 70fcdcd..ca46ba7 100644 --- a/doc/ErrorHandling.md +++ b/doc/ErrorHandling.md @@ -1,6 +1,6 @@ # Error handling -The template-wide error model: a typed taxonomy produced at the Access boundary, +The app-wide error model: a typed taxonomy produced at the Access boundary, a screen-level convention for rendering loading/error states, and an app-shell error boundary. Built on top of the HTTP client's error mapping (see [HTTP.md](HTTP.md)). The doctrine: *repositories throw typed errors, screens diff --git a/doc/FirebaseRemoteConfig.md b/doc/FirebaseRemoteConfig.md index 5588f03..4bacfcf 100644 --- a/doc/FirebaseRemoteConfig.md +++ b/doc/FirebaseRemoteConfig.md @@ -5,7 +5,7 @@ The real remote-config backend behind the [remote-config seam](RemoteConfig.md) values the operational features read: the forced-update minimum version and the kill-switch flag. -Firebase is an **opt-in platform integration**: the base template ships **without** +Firebase is an **opt-in platform integration**: the app ships **without** the SDK or any Firebase keys (this repo is public), runs against [`StaticRemoteConfigProvider`](../src/access/remoteConfig/StaticRemoteConfigProvider.ts), and prebuilds/boots clean. A project turns Firebase on with the steps below. @@ -34,7 +34,7 @@ Set these two keys (and their values) in the Firebase console. ## Wiring (opt-in, keeps the default bundle SDK-free) -The composition root never imports the native gateway, so the base template's +The composition root never imports the native gateway, so the app's bundle and native build carry no Firebase. Activation is a one-line change at the app entry, passing the opt-in factory from [`platformIntegrations`](../src/framework/composition/platformIntegrations.ts): diff --git a/doc/ForcedUpdate.md b/doc/ForcedUpdate.md index 793e89d..8615cdf 100644 --- a/doc/ForcedUpdate.md +++ b/doc/ForcedUpdate.md @@ -1,7 +1,7 @@ # Forced update Blocks an out-of-date app until the user updates from the store. It is one half of -the template's remote-intervention story (the other is the +the app's remote-intervention story (the other is the [kill switch](KillSwitch.md)); there are no OTA updates. ## How it works diff --git a/doc/Forms.md b/doc/Forms.md index f1ff85d..94cc3ac 100644 --- a/doc/Forms.md +++ b/doc/Forms.md @@ -46,7 +46,7 @@ const submit = form.handleSubmit(async (values) => { }); ``` -There is no backend in the template, so a valid submit reports a domain event +There is no backend in the app, so a valid submit reports a domain event through the [analytics seam](Analytics.md) and treats the completed form as a positive moment for [app reviews](AppReviews.md). Swap the body for a Business-service call when a backend exists. diff --git a/doc/GettingStarted.md b/doc/GettingStarted.md index 93de90c..6bdfe54 100644 --- a/doc/GettingStarted.md +++ b/doc/GettingStarted.md @@ -1,6 +1,6 @@ # Getting Started -How to build and run the template on an Android emulator and an iOS +How to build and run the app on an Android emulator and an iOS simulator. The project uses **Expo with Continuous Native Generation**: the `android/` and `ios/` projects are generated by `expo prebuild` and are **gitignored — never hand-edit or commit them**. There is no EAS Build and no diff --git a/doc/HTTP.md b/doc/HTTP.md index 77be69f..3c957d1 100644 --- a/doc/HTTP.md +++ b/doc/HTTP.md @@ -80,7 +80,7 @@ against the [`TokenProvider`](../src/access/http/TokenProvider.ts) interface: 4. If refresh yields no token, call `onSessionExpired()` and reject with `UnauthorizedError`. -The template ships a `MockTokenProvider` (no-auth) so the client works for the +The app ships a `MockTokenProvider` (no-auth) so the client works for the unauthenticated sample API while the flow stays fully exercised by tests. ## Testing diff --git a/doc/KillSwitch.md b/doc/KillSwitch.md index 3d8735e..0f9e192 100644 --- a/doc/KillSwitch.md +++ b/doc/KillSwitch.md @@ -2,7 +2,7 @@ Remotely disables the app behind a message screen while a server-side flag is active, and **recovers automatically** when it lifts. Together -with [forced update](ForcedUpdate.md) it is the template's remote-intervention +with [forced update](ForcedUpdate.md) it is the app's remote-intervention story (there are no OTA updates). ## How it works diff --git a/doc/ProjectGenerator.md b/doc/ProjectGenerator.md index 7dac37f..b122393 100644 --- a/doc/ProjectGenerator.md +++ b/doc/ProjectGenerator.md @@ -80,10 +80,12 @@ Maestro `appId`s, the CI signing conventions, and the Firebase example configs). generator itself (`cli/`, plus its `doc/` page). The generator also strips its own wiring — the `generate` script, the cli typecheck step, and the cli Jest root — so the project's quality gates keep working once `cli/` is gone. -- **Scrubbed in place**: the `Template_Validation` stage in - `build/azure-pipelines.yml` and its section in `doc/AzurePipelines.md` (both - bracketed by `template-only` markers) are removed from those files, leaving the - rest of the pipeline and doc intact. +- **Scrubbed in place** (regions bracketed by `template-only` markers, so the rest + of each file survives): the `Template_Validation` stage in + `build/azure-pipelines.yml` and its section in `doc/AzurePipelines.md`; the + "this repository is a template" framing in `CLAUDE.md`; and the doc index's link + to this page in `doc/README.md`. Prose that describes the app rather than the + template is reworded at the source, so it needs no marker. - **Formatting**: files it edited are re-run through Prettier, because shortening or lengthening an identifier can change how a line wraps. @@ -111,8 +113,10 @@ pure functions in `cli/generate.ts`: `applyReplacements` performs the substitution — longest identifier first, so a shorter one can never partially match a longer one. - `stripTemplateOnlyBlocks` removes each `template-only:begin`…`template-only:end` - region from the pipeline and its doc (the `Template_Validation` stage), so - files that must survive generation keep everything *except* the marked block. + region from the files that must survive generation (the `Template_Validation` + stage in the pipeline and its doc, the template framing in `CLAUDE.md`, and the + generator link in the doc index), so each keeps everything *except* the marked + block. `stripTemplateOnlyBlocksInFiles` applies it to that file list. - `generate` orchestrates substitution, README rewrite, cleanup, un-wiring, and the block strip; `formatFiles` runs the Prettier pass. diff --git a/doc/README.md b/doc/README.md index 2f3e862..2ecdc5c 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,10 +1,10 @@ # Documentation -Per-concern documentation for the React Native Application Template. Each page is +Per-concern documentation for React Native App Template. Each page is a self-contained recipe for one topic; read [Architecture.md](Architecture.md) first for how the pieces fit together, then dip into the rest as needed. -New to the template? Start with [Getting Started](GettingStarted.md), then walk +New here? Start with [Getting Started](GettingStarted.md), then walk the [Dad Jokes](DadJokes.md) sample feature — the canonical vertical slice every new feature is cloned from. @@ -57,4 +57,6 @@ new feature is cloned from. | Testing (three-tier strategy) | [Testing.md](Testing.md) | | Security scan | [SecurityScan.md](SecurityScan.md) | | CI pipeline | [AzurePipelines.md](AzurePipelines.md) | + | Project generator CLI | [ProjectGenerator.md](ProjectGenerator.md) | + diff --git a/doc/RemoteConfig.md b/doc/RemoteConfig.md index fd43dae..57f1e23 100644 --- a/doc/RemoteConfig.md +++ b/doc/RemoteConfig.md @@ -4,7 +4,7 @@ The Access seam the operational features read remote, server-controlled values from — currently the forced-update minimum version and the kill-switch flag. A single provider covers both values. -The template builds **entirely against the interface** with a controllable mock, +The app builds **entirely against the interface** with a controllable mock, so the app runs with no backend. The real `@react-native-firebase/remote-config` implementation now exists as an **opt-in** provider — see [FirebaseRemoteConfig.md](FirebaseRemoteConfig.md); @@ -23,7 +23,7 @@ interface RemoteConfigProvider { ``` [`RemoteConfigValues`](../src/access/remoteConfig/RemoteConfig.ts) is the whole -schema the template needs — the two typed values and their remote-config wire keys: +schema the app needs — the two typed values and their remote-config wire keys: | Typed value | Remote key | Default | |-------------|------------|---------| diff --git a/doc/SecurityScan.md b/doc/SecurityScan.md index c118b4c..f3fca70 100644 --- a/doc/SecurityScan.md +++ b/doc/SecurityScan.md @@ -1,6 +1,6 @@ # Security Scan -The template ships **two complementary security scans**: +The app ships **two complementary security scans**: 1. A **supply-chain (SCA) dependency audit** — the baseline, built for the JavaScript/React Native ecosystem (nightly, non-blocking). diff --git a/doc/Testing.md b/doc/Testing.md index 9c31d3c..c0e3a60 100644 --- a/doc/Testing.md +++ b/doc/Testing.md @@ -1,6 +1,6 @@ # Testing -The headless-first, three-tier testing strategy for this template. +The headless-first, three-tier testing strategy for this app. The guiding rule: **prefer the lowest tier that can catch the bug.** Almost all logic lives in plain-TS Access/Business units and is covered at Tier 1; the UI