diff --git a/angular.json b/angular.json
index 473e9db..80f0bfb 100644
--- a/angular.json
+++ b/angular.json
@@ -27,6 +27,9 @@
"test": {
"builder": "@angular-devkit/build-angular:jest",
"options": {
+ "exclude": [
+ "**/schematics/ng-add/*.spec.ts"
+ ],
"tsConfig": "projects/angular-redux/tsconfig.spec.json",
"polyfills": [
"zone.js",
diff --git a/package.json b/package.json
index 2eb0e12..bc7d0ea 100644
--- a/package.json
+++ b/package.json
@@ -4,9 +4,18 @@
"scripts": {
"ng": "ng",
"start": "ng serve",
- "build": "ng build",
+ "build": "ng build angular-redux && tsc -p projects/angular-redux/tsconfig.schematics.json && yarn build:copy",
+ "build:copy": "cd ./projects/angular-redux/schematics && copyfiles \"**/*.json\" ../../../dist/angular-redux/schematics",
+ "build:ng": "ng build",
"watch": "ng build --watch --configuration development",
- "test": "ng test"
+ "test": "yarn test:ng && yarn test:schematics",
+ "test:ng": "ng test",
+ "test:schematics": "cd projects/angular-redux/schematics && jest"
+ },
+ "workspaces": {
+ "packages": [
+ "projects/*"
+ ]
},
"private": true,
"dependencies": {
@@ -32,12 +41,16 @@
"@testing-library/dom": "^10.0.0",
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/user-event": "^14.5.2",
+ "@types/copyfiles": "^2",
"@types/jasmine": "~5.1.0",
"@types/jest": "^29.5.12",
+ "@types/node": "^22.5.4",
+ "copyfiles": "^2.4.1",
"jasmine-core": "~5.2.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"ng-packagr": "^18.2.0",
+ "ts-jest": "^29.2.5",
"typescript": "~5.5.2"
},
"packageManager": "yarn@4.1.0"
diff --git a/projects/angular-redux/README.md b/projects/angular-redux/README.md
index 015dfb8..731d2a5 100644
--- a/projects/angular-redux/README.md
+++ b/projects/angular-redux/README.md
@@ -3,7 +3,6 @@
Official Angular bindings for [Redux](https://github.com/reduxjs/redux).
Performant and flexible.
-
 [](https://www.npmjs.com/package/@reduxjs/angular-redux)
[](https://www.npmjs.com/package/@reduxjs/angular-redux)
@@ -14,6 +13,32 @@ Performant and flexible.
Angular Redux requires **Angular 17.3 or later**.
+### Installing with `ng add`
+
+You can install the Store to your project with the following `ng add` command (details here):
+
+```sh
+ng add @reduxjs/angular-redux@latest
+```
+
+#### Optional `ng add` flags
+
+| flag | description | value type | default value |
+|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|---------------
+| `--path` | Path to the module that you wish to add the import for the StoreModule to. | `string` |
+| `--project` | Name of the project defined in your `angular.json` to help locating the module to add the `provideRedux` to. | `string` |
+| `--module` | Name of file containing the module that you wish to add the import for the `provideRedux` to. Can also include the relative path to the file. For example, `src/app/app.module.ts`. | `string` | `app`
+| `--storePath` | The file path to create the state in. | `string` | `store` |
+
+This command will automate the following steps:
+
+1. Update `package.json` > `dependencies` with Redux, Redux Toolkit, and Angular Redux
+2. Run `npm install` to install those dependencies.
+3. Update your `src/app/app.module.ts` > `imports` array with `provideRedux({store})`
+4. If the project is using a `standalone bootstrap`, it adds `provideRedux({store})` into the application config.
+
+## Installing with `npm` or `yarn`
+
To use React Redux with your Angular app, install it as a dependency:
```bash
@@ -35,7 +60,7 @@ modules](https://webpack.js.org/api/module-methods/#commonjs).
The following Angular component works as-expected:
-```angular-ts
+```typescript
import { Component } from '@angular/core'
import { injectSelector, injectDispatch } from "@reduxjs/angular-redux";
import { decrement, increment } from './store/counter-slice'
diff --git a/projects/angular-redux/package.json b/projects/angular-redux/package.json
index bdee58d..f49196f 100644
--- a/projects/angular-redux/package.json
+++ b/projects/angular-redux/package.json
@@ -13,13 +13,18 @@
"peerDependencies": {
"@angular/common": ">=17.3.0",
"@angular/core": ">=17.3.0",
+ "@reduxjs/toolkit": "^2.2.7",
"redux": "^5.0.0"
},
"peerDependenciesMeta": {
+ "@reduxjs/toolkit": {
+ "optional": true
+ },
"redux": {
"optional": true
}
},
+ "schematics": "./schematics/collection.json",
"dependencies": {
"tslib": "^2.3.0"
},
diff --git a/projects/angular-redux/schematics-core/README.md b/projects/angular-redux/schematics-core/README.md
new file mode 100644
index 0000000..ac0e438
--- /dev/null
+++ b/projects/angular-redux/schematics-core/README.md
@@ -0,0 +1,4 @@
+This code is originally from NgRx:
+
+https://github.com/ngrx/platform/tree/main/modules/schematics-core
+https://github.com/ngrx/platform/tree/main/modules/store/schematics-core
diff --git a/projects/angular-redux/schematics-core/index.ts b/projects/angular-redux/schematics-core/index.ts
new file mode 100644
index 0000000..caf02f9
--- /dev/null
+++ b/projects/angular-redux/schematics-core/index.ts
@@ -0,0 +1,84 @@
+import {
+ dasherize,
+ decamelize,
+ camelize,
+ classify,
+ underscore,
+ group,
+ capitalize,
+ featurePath,
+ pluralize,
+} from './utility/strings';
+
+export {
+ findNodes,
+ getSourceNodes,
+ getDecoratorMetadata,
+ getContentOfKeyLiteral,
+ insertAfterLastOccurrence,
+ insertImport,
+ addBootstrapToModule,
+ addDeclarationToModule,
+ addExportToModule,
+ addImportToModule,
+ addProviderToComponent,
+ addProviderToModule,
+ replaceImport,
+ containsProperty,
+} from './utility/ast-utils';
+
+export {
+ NoopChange,
+ InsertChange,
+ RemoveChange,
+ ReplaceChange,
+ createReplaceChange,
+ createChangeRecorder,
+ commitChanges
+} from './utility/change';
+export type {
+ Host,
+ Change
+} from './utility/change';
+
+export {getWorkspace, getWorkspacePath} from './utility/config';
+export type { AppConfig } from './utility/config';
+
+export { findComponentFromOptions } from './utility/find-component';
+
+export {
+ findModule,
+ findModuleFromOptions,
+ buildRelativePath
+} from './utility/find-module';
+export type { ModuleOptions } from './utility/find-module';
+
+export { findPropertyInAstObject } from './utility/json-utilts';
+
+export { getProjectPath, getProject, isLib } from './utility/project';
+
+export const stringUtils = {
+ dasherize,
+ decamelize,
+ camelize,
+ classify,
+ underscore,
+ group,
+ capitalize,
+ featurePath,
+ pluralize,
+};
+
+export { parseName } from './utility/parse-name';
+
+export { addPackageToPackageJson } from './utility/package';
+
+export {
+ visitTSSourceFiles,
+ visitNgModuleImports,
+ visitNgModuleExports,
+ visitComponents,
+ visitDecorator,
+ visitNgModules,
+ visitTemplates,
+} from './utility/visitors';
diff --git a/projects/angular-redux/schematics-core/testing/create-app-module.ts b/projects/angular-redux/schematics-core/testing/create-app-module.ts
new file mode 100644
index 0000000..ebf3b82
--- /dev/null
+++ b/projects/angular-redux/schematics-core/testing/create-app-module.ts
@@ -0,0 +1,60 @@
+import { UnitTestTree } from '@angular-devkit/schematics/testing';
+
+export function createAppModule(
+ tree: UnitTestTree,
+ path?: string
+): UnitTestTree {
+ tree.create(
+ path || '/src/app/app.module.ts',
+ `
+ import { BrowserModule } from '@angular/platform-browser';
+ import { NgModule } from '@angular/core';
+ import { AppComponent } from './app.component';
+
+ @NgModule({
+ declarations: [
+ AppComponent
+ ],
+ imports: [
+ BrowserModule
+ ],
+ providers: [],
+ bootstrap: [AppComponent]
+ })
+ export class AppModule { }
+ `
+ );
+
+ return tree;
+}
+
+export function createAppModuleWithEffects(
+ tree: UnitTestTree,
+ path: string,
+ effects?: string
+): UnitTestTree {
+ tree.create(
+ path || '/src/app/app.module.ts',
+ `
+ import { BrowserModule } from '@angular/platform-browser';
+ import { NgModule } from '@angular/core';
+ import { AppComponent } from './app.component';
+ import { EffectsModule } from '@ngrx/effects';
+
+ @NgModule({
+ declarations: [
+ AppComponent
+ ],
+ imports: [
+ BrowserModule,
+ ${effects}
+ ],
+ providers: [],
+ bootstrap: [AppComponent]
+ })
+ export class AppModule { }
+ `
+ );
+
+ return tree;
+}
diff --git a/projects/angular-redux/schematics-core/testing/create-package.ts b/projects/angular-redux/schematics-core/testing/create-package.ts
new file mode 100644
index 0000000..23a5d50
--- /dev/null
+++ b/projects/angular-redux/schematics-core/testing/create-package.ts
@@ -0,0 +1,26 @@
+import { Tree } from '@angular-devkit/schematics';
+import {
+ UnitTestTree,
+ SchematicTestRunner,
+} from '@angular-devkit/schematics/testing';
+
+export const packagePath = '/package.json';
+
+export function createPackageJson(
+ prefix: string,
+ pkg: string,
+ tree: UnitTestTree,
+ version = '5.2.0',
+ packagePath = '/package.json'
+) {
+ tree.create(
+ packagePath,
+ `{
+ "dependencies": {
+ "@ngrx/${pkg}": "${prefix}${version}"
+ }
+ }`
+ );
+
+ return tree;
+}
diff --git a/projects/angular-redux/schematics-core/testing/create-reducers.ts b/projects/angular-redux/schematics-core/testing/create-reducers.ts
new file mode 100644
index 0000000..2ed0625
--- /dev/null
+++ b/projects/angular-redux/schematics-core/testing/create-reducers.ts
@@ -0,0 +1,34 @@
+import { UnitTestTree } from '@angular-devkit/schematics/testing';
+
+export function createReducers(
+ tree: UnitTestTree,
+ path?: string,
+ project = 'bar'
+) {
+ tree.create(
+ path || `/projects/${project}/src/app/reducers/index.ts`,
+ `
+ import { isDevMode } from '@angular/core';
+ import {
+ ActionReducer,
+ ActionReducerMap,
+ createFeatureSelector,
+ createSelector,
+ MetaReducer
+ } from '@ngrx/${'store'}';
+
+ export interface State {
+
+ }
+
+ export const reducers: ActionReducerMap = {
+
+ };
+
+
+ export const metaReducers: MetaReducer[] = isDevMode() ? [] : [];
+ `
+ );
+
+ return tree;
+}
diff --git a/projects/angular-redux/schematics-core/testing/create-workspace.ts b/projects/angular-redux/schematics-core/testing/create-workspace.ts
new file mode 100644
index 0000000..c5526c2
--- /dev/null
+++ b/projects/angular-redux/schematics-core/testing/create-workspace.ts
@@ -0,0 +1,69 @@
+import {
+ SchematicTestRunner,
+ UnitTestTree,
+} from '@angular-devkit/schematics/testing';
+
+export const defaultWorkspaceOptions = {
+ name: 'workspace',
+ newProjectRoot: 'projects',
+ version: '6.0.0',
+};
+
+export const defaultAppOptions = {
+ name: 'bar',
+ inlineStyle: false,
+ inlineTemplate: false,
+ viewEncapsulation: 'Emulated',
+ routing: false,
+ style: 'css',
+ skipTests: false,
+ standalone: false,
+};
+
+const defaultLibOptions = {
+ name: 'baz',
+};
+
+export function getTestProjectPath(
+ workspaceOptions: any = defaultWorkspaceOptions,
+ appOptions: any = defaultAppOptions
+) {
+ return `/${workspaceOptions.newProjectRoot}/${appOptions.name}`;
+}
+
+export async function createWorkspace(
+ schematicRunner: SchematicTestRunner,
+ appTree: UnitTestTree,
+ workspaceOptions = defaultWorkspaceOptions,
+ appOptions = defaultAppOptions,
+ libOptions = defaultLibOptions
+) {
+ appTree = await schematicRunner.runExternalSchematic(
+ '@schematics/angular',
+ 'workspace',
+ workspaceOptions
+ );
+
+ appTree = await schematicRunner.runExternalSchematic(
+ '@schematics/angular',
+ 'application',
+ appOptions,
+ appTree
+ );
+
+ appTree = await schematicRunner.runExternalSchematic(
+ '@schematics/angular',
+ 'application',
+ { ...appOptions, name: 'bar-standalone', standalone: true },
+ appTree
+ );
+
+ appTree = await schematicRunner.runExternalSchematic(
+ '@schematics/angular',
+ 'library',
+ libOptions,
+ appTree
+ );
+
+ return appTree;
+}
diff --git a/projects/angular-redux/schematics-core/testing/index.ts b/projects/angular-redux/schematics-core/testing/index.ts
new file mode 100644
index 0000000..fd1a5cb
--- /dev/null
+++ b/projects/angular-redux/schematics-core/testing/index.ts
@@ -0,0 +1,3 @@
+export * from './create-app-module';
+export * from './create-reducers';
+export * from './create-workspace';
diff --git a/projects/angular-redux/schematics-core/testing/update.ts b/projects/angular-redux/schematics-core/testing/update.ts
new file mode 100644
index 0000000..0910a57
--- /dev/null
+++ b/projects/angular-redux/schematics-core/testing/update.ts
@@ -0,0 +1,2 @@
+export const upgradeVersion = '6.0.0';
+export const versionPrefixes = ['~', '^', ''];
diff --git a/projects/angular-redux/schematics-core/utility/ast-utils.ts b/projects/angular-redux/schematics-core/utility/ast-utils.ts
new file mode 100644
index 0000000..5f7475c
--- /dev/null
+++ b/projects/angular-redux/schematics-core/utility/ast-utils.ts
@@ -0,0 +1,920 @@
+/* istanbul ignore file */
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import * as ts from 'typescript';
+import {
+ Change,
+ InsertChange,
+ NoopChange,
+ createReplaceChange,
+ ReplaceChange,
+ RemoveChange,
+ createRemoveChange,
+} from './change';
+import { Path } from '@angular-devkit/core';
+
+/**
+ * Find all nodes from the AST in the subtree of node of SyntaxKind kind.
+ * @param node
+ * @param kind
+ * @param max The maximum number of items to return.
+ * @return all nodes of kind, or [] if none is found
+ */
+export function findNodes(
+ node: ts.Node,
+ kind: ts.SyntaxKind,
+ max = Infinity
+): ts.Node[] {
+ if (!node || max == 0) {
+ return [];
+ }
+
+ const arr: ts.Node[] = [];
+ if (node.kind === kind) {
+ arr.push(node);
+ max--;
+ }
+ if (max > 0) {
+ for (const child of node.getChildren()) {
+ findNodes(child, kind, max).forEach((node) => {
+ if (max > 0) {
+ arr.push(node);
+ }
+ max--;
+ });
+
+ if (max <= 0) {
+ break;
+ }
+ }
+ }
+
+ return arr;
+}
+
+/**
+ * Get all the nodes from a source.
+ * @param sourceFile The source file object.
+ * @returns {Observable} An observable of all the nodes in the source.
+ */
+export function getSourceNodes(sourceFile: ts.SourceFile): ts.Node[] {
+ const nodes: ts.Node[] = [sourceFile];
+ const result = [];
+
+ while (nodes.length > 0) {
+ const node = nodes.shift();
+
+ if (node) {
+ result.push(node);
+ if (node.getChildCount(sourceFile) >= 0) {
+ nodes.unshift(...node.getChildren());
+ }
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Helper for sorting nodes.
+ * @return function to sort nodes in increasing order of position in sourceFile
+ */
+function nodesByPosition(first: ts.Node, second: ts.Node): number {
+ return first.pos - second.pos;
+}
+
+/**
+ * Insert `toInsert` after the last occurence of `ts.SyntaxKind[nodes[i].kind]`
+ * or after the last of occurence of `syntaxKind` if the last occurence is a sub child
+ * of ts.SyntaxKind[nodes[i].kind] and save the changes in file.
+ *
+ * @param nodes insert after the last occurence of nodes
+ * @param toInsert string to insert
+ * @param file file to insert changes into
+ * @param fallbackPos position to insert if toInsert happens to be the first occurence
+ * @param syntaxKind the ts.SyntaxKind of the subchildren to insert after
+ * @return Change instance
+ * @throw Error if toInsert is first occurence but fall back is not set
+ */
+export function insertAfterLastOccurrence(
+ nodes: ts.Node[],
+ toInsert: string,
+ file: string,
+ fallbackPos: number,
+ syntaxKind?: ts.SyntaxKind
+): Change {
+ let lastItem = nodes.sort(nodesByPosition).pop();
+ if (!lastItem) {
+ throw new Error();
+ }
+ if (syntaxKind) {
+ lastItem = findNodes(lastItem, syntaxKind).sort(nodesByPosition).pop();
+ }
+ if (!lastItem && fallbackPos == undefined) {
+ throw new Error(
+ `tried to insert ${toInsert} as first occurence with no fallback position`
+ );
+ }
+ const lastItemPosition: number = lastItem ? lastItem.end : fallbackPos;
+
+ return new InsertChange(file, lastItemPosition, toInsert);
+}
+
+export function getContentOfKeyLiteral(
+ _source: ts.SourceFile,
+ node: ts.Node
+): string | null {
+ if (node.kind == ts.SyntaxKind.Identifier) {
+ return (node as ts.Identifier).text;
+ } else if (node.kind == ts.SyntaxKind.StringLiteral) {
+ return (node as ts.StringLiteral).text;
+ } else {
+ return null;
+ }
+}
+
+function _angularImportsFromNode(
+ node: ts.ImportDeclaration,
+ _sourceFile: ts.SourceFile
+): { [name: string]: string } {
+ const ms = node.moduleSpecifier;
+ let modulePath: string;
+ switch (ms.kind) {
+ case ts.SyntaxKind.StringLiteral:
+ modulePath = (ms as ts.StringLiteral).text;
+ break;
+ default:
+ return {};
+ }
+
+ if (!modulePath.startsWith('@angular/')) {
+ return {};
+ }
+
+ if (node.importClause) {
+ if (node.importClause.name) {
+ // This is of the form `import Name from 'path'`. Ignore.
+ return {};
+ } else if (node.importClause.namedBindings) {
+ const nb = node.importClause.namedBindings;
+ if (nb.kind == ts.SyntaxKind.NamespaceImport) {
+ // This is of the form `import * as name from 'path'`. Return `name.`.
+ return {
+ [(nb as ts.NamespaceImport).name.text + '.']: modulePath,
+ };
+ } else {
+ // This is of the form `import {a,b,c} from 'path'`
+ const namedImports = nb as ts.NamedImports;
+
+ return namedImports.elements
+ .map((is: ts.ImportSpecifier) =>
+ is.propertyName ? is.propertyName.text : is.name.text
+ )
+ .reduce((acc: { [name: string]: string }, curr: string) => {
+ acc[curr] = modulePath;
+
+ return acc;
+ }, {});
+ }
+ }
+
+ return {};
+ } else {
+ // This is of the form `import 'path';`. Nothing to do.
+ return {};
+ }
+}
+
+export function getDecoratorMetadata(
+ source: ts.SourceFile,
+ identifier: string,
+ module: string
+): ts.Node[] {
+ const angularImports: { [name: string]: string } = findNodes(
+ source,
+ ts.SyntaxKind.ImportDeclaration
+ )
+ .map((node) =>
+ _angularImportsFromNode(node as ts.ImportDeclaration, source)
+ )
+ .reduce(
+ (
+ acc: { [name: string]: string },
+ current: { [name: string]: string }
+ ) => {
+ for (const key of Object.keys(current)) {
+ acc[key] = current[key];
+ }
+
+ return acc;
+ },
+ {}
+ );
+
+ return getSourceNodes(source)
+ .filter((node) => {
+ return (
+ node.kind == ts.SyntaxKind.Decorator &&
+ (node as ts.Decorator).expression.kind == ts.SyntaxKind.CallExpression
+ );
+ })
+ .map((node) => (node as ts.Decorator).expression as ts.CallExpression)
+ .filter((expr) => {
+ if (expr.expression.kind == ts.SyntaxKind.Identifier) {
+ const id = expr.expression as ts.Identifier;
+
+ return (
+ id.getFullText(source) == identifier &&
+ angularImports[id.getFullText(source)] === module
+ );
+ } else if (
+ expr.expression.kind == ts.SyntaxKind.PropertyAccessExpression
+ ) {
+ // This covers foo.NgModule when importing * as foo.
+ const paExpr = expr.expression as ts.PropertyAccessExpression;
+ // If the left expression is not an identifier, just give up at that point.
+ if (paExpr.expression.kind !== ts.SyntaxKind.Identifier) {
+ return false;
+ }
+
+ const id = paExpr.name.text;
+ const moduleId = (paExpr.expression as ts.Identifier).getText(source);
+
+ return id === identifier && angularImports[moduleId + '.'] === module;
+ }
+
+ return false;
+ })
+ .filter(
+ (expr) =>
+ expr.arguments[0] &&
+ expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression
+ )
+ .map((expr) => expr.arguments[0] as ts.ObjectLiteralExpression);
+}
+
+function _addSymbolToNgModuleMetadata(
+ source: ts.SourceFile,
+ ngModulePath: string,
+ metadataField: string,
+ symbolName: string,
+ importPath: string
+): Change[] {
+ const nodes = getDecoratorMetadata(source, 'NgModule', '@angular/core');
+ let node: any = nodes[0]; // eslint-disable-line @typescript-eslint/no-explicit-any
+
+ // Find the decorator declaration.
+ if (!node) {
+ return [];
+ }
+
+ // Get all the children property assignment of object literals.
+ const matchingProperties: ts.ObjectLiteralElement[] = (
+ node as ts.ObjectLiteralExpression
+ ).properties
+ .filter((prop) => prop.kind == ts.SyntaxKind.PropertyAssignment)
+ // Filter out every fields that's not "metadataField". Also handles string literals
+ // (but not expressions).
+ .filter((prop: any) => {
+ const name = prop.name;
+ switch (name.kind) {
+ case ts.SyntaxKind.Identifier:
+ return (name as ts.Identifier).getText(source) == metadataField;
+ case ts.SyntaxKind.StringLiteral:
+ return (name as ts.StringLiteral).text == metadataField;
+ }
+
+ return false;
+ });
+
+ // Get the last node of the array literal.
+ if (!matchingProperties) {
+ return [];
+ }
+ if (matchingProperties.length == 0) {
+ // We haven't found the field in the metadata declaration. Insert a new field.
+ const expr = node as ts.ObjectLiteralExpression;
+ let position: number;
+ let toInsert: string;
+ if (expr.properties.length == 0) {
+ position = expr.getEnd() - 1;
+ toInsert = ` ${metadataField}: [${symbolName}]\n`;
+ } else {
+ node = expr.properties[expr.properties.length - 1];
+ position = node.getEnd();
+ // Get the indentation of the last element, if any.
+ const text = node.getFullText(source);
+ const matches = text.match(/^\r?\n\s*/);
+ if (matches.length > 0) {
+ toInsert = `,${matches[0]}${metadataField}: [${symbolName}]`;
+ } else {
+ toInsert = `, ${metadataField}: [${symbolName}]`;
+ }
+ }
+ const newMetadataProperty = new InsertChange(
+ ngModulePath,
+ position,
+ toInsert
+ );
+ const newMetadataImport = insertImport(
+ source,
+ ngModulePath,
+ symbolName.replace(/\..*$/, ''),
+ importPath
+ );
+
+ return [newMetadataProperty, newMetadataImport];
+ }
+
+ const assignment = matchingProperties[0] as ts.PropertyAssignment;
+
+ // If it's not an array, nothing we can do really.
+ if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
+ return [];
+ }
+
+ const arrLiteral = assignment.initializer as ts.ArrayLiteralExpression;
+ if (arrLiteral.elements.length == 0) {
+ // Forward the property.
+ node = arrLiteral;
+ } else {
+ node = arrLiteral.elements;
+ }
+
+ if (!node) {
+ console.log(
+ 'No app module found. Please add your new class to your component.'
+ );
+
+ return [];
+ }
+
+ if (Array.isArray(node)) {
+ const nodeArray = node as {} as Array;
+ const symbolsArray = nodeArray.map((node) => node.getText());
+ if (symbolsArray.includes(symbolName)) {
+ return [];
+ }
+
+ node = node[node.length - 1];
+
+ const effectsModule = nodeArray.find(
+ (node) =>
+ (node.getText().includes('EffectsModule.forRoot') &&
+ symbolName.includes('EffectsModule.forRoot')) ||
+ (node.getText().includes('EffectsModule.forFeature') &&
+ symbolName.includes('EffectsModule.forFeature'))
+ );
+
+ if (effectsModule && symbolName.includes('EffectsModule')) {
+ const effectsArgs = (effectsModule as any).arguments.shift();
+
+ if (
+ effectsArgs &&
+ effectsArgs.kind === ts.SyntaxKind.ArrayLiteralExpression
+ ) {
+ const effectsElements = (effectsArgs as ts.ArrayLiteralExpression)
+ .elements;
+ const [, effectsSymbol] = (symbolName).match(/\[(.*)\]/);
+
+ let epos;
+ if (effectsElements.length === 0) {
+ epos = effectsArgs.getStart() + 1;
+ return [new InsertChange(ngModulePath, epos, effectsSymbol)];
+ } else {
+ const lastEffect = effectsElements[
+ effectsElements.length - 1
+ ] as ts.Expression;
+ epos = lastEffect.getEnd();
+ // Get the indentation of the last element, if any.
+ const text: any = lastEffect.getFullText(source);
+
+ let effectInsert: string;
+ if (text.match('^\r?\r?\n')) {
+ effectInsert = `,${text.match(/^\r?\n\s+/)[0]}${effectsSymbol}`;
+ } else {
+ effectInsert = `, ${effectsSymbol}`;
+ }
+
+ return [new InsertChange(ngModulePath, epos, effectInsert)];
+ }
+ } else {
+ return [];
+ }
+ }
+ }
+
+ let toInsert: string;
+ let position = node.getEnd();
+ if (node.kind == ts.SyntaxKind.ObjectLiteralExpression) {
+ // We haven't found the field in the metadata declaration. Insert a new
+ // field.
+ const expr = node as ts.ObjectLiteralExpression;
+ if (expr.properties.length == 0) {
+ position = expr.getEnd() - 1;
+ toInsert = ` ${metadataField}: [${symbolName}]\n`;
+ } else {
+ node = expr.properties[expr.properties.length - 1];
+ position = node.getEnd();
+ // Get the indentation of the last element, if any.
+ const text = node.getFullText(source);
+ if (text.match('^\r?\r?\n')) {
+ toInsert = `,${
+ text.match(/^\r?\n\s+/)[0]
+ }${metadataField}: [${symbolName}]`;
+ } else {
+ toInsert = `, ${metadataField}: [${symbolName}]`;
+ }
+ }
+ } else if (node.kind == ts.SyntaxKind.ArrayLiteralExpression) {
+ // We found the field but it's empty. Insert it just before the `]`.
+ position--;
+ toInsert = `${symbolName}`;
+ } else {
+ // Get the indentation of the last element, if any.
+ const text = node.getFullText(source);
+ if (text.match(/^\r?\n/)) {
+ toInsert = `,${text.match(/^\r?\n(\r?)\s+/)[0]}${symbolName}`;
+ } else {
+ toInsert = `, ${symbolName}`;
+ }
+ }
+ const insert = new InsertChange(ngModulePath, position, toInsert);
+ const importInsert: Change = insertImport(
+ source,
+ ngModulePath,
+ symbolName.replace(/\..*$/, ''),
+ importPath
+ );
+
+ return [insert, importInsert];
+}
+
+function _addSymbolToComponentMetadata(
+ source: ts.SourceFile,
+ componentPath: string,
+ metadataField: string,
+ symbolName: string,
+ importPath: string
+): Change[] {
+ const nodes = getDecoratorMetadata(source, 'Component', '@angular/core');
+ let node: any = nodes[0]; // eslint-disable-line @typescript-eslint/no-explicit-any
+
+ // Find the decorator declaration.
+ if (!node) {
+ return [];
+ }
+
+ // Get all the children property assignment of object literals.
+ const matchingProperties: ts.ObjectLiteralElement[] = (
+ node as ts.ObjectLiteralExpression
+ ).properties
+ .filter((prop) => prop.kind == ts.SyntaxKind.PropertyAssignment)
+ // Filter out every fields that's not "metadataField". Also handles string literals
+ // (but not expressions).
+ .filter((prop: any) => {
+ const name = prop.name;
+ switch (name.kind) {
+ case ts.SyntaxKind.Identifier:
+ return (name as ts.Identifier).getText(source) == metadataField;
+ case ts.SyntaxKind.StringLiteral:
+ return (name as ts.StringLiteral).text == metadataField;
+ }
+
+ return false;
+ });
+
+ // Get the last node of the array literal.
+ if (!matchingProperties) {
+ return [];
+ }
+ if (matchingProperties.length == 0) {
+ // We haven't found the field in the metadata declaration. Insert a new field.
+ const expr = node as ts.ObjectLiteralExpression;
+ let position: number;
+ let toInsert: string;
+ if (expr.properties.length == 0) {
+ position = expr.getEnd() - 1;
+ toInsert = ` ${metadataField}: [${symbolName}]\n`;
+ } else {
+ node = expr.properties[expr.properties.length - 1];
+ position = node.getEnd();
+ // Get the indentation of the last element, if any.
+ const text = node.getFullText(source);
+ const matches = text.match(/^\r?\n\s*/);
+ if (matches.length > 0) {
+ toInsert = `,${matches[0]}${metadataField}: [${symbolName}]`;
+ } else {
+ toInsert = `, ${metadataField}: [${symbolName}]`;
+ }
+ }
+ const newMetadataProperty = new InsertChange(
+ componentPath,
+ position,
+ toInsert
+ );
+ const newMetadataImport = insertImport(
+ source,
+ componentPath,
+ symbolName.replace(/\..*$/, ''),
+ importPath
+ );
+
+ return [newMetadataProperty, newMetadataImport];
+ }
+
+ const assignment = matchingProperties[0] as ts.PropertyAssignment;
+
+ // If it's not an array, nothing we can do really.
+ if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
+ return [];
+ }
+
+ const arrLiteral = assignment.initializer as ts.ArrayLiteralExpression;
+ if (arrLiteral.elements.length == 0) {
+ // Forward the property.
+ node = arrLiteral;
+ } else {
+ node = arrLiteral.elements;
+ }
+
+ if (!node) {
+ console.log(
+ 'No component found. Please add your new class to your component.'
+ );
+
+ return [];
+ }
+
+ if (Array.isArray(node)) {
+ const nodeArray = node as {} as Array;
+ const symbolsArray = nodeArray.map((node) => node.getText());
+ if (symbolsArray.includes(symbolName)) {
+ return [];
+ }
+
+ node = node[node.length - 1];
+ }
+
+ let toInsert: string;
+ let position = node.getEnd();
+ if (node.kind == ts.SyntaxKind.ObjectLiteralExpression) {
+ // We haven't found the field in the metadata declaration. Insert a new
+ // field.
+ const expr = node as ts.ObjectLiteralExpression;
+ if (expr.properties.length == 0) {
+ position = expr.getEnd() - 1;
+ toInsert = ` ${metadataField}: [${symbolName}]\n`;
+ } else {
+ node = expr.properties[expr.properties.length - 1];
+ position = node.getEnd();
+ // Get the indentation of the last element, if any.
+ const text = node.getFullText(source);
+ if (text.match('^\r?\r?\n')) {
+ toInsert = `,${
+ text.match(/^\r?\n\s+/)[0]
+ }${metadataField}: [${symbolName}]`;
+ } else {
+ toInsert = `, ${metadataField}: [${symbolName}]`;
+ }
+ }
+ } else if (node.kind == ts.SyntaxKind.ArrayLiteralExpression) {
+ // We found the field but it's empty. Insert it just before the `]`.
+ position--;
+ toInsert = `${symbolName}`;
+ } else {
+ // Get the indentation of the last element, if any.
+ const text = node.getFullText(source);
+ if (text.match(/^\r?\n/)) {
+ toInsert = `,${text.match(/^\r?\n(\r?)\s+/)[0]}${symbolName}`;
+ } else {
+ toInsert = `, ${symbolName}`;
+ }
+ }
+ const insert = new InsertChange(componentPath, position, toInsert);
+ const importInsert: Change = insertImport(
+ source,
+ componentPath,
+ symbolName.replace(/\..*$/, ''),
+ importPath
+ );
+
+ return [insert, importInsert];
+}
+
+/**
+ * Custom function to insert a declaration (component, pipe, directive)
+ * into NgModule declarations. It also imports the component.
+ */
+export function addDeclarationToModule(
+ source: ts.SourceFile,
+ modulePath: string,
+ classifiedName: string,
+ importPath: string
+): Change[] {
+ return _addSymbolToNgModuleMetadata(
+ source,
+ modulePath,
+ 'declarations',
+ classifiedName,
+ importPath
+ );
+}
+
+/**
+ * Custom function to insert a declaration (component, pipe, directive)
+ * into NgModule declarations. It also imports the component.
+ */
+export function addImportToModule(
+ source: ts.SourceFile,
+ modulePath: string,
+ classifiedName: string,
+ importPath: string
+): Change[] {
+ return _addSymbolToNgModuleMetadata(
+ source,
+ modulePath,
+ 'imports',
+ classifiedName,
+ importPath
+ );
+}
+
+/**
+ * Custom function to insert a provider into NgModule. It also imports it.
+ */
+export function addProviderToModule(
+ source: ts.SourceFile,
+ modulePath: string,
+ classifiedName: string,
+ importPath: string
+): Change[] {
+ return _addSymbolToNgModuleMetadata(
+ source,
+ modulePath,
+ 'providers',
+ classifiedName,
+ importPath
+ );
+}
+
+/**
+ * Custom function to insert a provider into Component. It also imports it.
+ */
+export function addProviderToComponent(
+ source: ts.SourceFile,
+ componentPath: string,
+ classifiedName: string,
+ importPath: string
+): Change[] {
+ return _addSymbolToComponentMetadata(
+ source,
+ componentPath,
+ 'providers',
+ classifiedName,
+ importPath
+ );
+}
+
+/**
+ * Custom function to insert an export into NgModule. It also imports it.
+ */
+export function addExportToModule(
+ source: ts.SourceFile,
+ modulePath: string,
+ classifiedName: string,
+ importPath: string
+): Change[] {
+ return _addSymbolToNgModuleMetadata(
+ source,
+ modulePath,
+ 'exports',
+ classifiedName,
+ importPath
+ );
+}
+
+/**
+ * Custom function to insert an export into NgModule. It also imports it.
+ */
+export function addBootstrapToModule(
+ source: ts.SourceFile,
+ modulePath: string,
+ classifiedName: string,
+ importPath: string
+): Change[] {
+ return _addSymbolToNgModuleMetadata(
+ source,
+ modulePath,
+ 'bootstrap',
+ classifiedName,
+ importPath
+ );
+}
+
+/**
+ * Add Import `import { symbolName } from fileName` if the import doesn't exit
+ * already. Assumes fileToEdit can be resolved and accessed.
+ * @param fileToEdit (file we want to add import to)
+ * @param symbolName (item to import)
+ * @param fileName (path to the file)
+ * @param isDefault (if true, import follows style for importing default exports)
+ * @return Change
+ */
+
+export function insertImport(
+ source: ts.SourceFile,
+ fileToEdit: string,
+ symbolName: string,
+ fileName: string,
+ isDefault = false
+): Change {
+ const rootNode = source;
+ const allImports = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
+
+ // get nodes that map to import statements from the file fileName
+ const relevantImports = allImports.filter((node) => {
+ // StringLiteral of the ImportDeclaration is the import file (fileName in this case).
+ const importFiles = node
+ .getChildren()
+ .filter((child) => child.kind === ts.SyntaxKind.StringLiteral)
+ .map((n) => (n as ts.StringLiteral).text);
+
+ return importFiles.filter((file) => file === fileName).length === 1;
+ });
+
+ if (relevantImports.length > 0) {
+ let importsAsterisk = false;
+ // imports from import file
+ const imports: ts.Node[] = [];
+ relevantImports.forEach((n) => {
+ Array.prototype.push.apply(
+ imports,
+ findNodes(n, ts.SyntaxKind.Identifier)
+ );
+ if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) {
+ importsAsterisk = true;
+ }
+ });
+
+ // if imports * from fileName, don't add symbolName
+ if (importsAsterisk) {
+ return new NoopChange();
+ }
+
+ const importTextNodes = imports.filter(
+ (n) => (n as ts.Identifier).text === symbolName
+ );
+
+ // insert import if it's not there
+ if (importTextNodes.length === 0) {
+ const fallbackPos =
+ findNodes(
+ relevantImports[0],
+ ts.SyntaxKind.CloseBraceToken
+ )[0].getStart() ||
+ findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].getStart();
+
+ return insertAfterLastOccurrence(
+ imports,
+ `, ${symbolName}`,
+ fileToEdit,
+ fallbackPos
+ );
+ }
+
+ return new NoopChange();
+ }
+
+ // no such import declaration exists
+ const useStrict = findNodes(rootNode, ts.SyntaxKind.StringLiteral).filter(
+ (n) => n.getText() === 'use strict'
+ );
+ let fallbackPos = 0;
+ if (useStrict.length > 0) {
+ fallbackPos = useStrict[0].end;
+ }
+ const open = isDefault ? '' : '{ ';
+ const close = isDefault ? '' : ' }';
+ // if there are no imports or 'use strict' statement, insert import at beginning of file
+ const insertAtBeginning = allImports.length === 0 && useStrict.length === 0;
+ const separator = insertAtBeginning ? '' : ';\n';
+ const toInsert =
+ `${separator}import ${open}${symbolName}${close}` +
+ ` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`;
+
+ return insertAfterLastOccurrence(
+ allImports,
+ toInsert,
+ fileToEdit,
+ fallbackPos,
+ ts.SyntaxKind.StringLiteral
+ );
+}
+
+export function replaceImport(
+ sourceFile: ts.SourceFile,
+ path: Path,
+ importFrom: string,
+ importAsIs: string,
+ importToBe: string
+): (ReplaceChange | RemoveChange)[] {
+ const imports = sourceFile.statements
+ .filter(ts.isImportDeclaration)
+ .filter(
+ ({ moduleSpecifier }) =>
+ moduleSpecifier.getText(sourceFile) === `'${importFrom}'` ||
+ moduleSpecifier.getText(sourceFile) === `"${importFrom}"`
+ );
+
+ if (imports.length === 0) {
+ return [];
+ }
+
+ const importText = (specifier: ts.ImportSpecifier) => {
+ if (specifier.name.text) {
+ return specifier.name.text;
+ }
+
+ // if import is renamed
+ if (specifier.propertyName && specifier.propertyName.text) {
+ return specifier.propertyName.text;
+ }
+
+ return '';
+ };
+
+ const changes = imports.map((p) => {
+ const namedImports = p?.importClause?.namedBindings as ts.NamedImports;
+ if (!namedImports) {
+ return [];
+ }
+
+ const importSpecifiers = namedImports.elements;
+ const isAlreadyImported = importSpecifiers
+ .map(importText)
+ .includes(importToBe);
+
+ const importChanges = importSpecifiers.map((specifier, index) => {
+ const text = importText(specifier);
+
+ // import is not the one we're looking for, can be skipped
+ if (text !== importAsIs) {
+ return undefined;
+ }
+
+ // identifier has not been imported, simply replace the old text with the new text
+ if (!isAlreadyImported) {
+ return createReplaceChange(
+ sourceFile,
+ specifier,
+ importAsIs,
+ importToBe
+ );
+ }
+
+ const nextIdentifier = importSpecifiers[index + 1];
+ // identifer is not the last, also clean up the comma
+ if (nextIdentifier) {
+ return createRemoveChange(
+ sourceFile,
+ specifier,
+ specifier.getStart(sourceFile),
+ nextIdentifier.getStart(sourceFile)
+ );
+ }
+
+ // there are no imports following, just remove it
+ return createRemoveChange(
+ sourceFile,
+ specifier,
+ specifier.getStart(sourceFile),
+ specifier.getEnd()
+ );
+ });
+
+ return importChanges.filter(Boolean) as (ReplaceChange | RemoveChange)[];
+ });
+
+ return changes.reduce((imports, curr) => imports.concat(curr), []);
+}
+
+export function containsProperty(
+ objectLiteral: ts.ObjectLiteralExpression,
+ propertyName: string
+) {
+ return (
+ objectLiteral &&
+ objectLiteral.properties.some(
+ (prop) =>
+ ts.isPropertyAssignment(prop) &&
+ ts.isIdentifier(prop.name) &&
+ prop.name.text === propertyName
+ )
+ );
+}
diff --git a/projects/angular-redux/schematics-core/utility/change.ts b/projects/angular-redux/schematics-core/utility/change.ts
new file mode 100644
index 0000000..3b8ed0c
--- /dev/null
+++ b/projects/angular-redux/schematics-core/utility/change.ts
@@ -0,0 +1,187 @@
+import * as ts from 'typescript';
+import { Tree, UpdateRecorder } from '@angular-devkit/schematics';
+import { Path } from '@angular-devkit/core';
+
+/* istanbul ignore file */
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+export interface Host {
+ write(path: string, content: string): Promise;
+ read(path: string): Promise;
+}
+
+export interface Change {
+ apply(host: Host): Promise;
+
+ // The file this change should be applied to. Some changes might not apply to
+ // a file (maybe the config).
+ readonly path: string | null;
+
+ // The order this change should be applied. Normally the position inside the file.
+ // Changes are applied from the bottom of a file to the top.
+ readonly order: number;
+
+ // The description of this change. This will be outputted in a dry or verbose run.
+ readonly description: string;
+}
+
+/**
+ * An operation that does nothing.
+ */
+export class NoopChange implements Change {
+ description = 'No operation.';
+ order = Infinity;
+ path = null;
+ apply() {
+ return Promise.resolve();
+ }
+}
+
+/**
+ * Will add text to the source code.
+ */
+export class InsertChange implements Change {
+ order: number;
+ description: string;
+
+ constructor(public path: string, public pos: number, public toAdd: string) {
+ if (pos < 0) {
+ throw new Error('Negative positions are invalid');
+ }
+ this.description = `Inserted ${toAdd} into position ${pos} of ${path}`;
+ this.order = pos;
+ }
+
+ /**
+ * This method does not insert spaces if there is none in the original string.
+ */
+ apply(host: Host) {
+ return host.read(this.path).then((content) => {
+ const prefix = content.substring(0, this.pos);
+ const suffix = content.substring(this.pos);
+
+ return host.write(this.path, `${prefix}${this.toAdd}${suffix}`);
+ });
+ }
+}
+
+/**
+ * Will remove text from the source code.
+ */
+export class RemoveChange implements Change {
+ order: number;
+ description: string;
+
+ constructor(public path: string, public pos: number, public end: number) {
+ if (pos < 0 || end < 0) {
+ throw new Error('Negative positions are invalid');
+ }
+ this.description = `Removed text in position ${pos} to ${end} of ${path}`;
+ this.order = pos;
+ }
+
+ apply(host: Host): Promise {
+ return host.read(this.path).then((content) => {
+ const prefix = content.substring(0, this.pos);
+ const suffix = content.substring(this.end);
+
+ // TODO: throw error if toRemove doesn't match removed string.
+ return host.write(this.path, `${prefix}${suffix}`);
+ });
+ }
+}
+
+/**
+ * Will replace text from the source code.
+ */
+export class ReplaceChange implements Change {
+ order: number;
+ description: string;
+
+ constructor(
+ public path: string,
+ public pos: number,
+ public oldText: string,
+ public newText: string
+ ) {
+ if (pos < 0) {
+ throw new Error('Negative positions are invalid');
+ }
+ this.description = `Replaced ${oldText} into position ${pos} of ${path} with ${newText}`;
+ this.order = pos;
+ }
+
+ apply(host: Host): Promise {
+ return host.read(this.path).then((content) => {
+ const prefix = content.substring(0, this.pos);
+ const suffix = content.substring(this.pos + this.oldText.length);
+ const text = content.substring(this.pos, this.pos + this.oldText.length);
+
+ if (text !== this.oldText) {
+ return Promise.reject(
+ new Error(`Invalid replace: "${text}" != "${this.oldText}".`)
+ );
+ }
+
+ // TODO: throw error if oldText doesn't match removed string.
+ return host.write(this.path, `${prefix}${this.newText}${suffix}`);
+ });
+ }
+}
+
+export function createReplaceChange(
+ sourceFile: ts.SourceFile,
+ node: ts.Node,
+ oldText: string,
+ newText: string
+): ReplaceChange {
+ return new ReplaceChange(
+ sourceFile.fileName,
+ node.getStart(sourceFile),
+ oldText,
+ newText
+ );
+}
+
+export function createRemoveChange(
+ sourceFile: ts.SourceFile,
+ node: ts.Node,
+ from = node.getStart(sourceFile),
+ to = node.getEnd()
+): RemoveChange {
+ return new RemoveChange(sourceFile.fileName, from, to);
+}
+
+export function createChangeRecorder(
+ tree: Tree,
+ path: string,
+ changes: Change[]
+): UpdateRecorder {
+ const recorder = tree.beginUpdate(path);
+ for (const change of changes) {
+ if (change instanceof InsertChange) {
+ recorder.insertLeft(change.pos, change.toAdd);
+ } else if (change instanceof RemoveChange) {
+ recorder.remove(change.pos, change.end - change.pos);
+ } else if (change instanceof ReplaceChange) {
+ recorder.remove(change.pos, change.oldText.length);
+ recorder.insertLeft(change.pos, change.newText);
+ }
+ }
+ return recorder;
+}
+
+export function commitChanges(tree: Tree, path: string, changes: Change[]) {
+ if (changes.length === 0) {
+ return false;
+ }
+
+ const recorder = createChangeRecorder(tree, path, changes);
+ tree.commitUpdate(recorder);
+ return true;
+}
diff --git a/projects/angular-redux/schematics-core/utility/config.ts b/projects/angular-redux/schematics-core/utility/config.ts
new file mode 100644
index 0000000..06daabc
--- /dev/null
+++ b/projects/angular-redux/schematics-core/utility/config.ts
@@ -0,0 +1,147 @@
+import { SchematicsException, Tree } from '@angular-devkit/schematics';
+
+// The interfaces below are generated from the Angular CLI configuration schema
+// https://github.com/angular/angular-cli/blob/master/packages/@angular/cli/lib/config/schema.json
+export interface AppConfig {
+ /**
+ * Name of the app.
+ */
+ name?: string;
+ /**
+ * Directory where app files are placed.
+ */
+ appRoot?: string;
+ /**
+ * The root directory of the app.
+ */
+ root?: string;
+ /**
+ * The output directory for build results.
+ */
+ outDir?: string;
+ /**
+ * List of application assets.
+ */
+ assets?: (
+ | string
+ | {
+ /**
+ * The pattern to match.
+ */
+ glob?: string;
+ /**
+ * The dir to search within.
+ */
+ input?: string;
+ /**
+ * The output path (relative to the outDir).
+ */
+ output?: string;
+ }
+ )[];
+ /**
+ * URL where files will be deployed.
+ */
+ deployUrl?: string;
+ /**
+ * Base url for the application being built.
+ */
+ baseHref?: string;
+ /**
+ * The runtime platform of the app.
+ */
+ platform?: 'browser' | 'server';
+ /**
+ * The name of the start HTML file.
+ */
+ index?: string;
+ /**
+ * The name of the main entry-point file.
+ */
+ main?: string;
+ /**
+ * The name of the polyfills file.
+ */
+ polyfills?: string;
+ /**
+ * The name of the test entry-point file.
+ */
+ test?: string;
+ /**
+ * The name of the TypeScript configuration file.
+ */
+ tsconfig?: string;
+ /**
+ * The name of the TypeScript configuration file for unit tests.
+ */
+ testTsconfig?: string;
+ /**
+ * The prefix to apply to generated selectors.
+ */
+ prefix?: string;
+ /**
+ * Experimental support for a service worker from @angular/service-worker.
+ */
+ serviceWorker?: boolean;
+ /**
+ * Global styles to be included in the build.
+ */
+ styles?: (
+ | string
+ | {
+ input?: string;
+ [name: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
+ }
+ )[];
+ /**
+ * Options to pass to style preprocessors
+ */
+ stylePreprocessorOptions?: {
+ /**
+ * Paths to include. Paths will be resolved to project root.
+ */
+ includePaths?: string[];
+ };
+ /**
+ * Global scripts to be included in the build.
+ */
+ scripts?: (
+ | string
+ | {
+ input: string;
+ [name: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
+ }
+ )[];
+ /**
+ * Source file for environment config.
+ */
+ environmentSource?: string;
+ /**
+ * Name and corresponding file for environment config.
+ */
+ environments?: {
+ [name: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
+ };
+ appShell?: {
+ app: string;
+ route: string;
+ };
+}
+
+export function getWorkspacePath(host: Tree): string {
+ const possibleFiles = ['/angular.json', '/.angular.json', '/workspace.json'];
+ const path = possibleFiles.filter((path) => host.exists(path))[0];
+
+ return path;
+}
+
+export function getWorkspace(host: Tree) {
+ const path = getWorkspacePath(host);
+ const configBuffer = host.read(path);
+ if (configBuffer === null) {
+ throw new SchematicsException(`Could not find (${path})`);
+ }
+ const config = configBuffer.toString();
+
+ return JSON.parse(config);
+}
diff --git a/projects/angular-redux/schematics-core/utility/find-component.ts b/projects/angular-redux/schematics-core/utility/find-component.ts
new file mode 100644
index 0000000..bb59ec3
--- /dev/null
+++ b/projects/angular-redux/schematics-core/utility/find-component.ts
@@ -0,0 +1,145 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import {
+ Path,
+ join,
+ normalize,
+ relative,
+ strings,
+ basename,
+ extname,
+ dirname,
+} from '@angular-devkit/core';
+import { DirEntry, Tree } from '@angular-devkit/schematics';
+
+export interface ComponentOptions {
+ component?: string;
+ name: string;
+ flat?: boolean;
+ path?: string;
+ skipImport?: boolean;
+}
+
+/**
+ * Find the component referred by a set of options passed to the schematics.
+ */
+export function findComponentFromOptions(
+ host: Tree,
+ options: ComponentOptions
+): Path | undefined {
+ if (options.hasOwnProperty('skipImport') && options.skipImport) {
+ return undefined;
+ }
+
+ if (!options.component) {
+ const pathToCheck =
+ (options.path || '') +
+ (options.flat ? '' : '/' + strings.dasherize(options.name));
+
+ return normalize(findComponent(host, pathToCheck));
+ } else {
+ const componentPath = normalize(
+ '/' + options.path + '/' + options.component
+ );
+ const componentBaseName = normalize(componentPath).split('/').pop();
+
+ if (host.exists(componentPath)) {
+ return normalize(componentPath);
+ } else if (host.exists(componentPath + '.ts')) {
+ return normalize(componentPath + '.ts');
+ } else if (host.exists(componentPath + '.component.ts')) {
+ return normalize(componentPath + '.component.ts');
+ } else if (
+ host.exists(componentPath + '/' + componentBaseName + '.component.ts')
+ ) {
+ return normalize(
+ componentPath + '/' + componentBaseName + '.component.ts'
+ );
+ } else {
+ throw new Error(
+ `Specified component path ${componentPath} does not exist`
+ );
+ }
+ }
+}
+
+/**
+ * Function to find the "closest" component to a generated file's path.
+ */
+export function findComponent(host: Tree, generateDir: string): Path {
+ let dir: DirEntry | null = host.getDir('/' + generateDir);
+
+ const componentRe = /\.component\.ts$/;
+
+ while (dir) {
+ const matches = dir.subfiles.filter((p) => componentRe.test(p));
+
+ if (matches.length == 1) {
+ return join(dir.path, matches[0]);
+ } else if (matches.length > 1) {
+ throw new Error(
+ 'More than one component matches. Use skip-import option to skip importing ' +
+ 'the component store into the closest component.'
+ );
+ }
+
+ dir = dir.parent;
+ }
+
+ throw new Error(
+ 'Could not find an Component. Use the skip-import ' +
+ 'option to skip importing in Component.'
+ );
+}
+
+/**
+ * Build a relative path from one file path to another file path.
+ */
+export function buildRelativePath(from: string, to: string): string {
+ const {
+ path: fromPath,
+ filename: fromFileName,
+ directory: fromDirectory,
+ } = parsePath(from);
+ const {
+ path: toPath,
+ filename: toFileName,
+ directory: toDirectory,
+ } = parsePath(to);
+ const relativePath = relative(fromDirectory, toDirectory);
+ const fixedRelativePath = relativePath.startsWith('.')
+ ? relativePath
+ : `./${relativePath}`;
+
+ return !toFileName || toFileName === 'index.ts'
+ ? fixedRelativePath
+ : `${
+ fixedRelativePath.endsWith('/')
+ ? fixedRelativePath
+ : fixedRelativePath + '/'
+ }${convertToTypeScriptFileName(toFileName)}`;
+}
+
+function parsePath(path: string) {
+ const pathNormalized = normalize(path) as Path;
+ const filename = extname(pathNormalized) ? basename(pathNormalized) : '';
+ const directory = filename ? dirname(pathNormalized) : pathNormalized;
+ return {
+ path: pathNormalized,
+ filename,
+ directory,
+ };
+}
+/**
+ * Strips the typescript extension and clears index filenames
+ * foo.ts -> foo
+ * index.ts -> empty
+ */
+function convertToTypeScriptFileName(filename: string | undefined) {
+ return filename ? filename.replace(/(\.ts)|(index\.ts)$/, '') : '';
+}
diff --git a/projects/angular-redux/schematics-core/utility/find-module.ts b/projects/angular-redux/schematics-core/utility/find-module.ts
new file mode 100644
index 0000000..134a59f
--- /dev/null
+++ b/projects/angular-redux/schematics-core/utility/find-module.ts
@@ -0,0 +1,140 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import {
+ Path,
+ join,
+ normalize,
+ relative,
+ strings,
+ basename,
+ extname,
+ dirname,
+} from '@angular-devkit/core';
+import { DirEntry, Tree } from '@angular-devkit/schematics';
+
+export interface ModuleOptions {
+ module?: string;
+ name: string;
+ flat?: boolean;
+ path?: string;
+ skipImport?: boolean;
+}
+
+/**
+ * Find the module referred by a set of options passed to the schematics.
+ */
+export function findModuleFromOptions(
+ host: Tree,
+ options: ModuleOptions
+): Path | undefined {
+ if (options.hasOwnProperty('skipImport') && options.skipImport) {
+ return undefined;
+ }
+
+ if (!options.module) {
+ const pathToCheck =
+ (options.path || '') +
+ (options.flat ? '' : '/' + strings.dasherize(options.name));
+
+ return normalize(findModule(host, pathToCheck));
+ } else {
+ const modulePath = normalize('/' + options.path + '/' + options.module);
+ const moduleBaseName = normalize(modulePath).split('/').pop();
+
+ if (host.exists(modulePath)) {
+ return normalize(modulePath);
+ } else if (host.exists(modulePath + '.ts')) {
+ return normalize(modulePath + '.ts');
+ } else if (host.exists(modulePath + '.module.ts')) {
+ return normalize(modulePath + '.module.ts');
+ } else if (host.exists(modulePath + '/' + moduleBaseName + '.module.ts')) {
+ return normalize(modulePath + '/' + moduleBaseName + '.module.ts');
+ } else {
+ throw new Error(`Specified module path ${modulePath} does not exist`);
+ }
+ }
+}
+
+/**
+ * Function to find the "closest" module to a generated file's path.
+ */
+export function findModule(host: Tree, generateDir: string): Path {
+ let dir: DirEntry | null = host.getDir('/' + generateDir);
+
+ const moduleRe = /\.module\.ts$/;
+ const routingModuleRe = /-routing\.module\.ts/;
+
+ while (dir) {
+ const matches = dir.subfiles.filter(
+ (p) => moduleRe.test(p) && !routingModuleRe.test(p)
+ );
+
+ if (matches.length == 1) {
+ return join(dir.path, matches[0]);
+ } else if (matches.length > 1) {
+ throw new Error(
+ 'More than one module matches. Use skip-import option to skip importing ' +
+ 'the component into the closest module.'
+ );
+ }
+
+ dir = dir.parent;
+ }
+
+ throw new Error(
+ 'Could not find an NgModule. Use the skip-import ' +
+ 'option to skip importing in NgModule.'
+ );
+}
+
+/**
+ * Build a relative path from one file path to another file path.
+ */
+export function buildRelativePath(from: string, to: string): string {
+ const {
+ path: fromPath,
+ filename: fromFileName,
+ directory: fromDirectory,
+ } = parsePath(from);
+ const {
+ path: toPath,
+ filename: toFileName,
+ directory: toDirectory,
+ } = parsePath(to);
+ const relativePath = relative(fromDirectory, toDirectory);
+ const fixedRelativePath = relativePath.startsWith('.')
+ ? relativePath
+ : `./${relativePath}`;
+
+ return !toFileName || toFileName === 'index.ts'
+ ? fixedRelativePath
+ : `${
+ fixedRelativePath.endsWith('/')
+ ? fixedRelativePath
+ : fixedRelativePath + '/'
+ }${convertToTypeScriptFileName(toFileName)}`;
+}
+
+function parsePath(path: string) {
+ const pathNormalized = normalize(path) as Path;
+ const filename = extname(pathNormalized) ? basename(pathNormalized) : '';
+ const directory = filename ? dirname(pathNormalized) : pathNormalized;
+ return {
+ path: pathNormalized,
+ filename,
+ directory,
+ };
+}
+/**
+ * Strips the typescript extension and clears index filenames
+ * foo.ts -> foo
+ * index.ts -> empty
+ */
+function convertToTypeScriptFileName(filename: string | undefined) {
+ return filename ? filename.replace(/(\.ts)|(index\.ts)$/, '') : '';
+}
diff --git a/projects/angular-redux/schematics-core/utility/json-utilts.ts b/projects/angular-redux/schematics-core/utility/json-utilts.ts
new file mode 100644
index 0000000..be6b7b8
--- /dev/null
+++ b/projects/angular-redux/schematics-core/utility/json-utilts.ts
@@ -0,0 +1,14 @@
+// https://github.com/angular/angular-cli/blob/master/packages/schematics/angular/utility/json-utils.ts
+export function findPropertyInAstObject(
+ node: any,
+ propertyName: string
+): any | null {
+ let maybeNode: any | null = null;
+ for (const property of node.properties) {
+ if (property.key.value == propertyName) {
+ maybeNode = property.value;
+ }
+ }
+
+ return maybeNode;
+}
diff --git a/projects/angular-redux/schematics-core/utility/package.ts b/projects/angular-redux/schematics-core/utility/package.ts
new file mode 100644
index 0000000..9ebbbed
--- /dev/null
+++ b/projects/angular-redux/schematics-core/utility/package.ts
@@ -0,0 +1,27 @@
+import { Tree } from '@angular-devkit/schematics';
+
+/**
+ * Adds a package to the package.json
+ */
+export function addPackageToPackageJson(
+ host: Tree,
+ type: string,
+ pkg: string,
+ version: string
+): Tree {
+ if (host.exists('package.json')) {
+ const sourceText = host.read('package.json')?.toString('utf-8') ?? '{}';
+ const json = JSON.parse(sourceText);
+ if (!json[type]) {
+ json[type] = {};
+ }
+
+ if (!json[type][pkg]) {
+ json[type][pkg] = version;
+ }
+
+ host.overwrite('package.json', JSON.stringify(json, null, 2));
+ }
+
+ return host;
+}
diff --git a/projects/angular-redux/schematics-core/utility/parse-name.ts b/projects/angular-redux/schematics-core/utility/parse-name.ts
new file mode 100644
index 0000000..a48f56b
--- /dev/null
+++ b/projects/angular-redux/schematics-core/utility/parse-name.ts
@@ -0,0 +1,16 @@
+import { Path, basename, dirname, normalize } from '@angular-devkit/core';
+
+export interface Location {
+ name: string;
+ path: Path;
+}
+
+export function parseName(path: string, name: string): Location {
+ const nameWithoutPath = basename(name as Path);
+ const namePath = dirname((path + '/' + name) as Path);
+
+ return {
+ name: nameWithoutPath,
+ path: normalize('/' + namePath),
+ };
+}
diff --git a/projects/angular-redux/schematics-core/utility/project.ts b/projects/angular-redux/schematics-core/utility/project.ts
new file mode 100644
index 0000000..36b546f
--- /dev/null
+++ b/projects/angular-redux/schematics-core/utility/project.ts
@@ -0,0 +1,76 @@
+// import { TargetDefinition } from '@angular-devkit/core/src/workspace';
+import { getWorkspace } from './config';
+import { SchematicsException, Tree } from '@angular-devkit/schematics';
+
+export interface WorkspaceProject {
+ root: string;
+ projectType: string;
+ architect: {
+ // [key: string]: TargetDefinition;
+ [key: string]: any;
+ };
+}
+
+export function getProject(
+ host: Tree,
+ options: { project?: string | undefined; path?: string | undefined }
+): WorkspaceProject {
+ const workspace = getWorkspace(host);
+
+ if (!options.project) {
+ const defaultProject = (workspace as { defaultProject?: string })
+ .defaultProject;
+ options.project =
+ defaultProject !== undefined
+ ? defaultProject
+ : Object.keys(workspace.projects)[0];
+ }
+
+ return workspace.projects[options.project];
+}
+
+export function getProjectPath(
+ host: Tree,
+ options: { project?: string | undefined; path?: string | undefined }
+) {
+ const project = getProject(host, options);
+
+ if (project.root.slice(-1) === '/') {
+ project.root = project.root.substring(0, project.root.length - 1);
+ }
+
+ if (options.path === undefined) {
+ const projectDirName =
+ project.projectType === 'application' ? 'app' : 'lib';
+
+ return `${project.root ? `/${project.root}` : ''}/src/${projectDirName}`;
+ }
+
+ return options.path;
+}
+
+export function isLib(
+ host: Tree,
+ options: { project?: string | undefined; path?: string | undefined }
+) {
+ const project = getProject(host, options);
+
+ 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 && !projectOptions?.browser) {
+ throw new SchematicsException(`Could not find the main file ${project}`);
+ }
+
+ return (projectOptions.browser || projectOptions.main) as string;
+}
diff --git a/projects/angular-redux/schematics-core/utility/standalone.ts b/projects/angular-redux/schematics-core/utility/standalone.ts
new file mode 100644
index 0000000..f0e172c
--- /dev/null
+++ b/projects/angular-redux/schematics-core/utility/standalone.ts
@@ -0,0 +1,470 @@
+// copied from https://github.com/angular/angular-cli/blob/17.3.x/packages/schematics/angular/private/standalone.ts
+import {
+ SchematicsException,
+ Tree,
+ UpdateRecorder,
+} from '@angular-devkit/schematics';
+import { dirname, join } from 'path';
+import { insertImport } from './ast-utils';
+import { InsertChange } from './change';
+import * as ts from 'typescript';
+
+/** App config that was resolved to its source node. */
+interface ResolvedAppConfig {
+ /** Tree-relative path of the file containing the app config. */
+ filePath: string;
+
+ /** Node defining the app config. */
+ node: ts.ObjectLiteralExpression;
+}
+
+/**
+ * Checks whether a providers function is being called in a `bootstrapApplication` call.
+ * @param tree File tree of the project.
+ * @param filePath Path of the file in which to check.
+ * @param functionName Name of the function to search for.
+ * @deprecated Private utility that will be removed. Use `addRootImport` or `addRootProvider` from
+ * `@schematics/angular/utility` instead.
+ */
+export function callsProvidersFunction(
+ tree: Tree,
+ filePath: string,
+ functionName: string
+): boolean {
+ const sourceFile = createSourceFile(tree, filePath);
+ const bootstrapCall = findBootstrapApplicationCall(sourceFile);
+ const appConfig = bootstrapCall
+ ? findAppConfig(bootstrapCall, tree, filePath)
+ : null;
+ const providersLiteral = appConfig
+ ? findProvidersLiteral(appConfig.node)
+ : null;
+
+ return !!providersLiteral?.elements.some(
+ (el) =>
+ ts.isCallExpression(el) &&
+ ts.isIdentifier(el.expression) &&
+ el.expression.text === functionName
+ );
+}
+
+/**
+ * Adds a providers function call to the `bootstrapApplication` call.
+ * @param tree File tree of the project.
+ * @param filePath Path to the file that should be updated.
+ * @param functionName Name of the function that should be called.
+ * @param importPath Path from which to import the function.
+ * @param args Arguments to use when calling the function.
+ * @return The file path that the provider was added to.
+ * @deprecated Private utility that will be removed. Use `addRootImport` or `addRootProvider` from
+ * `@schematics/angular/utility` instead.
+ */
+export function addFunctionalProvidersToStandaloneBootstrap(
+ tree: Tree,
+ filePath: string,
+ functionName: string,
+ importPath: string,
+ args: ts.Expression[] = []
+): string {
+ const sourceFile = createSourceFile(tree, filePath);
+ const bootstrapCall = findBootstrapApplicationCall(sourceFile);
+ const addImports = (file: ts.SourceFile, recorder: UpdateRecorder) => {
+ const change = insertImport(file, file.getText(), functionName, importPath);
+
+ if (change instanceof InsertChange) {
+ recorder.insertLeft(change.pos, change.toAdd);
+ }
+ };
+
+ if (!bootstrapCall) {
+ throw new SchematicsException(
+ `Could not find bootstrapApplication call in ${filePath}`
+ );
+ }
+
+ const providersCall = ts.factory.createCallExpression(
+ ts.factory.createIdentifier(functionName),
+ undefined,
+ args
+ );
+
+ // If there's only one argument, we have to create a new object literal.
+ if (bootstrapCall.arguments.length === 1) {
+ const recorder = tree.beginUpdate(filePath);
+ addNewAppConfigToCall(bootstrapCall, providersCall, recorder);
+ addImports(sourceFile, recorder);
+ tree.commitUpdate(recorder);
+
+ return filePath;
+ }
+
+ // If the config is a `mergeApplicationProviders` call, add another config to it.
+ if (isMergeAppConfigCall(bootstrapCall.arguments[1])) {
+ const recorder = tree.beginUpdate(filePath);
+ addNewAppConfigToCall(bootstrapCall.arguments[1], providersCall, recorder);
+ addImports(sourceFile, recorder);
+ tree.commitUpdate(recorder);
+
+ return filePath;
+ }
+
+ // Otherwise attempt to merge into the current config.
+ const appConfig = findAppConfig(bootstrapCall, tree, filePath);
+
+ if (!appConfig) {
+ throw new SchematicsException(
+ `Could not statically analyze config in bootstrapApplication call in ${filePath}`
+ );
+ }
+
+ const { filePath: configFilePath, node: config } = appConfig;
+ const recorder = tree.beginUpdate(configFilePath);
+ const providersLiteral = findProvidersLiteral(config);
+
+ addImports(config.getSourceFile(), recorder);
+
+ if (providersLiteral) {
+ // If there's a `providers` array, add the import to it.
+ addElementToArray(providersLiteral, providersCall, recorder);
+ } else {
+ // Otherwise add a `providers` array to the existing object literal.
+ addProvidersToObjectLiteral(config, providersCall, recorder);
+ }
+
+ tree.commitUpdate(recorder);
+
+ return configFilePath;
+}
+
+/**
+ * Finds the call to `bootstrapApplication` within a file.
+ * @deprecated Private utility that will be removed. Use `addRootImport` or `addRootProvider` from
+ * `@schematics/angular/utility` instead.
+ */
+export function findBootstrapApplicationCall(
+ sourceFile: ts.SourceFile
+): ts.CallExpression | null {
+ const localName = findImportLocalName(
+ sourceFile,
+ 'bootstrapApplication',
+ '@angular/platform-browser'
+ );
+
+ if (!localName) {
+ return null;
+ }
+
+ let result: ts.CallExpression | null = null;
+
+ sourceFile.forEachChild(function walk(node) {
+ if (
+ ts.isCallExpression(node) &&
+ ts.isIdentifier(node.expression) &&
+ node.expression.text === localName
+ ) {
+ result = node;
+ }
+
+ if (!result) {
+ node.forEachChild(walk);
+ }
+ });
+
+ return result;
+}
+
+/** Finds the `providers` array literal within an application config. */
+function findProvidersLiteral(
+ config: ts.ObjectLiteralExpression
+): ts.ArrayLiteralExpression | null {
+ for (const prop of config.properties) {
+ if (
+ ts.isPropertyAssignment(prop) &&
+ ts.isIdentifier(prop.name) &&
+ prop.name.text === 'providers' &&
+ ts.isArrayLiteralExpression(prop.initializer)
+ ) {
+ return prop.initializer;
+ }
+ }
+
+ return null;
+}
+
+/**
+ * Resolves the node that defines the app config from a bootstrap call.
+ * @param bootstrapCall Call for which to resolve the config.
+ * @param tree File tree of the project.
+ * @param filePath File path of the bootstrap call.
+ */
+function findAppConfig(
+ bootstrapCall: ts.CallExpression,
+ tree: Tree,
+ filePath: string
+): ResolvedAppConfig | null {
+ if (bootstrapCall.arguments.length > 1) {
+ const config = bootstrapCall.arguments[1];
+
+ if (ts.isObjectLiteralExpression(config)) {
+ return { filePath, node: config };
+ }
+
+ if (ts.isIdentifier(config)) {
+ return resolveAppConfigFromIdentifier(config, tree, filePath);
+ }
+ }
+
+ return null;
+}
+
+/**
+ * Resolves the app config from an identifier referring to it.
+ * @param identifier Identifier referring to the app config.
+ * @param tree File tree of the project.
+ * @param bootstapFilePath Path of the bootstrap call.
+ */
+function resolveAppConfigFromIdentifier(
+ identifier: ts.Identifier,
+ tree: Tree,
+ bootstapFilePath: string
+): ResolvedAppConfig | null {
+ const sourceFile = identifier.getSourceFile();
+
+ for (const node of sourceFile.statements) {
+ // Only look at relative imports. This will break if the app uses a path
+ // mapping to refer to the import, but in order to resolve those, we would
+ // need knowledge about the entire program.
+ if (
+ !ts.isImportDeclaration(node) ||
+ !node.importClause?.namedBindings ||
+ !ts.isNamedImports(node.importClause.namedBindings) ||
+ !ts.isStringLiteralLike(node.moduleSpecifier) ||
+ !node.moduleSpecifier.text.startsWith('.')
+ ) {
+ continue;
+ }
+
+ for (const specifier of node.importClause.namedBindings.elements) {
+ if (specifier.name.text !== identifier.text) {
+ continue;
+ }
+
+ // Look for a variable with the imported name in the file. Note that ideally we would use
+ // the type checker to resolve this, but we can't because these utilities are set up to
+ // operate on individual files, not the entire program.
+ const filePath = join(
+ dirname(bootstapFilePath),
+ node.moduleSpecifier.text + '.ts'
+ );
+ const importedSourceFile = createSourceFile(tree, filePath);
+ const resolvedVariable = findAppConfigFromVariableName(
+ importedSourceFile,
+ (specifier.propertyName || specifier.name).text
+ );
+
+ if (resolvedVariable) {
+ return { filePath, node: resolvedVariable };
+ }
+ }
+ }
+
+ const variableInSameFile = findAppConfigFromVariableName(
+ sourceFile,
+ identifier.text
+ );
+
+ return variableInSameFile
+ ? { filePath: bootstapFilePath, node: variableInSameFile }
+ : null;
+}
+
+/**
+ * Finds an app config within the top-level variables of a file.
+ * @param sourceFile File in which to search for the config.
+ * @param variableName Name of the variable containing the config.
+ */
+function findAppConfigFromVariableName(
+ sourceFile: ts.SourceFile,
+ variableName: string
+): ts.ObjectLiteralExpression | null {
+ for (const node of sourceFile.statements) {
+ if (ts.isVariableStatement(node)) {
+ for (const decl of node.declarationList.declarations) {
+ if (
+ ts.isIdentifier(decl.name) &&
+ decl.name.text === variableName &&
+ decl.initializer &&
+ ts.isObjectLiteralExpression(decl.initializer)
+ ) {
+ return decl.initializer;
+ }
+ }
+ }
+ }
+
+ return null;
+}
+
+/**
+ * Finds the local name of an imported symbol. Could be the symbol name itself or its alias.
+ * @param sourceFile File within which to search for the import.
+ * @param name Actual name of the import, not its local alias.
+ * @param moduleName Name of the module from which the symbol is imported.
+ */
+function findImportLocalName(
+ sourceFile: ts.SourceFile,
+ name: string,
+ moduleName: string
+): string | null {
+ for (const node of sourceFile.statements) {
+ // Only look for top-level imports.
+ if (
+ !ts.isImportDeclaration(node) ||
+ !ts.isStringLiteral(node.moduleSpecifier) ||
+ node.moduleSpecifier.text !== moduleName
+ ) {
+ continue;
+ }
+
+ // Filter out imports that don't have the right shape.
+ if (
+ !node.importClause ||
+ !node.importClause.namedBindings ||
+ !ts.isNamedImports(node.importClause.namedBindings)
+ ) {
+ continue;
+ }
+
+ // Look through the elements of the declaration for the specific import.
+ for (const element of node.importClause.namedBindings.elements) {
+ if ((element.propertyName || element.name).text === name) {
+ // The local name is always in `name`.
+ return element.name.text;
+ }
+ }
+ }
+
+ return null;
+}
+
+/** Creates a source file from a file path within a project. */
+function createSourceFile(tree: Tree, filePath: string): ts.SourceFile {
+ return ts.createSourceFile(
+ filePath,
+ tree.readText(filePath),
+ ts.ScriptTarget.Latest,
+ true
+ );
+}
+
+/**
+ * Creates a new app config object literal and adds it to a call expression as an argument.
+ * @param call Call to which to add the config.
+ * @param expression Expression that should inserted into the new config.
+ * @param recorder Recorder to which to log the change.
+ */
+function addNewAppConfigToCall(
+ call: ts.CallExpression,
+ expression: ts.Expression,
+ recorder: UpdateRecorder
+): void {
+ const newCall = ts.factory.updateCallExpression(
+ call,
+ call.expression,
+ call.typeArguments,
+ [
+ ...call.arguments,
+ ts.factory.createObjectLiteralExpression(
+ [
+ ts.factory.createPropertyAssignment(
+ 'providers',
+ ts.factory.createArrayLiteralExpression([expression])
+ ),
+ ],
+ true
+ ),
+ ]
+ );
+
+ recorder.remove(call.getStart(), call.getWidth());
+ recorder.insertRight(
+ call.getStart(),
+ ts
+ .createPrinter()
+ .printNode(ts.EmitHint.Unspecified, newCall, call.getSourceFile())
+ );
+}
+
+/**
+ * Adds an element to an array literal expression.
+ * @param node Array to which to add the element.
+ * @param element Element to be added.
+ * @param recorder Recorder to which to log the change.
+ */
+function addElementToArray(
+ node: ts.ArrayLiteralExpression,
+ element: ts.Expression,
+ recorder: UpdateRecorder
+): void {
+ const newLiteral = ts.factory.updateArrayLiteralExpression(node, [
+ ...node.elements,
+ element,
+ ]);
+ recorder.remove(node.getStart(), node.getWidth());
+ recorder.insertRight(
+ node.getStart(),
+ ts
+ .createPrinter()
+ .printNode(ts.EmitHint.Unspecified, newLiteral, node.getSourceFile())
+ );
+}
+
+/**
+ * Adds a `providers` property to an object literal.
+ * @param node Literal to which to add the `providers`.
+ * @param expression Provider that should be part of the generated `providers` array.
+ * @param recorder Recorder to which to log the change.
+ */
+function addProvidersToObjectLiteral(
+ node: ts.ObjectLiteralExpression,
+ expression: ts.Expression,
+ recorder: UpdateRecorder
+) {
+ const newOptionsLiteral = ts.factory.updateObjectLiteralExpression(node, [
+ ...node.properties,
+ ts.factory.createPropertyAssignment(
+ 'providers',
+ ts.factory.createArrayLiteralExpression([expression])
+ ),
+ ]);
+ recorder.remove(node.getStart(), node.getWidth());
+ recorder.insertRight(
+ node.getStart(),
+ ts
+ .createPrinter()
+ .printNode(
+ ts.EmitHint.Unspecified,
+ newOptionsLiteral,
+ node.getSourceFile()
+ )
+ );
+}
+
+/** Checks whether a node is a call to `mergeApplicationConfig`. */
+function isMergeAppConfigCall(node: ts.Node): node is ts.CallExpression {
+ if (!ts.isCallExpression(node)) {
+ return false;
+ }
+
+ const localName = findImportLocalName(
+ node.getSourceFile(),
+ 'mergeApplicationConfig',
+ '@angular/core'
+ );
+
+ return (
+ !!localName &&
+ ts.isIdentifier(node.expression) &&
+ node.expression.text === localName
+ );
+}
diff --git a/projects/angular-redux/schematics-core/utility/strings.ts b/projects/angular-redux/schematics-core/utility/strings.ts
new file mode 100644
index 0000000..38e9924
--- /dev/null
+++ b/projects/angular-redux/schematics-core/utility/strings.ts
@@ -0,0 +1,147 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+const STRING_DASHERIZE_REGEXP = /[ _]/g;
+const STRING_DECAMELIZE_REGEXP = /([a-z\d])([A-Z])/g;
+const STRING_CAMELIZE_REGEXP = /(-|_|\.|\s)+(.)?/g;
+const STRING_UNDERSCORE_REGEXP_1 = /([a-z\d])([A-Z]+)/g;
+const STRING_UNDERSCORE_REGEXP_2 = /-|\s+/g;
+
+/**
+ * Converts a camelized string into all lower case separated by underscores.
+ *
+ ```javascript
+ decamelize('innerHTML'); // 'inner_html'
+ decamelize('action_name'); // 'action_name'
+ decamelize('css-class-name'); // 'css-class-name'
+ decamelize('my favorite items'); // 'my favorite items'
+ ```
+ */
+export function decamelize(str: string): string {
+ return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase();
+}
+
+/**
+ Replaces underscores, spaces, or camelCase with dashes.
+
+ ```javascript
+ dasherize('innerHTML'); // 'inner-html'
+ dasherize('action_name'); // 'action-name'
+ dasherize('css-class-name'); // 'css-class-name'
+ dasherize('my favorite items'); // 'my-favorite-items'
+ ```
+ */
+export function dasherize(str?: string): string {
+ return decamelize(str || '').replace(STRING_DASHERIZE_REGEXP, '-');
+}
+
+/**
+ Returns the lowerCamelCase form of a string.
+
+ ```javascript
+ camelize('innerHTML'); // 'innerHTML'
+ camelize('action_name'); // 'actionName'
+ camelize('css-class-name'); // 'cssClassName'
+ camelize('my favorite items'); // 'myFavoriteItems'
+ camelize('My Favorite Items'); // 'myFavoriteItems'
+ ```
+ */
+export function camelize(str: string): string {
+ return str
+ .replace(
+ STRING_CAMELIZE_REGEXP,
+ (_match: string, _separator: string, chr: string) => {
+ return chr ? chr.toUpperCase() : '';
+ }
+ )
+ .replace(/^([A-Z])/, (match: string) => match.toLowerCase());
+}
+
+/**
+ Returns the UpperCamelCase form of a string.
+
+ ```javascript
+ 'innerHTML'.classify(); // 'InnerHTML'
+ 'action_name'.classify(); // 'ActionName'
+ 'css-class-name'.classify(); // 'CssClassName'
+ 'my favorite items'.classify(); // 'MyFavoriteItems'
+ ```
+ */
+export function classify(str: string): string {
+ return str
+ .split('.')
+ .map((part) => capitalize(camelize(part)))
+ .join('.');
+}
+
+/**
+ More general than decamelize. Returns the lower\_case\_and\_underscored
+ form of a string.
+
+ ```javascript
+ 'innerHTML'.underscore(); // 'inner_html'
+ 'action_name'.underscore(); // 'action_name'
+ 'css-class-name'.underscore(); // 'css_class_name'
+ 'my favorite items'.underscore(); // 'my_favorite_items'
+ ```
+ */
+export function underscore(str: string): string {
+ return str
+ .replace(STRING_UNDERSCORE_REGEXP_1, '$1_$2')
+ .replace(STRING_UNDERSCORE_REGEXP_2, '_')
+ .toLowerCase();
+}
+
+/**
+ Returns the Capitalized form of a string
+
+ ```javascript
+ 'innerHTML'.capitalize() // 'InnerHTML'
+ 'action_name'.capitalize() // 'Action_name'
+ 'css-class-name'.capitalize() // 'Css-class-name'
+ 'my favorite items'.capitalize() // 'My favorite items'
+ ```
+ */
+export function capitalize(str: string): string {
+ return str.charAt(0).toUpperCase() + str.substring(1);
+}
+
+/**
+ Returns the plural form of a string
+
+ ```javascript
+ 'innerHTML'.pluralize() // 'innerHTMLs'
+ 'action_name'.pluralize() // 'actionNames'
+ 'css-class-name'.pluralize() // 'cssClassNames'
+ 'regex'.pluralize() // 'regexes'
+ 'user'.pluralize() // 'users'
+ ```
+ */
+export function pluralize(str: string): string {
+ return camelize(
+ [/([^aeiou])y$/, /()fe?$/, /([^aeiou]o|[sxz]|[cs]h)$/].map(
+ (c, i) => (str = str.replace(c, `$1${'iv'[i] || ''}e`))
+ ) && str + 's'
+ );
+}
+
+export function group(name: string, group: string | undefined) {
+ return group ? `${group}/${name}` : name;
+}
+
+export function featurePath(
+ group: boolean | undefined,
+ flat: boolean | undefined,
+ path: string,
+ name: string
+) {
+ if (group && !flat) {
+ return `../../${path}/${name}/`;
+ }
+
+ return group ? `../${path}/` : './';
+}
diff --git a/projects/angular-redux/schematics-core/utility/update.ts b/projects/angular-redux/schematics-core/utility/update.ts
new file mode 100644
index 0000000..e69de29
diff --git a/projects/angular-redux/schematics-core/utility/visitors.ts b/projects/angular-redux/schematics-core/utility/visitors.ts
new file mode 100644
index 0000000..fa4edd0
--- /dev/null
+++ b/projects/angular-redux/schematics-core/utility/visitors.ts
@@ -0,0 +1,225 @@
+import * as ts from 'typescript';
+import { normalize, resolve } from '@angular-devkit/core';
+import { Tree, DirEntry } from '@angular-devkit/schematics';
+
+export function visitTSSourceFiles(
+ tree: Tree,
+ visitor: (
+ sourceFile: ts.SourceFile,
+ tree: Tree,
+ result?: Result
+ ) => Result | undefined
+): Result | undefined {
+ let result: Result | undefined = undefined;
+ for (const sourceFile of visit(tree.root)) {
+ result = visitor(sourceFile, tree, result);
+ }
+
+ return result;
+}
+
+export function visitTemplates(
+ tree: Tree,
+ visitor: (
+ template: {
+ fileName: string;
+ content: string;
+ inline: boolean;
+ start: number;
+ },
+ tree: Tree
+ ) => void
+): void {
+ visitTSSourceFiles(tree, (source) => {
+ visitComponents(source, (_, decoratorExpressionNode) => {
+ ts.forEachChild(decoratorExpressionNode, function findTemplates(n) {
+ if (ts.isPropertyAssignment(n) && ts.isIdentifier(n.name)) {
+ if (
+ n.name.text === 'template' &&
+ ts.isStringLiteralLike(n.initializer)
+ ) {
+ // Need to add an offset of one to the start because the template quotes are
+ // not part of the template content.
+ const templateStartIdx = n.initializer.getStart() + 1;
+ visitor(
+ {
+ fileName: source.fileName,
+ content: n.initializer.text,
+ inline: true,
+ start: templateStartIdx,
+ },
+ tree
+ );
+ return;
+ } else if (
+ n.name.text === 'templateUrl' &&
+ ts.isStringLiteralLike(n.initializer)
+ ) {
+ const parts = normalize(source.fileName).split('/').slice(0, -1);
+ const templatePath = resolve(
+ normalize(parts.join('/')),
+ normalize(n.initializer.text)
+ );
+ if (!tree.exists(templatePath)) {
+ return;
+ }
+
+ const fileContent = tree.read(templatePath);
+ if (!fileContent) {
+ return;
+ }
+
+ visitor(
+ {
+ fileName: templatePath,
+ content: fileContent.toString(),
+ inline: false,
+ start: 0,
+ },
+ tree
+ );
+ return;
+ }
+ }
+
+ ts.forEachChild(n, findTemplates);
+ });
+ });
+ });
+}
+
+export function visitNgModuleImports(
+ sourceFile: ts.SourceFile,
+ callback: (
+ importNode: ts.PropertyAssignment,
+ elementExpressions: ts.NodeArray
+ ) => void
+) {
+ visitNgModuleProperty(sourceFile, callback, 'imports');
+}
+
+export function visitNgModuleExports(
+ sourceFile: ts.SourceFile,
+ callback: (
+ exportNode: ts.PropertyAssignment,
+ elementExpressions: ts.NodeArray
+ ) => void
+) {
+ visitNgModuleProperty(sourceFile, callback, 'exports');
+}
+
+function visitNgModuleProperty(
+ sourceFile: ts.SourceFile,
+ callback: (
+ nodes: ts.PropertyAssignment,
+ elementExpressions: ts.NodeArray
+ ) => void,
+ property: string
+) {
+ visitNgModules(sourceFile, (_, decoratorExpressionNode) => {
+ ts.forEachChild(decoratorExpressionNode, function findTemplates(n) {
+ if (
+ ts.isPropertyAssignment(n) &&
+ ts.isIdentifier(n.name) &&
+ n.name.text === property &&
+ ts.isArrayLiteralExpression(n.initializer)
+ ) {
+ callback(n, n.initializer.elements);
+ return;
+ }
+
+ ts.forEachChild(n, findTemplates);
+ });
+ });
+}
+export function visitComponents(
+ sourceFile: ts.SourceFile,
+ callback: (
+ classDeclarationNode: ts.ClassDeclaration,
+ decoratorExpressionNode: ts.ObjectLiteralExpression
+ ) => void
+) {
+ visitDecorator(sourceFile, 'Component', callback);
+}
+
+export function visitNgModules(
+ sourceFile: ts.SourceFile,
+ callback: (
+ classDeclarationNode: ts.ClassDeclaration,
+ decoratorExpressionNode: ts.ObjectLiteralExpression
+ ) => void
+) {
+ visitDecorator(sourceFile, 'NgModule', callback);
+}
+
+export function visitDecorator(
+ sourceFile: ts.SourceFile,
+ decoratorName: string,
+ callback: (
+ classDeclarationNode: ts.ClassDeclaration,
+ decoratorExpressionNode: ts.ObjectLiteralExpression
+ ) => void
+) {
+ ts.forEachChild(sourceFile, function findClassDeclaration(node) {
+ if (!ts.isClassDeclaration(node)) {
+ ts.forEachChild(node, findClassDeclaration);
+ }
+
+ const classDeclarationNode = node as ts.ClassDeclaration;
+ const decorators = ts.getDecorators(classDeclarationNode);
+
+ if (!decorators || !decorators.length) {
+ return;
+ }
+
+ const componentDecorator = decorators.find((d) => {
+ return (
+ ts.isCallExpression(d.expression) &&
+ ts.isIdentifier(d.expression.expression) &&
+ d.expression.expression.text === decoratorName
+ );
+ });
+
+ if (!componentDecorator) {
+ return;
+ }
+
+ const { expression } = componentDecorator;
+ if (!ts.isCallExpression(expression)) {
+ return;
+ }
+
+ const [arg] = expression.arguments;
+ if (!arg || !ts.isObjectLiteralExpression(arg)) {
+ return;
+ }
+
+ callback(classDeclarationNode, arg);
+ });
+}
+
+function* visit(directory: DirEntry): IterableIterator {
+ for (const path of directory.subfiles) {
+ if (path.endsWith('.ts') && !path.endsWith('.d.ts')) {
+ const entry = directory.file(path);
+ if (entry) {
+ const content = entry.content;
+ const source = ts.createSourceFile(
+ entry.path,
+ content.toString().replace(/^\uFEFF/, ''),
+ ts.ScriptTarget.Latest,
+ true
+ );
+ yield source;
+ }
+ }
+ }
+
+ for (const path of directory.subdirs) {
+ if (path === 'node_modules') {
+ continue;
+ }
+
+ yield* visit(directory.dir(path));
+ }
+}
diff --git a/projects/angular-redux/schematics/collection.json b/projects/angular-redux/schematics/collection.json
new file mode 100644
index 0000000..a94b72b
--- /dev/null
+++ b/projects/angular-redux/schematics/collection.json
@@ -0,0 +1,10 @@
+{
+ "schematics": {
+ "ng-add": {
+ "aliases": ["init"],
+ "factory": "./ng-add",
+ "schema": "./ng-add/schema.json",
+ "description": "Adds initial setup for state managment"
+ }
+ }
+}
diff --git a/projects/angular-redux/schematics/jest.config.mjs b/projects/angular-redux/schematics/jest.config.mjs
new file mode 100644
index 0000000..991c5fc
--- /dev/null
+++ b/projects/angular-redux/schematics/jest.config.mjs
@@ -0,0 +1,13 @@
+export default {
+ displayName: 'Schematics',
+ coverageDirectory: '../../coverage/modules/schematics',
+ transform: {
+ '^.+\\.(ts|mjs|js)$': [
+ 'ts-jest',
+ {
+ tsconfig: '/tsconfig.spec.json'
+ },
+ ],
+ },
+ transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)']
+};
diff --git a/projects/angular-redux/schematics/ng-add/files/__storePath__/counter-slice.ts.template b/projects/angular-redux/schematics/ng-add/files/__storePath__/counter-slice.ts.template
new file mode 100644
index 0000000..e2a3c95
--- /dev/null
+++ b/projects/angular-redux/schematics/ng-add/files/__storePath__/counter-slice.ts.template
@@ -0,0 +1,30 @@
+import { createSlice } from '@reduxjs/toolkit'
+import type { PayloadAction } from '@reduxjs/toolkit'
+
+export interface CounterState {
+ value: number
+}
+
+const initialState: CounterState = {
+ value: 0,
+}
+
+export const counterSlice = createSlice({
+ name: 'counter',
+ initialState,
+ reducers: {
+ increment: (state) => {
+ state.value += 1
+ },
+ decrement: (state) => {
+ state.value -= 1
+ },
+ incrementByAmount: (state, action: PayloadAction) => {
+ state.value += action.payload
+ },
+ },
+})
+
+export const { increment, decrement, incrementByAmount } = counterSlice.actions
+
+export default counterSlice.reducer
diff --git a/projects/angular-redux/schematics/ng-add/files/__storePath__/index.ts.template b/projects/angular-redux/schematics/ng-add/files/__storePath__/index.ts.template
new file mode 100644
index 0000000..00d8944
--- /dev/null
+++ b/projects/angular-redux/schematics/ng-add/files/__storePath__/index.ts.template
@@ -0,0 +1,11 @@
+import { configureStore } from '@reduxjs/toolkit'
+import counterReducer from './counter-slice'
+
+export const store = configureStore({
+ reducer: {
+ counter: counterReducer,
+ },
+})
+
+export type RootState = ReturnType
+export type AppDispatch = typeof store.dispatch
diff --git a/projects/angular-redux/schematics/ng-add/index.spec.ts b/projects/angular-redux/schematics/ng-add/index.spec.ts
new file mode 100644
index 0000000..24e0312
--- /dev/null
+++ b/projects/angular-redux/schematics/ng-add/index.spec.ts
@@ -0,0 +1,113 @@
+import {
+ SchematicTestRunner,
+ UnitTestTree,
+} from '@angular-devkit/schematics/testing';
+import * as path from 'path';
+import { Schema as AngularReduxOptions } from './schema';
+import {
+ getTestProjectPath,
+ createWorkspace,
+} from '@reduxjs/angular-redux/schematics-core/testing';
+
+describe('Store ng-add Schematic', () => {
+ const schematicRunner = new SchematicTestRunner(
+ '@reduxjs/angular-redux',
+ path.join(__dirname, '../collection.json')
+ );
+ const defaultOptions: AngularReduxOptions = {
+ skipPackageJson: false,
+ project: 'bar',
+ module: 'app'
+ };
+
+ const projectPath = getTestProjectPath();
+ let appTree: UnitTestTree;
+
+ beforeEach(async () => {
+ appTree = await createWorkspace(schematicRunner, appTree);
+ });
+
+ it('should update package.json', async () => {
+ const options = { ...defaultOptions };
+
+ const tree = await schematicRunner.runSchematic('ng-add', options, appTree);
+
+ const packageJson = JSON.parse(tree.readContent('/package.json'));
+
+ expect(packageJson.dependencies['@reduxjs/angular-redux']).toBeDefined();
+ expect(packageJson.dependencies['redux']).toBeDefined();
+ expect(packageJson.dependencies['@reduxjs/toolkit']).toBeDefined();
+ });
+
+ it('should skip package.json update', async () => {
+ const options = { ...defaultOptions, skipPackageJson: true };
+
+ const tree = await schematicRunner.runSchematic('ng-add', options, appTree);
+ const packageJson = JSON.parse(tree.readContent('/package.json'));
+
+ expect(packageJson.dependencies['@reduxjs/angular-redux']).toBeUndefined();
+ expect(packageJson.dependencies['redux']).toBeUndefined();
+ expect(packageJson.dependencies['@reduxjs/toolkit']).toBeUndefined();
+ });
+
+ it('should create the initial store setup', async () => {
+ const options = { ...defaultOptions };
+
+ const tree = await schematicRunner.runSchematic('ng-add', options, appTree);
+ const files = tree.files;
+ expect(
+ files.indexOf(`${projectPath}/src/app/store/index.ts`)
+ ).toBeGreaterThanOrEqual(0);
+ });
+
+ it('should import into a specified module', async () => {
+ const options = { ...defaultOptions };
+
+ const tree = await schematicRunner.runSchematic('ng-add', options, appTree);
+ const content = tree.readContent(`${projectPath}/src/app/app.module.ts`);
+ expect(content).toMatch(
+ /import { store } from '\.\/store';/
+ );
+ });
+
+ it('should fail if specified module does not exist', async () => {
+ const options = { ...defaultOptions, module: '/src/app/app.moduleXXX.ts' };
+ let thrownError: Error | null = null;
+ try {
+ await schematicRunner.runSchematic('ng-add', options, appTree);
+ } catch (err: any) {
+ thrownError = err;
+ }
+ expect(thrownError).toBeDefined();
+ });
+
+ describe('Store ng-add Schematic for standalone application', () => {
+ const projectPath = getTestProjectPath(undefined, {
+ name: 'bar-standalone',
+ });
+ const standaloneDefaultOptions = {
+ ...defaultOptions,
+ project: 'bar-standalone',
+ };
+
+ 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(/provideRedux\(\{ store \}\)/);
+ expect(content).toMatch(
+ /import { store } from '\.\/store';/
+ );
+ expect(files.indexOf(`${projectPath}/src/app/store/index.ts`)).not.toBe(
+ -1
+ );
+ });
+ });
+});
diff --git a/projects/angular-redux/schematics/ng-add/index.ts b/projects/angular-redux/schematics/ng-add/index.ts
new file mode 100644
index 0000000..c17cda2
--- /dev/null
+++ b/projects/angular-redux/schematics/ng-add/index.ts
@@ -0,0 +1,223 @@
+import * as fs from "fs";
+import * as path from "path";
+import * as ts from 'typescript';
+import {
+ Rule,
+ SchematicContext,
+ SchematicsException,
+ Tree,
+ apply,
+ applyTemplates,
+ branchAndMerge,
+ chain,
+ mergeWith,
+ url,
+ noop,
+ move,
+ filter,
+} from '@angular-devkit/schematics';
+import {
+ NodePackageInstallTask,
+ RunSchematicTask,
+} from '@angular-devkit/schematics/tasks';
+import {
+ InsertChange,
+ addImportToModule,
+ buildRelativePath,
+ findModuleFromOptions,
+ getProjectPath,
+ insertImport,
+ stringUtils,
+ addPackageToPackageJson,
+ parseName,
+} from '../../schematics-core';
+import { Schema as AngularReduxOptions } from './schema';
+import { getProjectMainFile } from '../../schematics-core/utility/project';
+import {
+ addFunctionalProvidersToStandaloneBootstrap,
+ callsProvidersFunction,
+} from '../../schematics-core/utility/standalone';
+import { isStandaloneApp } from '@schematics/angular/utility/ng-ast-utils';
+
+function addImportToNgModule(options: AngularReduxOptions): Rule {
+ return (host: Tree) => {
+ const modulePath = options.module;
+
+ if (!modulePath) {
+ return host;
+ }
+
+ if (!host.exists(modulePath)) {
+ throw new Error('Specified module does not exist');
+ }
+
+ const text = host.read(modulePath);
+ if (text === null) {
+ throw new SchematicsException(`File ${modulePath} does not exist.`);
+ }
+ const sourceText = text.toString('utf-8');
+
+ const source = ts.createSourceFile(
+ modulePath,
+ sourceText,
+ ts.ScriptTarget.Latest,
+ true
+ );
+
+ const storeModuleSetup = `provideRedux({store: store})`;
+
+ const statePath = `/${options.path}/${options.storePath}`;
+ const relativePath = buildRelativePath(modulePath, statePath);
+ const [storeNgModuleImport] = addImportToModule(
+ source,
+ modulePath,
+ storeModuleSetup,
+ relativePath
+ );
+
+ let changes = [
+ insertImport(source, modulePath, 'provideRedux', '@reduxjs/angular-redux'),
+ insertImport(source, modulePath, 'store', relativePath),
+ storeNgModuleImport,
+ ];
+
+ const recorder = host.beginUpdate(modulePath);
+
+ for (const change of changes) {
+ if (change instanceof InsertChange) {
+ recorder.insertLeft(change.pos, change.toAdd);
+ }
+ }
+ host.commitUpdate(recorder);
+
+ return host;
+ };
+}
+
+const angularReduxPackageMeta = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../../package.json'), "utf8")) as unknown as {
+ version: string;
+ peerDependencies: {
+ [key: string]: string;
+ }
+};
+
+function addReduxDepsToPackageJson() {
+ return (host: Tree, context: SchematicContext) => {
+ addPackageToPackageJson(
+ host,
+ 'dependencies',
+ '@reduxjs/toolkit',
+ angularReduxPackageMeta.peerDependencies['@reduxjs/toolkit']
+ );
+ addPackageToPackageJson(
+ host,
+ 'dependencies',
+ 'redux',
+ angularReduxPackageMeta.peerDependencies['redux']
+ );
+ addPackageToPackageJson(
+ host,
+ 'dependencies',
+ '@reduxjs/angular-redux',
+ angularReduxPackageMeta.version
+ );
+ context.addTask(new NodePackageInstallTask());
+ return host;
+ };
+}
+
+function addStandaloneConfig(options: AngularReduxOptions): Rule {
+ return (host: Tree) => {
+ const mainFile = getProjectMainFile(host, options);
+
+ if (host.exists(mainFile)) {
+ const storeProviderFn = 'provideRedux';
+
+ if (callsProvidersFunction(host, mainFile, storeProviderFn)) {
+ // exit because the store config is already provided
+ return host;
+ }
+ const storeProviderOptions = [
+ ts.factory.createIdentifier('{ store }'),
+ ];
+ const patchedConfigFile = addFunctionalProvidersToStandaloneBootstrap(
+ host,
+ mainFile,
+ storeProviderFn,
+ '@reduxjs/angular-redux',
+ storeProviderOptions
+ );
+
+ // 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.storePath}`;
+ const relativePath = buildRelativePath(
+ `/${patchedConfigFile}`,
+ statePath
+ );
+
+ const recorder = host.beginUpdate(patchedConfigFile);
+
+ const change = insertImport(
+ source,
+ patchedConfigFile,
+ 'store',
+ 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: AngularReduxOptions): Rule {
+ return (host: Tree, context: SchematicContext) => {
+ const mainFile = getProjectMainFile(host, options);
+ const isStandalone = isStandaloneApp(host, mainFile);
+
+ options.path = getProjectPath(host, options);
+
+ const parsedPath = parseName(options.path, '');
+ options.path = parsedPath.path;
+
+ if (options.module && !isStandalone) {
+ options.module = findModuleFromOptions(host, {
+ name: '',
+ module: options.module,
+ path: options.path,
+ });
+ }
+
+ const templateSource = apply(url('./files'), [
+ applyTemplates({
+ ...stringUtils,
+ ...options,
+ }),
+ move(parsedPath.path),
+ ]);
+
+ const configOrModuleUpdate = isStandalone
+ ? addStandaloneConfig(options)
+ : addImportToNgModule(options);
+
+ return chain([
+ branchAndMerge(chain([configOrModuleUpdate, mergeWith(templateSource)])),
+ options && options.skipPackageJson ? noop() : addReduxDepsToPackageJson(),
+ ])(host, context);
+ };
+}
diff --git a/projects/angular-redux/schematics/ng-add/schema.json b/projects/angular-redux/schematics/ng-add/schema.json
new file mode 100644
index 0000000..7c2c0c6
--- /dev/null
+++ b/projects/angular-redux/schematics/ng-add/schema.json
@@ -0,0 +1,39 @@
+{
+ "$schema": "http://json-schema.org/schema",
+ "$id": "SchematicsAngularRedux",
+ "title": "Angular Redux Options Schema",
+ "type": "object",
+ "properties": {
+ "skipPackageJson": {
+ "type": "boolean",
+ "default": false,
+ "description": "Do not add Redux packages as dependencies to package.json (e.g., --skipPackageJson)."
+ },
+ "path": {
+ "type": "string",
+ "format": "path",
+ "description": "The path to create the state.",
+ "visible": false,
+ "$default": {
+ "$source": "workingDirectory"
+ }
+ },
+ "project": {
+ "type": "string",
+ "description": "The name of the project.",
+ "aliases": ["p"]
+ },
+ "module": {
+ "type": "string",
+ "default": "app",
+ "description": "Allows specification of the declaring module.",
+ "alias": "m",
+ "subtype": "filepath"
+ },
+ "storePath": {
+ "type": "string",
+ "default": "store"
+ }
+ },
+ "required": []
+}
diff --git a/projects/angular-redux/schematics/ng-add/schema.ts b/projects/angular-redux/schematics/ng-add/schema.ts
new file mode 100644
index 0000000..f9cc044
--- /dev/null
+++ b/projects/angular-redux/schematics/ng-add/schema.ts
@@ -0,0 +1,7 @@
+export interface Schema {
+ skipPackageJson?: boolean;
+ path?: string;
+ project?: string;
+ module?: string;
+ storePath?: string;
+}
diff --git a/projects/angular-redux/schematics/tsconfig.spec.json b/projects/angular-redux/schematics/tsconfig.spec.json
new file mode 100644
index 0000000..445ce27
--- /dev/null
+++ b/projects/angular-redux/schematics/tsconfig.spec.json
@@ -0,0 +1,15 @@
+{
+ "extends": "../../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../out-tsc/spec",
+ "emitDecoratorMetadata": true,
+ "types": [
+ "jest",
+ "node"
+ ]
+ },
+ "include": [
+ "**/*.spec.ts",
+ "**/*.d.ts"
+ ]
+}
diff --git a/projects/angular-redux/tsconfig.schematics.json b/projects/angular-redux/tsconfig.schematics.json
new file mode 100644
index 0000000..65c5169
--- /dev/null
+++ b/projects/angular-redux/tsconfig.schematics.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "baseUrl": ".",
+ "rootDir": ".",
+ "stripInternal": true,
+ "experimentalDecorators": true,
+ "moduleResolution": "node",
+ "downlevelIteration": true,
+ "outDir": "../../dist/angular-redux",
+ "sourceMap": true,
+ "inlineSources": true,
+ "lib": ["es2018", "dom"],
+ "skipLibCheck": true,
+ "strict": true
+ },
+ "include": ["migrations/**/*.ts", "schematics/**/*.ts", "schematics-core/**/*.ts"],
+ "exclude": ["**/*.spec.ts"],
+ "angularCompilerOptions": {
+ "skipMetadataEmit": true,
+ "enableSummariesForJit": false,
+ "enableIvy": false
+ }
+}
diff --git a/projects/angular-redux/tsconfig.spec.json b/projects/angular-redux/tsconfig.spec.json
index fe9e389..d120faa 100644
--- a/projects/angular-redux/tsconfig.spec.json
+++ b/projects/angular-redux/tsconfig.spec.json
@@ -12,5 +12,9 @@
"include": [
"**/*.spec.ts",
"**/*.d.ts"
+ ],
+ "exclude": [
+ "./schematics/**/*.ts",
+ "./schematics-core/**/*.ts"
]
}
diff --git a/yarn.lock b/yarn.lock
index a113618..c3e1db4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3153,20 +3153,42 @@ __metadata:
"@testing-library/dom": "npm:^10.0.0"
"@testing-library/jest-dom": "npm:^6.4.8"
"@testing-library/user-event": "npm:^14.5.2"
+ "@types/copyfiles": "npm:^2"
"@types/jasmine": "npm:~5.1.0"
"@types/jest": "npm:^29.5.12"
+ "@types/node": "npm:^22.5.4"
+ copyfiles: "npm:^2.4.1"
jasmine-core: "npm:~5.2.0"
jest: "npm:^29.7.0"
jest-environment-jsdom: "npm:^29.7.0"
ng-packagr: "npm:^18.2.0"
redux: "npm:^5.0.1"
rxjs: "npm:~7.8.0"
+ ts-jest: "npm:^29.2.5"
tslib: "npm:^2.3.0"
typescript: "npm:~5.5.2"
zone.js: "npm:~0.14.10"
languageName: unknown
linkType: soft
+"@reduxjs/angular-redux@workspace:projects/angular-redux":
+ version: 0.0.0-use.local
+ resolution: "@reduxjs/angular-redux@workspace:projects/angular-redux"
+ dependencies:
+ tslib: "npm:^2.3.0"
+ peerDependencies:
+ "@angular/common": ">=17.3.0"
+ "@angular/core": ">=17.3.0"
+ "@reduxjs/toolkit": ^2.2.7
+ redux: ^5.0.0
+ peerDependenciesMeta:
+ "@reduxjs/toolkit":
+ optional: true
+ redux:
+ optional: true
+ languageName: unknown
+ linkType: soft
+
"@reduxjs/toolkit@npm:^2.2.7":
version: 2.2.7
resolution: "@reduxjs/toolkit@npm:2.2.7"
@@ -3746,6 +3768,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/copyfiles@npm:^2":
+ version: 2.4.4
+ resolution: "@types/copyfiles@npm:2.4.4"
+ checksum: 10/0513199240828feda5f6ed04c69d6a642c47e6ab66b81214716807f948ed3e865e9b3d2b69f75cbcc6fbe2154630755c47ca473b3913f0a831179366c709a8cc
+ languageName: node
+ linkType: hard
+
"@types/estree@npm:1.0.5, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.5":
version: 1.0.5
resolution: "@types/estree@npm:1.0.5"
@@ -3887,7 +3916,7 @@ __metadata:
languageName: node
linkType: hard
-"@types/node@npm:*, @types/node@npm:^22.5.2":
+"@types/node@npm:*, @types/node@npm:^22.5.2, @types/node@npm:^22.5.4":
version: 22.5.4
resolution: "@types/node@npm:22.5.4"
dependencies:
@@ -4483,6 +4512,13 @@ __metadata:
languageName: node
linkType: hard
+"async@npm:^3.2.3":
+ version: 3.2.6
+ resolution: "async@npm:3.2.6"
+ checksum: 10/cb6e0561a3c01c4b56a799cc8bab6ea5fef45f069ab32500b6e19508db270ef2dffa55e5aed5865c5526e9907b1f8be61b27530823b411ffafb5e1538c86c368
+ languageName: node
+ linkType: hard
+
"asynckit@npm:^0.4.0":
version: 0.4.0
resolution: "asynckit@npm:0.4.0"
@@ -4761,6 +4797,15 @@ __metadata:
languageName: node
linkType: hard
+"bs-logger@npm:^0.2.6":
+ version: 0.2.6
+ resolution: "bs-logger@npm:0.2.6"
+ dependencies:
+ fast-json-stable-stringify: "npm:2.x"
+ checksum: 10/e6d3ff82698bb3f20ce64fb85355c5716a3cf267f3977abe93bf9c32a2e46186b253f48a028ae5b96ab42bacd2c826766d9ae8cf6892f9b944656be9113cf212
+ languageName: node
+ linkType: hard
+
"bser@npm:2.1.1":
version: 2.1.1
resolution: "bser@npm:2.1.1"
@@ -4899,7 +4944,7 @@ __metadata:
languageName: node
linkType: hard
-"chalk@npm:^4.0.0, chalk@npm:^4.1.0":
+"chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0":
version: 4.1.2
resolution: "chalk@npm:4.1.2"
dependencies:
@@ -5019,6 +5064,17 @@ __metadata:
languageName: node
linkType: hard
+"cliui@npm:^7.0.2":
+ version: 7.0.4
+ resolution: "cliui@npm:7.0.4"
+ dependencies:
+ string-width: "npm:^4.2.0"
+ strip-ansi: "npm:^6.0.0"
+ wrap-ansi: "npm:^7.0.0"
+ checksum: 10/db858c49af9d59a32d603987e6fddaca2ce716cd4602ba5a2bb3a5af1351eebe82aba8dff3ef3e1b331f7fa9d40ca66e67bdf8e7c327ce0ea959747ead65c0ef
+ languageName: node
+ linkType: hard
+
"cliui@npm:^8.0.1":
version: 8.0.1
resolution: "cliui@npm:8.0.1"
@@ -5245,6 +5301,24 @@ __metadata:
languageName: node
linkType: hard
+"copyfiles@npm:^2.4.1":
+ version: 2.4.1
+ resolution: "copyfiles@npm:2.4.1"
+ dependencies:
+ glob: "npm:^7.0.5"
+ minimatch: "npm:^3.0.3"
+ mkdirp: "npm:^1.0.4"
+ noms: "npm:0.0.0"
+ through2: "npm:^2.0.1"
+ untildify: "npm:^4.0.0"
+ yargs: "npm:^16.1.0"
+ bin:
+ copyfiles: copyfiles
+ copyup: copyfiles
+ checksum: 10/17070f88cbeaf62a9355341cb2521bacd48069e1ac8e7f95a3f69c848c53646f16ff0f94807a789e0f3eedc11407ec8d3980a13ab62e2add6ef81d0a5900fd85
+ languageName: node
+ linkType: hard
+
"core-js-compat@npm:^3.37.1, core-js-compat@npm:^3.38.0":
version: 3.38.1
resolution: "core-js-compat@npm:3.38.1"
@@ -5669,6 +5743,17 @@ __metadata:
languageName: node
linkType: hard
+"ejs@npm:^3.1.10":
+ version: 3.1.10
+ resolution: "ejs@npm:3.1.10"
+ dependencies:
+ jake: "npm:^10.8.5"
+ bin:
+ ejs: bin/cli.js
+ checksum: 10/a9cb7d7cd13b7b1cd0be5c4788e44dd10d92f7285d2f65b942f33e127230c054f99a42db4d99f766d8dbc6c57e94799593ee66a14efd7c8dd70c4812bf6aa384
+ languageName: node
+ linkType: hard
+
"electron-to-chromium@npm:^1.5.4":
version: 1.5.18
resolution: "electron-to-chromium@npm:1.5.18"
@@ -6308,7 +6393,7 @@ __metadata:
languageName: node
linkType: hard
-"fast-json-stable-stringify@npm:^2.0.0, fast-json-stable-stringify@npm:^2.1.0":
+"fast-json-stable-stringify@npm:2.x, fast-json-stable-stringify@npm:^2.0.0, fast-json-stable-stringify@npm:^2.1.0":
version: 2.1.0
resolution: "fast-json-stable-stringify@npm:2.1.0"
checksum: 10/2c20055c1fa43c922428f16ca8bb29f2807de63e5c851f665f7ac9790176c01c3b40335257736b299764a8d383388dabc73c8083b8e1bc3d99f0a941444ec60e
@@ -6349,6 +6434,15 @@ __metadata:
languageName: node
linkType: hard
+"filelist@npm:^1.0.4":
+ version: 1.0.4
+ resolution: "filelist@npm:1.0.4"
+ dependencies:
+ minimatch: "npm:^5.0.1"
+ checksum: 10/4b436fa944b1508b95cffdfc8176ae6947b92825483639ef1b9a89b27d82f3f8aa22b21eed471993f92709b431670d4e015b39c087d435a61e1bb04564cf51de
+ languageName: node
+ linkType: hard
+
"fill-range@npm:^7.1.1":
version: 7.1.1
resolution: "fill-range@npm:7.1.1"
@@ -6615,7 +6709,7 @@ __metadata:
languageName: node
linkType: hard
-"glob@npm:^7.1.3, glob@npm:^7.1.4":
+"glob@npm:^7.0.5, glob@npm:^7.1.3, glob@npm:^7.1.4":
version: 7.2.3
resolution: "glob@npm:7.2.3"
dependencies:
@@ -7038,7 +7132,7 @@ __metadata:
languageName: node
linkType: hard
-"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3":
+"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.1, inherits@npm:~2.0.3":
version: 2.0.4
resolution: "inherits@npm:2.0.4"
checksum: 10/cd45e923bee15186c07fa4c89db0aace24824c482fb887b528304694b2aa6ff8a898da8657046a5dcf3e46cd6db6c61629551f9215f208d7c3f157cf9b290521
@@ -7280,6 +7374,13 @@ __metadata:
languageName: node
linkType: hard
+"isarray@npm:0.0.1":
+ version: 0.0.1
+ resolution: "isarray@npm:0.0.1"
+ checksum: 10/49191f1425681df4a18c2f0f93db3adb85573bcdd6a4482539d98eac9e705d8961317b01175627e860516a2fc45f8f9302db26e5a380a97a520e272e2a40a8d4
+ languageName: node
+ linkType: hard
+
"isarray@npm:~1.0.0":
version: 1.0.0
resolution: "isarray@npm:1.0.0"
@@ -7386,6 +7487,20 @@ __metadata:
languageName: node
linkType: hard
+"jake@npm:^10.8.5":
+ version: 10.9.2
+ resolution: "jake@npm:10.9.2"
+ dependencies:
+ async: "npm:^3.2.3"
+ chalk: "npm:^4.0.2"
+ filelist: "npm:^1.0.4"
+ minimatch: "npm:^3.1.2"
+ bin:
+ jake: bin/cli.js
+ checksum: 10/3be324708f99f031e0aec49ef8fd872eb4583cbe8a29a0c875f554f6ac638ee4ea5aa759bb63723fd54f77ca6d7db851eaa78353301734ed3700db9cb109a0cd
+ languageName: node
+ linkType: hard
+
"jasmine-core@npm:~5.2.0":
version: 5.2.0
resolution: "jasmine-core@npm:5.2.0"
@@ -7778,7 +7893,7 @@ __metadata:
languageName: node
linkType: hard
-"jest-util@npm:^29.7.0":
+"jest-util@npm:^29.0.0, jest-util@npm:^29.7.0":
version: 29.7.0
resolution: "jest-util@npm:29.7.0"
dependencies:
@@ -8229,6 +8344,13 @@ __metadata:
languageName: node
linkType: hard
+"lodash.memoize@npm:^4.1.2":
+ version: 4.1.2
+ resolution: "lodash.memoize@npm:4.1.2"
+ checksum: 10/192b2168f310c86f303580b53acf81ab029761b9bd9caa9506a019ffea5f3363ea98d7e39e7e11e6b9917066c9d36a09a11f6fe16f812326390d8f3a54a1a6da
+ languageName: node
+ linkType: hard
+
"lodash@npm:^4.17.21":
version: 4.17.21
resolution: "lodash@npm:4.17.21"
@@ -8321,6 +8443,13 @@ __metadata:
languageName: node
linkType: hard
+"make-error@npm:^1.3.6":
+ version: 1.3.6
+ resolution: "make-error@npm:1.3.6"
+ checksum: 10/b86e5e0e25f7f777b77fabd8e2cbf15737972869d852a22b7e73c17623928fccb826d8e46b9951501d3f20e51ad74ba8c59ed584f610526a48f8ccf88aaec402
+ languageName: node
+ linkType: hard
+
"make-fetch-happen@npm:^13.0.0, make-fetch-happen@npm:^13.0.1":
version: 13.0.1
resolution: "make-fetch-happen@npm:13.0.1"
@@ -8479,7 +8608,7 @@ __metadata:
languageName: node
linkType: hard
-"minimatch@npm:^3.0.4, minimatch@npm:^3.1.1":
+"minimatch@npm:^3.0.3, minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2":
version: 3.1.2
resolution: "minimatch@npm:3.1.2"
dependencies:
@@ -8488,6 +8617,15 @@ __metadata:
languageName: node
linkType: hard
+"minimatch@npm:^5.0.1":
+ version: 5.1.6
+ resolution: "minimatch@npm:5.1.6"
+ dependencies:
+ brace-expansion: "npm:^2.0.1"
+ checksum: 10/126b36485b821daf96d33b5c821dac600cc1ab36c87e7a532594f9b1652b1fa89a1eebcaad4dff17c764dce1a7ac1531327f190fed5f97d8f6e5f889c116c429
+ languageName: node
+ linkType: hard
+
"minimatch@npm:^9.0.0, minimatch@npm:^9.0.4":
version: 9.0.5
resolution: "minimatch@npm:9.0.5"
@@ -8581,7 +8719,7 @@ __metadata:
languageName: node
linkType: hard
-"mkdirp@npm:^1.0.3":
+"mkdirp@npm:^1.0.3, mkdirp@npm:^1.0.4":
version: 1.0.4
resolution: "mkdirp@npm:1.0.4"
bin:
@@ -8853,6 +8991,16 @@ __metadata:
languageName: node
linkType: hard
+"noms@npm:0.0.0":
+ version: 0.0.0
+ resolution: "noms@npm:0.0.0"
+ dependencies:
+ inherits: "npm:^2.0.1"
+ readable-stream: "npm:~1.0.31"
+ checksum: 10/a05f056dabf764c86472b6b5aad10455f3adcb6971f366cdf36a72b559b29310a940e316bca30802f2804fdd41707941366224f4cba80c4f53071512245bf200
+ languageName: node
+ linkType: hard
+
"nopt@npm:^7.0.0":
version: 7.2.1
resolution: "nopt@npm:7.2.1"
@@ -9658,7 +9806,7 @@ __metadata:
languageName: node
linkType: hard
-"readable-stream@npm:^2.0.1":
+"readable-stream@npm:^2.0.1, readable-stream@npm:~2.3.6":
version: 2.3.8
resolution: "readable-stream@npm:2.3.8"
dependencies:
@@ -9684,6 +9832,18 @@ __metadata:
languageName: node
linkType: hard
+"readable-stream@npm:~1.0.31":
+ version: 1.0.34
+ resolution: "readable-stream@npm:1.0.34"
+ dependencies:
+ core-util-is: "npm:~1.0.0"
+ inherits: "npm:~2.0.1"
+ isarray: "npm:0.0.1"
+ string_decoder: "npm:~0.10.x"
+ checksum: 10/20537fca5a8ffd4af0f483be1cce0e981ed8cbb1087e0c762e2e92ae77f1005627272cebed8422f28047b465056aa1961fefd24baf532ca6a3616afea6811ae0
+ languageName: node
+ linkType: hard
+
"readdirp@npm:~3.6.0":
version: 3.6.0
resolution: "readdirp@npm:3.6.0"
@@ -10226,7 +10386,7 @@ __metadata:
languageName: node
linkType: hard
-"semver@npm:7.6.3, semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.5.4":
+"semver@npm:7.6.3, semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.3":
version: 7.6.3
resolution: "semver@npm:7.6.3"
bin:
@@ -10702,6 +10862,13 @@ __metadata:
languageName: node
linkType: hard
+"string_decoder@npm:~0.10.x":
+ version: 0.10.31
+ resolution: "string_decoder@npm:0.10.31"
+ checksum: 10/cc43e6b1340d4c7843da0e37d4c87a4084c2342fc99dcf6563c3ec273bb082f0cbd4ebf25d5da19b04fb16400d393885fda830be5128e1c416c73b5a6165f175
+ languageName: node
+ linkType: hard
+
"string_decoder@npm:~1.1.1":
version: 1.1.1
resolution: "string_decoder@npm:1.1.1"
@@ -10884,6 +11051,16 @@ __metadata:
languageName: node
linkType: hard
+"through2@npm:^2.0.1":
+ version: 2.0.5
+ resolution: "through2@npm:2.0.5"
+ dependencies:
+ readable-stream: "npm:~2.3.6"
+ xtend: "npm:~4.0.1"
+ checksum: 10/cd71f7dcdc7a8204fea003a14a433ef99384b7d4e31f5497e1f9f622b3cf3be3691f908455f98723bdc80922a53af7fa10c3b7abbe51c6fd3d536dbc7850e2c4
+ languageName: node
+ linkType: hard
+
"thunky@npm:^1.0.2":
version: 1.1.0
resolution: "thunky@npm:1.1.0"
@@ -10969,6 +11146,43 @@ __metadata:
languageName: node
linkType: hard
+"ts-jest@npm:^29.2.5":
+ version: 29.2.5
+ resolution: "ts-jest@npm:29.2.5"
+ dependencies:
+ bs-logger: "npm:^0.2.6"
+ ejs: "npm:^3.1.10"
+ fast-json-stable-stringify: "npm:^2.1.0"
+ jest-util: "npm:^29.0.0"
+ json5: "npm:^2.2.3"
+ lodash.memoize: "npm:^4.1.2"
+ make-error: "npm:^1.3.6"
+ semver: "npm:^7.6.3"
+ yargs-parser: "npm:^21.1.1"
+ peerDependencies:
+ "@babel/core": ">=7.0.0-beta.0 <8"
+ "@jest/transform": ^29.0.0
+ "@jest/types": ^29.0.0
+ babel-jest: ^29.0.0
+ jest: ^29.0.0
+ typescript: ">=4.3 <6"
+ peerDependenciesMeta:
+ "@babel/core":
+ optional: true
+ "@jest/transform":
+ optional: true
+ "@jest/types":
+ optional: true
+ babel-jest:
+ optional: true
+ esbuild:
+ optional: true
+ bin:
+ ts-jest: cli.js
+ checksum: 10/f89e562816861ec4510840a6b439be6145f688b999679328de8080dc8e66481325fc5879519b662163e33b7578f35243071c38beb761af34e5fe58e3e326a958
+ languageName: node
+ linkType: hard
+
"tslib@npm:2.6.3":
version: 2.6.3
resolution: "tslib@npm:2.6.3"
@@ -11122,6 +11336,13 @@ __metadata:
languageName: node
linkType: hard
+"untildify@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "untildify@npm:4.0.0"
+ checksum: 10/39ced9c418a74f73f0a56e1ba4634b4d959422dff61f4c72a8e39f60b99380c1b45ed776fbaa0a4101b157e4310d873ad7d114e8534ca02609b4916bb4187fb9
+ languageName: node
+ linkType: hard
+
"update-browserslist-db@npm:^1.1.0":
version: 1.1.0
resolution: "update-browserslist-db@npm:1.1.0"
@@ -11624,6 +11845,13 @@ __metadata:
languageName: node
linkType: hard
+"xtend@npm:~4.0.1":
+ version: 4.0.2
+ resolution: "xtend@npm:4.0.2"
+ checksum: 10/ac5dfa738b21f6e7f0dd6e65e1b3155036d68104e67e5d5d1bde74892e327d7e5636a076f625599dc394330a731861e87343ff184b0047fef1360a7ec0a5a36a
+ languageName: node
+ linkType: hard
+
"y18n@npm:^5.0.5":
version: 5.0.8
resolution: "y18n@npm:5.0.8"
@@ -11645,6 +11873,13 @@ __metadata:
languageName: node
linkType: hard
+"yargs-parser@npm:^20.2.2":
+ version: 20.2.9
+ resolution: "yargs-parser@npm:20.2.9"
+ checksum: 10/0188f430a0f496551d09df6719a9132a3469e47fe2747208b1dd0ab2bb0c512a95d0b081628bbca5400fb20dbf2fabe63d22badb346cecadffdd948b049f3fcc
+ languageName: node
+ linkType: hard
+
"yargs-parser@npm:^21.1.1":
version: 21.1.1
resolution: "yargs-parser@npm:21.1.1"
@@ -11667,6 +11902,21 @@ __metadata:
languageName: node
linkType: hard
+"yargs@npm:^16.1.0":
+ version: 16.2.0
+ resolution: "yargs@npm:16.2.0"
+ dependencies:
+ cliui: "npm:^7.0.2"
+ escalade: "npm:^3.1.1"
+ get-caller-file: "npm:^2.0.5"
+ require-directory: "npm:^2.1.1"
+ string-width: "npm:^4.2.0"
+ y18n: "npm:^5.0.5"
+ yargs-parser: "npm:^20.2.2"
+ checksum: 10/807fa21211d2117135d557f95fcd3c3d390530cda2eca0c840f1d95f0f40209dcfeb5ec18c785a1f3425896e623e3b2681e8bb7b6600060eda1c3f4804e7957e
+ languageName: node
+ linkType: hard
+
"yocto-queue@npm:^0.1.0":
version: 0.1.0
resolution: "yocto-queue@npm:0.1.0"