Skip to content

Commit 7aec84d

Browse files
authored
feat(store): add support of standalone API for ng add store (#3874)
1 parent 5f07eda commit 7aec84d

File tree

7 files changed

+166
-6
lines changed

7 files changed

+166
-6
lines changed

modules/schematics-core/testing/create-workspace.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ export async function createWorkspace(
5050
appTree
5151
);
5252

53+
appTree = await schematicRunner.runExternalSchematic(
54+
'@schematics/angular',
55+
'application',
56+
{ ...appOptions, name: 'bar-standalone', standalone: true },
57+
appTree
58+
);
59+
5360
appTree = await schematicRunner.runExternalSchematic(
5461
'@schematics/angular',
5562
'library',

modules/store/schematics-core/utility/project.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
import { TargetDefinition } from '@angular-devkit/core/src/workspace';
12
import { getWorkspace } from './config';
2-
import { Tree } from '@angular-devkit/schematics';
3+
import { SchematicsException, Tree } from '@angular-devkit/schematics';
34

45
export interface WorkspaceProject {
56
root: string;
67
projectType: string;
8+
architect: {
9+
[key: string]: TargetDefinition;
10+
};
711
}
812

913
export function getProject(
@@ -52,3 +56,20 @@ export function isLib(
5256

5357
return project.projectType === 'library';
5458
}
59+
60+
export function getProjectMainFile(
61+
host: Tree,
62+
options: { project?: string | undefined; path?: string | undefined }
63+
) {
64+
if (isLib(host, options)) {
65+
throw new SchematicsException(`Invalid project type`);
66+
}
67+
const project = getProject(host, options);
68+
const projectOptions = project.architect['build'].options;
69+
70+
if (!projectOptions?.main) {
71+
throw new SchematicsException(`Could not find the main file`);
72+
}
73+
74+
return projectOptions.main as string;
75+
}

modules/store/schematics/ng-add/index.spec.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,4 +161,54 @@ describe('Store ng-add Schematic', () => {
161161
},
162162
});
163163
});
164+
165+
describe('Store ng-add Schematic for standalone application', () => {
166+
const projectPath = getTestProjectPath(undefined, {
167+
name: 'bar-standalone',
168+
});
169+
const standaloneDefaultOptions = {
170+
...defaultOptions,
171+
project: 'bar-standalone',
172+
standalone: true,
173+
};
174+
175+
it('provides minimal store setup', async () => {
176+
const options = { ...standaloneDefaultOptions, minimal: true };
177+
const tree = await schematicRunner.runSchematic(
178+
'ng-add',
179+
options,
180+
appTree
181+
);
182+
183+
const content = tree.readContent(`${projectPath}/src/app/app.config.ts`);
184+
const files = tree.files;
185+
186+
expect(content).toMatch(/provideStore\(\)/);
187+
expect(content).not.toMatch(
188+
/import { reducers, metaReducers } from '\.\/reducers';/
189+
);
190+
expect(files.indexOf(`${projectPath}/src/app/reducers/index.ts`)).toBe(
191+
-1
192+
);
193+
});
194+
it('provides full store setup', async () => {
195+
const options = { ...standaloneDefaultOptions };
196+
const tree = await schematicRunner.runSchematic(
197+
'ng-add',
198+
options,
199+
appTree
200+
);
201+
202+
const content = tree.readContent(`${projectPath}/src/app/app.config.ts`);
203+
const files = tree.files;
204+
205+
expect(content).toMatch(/provideStore\(reducers, \{ metaReducers \}\)/);
206+
expect(content).toMatch(
207+
/import { reducers, metaReducers } from '\.\/reducers';/
208+
);
209+
expect(
210+
files.indexOf(`${projectPath}/src/app/reducers/index.ts`)
211+
).toBeGreaterThanOrEqual(0);
212+
});
213+
});
164214
});

modules/store/schematics/ng-add/index.ts

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ import {
3131
parseName,
3232
} from '../../schematics-core';
3333
import { Schema as RootStoreOptions } from './schema';
34+
import {
35+
addFunctionalProvidersToStandaloneBootstrap,
36+
callsProvidersFunction,
37+
} from '@schematics/angular/private/standalone';
38+
import { getProjectMainFile } from '../../schematics-core/utility/project';
3439

