From d5e51627214448bcaf3ec4c6ffb5af9569dbf4f5 Mon Sep 17 00:00:00 2001 From: Dmytro Mezhenskyi Date: Fri, 5 May 2023 12:30:03 +0200 Subject: [PATCH 1/2] feat(schematics) add support of standalone API for ng-add store allow to set up store for the standalone application when ng-add schematic is executed to improve DX Closes #3536 --- .../testing/create-workspace.ts | 7 ++ .../store/schematics-core/utility/project.ts | 23 +++++- modules/store/schematics/ng-add/index.spec.ts | 50 +++++++++++ modules/store/schematics/ng-add/index.ts | 82 ++++++++++++++++++- modules/store/schematics/ng-add/schema.json | 5 ++ modules/store/schematics/ng-add/schema.ts | 1 + .../ngrx.io/content/guide/store/install.md | 4 +- 7 files changed, 166 insertions(+), 6 deletions(-) diff --git a/modules/schematics-core/testing/create-workspace.ts b/modules/schematics-core/testing/create-workspace.ts index 5adb2dde79..90db38bc79 100644 --- a/modules/schematics-core/testing/create-workspace.ts +++ b/modules/schematics-core/testing/create-workspace.ts @@ -50,6 +50,13 @@ export async function createWorkspace( appTree ); + appTree = await schematicRunner.runExternalSchematic( + '@schematics/angular', + 'application', + { ...appOptions, name: 'bar-standalone', standalone: true }, + appTree + ); + appTree = await schematicRunner.runExternalSchematic( '@schematics/angular', 'library', diff --git a/modules/store/schematics-core/utility/project.ts b/modules/store/schematics-core/utility/project.ts index 9cadb16de2..824c39aacd 100644 --- a/modules/store/schematics-core/utility/project.ts +++ b/modules/store/schematics-core/utility/project.ts @@ -1,9 +1,13 @@ +import { TargetDefinition } from '@angular-devkit/core/src/workspace'; import { getWorkspace } from './config'; -import { Tree } from '@angular-devkit/schematics'; +import { SchematicsException, Tree } from '@angular-devkit/schematics'; export interface WorkspaceProject { root: string; projectType: string; + architect: { + [key: string]: TargetDefinition; + }; } export function getProject( @@ -52,3 +56,20 @@ export function isLib( return project.projectType === 'library'; } + +export function getProjectMainFile( + host: Tree, + options: { project?: string | undefined; path?: string | undefined } +) { + if (isLib(host, options)) { + throw new SchematicsException(`Invalid project type`); + } + const project = getProject(host, options); + const projectOptions = project.architect['build'].options; + + if (!projectOptions?.main) { + throw new SchematicsException(`Could not find the main file`); + } + + return projectOptions.main as string; +} diff --git a/modules/store/schematics/ng-add/index.spec.ts b/modules/store/schematics/ng-add/index.spec.ts index a84665b9a0..15a31f9d7e 100644 --- a/modules/store/schematics/ng-add/index.spec.ts +++ b/modules/store/schematics/ng-add/index.spec.ts @@ -161,4 +161,54 @@ describe('Store ng-add Schematic', () => { }, }); }); + + describe('Store ng-add Schematic for standalone application', () => { + const projectPath = getTestProjectPath(undefined, { + name: 'bar-standalone', + }); + const standaloneDefaultOptions = { + ...defaultOptions, + project: 'bar-standalone', + standalone: true, + }; + + it('provides minimal store setup', async () => { + const options = { ...standaloneDefaultOptions, minimal: true }; + const tree = await schematicRunner.runSchematic( + 'ng-add', + options, + appTree + ); + + const content = tree.readContent(`${projectPath}/src/app/app.config.ts`); + const files = tree.files; + + expect(content).toMatch(/provideStore\(\)/); + expect(content).not.toMatch( + /import { reducers, metaReducers } from '\.\/reducers';/ + ); + expect(files.indexOf(`${projectPath}/src/app/reducers/index.ts`)).toBe( + -1 + ); + }); + it('provides full store setup', async () => { + const options = { ...standaloneDefaultOptions }; + const tree = await schematicRunner.runSchematic( + 'ng-add', + options, + appTree + ); + + const content = tree.readContent(`${projectPath}/src/app/app.config.ts`); + const files = tree.files; + + expect(content).toMatch(/provideStore\(reducers, \{ metaReducers \}\)/); + expect(content).toMatch( + /import { reducers, metaReducers } from '\.\/reducers';/ + ); + expect( + files.indexOf(`${projectPath}/src/app/reducers/index.ts`) + ).toBeGreaterThanOrEqual(0); + }); + }); }); diff --git a/modules/store/schematics/ng-add/index.ts b/modules/store/schematics/ng-add/index.ts index d4b576720f..831f912802 100644 --- a/modules/store/schematics/ng-add/index.ts +++ b/modules/store/schematics/ng-add/index.ts @@ -31,6 +31,11 @@ import { parseName, } from '../../schematics-core'; import { Schema as RootStoreOptions } from './schema'; +import { + addFunctionalProvidersToStandaloneBootstrap, + callsProvidersFunction, +} from '@schematics/angular/private/standalone'; +import { getProjectMainFile } from '../../schematics-core/utility/project'; function addImportToNgModule(options: RootStoreOptions): Rule { return (host: Tree) => { @@ -138,6 +143,73 @@ function addNgRxESLintPlugin() { }; } +function addStandaloneConfig(options: RootStoreOptions): Rule { + return (host: Tree) => { + const mainFile = getProjectMainFile(host, options); + + if (host.exists(mainFile)) { + const storeProviderFn = 'provideStore'; + + if (callsProvidersFunction(host, mainFile, storeProviderFn)) { + // exit because the store config is already provided + return host; + } + const storeProviderOptions = options.minimal + ? [] + : [ + ts.factory.createIdentifier('reducers'), + ts.factory.createIdentifier('{ metaReducers }'), + ]; + const patchedConfigFile = addFunctionalProvidersToStandaloneBootstrap( + host, + mainFile, + storeProviderFn, + '@ngrx/store', + storeProviderOptions + ); + + if (options.minimal) { + // no need to add imports if it is minimal + return host; + } + + // insert reducers import into the patched file + const configFileContent = host.read(patchedConfigFile); + const source = ts.createSourceFile( + patchedConfigFile, + configFileContent?.toString('utf-8') || '', + ts.ScriptTarget.Latest, + true + ); + const statePath = `/${options.path}/${options.statePath}`; + const relativePath = buildRelativePath( + `/${patchedConfigFile}`, + statePath + ); + + const recorder = host.beginUpdate(patchedConfigFile); + + const change = insertImport( + source, + patchedConfigFile, + 'reducers, metaReducers', + relativePath + ); + + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } + + host.commitUpdate(recorder); + + return host; + } + throw new SchematicsException( + `Main file not found for a project ${options.project}` + ); + }; +} + export default function (options: RootStoreOptions): Rule { return (host: Tree, context: SchematicContext) => { options.path = getProjectPath(host, options); @@ -145,7 +217,7 @@ export default function (options: RootStoreOptions): Rule { const parsedPath = parseName(options.path, ''); options.path = parsedPath.path; - if (options.module) { + if (options.module && !options.standalone) { options.module = findModuleFromOptions(host, { name: '', module: options.module, @@ -166,10 +238,12 @@ export default function (options: RootStoreOptions): Rule { move(parsedPath.path), ]); + const configOrModuleUpdate = options.standalone + ? addStandaloneConfig(options) + : addImportToNgModule(options); + return chain([ - branchAndMerge( - chain([addImportToNgModule(options), mergeWith(templateSource)]) - ), + branchAndMerge(chain([configOrModuleUpdate, mergeWith(templateSource)])), options && options.skipPackageJson ? noop() : addNgRxStoreToPackageJson(), options && options.skipESLintPlugin ? noop() : addNgRxESLintPlugin(), ])(host, context); diff --git a/modules/store/schematics/ng-add/schema.json b/modules/store/schematics/ng-add/schema.json index 9135513c3d..fa63df003d 100644 --- a/modules/store/schematics/ng-add/schema.json +++ b/modules/store/schematics/ng-add/schema.json @@ -49,6 +49,11 @@ "type": "boolean", "default": false, "description": "Do not register the NgRx ESLint Plugin." + }, + "standalone": { + "type": "boolean", + "default": false, + "description": "Configure store for standalone application" } }, "required": [] diff --git a/modules/store/schematics/ng-add/schema.ts b/modules/store/schematics/ng-add/schema.ts index d2725b4910..a95b629451 100644 --- a/modules/store/schematics/ng-add/schema.ts +++ b/modules/store/schematics/ng-add/schema.ts @@ -10,4 +10,5 @@ export interface Schema { */ minimal?: boolean; skipESLintPlugin?: boolean; + standalone?: boolean; } diff --git a/projects/ngrx.io/content/guide/store/install.md b/projects/ngrx.io/content/guide/store/install.md index fdc8ac489b..0862f50db8 100644 --- a/projects/ngrx.io/content/guide/store/install.md +++ b/projects/ngrx.io/content/guide/store/install.md @@ -17,12 +17,14 @@ ng add @ngrx/store@latest | `--minimal` | Flag to only provide minimal setup for the root state management. Only registers `StoreModule.forRoot()` in the provided `module` with an empty object, and default runtime checks. | `boolean` |`true` | `--statePath` | The file path to create the state in. | `string` | `reducers` | | `--stateInterface` | The type literal of the defined interface for the state. | `string` | `State` | +| `--standalone` | Flag to configure store for standalone application. | `boolean` |`false` | This command will automate the following steps: 1. Update `package.json` > `dependencies` with `@ngrx/store`. 2. Run `npm install` to install those dependencies. -3. Update your `src/app/app.module.ts` > `imports` array with `StoreModule.forRoot({})`. +3. Update your `src/app/app.module.ts` > `imports` array with `StoreModule.forRoot({})` +4. If the flag `--standalone` is provided, it will add `provideStore()` into an applicaiton config. ```sh ng add @ngrx/store@latest --no-minimal From 4720baf70cef2da1886cdb2b5983737c85ed6e59 Mon Sep 17 00:00:00 2001 From: Dmytro Mezhenskyi Date: Sat, 6 May 2023 17:55:01 +0200 Subject: [PATCH 2/2] docs(schematics) improve feature description in documentation Co-authored-by: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> --- projects/ngrx.io/content/guide/store/install.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/ngrx.io/content/guide/store/install.md b/projects/ngrx.io/content/guide/store/install.md index 0862f50db8..4488d1201b 100644 --- a/projects/ngrx.io/content/guide/store/install.md +++ b/projects/ngrx.io/content/guide/store/install.md @@ -24,7 +24,7 @@ This command will automate the following steps: 1. Update `package.json` > `dependencies` with `@ngrx/store`. 2. Run `npm install` to install those dependencies. 3. Update your `src/app/app.module.ts` > `imports` array with `StoreModule.forRoot({})` -4. If the flag `--standalone` is provided, it will add `provideStore()` into an applicaiton config. +4. If the flag `--standalone` is provided, it adds `provideStore()` into the application config. ```sh ng add @ngrx/store@latest --no-minimal