Skip to content

Commit

Permalink
feat(store): add support of standalone API for ng add store (#3874)
Browse files Browse the repository at this point in the history
  • Loading branch information
DMezhenskyi committed May 7, 2023
1 parent 5f07eda commit 7aec84d
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 6 deletions.
7 changes: 7 additions & 0 deletions modules/schematics-core/testing/create-workspace.ts
Expand Up @@ -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',
Expand Down
23 changes: 22 additions & 1 deletion 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(
Expand Down Expand Up @@ -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;
}
50 changes: 50 additions & 0 deletions modules/store/schematics/ng-add/index.spec.ts
Expand Up @@ -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);
});
});
});
82 changes: 78 additions & 4 deletions modules/store/schematics/ng-add/index.ts
Expand Up @@ -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) => {
Expand Down Expand Up @@ -138,14 +143,81 @@ 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);

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,
Expand All @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions modules/store/schematics/ng-add/schema.json
Expand Up @@ -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": []
Expand Down
1 change: 1 addition & 0 deletions modules/store/schematics/ng-add/schema.ts
Expand Up @@ -10,4 +10,5 @@ export interface Schema {
*/
minimal?: boolean;
skipESLintPlugin?: boolean;
standalone?: boolean;
}
4 changes: 3 additions & 1 deletion projects/ngrx.io/content/guide/store/install.md
Expand Up @@ -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 adds `provideStore()` into the application config.

```sh
ng add @ngrx/store@latest --no-minimal
Expand Down

0 comments on commit 7aec84d

Please sign in to comment.