3540
function addImportToNgModule(options: RootStoreOptions): Rule {
3641
return (host: Tree) => {
@@ -138,14 +143,81 @@ function addNgRxESLintPlugin() {
138143
};
139144
}
140145

146+
function addStandaloneConfig(options: RootStoreOptions): Rule {
147+
return (host: Tree) => {
148+
const mainFile = getProjectMainFile(host, options);
149+
150+
if (host.exists(mainFile)) {
151+
const storeProviderFn = 'provideStore';
152+
153+
if (callsProvidersFunction(host, mainFile, storeProviderFn)) {
154+
// exit because the store config is already provided
155+
return host;
156+
}
157+
const storeProviderOptions = options.minimal
158+
? []
159+
: [
160+
ts.factory.createIdentifier('reducers'),
161+
ts.factory.createIdentifier('{ metaReducers }'),
162+
];
163+
const patchedConfigFile = addFunctionalProvidersToStandaloneBootstrap(
164+
host,
165+
mainFile,
166+
storeProviderFn,
167+
'@ngrx/store',
168+
storeProviderOptions
169+
);
170+
171+
if (options.minimal) {
172+
// no need to add imports if it is minimal
173+
return host;
174+
}
175+
176+
// insert reducers import into the patched file
177+
const configFileContent = host.read(patchedConfigFile);
178+
const source = ts.createSourceFile(
179+
patchedConfigFile,
180+
configFileContent?.toString('utf-8') || '',
181+
ts.ScriptTarget.Latest,
182+
true
183+
);
184+
const statePath = `/${options.path}/${options.statePath}`;
185+
const relativePath = buildRelativePath(
186+
`/${patchedConfigFile}`,
187+
statePath
188+
);
189+
190+
const recorder = host.beginUpdate(patchedConfigFile);
191+
192+
const change = insertImport(
193+
source,
194+
patchedConfigFile,
195+
'reducers, metaReducers',
196+
relativePath
197+
);
198+
199+
if (change instanceof InsertChange) {
200+
recorder.insertLeft(change.pos, change.toAdd);
201+
}
202+
203+
host.commitUpdate(recorder);
204+
205+
return host;
206+
}
207+
throw new SchematicsException(
208+
`Main file not found for a project ${options.project}`
209+
);
210+
};
211+
}
212+
141213
export default function (options: RootStoreOptions): Rule {
142214
return (host: Tree, context: SchematicContext) => {
143215
options.path = getProjectPath(host, options);
144216

145217
const parsedPath = parseName(options.path, '');
146218
options.path = parsedPath.path;
147219

148-
if (options.module) {
220+
if (options.module && !options.standalone) {
149221
options.module = findModuleFromOptions(host, {
150222
name: '',
151223
module: options.module,
@@ -166,10 +238,12 @@ export default function (options: RootStoreOptions): Rule {
166238
move(parsedPath.path),
167239
]);
168240

241+
const configOrModuleUpdate = options.standalone
242+
? addStandaloneConfig(options)
243+
: addImportToNgModule(options);
244+
169245
return chain([
170-
branchAndMerge(
171-
chain([addImportToNgModule(options), mergeWith(templateSource)])
172-
),
246+
branchAndMerge(chain([configOrModuleUpdate, mergeWith(templateSource)])),
173247
options && options.skipPackageJson ? noop() : addNgRxStoreToPackageJson(),
174248
options && options.skipESLintPlugin ? noop() : addNgRxESLintPlugin(),
175249
])(host, context);

modules/store/schematics/ng-add/schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@
4949
"type": "boolean",
5050
"default": false,
5151
"description": "Do not register the NgRx ESLint Plugin."
52+
},
53+
"standalone": {
54+
"type": "boolean",
55+
"default": false,
56+
"description": "Configure store for standalone application"
5257
}
5358
},
5459
"required": []

modules/store/schematics/ng-add/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ export interface Schema {
1010
*/
1111
minimal?: boolean;
1212
skipESLintPlugin?: boolean;
13+
standalone?: boolean;
1314
}

projects/ngrx.io/content/guide/store/install.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ ng add @ngrx/store@latest
1717
| `--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`
1818
| `--statePath` | The file path to create the state in. | `string` | `reducers` |
1919
| `--stateInterface` | The type literal of the defined interface for the state. | `string` | `State` |
20+
| `--standalone` | Flag to configure store for standalone application. | `boolean` |`false` |
2021

2122
This command will automate the following steps:
2223

2324
1. Update `package.json` > `dependencies` with `@ngrx/store`.
2425
2. Run `npm install` to install those dependencies.
25-
3. Update your `src/app/app.module.ts` > `imports` array with `StoreModule.forRoot({})`.
26+
3. Update your `src/app/app.module.ts` > `imports` array with `StoreModule.forRoot({})`
27+
4. If the flag `--standalone` is provided, it adds `provideStore()` into the application config.
2628

2729
```sh
2830
ng add @ngrx/store@latest --no-minimal

0 commit comments

Comments
 (0)