├── tools ├── generators │ └── .gitkeep └── tsconfig.tools.json ├── apps ├── rickandmorty │ ├── src │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── app │ │ │ ├── episode │ │ │ │ ├── episode │ │ │ │ │ ├── episode.component.scss │ │ │ │ │ ├── episode.component.html │ │ │ │ │ ├── episode.resolver.spec.ts │ │ │ │ │ ├── episode.component.spec.ts │ │ │ │ │ ├── episode.component.ts │ │ │ │ │ └── episode.resolver.ts │ │ │ │ ├── episode-list │ │ │ │ │ ├── episode-list.component.scss │ │ │ │ │ ├── episode-list.resolver.spec.ts │ │ │ │ │ ├── episode-list.resolver.ts │ │ │ │ │ ├── episode-list.component.spec.ts │ │ │ │ │ ├── episode-list.component.html │ │ │ │ │ └── episode-list.component.ts │ │ │ │ ├── episode.module.ts │ │ │ │ └── episode-routing.module.ts │ │ │ ├── location │ │ │ │ ├── location │ │ │ │ │ ├── location.component.scss │ │ │ │ │ ├── location.resolver.spec.ts │ │ │ │ │ ├── location.component.html │ │ │ │ │ ├── location.component.spec.ts │ │ │ │ │ ├── location.component.ts │ │ │ │ │ └── location.resolver.ts │ │ │ │ ├── location-list │ │ │ │ │ ├── location-list.component.scss │ │ │ │ │ ├── location-list.resolver.spec.ts │ │ │ │ │ ├── location-list.resolver.ts │ │ │ │ │ ├── location-list.component.spec.ts │ │ │ │ │ ├── location-list.component.ts │ │ │ │ │ └── location-list.component.html │ │ │ │ ├── location.module.ts │ │ │ │ └── location-routing.module.ts │ │ │ ├── character │ │ │ │ ├── character │ │ │ │ │ ├── character.component.scss │ │ │ │ │ ├── character.resolver.spec.ts │ │ │ │ │ ├── character.component.spec.ts │ │ │ │ │ ├── character.component.ts │ │ │ │ │ ├── character.resolver.ts │ │ │ │ │ └── character.component.html │ │ │ │ ├── character-list │ │ │ │ │ ├── character-list.component.scss │ │ │ │ │ ├── character-list.resolver.spec.ts │ │ │ │ │ ├── character-list.resolver.ts │ │ │ │ │ ├── character-list.component.spec.ts │ │ │ │ │ ├── character-list.component.ts │ │ │ │ │ └── character-list.component.html │ │ │ │ ├── character-routing.module.ts │ │ │ │ └── character.module.ts │ │ │ ├── app.component.less │ │ │ ├── app.component.ts │ │ │ ├── app.component.spec.ts │ │ │ ├── app.server.module.ts │ │ │ ├── app-routing.module.ts │ │ │ ├── app.component.html │ │ │ └── app.module.ts │ │ ├── test-setup.ts │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── styles.less │ │ ├── main.server.ts │ │ ├── index.html │ │ ├── main.ts │ │ └── polyfills.ts │ ├── tsconfig.editor.json │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ ├── tsconfig.json │ ├── tsconfig.server.json │ ├── jest.config.js │ ├── ngsw-config.json │ ├── .browserslistrc │ ├── .eslintrc.json │ └── server.ts └── rickandmorty-e2e │ ├── src │ ├── support │ │ ├── app.po.ts │ │ ├── index.ts │ │ └── commands.ts │ ├── fixtures │ │ └── example.json │ ├── integration │ │ └── app.spec.ts │ └── plugins │ │ └── index.js │ ├── tsconfig.json │ ├── tsconfig.e2e.json │ ├── .eslintrc.json │ └── cypress.json ├── .prettierrc ├── libs ├── rickandmorty │ ├── api │ │ ├── README.md │ │ ├── src │ │ │ ├── test-setup.ts │ │ │ ├── lib │ │ │ │ ├── interfaces │ │ │ │ │ ├── filter.ts │ │ │ │ │ ├── api-options.ts │ │ │ │ │ ├── ping.ts │ │ │ │ │ ├── raw-pagination.ts │ │ │ │ │ ├── episode-filter.ts │ │ │ │ │ ├── raw-response.ts │ │ │ │ │ ├── location-filter.ts │ │ │ │ │ ├── raw-episode.ts │ │ │ │ │ ├── raw-location.ts │ │ │ │ │ ├── character-filter.ts │ │ │ │ │ ├── raw-character.ts │ │ │ │ │ ├── response.ts │ │ │ │ │ ├── pagination.ts │ │ │ │ │ ├── episode.ts │ │ │ │ │ ├── location.ts │ │ │ │ │ └── character.ts │ │ │ │ ├── enums │ │ │ │ │ ├── character-status.ts │ │ │ │ │ └── character-gender.ts │ │ │ │ ├── helpers │ │ │ │ │ ├── coerce-array.ts │ │ │ │ │ ├── is-response-with-pagination.ts │ │ │ │ │ ├── extract-ids-from-url.ts │ │ │ │ │ ├── extract-search-params-from-url.ts │ │ │ │ │ └── response-factory.ts │ │ │ │ ├── tokens.ts │ │ │ │ ├── api.module.ts │ │ │ │ ├── api.service.ts │ │ │ │ └── api.service.spec.ts │ │ │ └── index.ts │ │ ├── ng-package.json │ │ ├── tsconfig.lib.prod.json │ │ ├── tsconfig.spec.json │ │ ├── tsconfig.json │ │ ├── package.json │ │ ├── tsconfig.lib.json │ │ ├── jest.config.js │ │ └── .eslintrc.json │ └── utils │ │ ├── README.md │ │ ├── src │ │ ├── test-setup.ts │ │ ├── lib │ │ │ ├── types │ │ │ │ ├── extended-character.ts │ │ │ │ ├── extended-episode.ts │ │ │ │ └── extended-location.ts │ │ │ ├── filter.service.spec.ts │ │ │ └── filter.service.ts │ │ └── index.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.spec.json │ │ ├── tsconfig.lib.json │ │ ├── jest.config.js │ │ └── .eslintrc.json └── ngx-ssr │ ├── cache │ ├── test-setup.ts │ ├── express │ │ ├── ng-package.json │ │ └── index.ts │ ├── ng-package.json │ ├── src │ │ ├── index.ts │ │ └── lib │ │ │ ├── cache-controller.ts │ │ │ ├── lru-cache.factory.ts │ │ │ ├── ngx-ssr-cache.module.ts │ │ │ ├── lru-cache.spec.ts │ │ │ ├── cache.interceptor.ts │ │ │ ├── lru-cache.ts │ │ │ └── cache.interceptor.spec.ts │ ├── tsconfig.spec.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.json │ ├── package.json │ ├── tsconfig.lib.json │ ├── jest.config.js │ ├── .eslintrc.json │ └── README.md │ ├── timeout │ ├── src │ │ ├── test-setup.ts │ │ ├── index.ts │ │ └── lib │ │ │ ├── ngx-ssr-timeout.module.ts │ │ │ ├── timeout.interceptor.ts │ │ │ └── timeout.interceptor.spec.ts │ ├── ng-package.json │ ├── tsconfig.spec.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.json │ ├── package.json │ ├── tsconfig.lib.json │ ├── README.md │ ├── jest.config.js │ └── .eslintrc.json │ └── platform │ ├── src │ ├── test-setup.ts │ ├── index.ts │ └── lib │ │ ├── ngx-ssr-platform.module.ts │ │ ├── tokens.ts │ │ ├── if-is-server.directive.ts │ │ ├── if-is-browser.directive.ts │ │ ├── if-is-server.directive.spec.ts │ │ └── if-is-browser.directive.spec.ts │ ├── ng-package.json │ ├── tsconfig.spec.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.json │ ├── package.json │ ├── tsconfig.lib.json │ ├── jest.config.js │ ├── README.md │ └── .eslintrc.json ├── .prettierignore ├── jest.preset.js ├── jest.config.js ├── .firebaserc ├── .vscode └── extensions.json ├── .editorconfig ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── release.yml │ ├── pr.yml │ └── codeql-analysis.yml └── PULL_REQUEST_TEMPLATE ├── .gitignore ├── firebase.json ├── .eslintrc.json ├── tsconfig.base.json ├── README.md ├── LICENSE ├── CONTRIBUTING.md ├── nx.json ├── decorate-angular-cli.js ├── CODE_OF_CONDUCT.md ├── package.json ├── migrations.json └── angular.json /tools/generators/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/README.md: -------------------------------------------------------------------------------- 1 | # ngx-rickandmorty 2 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/episode/episode/episode.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/location/location/location.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libs/ngx-ssr/cache/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; 2 | -------------------------------------------------------------------------------- /libs/rickandmorty/utils/README.md: -------------------------------------------------------------------------------- 1 | # @ngx-ssr/rickandmorty/utils 2 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/character/character/character.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/episode/episode-list/episode-list.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/location/location-list/location-list.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/character/character-list/character-list.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /libs/ngx-ssr/timeout/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /libs/ngx-ssr/platform/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /libs/rickandmorty/utils/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /apps/rickandmorty-e2e/src/support/app.po.ts: -------------------------------------------------------------------------------- 1 | export const getGreeting = () => cy.get('h1'); 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nrwl/jest/preset'); 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/interfaces/filter.ts: -------------------------------------------------------------------------------- 1 | export interface Filter { 2 | page?: number; 3 | } 4 | -------------------------------------------------------------------------------- /libs/ngx-ssr/cache/express/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "lib": { 3 | "entryFile": "index.ts" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IKatsuba/ngx-ssr/HEAD/apps/rickandmorty/src/favicon.ico -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/interfaces/api-options.ts: -------------------------------------------------------------------------------- 1 | export interface ApiOptions { 2 | baseUrl: string; 3 | } 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { getJestProjects } = require('@nrwl/jest'); 2 | 3 | module.exports = { projects: getJestProjects() }; 4 | -------------------------------------------------------------------------------- /libs/ngx-ssr/timeout/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/ngx-ssr-timeout.module'; 2 | export * from './lib/timeout.interceptor'; 3 | -------------------------------------------------------------------------------- /apps/rickandmorty-e2e/src/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io" 4 | } 5 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/styles.less: -------------------------------------------------------------------------------- 1 | @import '~@taiga-ui/core/styles/taiga-ui-global'; 2 | @import '~@taiga-ui/core/styles/taiga-ui-theme'; 3 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/app.component.less: -------------------------------------------------------------------------------- 1 | tui-tabs { 2 | position: sticky; 3 | top: 0; 4 | background-color: white; 5 | z-index: 1; 6 | } 7 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/interfaces/ping.ts: -------------------------------------------------------------------------------- 1 | export interface PingResponse { 2 | characters: string; 3 | locations: string; 4 | episodes: string; 5 | } 6 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/enums/character-status.ts: -------------------------------------------------------------------------------- 1 | export const enum CharacterStatus { 2 | Alive = 'Alive', 3 | Dead = 'Dead', 4 | Unknown = 'unknown', 5 | } 6 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/helpers/coerce-array.ts: -------------------------------------------------------------------------------- 1 | export function coerceArray(value: T | T[]): T[] { 2 | return Array.isArray(value) ? value : [value]; 3 | } 4 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/interfaces/raw-pagination.ts: -------------------------------------------------------------------------------- 1 | export interface RawPagination { 2 | count: number; 3 | pages: number; 4 | next: string; 5 | prev: string; 6 | } 7 | -------------------------------------------------------------------------------- /apps/rickandmorty/tsconfig.editor.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["**/*.ts"], 4 | "compilerOptions": { 5 | "types": ["jest", "node"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/enums/character-gender.ts: -------------------------------------------------------------------------------- 1 | export const enum CharacterGender { 2 | Female = 'Female', 3 | Male = 'Male', 4 | Genderless = 'Genderless', 5 | Unknown = 'unknown', 6 | } 7 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/interfaces/episode-filter.ts: -------------------------------------------------------------------------------- 1 | import { Filter } from './filter'; 2 | 3 | export interface EpisodeFilter extends Filter { 4 | name?: string; 5 | episode?: string; 6 | } 7 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/interfaces/raw-response.ts: -------------------------------------------------------------------------------- 1 | import { RawPagination } from './raw-pagination'; 2 | 3 | export interface RawResponse { 4 | info: RawPagination; 5 | results: T[]; 6 | } 7 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "targets": { 3 | "ng-rickandmorty": { 4 | "hosting": { 5 | "rickandmorty": [ 6 | "ng-rickandmorty" 7 | ] 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /libs/ngx-ssr/platform/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/if-is-server.directive'; 2 | export * from './lib/if-is-browser.directive'; 3 | export * from './lib/ngx-ssr-platform.module'; 4 | export * from './lib/tokens'; 5 | -------------------------------------------------------------------------------- /apps/rickandmorty-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.e2e.json" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /apps/rickandmorty/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": [] 6 | }, 7 | "files": ["src/main.ts", "src/polyfills.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /libs/ngx-ssr/cache/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../../dist/libs/ngx-ssr/cache", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/interfaces/location-filter.ts: -------------------------------------------------------------------------------- 1 | import { Filter } from './filter'; 2 | 3 | export interface LocationFilter extends Filter { 4 | name?: string; 5 | type?: string; 6 | dimension?: string; 7 | } 8 | -------------------------------------------------------------------------------- /libs/rickandmorty/utils/src/lib/types/extended-character.ts: -------------------------------------------------------------------------------- 1 | import { Character, Episode } from 'ngx-rickandmorty'; 2 | 3 | export interface ExtendedCharacter extends Omit { 4 | episode: Episode[]; 5 | } 6 | -------------------------------------------------------------------------------- /libs/rickandmorty/utils/src/lib/types/extended-episode.ts: -------------------------------------------------------------------------------- 1 | import { Character, Episode } from 'ngx-rickandmorty'; 2 | 3 | export interface ExtendedEpisode extends Omit { 4 | characters: Character[]; 5 | } 6 | -------------------------------------------------------------------------------- /libs/ngx-ssr/platform/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../../dist/libs/ngx-ssr/platform", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/ngx-ssr/timeout/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../../dist/libs/ngx-ssr/timeout", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../../dist/libs/rickandmorty/api", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/rickandmorty/utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/filter.service'; 2 | 3 | export * from './lib/types/extended-character'; 4 | export * from './lib/types/extended-episode'; 5 | export * from './lib/types/extended-location'; 6 | -------------------------------------------------------------------------------- /libs/rickandmorty/utils/src/lib/types/extended-location.ts: -------------------------------------------------------------------------------- 1 | import { Character, Location } from 'ngx-rickandmorty'; 2 | 3 | export interface ExtendedLocation extends Omit { 4 | residents: Character[]; 5 | } 6 | -------------------------------------------------------------------------------- /libs/ngx-ssr/cache/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/cache.interceptor'; 2 | export * from './lib/cache-controller'; 3 | export * from './lib/lru-cache.factory'; 4 | export * from './lib/lru-cache'; 5 | export * from './lib/ngx-ssr-cache.module'; 6 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/interfaces/raw-episode.ts: -------------------------------------------------------------------------------- 1 | export interface RawEpisode { 2 | id: number; 3 | name: string; 4 | air_date: string; 5 | episode: string; 6 | characters: string[]; 7 | url: string; 8 | created: string; 9 | } 10 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/interfaces/raw-location.ts: -------------------------------------------------------------------------------- 1 | export interface RawLocation { 2 | id: number; 3 | name: string; 4 | type: string; 5 | dimension: string; 6 | residents: string[]; 7 | url: string; 8 | created: string; 9 | } 10 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ram-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.less'], 7 | }) 8 | export class AppComponent {} 9 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/helpers/is-response-with-pagination.ts: -------------------------------------------------------------------------------- 1 | import { RawResponse } from '../interfaces/raw-response'; 2 | 3 | export function isRawResponse(value: any): value is RawResponse { 4 | return !!value?.info && !!value?.results; 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "angular.ng-template", 5 | "ms-vscode.vscode-typescript-tslint-plugin", 6 | "esbenp.prettier-vscode", 7 | "firsttris.vscode-jest-runner" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /libs/rickandmorty/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"] 9 | }, 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /apps/rickandmorty-e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "outDir": "../../dist/out-tsc", 6 | "allowJs": true, 7 | "types": ["cypress", "node"] 8 | }, 9 | "include": ["src/**/*.ts", "src/**/*.js"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/rickandmorty/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "files": ["src/test-setup.ts"], 9 | "include": ["**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /libs/ngx-ssr/platform/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "files": ["src/test-setup.ts"], 9 | "include": ["**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /libs/ngx-ssr/timeout/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "files": ["src/test-setup.ts"], 9 | "include": ["**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "enableIvy": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "files": ["src/test-setup.ts"], 9 | "include": ["**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /libs/rickandmorty/utils/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "files": ["src/test-setup.ts"], 9 | "include": ["**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /libs/ngx-ssr/cache/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "files": [ 9 | "test-setup.ts" 10 | ], 11 | "include": ["**/*.spec.ts", "**/*.d.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /apps/rickandmorty/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | }, 12 | { 13 | "path": "./tsconfig.editor.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /libs/ngx-ssr/cache/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "enableIvy": true, 9 | "compilationMode": "partial" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /libs/ngx-ssr/platform/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "enableIvy": true, 9 | "compilationMode": "partial" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /libs/ngx-ssr/timeout/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "enableIvy": true, 9 | "compilationMode": "partial" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /libs/ngx-ssr/cache/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.lib.prod.json" 11 | }, 12 | { 13 | "path": "./tsconfig.spec.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /libs/ngx-ssr/platform/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.lib.prod.json" 11 | }, 12 | { 13 | "path": "./tsconfig.spec.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /libs/ngx-ssr/timeout/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.lib.prod.json" 11 | }, 12 | { 13 | "path": "./tsconfig.spec.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.lib.prod.json" 11 | }, 12 | { 13 | "path": "./tsconfig.spec.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/main.server.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | 3 | import { environment } from './environments/environment'; 4 | 5 | if (environment.production) { 6 | enableProdMode(); 7 | } 8 | 9 | export { AppServerModule } from './app/app.server.module'; 10 | export { renderModule, renderModuleFactory } from '@angular/platform-server'; 11 | -------------------------------------------------------------------------------- /libs/ngx-ssr/cache/src/lib/cache-controller.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | 3 | export abstract class CacheController { 4 | abstract set( 5 | key: string, 6 | value: T, 7 | maxAge?: number 8 | ): Observable; 9 | 10 | abstract get(key: string): Observable; 11 | 12 | abstract size(): Observable; 13 | } 14 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Rick and Morty 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/interfaces/character-filter.ts: -------------------------------------------------------------------------------- 1 | import { CharacterGender } from '../enums/character-gender'; 2 | import { CharacterStatus } from '../enums/character-status'; 3 | import { Filter } from './filter'; 4 | 5 | export interface CharacterFilter extends Filter { 6 | name?: string; 7 | status?: CharacterStatus; 8 | species?: string; 9 | type?: string; 10 | gender?: CharacterGender; 11 | } 12 | -------------------------------------------------------------------------------- /libs/rickandmorty/utils/src/lib/filter.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { FilterService } from './filter.service'; 4 | 5 | describe('FilterService', () => { 6 | let service: FilterService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | }); 11 | 12 | xit('should be created', () => { 13 | expect(service).toBeTruthy(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /apps/rickandmorty/tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.app.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/server", 5 | "target": "es2016", 6 | "types": [ 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/main.server.ts", 12 | "server.ts" 13 | ], 14 | "angularCompilerOptions": { 15 | "entryModule": "./src/app/app.server.module#AppServerModule" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/episode/episode/episode.component.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ episode.name }}

3 |

Episode: {{ episode.episode }}

4 |

5 | Characters: 6 | 11 | {{ character.name }} 12 | , 13 | 14 |

15 |
16 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/episode/episode/episode.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { EpisodeResolver } from './episode.resolver'; 4 | 5 | describe('EpisodeService', () => { 6 | let service: EpisodeResolver; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | }); 11 | 12 | xit('should be created', () => { 13 | expect(service).toBeTruthy(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/location/location/location.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { LocationResolver } from './location.resolver'; 4 | 5 | describe('EpisodeService', () => { 6 | let service: LocationResolver; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | }); 11 | 12 | xit('should be created', () => { 13 | expect(service).toBeTruthy(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/helpers/extract-ids-from-url.ts: -------------------------------------------------------------------------------- 1 | export function extractIdsFromUrl(url: string): number[] { 2 | if (!url) { 3 | return []; 4 | } 5 | 6 | const { pathname } = new URL(url); 7 | 8 | const maybeIds = pathname.split('/').pop().split(','); 9 | 10 | if (maybeIds.every((maybeId) => /\d+/.test(maybeId))) { 11 | return maybeIds.map((maybeId) => parseInt(maybeId, 10)); 12 | } 13 | 14 | return []; 15 | } 16 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/helpers/extract-search-params-from-url.ts: -------------------------------------------------------------------------------- 1 | export function extractSearchParamsFromUrl( 2 | url: string 3 | ): T & { ids?: number[] } { 4 | if (!url) { 5 | return null; 6 | } 7 | 8 | const { searchParams } = new URL(url); 9 | 10 | const params: Record = {}; 11 | 12 | searchParams.forEach((value, key) => { 13 | params[key] = value; 14 | }); 15 | 16 | return params as T; 17 | } 18 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/character/character/character.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { CharacterResolver } from './character.resolver'; 4 | 5 | describe('CharacterService', () => { 6 | let service: CharacterResolver; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | }); 11 | 12 | xit('should be created', () => { 13 | expect(service).toBeTruthy(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /apps/rickandmorty-e2e/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:cypress/recommended", "../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["src/plugins/index.js"], 11 | "rules": { 12 | "@typescript-eslint/no-var-requires": "off", 13 | "no-undef": "off" 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/episode/episode-list/episode-list.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { EpisodeListResolver } from './episode-list.resolver'; 4 | 5 | describe('EpisodeListService', () => { 6 | let service: EpisodeListResolver; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | }); 11 | 12 | xit('should be created', () => { 13 | expect(service).toBeTruthy(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-rickandmorty", 3 | "version": "0.0.0-development", 4 | "keywords": [ 5 | "angular", 6 | "rick", 7 | "morty", 8 | "api", 9 | "rickandmorty" 10 | ], 11 | "peerDependencies": { 12 | "@angular/common": ">=12.0.0", 13 | "@angular/core": ">=12.0.0" 14 | }, 15 | "dependencies": { 16 | "tslib": "^2.0.0" 17 | }, 18 | "publishConfig": { 19 | "access": "public" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/location/location-list/location-list.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { LocationListResolver } from './location-list.resolver'; 4 | 5 | describe('EpisodeListService', () => { 6 | let service: LocationListResolver; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | }); 11 | 12 | xit('should be created', () => { 13 | expect(service).toBeTruthy(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/location/location/location.component.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ location.name }}

3 |

Dimension: {{ location.dimension }}

4 |

Type: {{ location.type }}

5 |

6 | Residents: 7 | 12 | {{ resident.name }}, 13 | 14 |

15 |
16 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/character/character-list/character-list.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { CharacterListResolver } from './character-list.resolver'; 4 | 5 | describe('CharacterListService', () => { 6 | let service: CharacterListResolver; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | }); 11 | 12 | xit('should be created', () => { 13 | expect(service).toBeTruthy(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /apps/rickandmorty-e2e/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileServerFolder": ".", 3 | "fixturesFolder": "./src/fixtures", 4 | "integrationFolder": "./src/integration", 5 | "modifyObstructiveCode": false, 6 | "pluginsFile": "./src/plugins/index", 7 | "supportFile": "./src/support/index.ts", 8 | "video": true, 9 | "videosFolder": "../../dist/cypress/apps/rickandmorty-e2e/videos", 10 | "screenshotsFolder": "../../dist/cypress/apps/rickandmorty-e2e/screenshots", 11 | "chromeWebSecurity": false 12 | } 13 | -------------------------------------------------------------------------------- /apps/rickandmorty-e2e/src/integration/app.spec.ts: -------------------------------------------------------------------------------- 1 | import { getGreeting } from '../support/app.po'; 2 | 3 | describe('rickandmorty', () => { 4 | beforeEach(() => cy.visit('/')); 5 | 6 | xit('should display welcome message', () => { 7 | // Custom command example, see `../support/commands.ts` file 8 | cy.login('my-email@something.com', 'myPassword'); 9 | 10 | // Function helper example, see `../support/app.po.ts` file 11 | getGreeting().contains('Welcome to rickandmorty!'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /libs/ngx-ssr/platform/src/lib/ngx-ssr-platform.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { IfIsBrowserDirective } from './if-is-browser.directive'; 4 | import { IfIsServerDirective } from './if-is-server.directive'; 5 | 6 | @NgModule({ 7 | imports: [CommonModule], 8 | declarations: [IfIsBrowserDirective, IfIsServerDirective], 9 | exports: [IfIsBrowserDirective, IfIsServerDirective], 10 | }) 11 | export class NgxSsrPlatformModule {} 12 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | document.addEventListener('DOMContentLoaded', () => { 12 | platformBrowserDynamic() 13 | .bootstrapModule(AppModule) 14 | .catch((err) => console.error(err)); 15 | }); 16 | -------------------------------------------------------------------------------- /libs/ngx-ssr/cache/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ngx-ssr/cache", 3 | "version": "0.0.0-development", 4 | "keywords": [ 5 | "angular", 6 | "angulae ssr", 7 | "universal", 8 | "angular/universal", 9 | "ssr", 10 | "cache", 11 | "lru-cache", 12 | "interceptor" 13 | ], 14 | "peerDependencies": { 15 | "@angular/common": ">=12.0.0", 16 | "@angular/core": ">=12.0.0" 17 | }, 18 | "dependencies": { 19 | "tslib": "^2.0.0" 20 | }, 21 | "publishConfig": { 22 | "access": "public" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /libs/ngx-ssr/timeout/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ngx-ssr/timeout", 3 | "version": "0.0.0-development", 4 | "keywords": [ 5 | "angular", 6 | "angulae ssr", 7 | "universal", 8 | "angular/universal", 9 | "ssr", 10 | "timeout", 11 | "http", 12 | "interceptor" 13 | ], 14 | "peerDependencies": { 15 | "@angular/common": ">=12.0.0", 16 | "@angular/core": ">=12.0.0" 17 | }, 18 | "dependencies": { 19 | "tslib": "^2.0.0" 20 | }, 21 | "publishConfig": { 22 | "access": "public" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /libs/ngx-ssr/cache/src/lib/lru-cache.factory.ts: -------------------------------------------------------------------------------- 1 | import { inject, InjectionToken } from '@angular/core'; 2 | import { LRUCache, LRUCacheOptions } from './lru-cache'; 3 | 4 | export const LRU_CACHE_OPTIONS = new InjectionToken( 5 | 'LRU Cache Options', 6 | { 7 | factory(): LRUCacheOptions { 8 | return { 9 | maxSize: Infinity, 10 | maxAge: Infinity, 11 | }; 12 | }, 13 | } 14 | ); 15 | 16 | export function lruCacheFactory(): LRUCache { 17 | return new LRUCache(inject(LRU_CACHE_OPTIONS)); 18 | } 19 | -------------------------------------------------------------------------------- /libs/ngx-ssr/platform/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ngx-ssr/platform", 3 | "version": "0.0.0-development", 4 | "keywords": [ 5 | "angular", 6 | "angulae ssr", 7 | "universal", 8 | "angular/universal", 9 | "ssr", 10 | "platform", 11 | "is-server", 12 | "is-browser" 13 | ], 14 | "peerDependencies": { 15 | "@angular/common": ">=12.0.0", 16 | "@angular/core": ">=12.0.0" 17 | }, 18 | "dependencies": { 19 | "tslib": "^2.0.0" 20 | }, 21 | "publishConfig": { 22 | "access": "public" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async () => { 6 | await TestBed.configureTestingModule({ 7 | declarations: [AppComponent], 8 | }).compileComponents(); 9 | }); 10 | 11 | xit('should create the app', () => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.componentInstance; 14 | expect(app).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/tokens.ts: -------------------------------------------------------------------------------- 1 | import { inject, InjectionToken } from '@angular/core'; 2 | import { ApiOptions } from './interfaces/api-options'; 3 | 4 | export const API_OPTIONS = new InjectionToken('RaM API Options', { 5 | providedIn: 'root', 6 | factory() { 7 | return { 8 | baseUrl: 'https://rickandmortyapi.com/api/', 9 | }; 10 | }, 11 | }); 12 | 13 | export const BASE_URL = new InjectionToken('Base RaM API url', { 14 | providedIn: 'root', 15 | factory() { 16 | return inject(API_OPTIONS).baseUrl; 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /libs/ngx-ssr/cache/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"] 11 | }, 12 | "angularCompilerOptions": { 13 | "skipTemplateCodegen": true, 14 | "strictMetadataEmit": true, 15 | "enableResourceInlining": true 16 | }, 17 | "exclude": [ 18 | "test-setup.ts", "**/*.spec.ts"], 19 | "include": ["**/*.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /libs/ngx-ssr/platform/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"] 11 | }, 12 | "angularCompilerOptions": { 13 | "skipTemplateCodegen": true, 14 | "strictMetadataEmit": true, 15 | "enableResourceInlining": true 16 | }, 17 | "exclude": ["src/test-setup.ts", "**/*.spec.ts"], 18 | "include": ["**/*.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /libs/ngx-ssr/timeout/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"] 11 | }, 12 | "angularCompilerOptions": { 13 | "skipTemplateCodegen": true, 14 | "strictMetadataEmit": true, 15 | "enableResourceInlining": true 16 | }, 17 | "exclude": ["src/test-setup.ts", "**/*.spec.ts"], 18 | "include": ["**/*.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"] 11 | }, 12 | "angularCompilerOptions": { 13 | "skipTemplateCodegen": true, 14 | "strictMetadataEmit": true, 15 | "enableResourceInlining": true 16 | }, 17 | "exclude": ["src/test-setup.ts", "**/*.spec.ts"], 18 | "include": ["**/*.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /libs/rickandmorty/utils/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"] 11 | }, 12 | "angularCompilerOptions": { 13 | "skipTemplateCodegen": true, 14 | "strictMetadataEmit": true, 15 | "enableResourceInlining": true 16 | }, 17 | "exclude": ["src/test-setup.ts", "**/*.spec.ts"], 18 | "include": ["**/*.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/interfaces/raw-character.ts: -------------------------------------------------------------------------------- 1 | import { CharacterGender } from '../enums/character-gender'; 2 | import { CharacterStatus } from '../enums/character-status'; 3 | 4 | export interface RawCharacter { 5 | id: number; 6 | name: string; 7 | status: CharacterStatus; 8 | species: string; 9 | type: string; 10 | gender: CharacterGender; 11 | origin: { 12 | name: string; 13 | url: string; 14 | }; 15 | location: { 16 | name: string; 17 | url: string; 18 | }; 19 | image: string; 20 | episode: string[]; 21 | url: string; 22 | created: string; 23 | } 24 | -------------------------------------------------------------------------------- /libs/ngx-ssr/platform/src/lib/tokens.ts: -------------------------------------------------------------------------------- 1 | import { inject, InjectionToken, PLATFORM_ID } from '@angular/core'; 2 | import { isPlatformBrowser, isPlatformServer } from '@angular/common'; 3 | 4 | export const IS_SERVER_PLATFORM = new InjectionToken('Is server?', { 5 | factory() { 6 | const platform = inject(PLATFORM_ID); 7 | 8 | return isPlatformServer(platform); 9 | }, 10 | }); 11 | 12 | export const IS_BROWSER_PLATFORM = new InjectionToken('Is browser?', { 13 | factory() { 14 | const platform = inject(PLATFORM_ID); 15 | 16 | return isPlatformBrowser(platform); 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/api.module.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders, NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { API_OPTIONS } from './tokens'; 4 | import { ApiOptions } from './interfaces/api-options'; 5 | 6 | @NgModule({ 7 | imports: [CommonModule], 8 | }) 9 | export class ApiModule { 10 | public static configure(options: ApiOptions): ModuleWithProviders { 11 | return { 12 | ngModule: ApiModule, 13 | providers: [ 14 | { 15 | provide: API_OPTIONS, 16 | useValue: options, 17 | }, 18 | ], 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/interfaces/response.ts: -------------------------------------------------------------------------------- 1 | import { Pagination } from './pagination'; 2 | import { RawResponse } from './raw-response'; 3 | import { Filter } from './filter'; 4 | 5 | export class Response { 6 | info: Pagination; 7 | results: T[]; 8 | 9 | public static fromJson( 10 | { info, results }: RawResponse, 11 | modelFactory: (rawModel: any) => T 12 | ): Response { 13 | const response = new Response(); 14 | 15 | response.info = Pagination.fromJson(info); 16 | response.results = results.map(modelFactory); 17 | 18 | return response; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /libs/ngx-ssr/platform/src/lib/if-is-server.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, 3 | Inject, 4 | TemplateRef, 5 | ViewContainerRef, 6 | } from '@angular/core'; 7 | import { IS_SERVER_PLATFORM } from './tokens'; 8 | 9 | @Directive({ 10 | // eslint-disable-next-line @angular-eslint/directive-selector 11 | selector: '[ifIsServer]', 12 | }) 13 | export class IfIsServerDirective { 14 | constructor( 15 | @Inject(IS_SERVER_PLATFORM) isServer: boolean, 16 | templateRef: TemplateRef, 17 | viewContainer: ViewContainerRef 18 | ) { 19 | if (isServer) { 20 | viewContainer.createEmbeddedView(templateRef); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /libs/ngx-ssr/platform/src/lib/if-is-browser.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, 3 | Inject, 4 | TemplateRef, 5 | ViewContainerRef, 6 | } from '@angular/core'; 7 | import { IS_BROWSER_PLATFORM } from './tokens'; 8 | 9 | @Directive({ 10 | // eslint-disable-next-line @angular-eslint/directive-selector 11 | selector: '[ifIsBrowser]', 12 | }) 13 | export class IfIsBrowserDirective { 14 | constructor( 15 | @Inject(IS_BROWSER_PLATFORM) isBrowser: boolean, 16 | templateRef: TemplateRef, 17 | viewContainer: ViewContainerRef 18 | ) { 19 | if (isBrowser) { 20 | viewContainer.createEmbeddedView(templateRef); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # ================================================================================== 2 | # ================================================================================== 3 | # ngx-ssr codeowners 4 | # ================================================================================== 5 | # ================================================================================== 6 | # 7 | # Configuration of code ownership and review approvals for the angular/angular repo. 8 | # 9 | # More info: https://help.github.com/articles/about-codeowners/ 10 | # 11 | 12 | * @IKatsuba 13 | # will be requested for review when someone opens a pull request 14 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/episode/episode-list/episode-list.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; 3 | import { ApiService, Episode, EpisodeFilter, Response } from 'ngx-rickandmorty'; 4 | import { Observable } from 'rxjs'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class EpisodeListResolver 10 | implements Resolve> { 11 | constructor(private api: ApiService) {} 12 | 13 | resolve( 14 | route: ActivatedRouteSnapshot 15 | ): Observable> { 16 | return this.api.getEpisode(route.queryParams); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/rickandmorty-e2e/src/support/index.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/app.server.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { 3 | ServerModule, 4 | ServerTransferStateModule, 5 | } from '@angular/platform-server'; 6 | import { UniversalModule } from '@ng-web-apis/universal'; 7 | 8 | import { AppModule } from './app.module'; 9 | import { AppComponent } from './app.component'; 10 | import { NgxSsrTimeoutModule } from '@ngx-ssr/timeout'; 11 | 12 | @NgModule({ 13 | imports: [ 14 | UniversalModule, 15 | AppModule, 16 | ServerModule, 17 | ServerTransferStateModule, 18 | NgxSsrTimeoutModule.forRoot({ timeout: 500 }), 19 | ], 20 | bootstrap: [AppComponent], 21 | }) 22 | export class AppServerModule {} 23 | -------------------------------------------------------------------------------- /libs/ngx-ssr/timeout/README.md: -------------------------------------------------------------------------------- 1 | 2 | Install package 3 | 4 | ```bash 5 | npm i @ngx-ssr/timeout 6 | ``` 7 | 8 | Use `NgxSsrTimeoutModule` to set timeouts for all requests 9 | 10 | ```ts 11 | import { NgModule } from '@angular/core'; 12 | import { 13 | ServerModule, 14 | } from '@angular/platform-server'; 15 | import { AppModule } from './app.module'; 16 | import { AppComponent } from './app.component'; 17 | import { NgxSsrTimeoutModule } from '@ngx-ssr/timeout'; 18 | 19 | @NgModule({ 20 | imports: [ 21 | AppModule, 22 | ServerModule, 23 | NgxSsrTimeoutModule.forRoot({ timeout: 500 }), 24 | ], 25 | bootstrap: [AppComponent], 26 | }) 27 | export class AppServerModule { 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/helpers/response-factory.ts: -------------------------------------------------------------------------------- 1 | import { isRawResponse } from './is-response-with-pagination'; 2 | import { Response } from '../interfaces/response'; 3 | import { coerceArray } from './coerce-array'; 4 | 5 | export function responseFactory( 6 | rawResponse: any, 7 | modelFactory: (rawModel: any) => T 8 | ): T[] | Response { 9 | if (!rawResponse) { 10 | return null; 11 | } 12 | 13 | if (isRawResponse(rawResponse)) { 14 | return Response.fromJson(rawResponse, modelFactory); 15 | } 16 | 17 | if (Array.isArray(rawResponse)) { 18 | return rawResponse.map(modelFactory); 19 | } 20 | 21 | return coerceArray(modelFactory(rawResponse)); 22 | } 23 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/interfaces/pagination.ts: -------------------------------------------------------------------------------- 1 | import { RawPagination } from './raw-pagination'; 2 | import { extractSearchParamsFromUrl } from '../helpers/extract-search-params-from-url'; 3 | 4 | export class Pagination { 5 | count: number; 6 | pages: number; 7 | next: T; 8 | prev: T; 9 | 10 | public static fromJson({ 11 | next, 12 | prev, 13 | ...common 14 | }: RawPagination): Pagination { 15 | const pagination = new Pagination(); 16 | 17 | Object.assign(pagination, common); 18 | 19 | pagination.next = extractSearchParamsFromUrl(next); 20 | 21 | pagination.prev = extractSearchParamsFromUrl(prev); 22 | 23 | return pagination; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/rickandmorty/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'rickandmorty', 3 | preset: '../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | stringifyContentPathRegex: '\\.(html|svg)$', 8 | 9 | tsconfig: '/tsconfig.spec.json', 10 | }, 11 | }, 12 | coverageDirectory: '../../coverage/apps/rickandmorty', 13 | snapshotSerializers: [ 14 | 'jest-preset-angular/build/serializers/no-ng-attributes', 15 | 'jest-preset-angular/build/serializers/ng-snapshot', 16 | 'jest-preset-angular/build/serializers/html-comment', 17 | ], 18 | transform: { '^.+\\.(ts|js|html)$': 'jest-preset-angular' }, 19 | }; 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | 41 | # Firebase 42 | .firebase 43 | -------------------------------------------------------------------------------- /libs/ngx-ssr/cache/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'ngx-ssr-cache', 3 | preset: '../../../jest.preset.js', 4 | setupFilesAfterEnv: ['/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | stringifyContentPathRegex: '\\.(html|svg)$', 8 | 9 | tsconfig: '/tsconfig.spec.json', 10 | }, 11 | }, 12 | coverageDirectory: '../../../coverage/libs/ngx-ssr/cache', 13 | snapshotSerializers: [ 14 | 'jest-preset-angular/build/serializers/no-ng-attributes', 15 | 'jest-preset-angular/build/serializers/ng-snapshot', 16 | 'jest-preset-angular/build/serializers/html-comment', 17 | ], 18 | transform: { '^.+\\.(ts|js|html)$': 'jest-preset-angular' }, 19 | }; 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: IKatsuba 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /libs/ngx-ssr/platform/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'ngx-ssr-platform', 3 | preset: '../../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | stringifyContentPathRegex: '\\.(html|svg)$', 8 | 9 | tsconfig: '/tsconfig.spec.json', 10 | }, 11 | }, 12 | coverageDirectory: '../../../coverage/libs/ngx-ssr/platform', 13 | snapshotSerializers: [ 14 | 'jest-preset-angular/build/serializers/no-ng-attributes', 15 | 'jest-preset-angular/build/serializers/ng-snapshot', 16 | 'jest-preset-angular/build/serializers/html-comment', 17 | ], 18 | transform: { '^.+\\.(ts|js|html)$': 'jest-preset-angular' }, 19 | }; 20 | -------------------------------------------------------------------------------- /libs/ngx-ssr/timeout/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'ngx-ssr-timeout', 3 | preset: '../../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | stringifyContentPathRegex: '\\.(html|svg)$', 8 | 9 | tsconfig: '/tsconfig.spec.json', 10 | }, 11 | }, 12 | coverageDirectory: '../../../coverage/libs/ngx-ssr/timeout', 13 | snapshotSerializers: [ 14 | 'jest-preset-angular/build/serializers/no-ng-attributes', 15 | 'jest-preset-angular/build/serializers/ng-snapshot', 16 | 'jest-preset-angular/build/serializers/html-comment', 17 | ], 18 | transform: { '^.+\\.(ts|js|html)$': 'jest-preset-angular' }, 19 | }; 20 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'rickandmorty-api', 3 | preset: '../../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | stringifyContentPathRegex: '\\.(html|svg)$', 8 | 9 | tsconfig: '/tsconfig.spec.json', 10 | }, 11 | }, 12 | coverageDirectory: '../../../coverage/libs/rickandmorty/api', 13 | snapshotSerializers: [ 14 | 'jest-preset-angular/build/serializers/no-ng-attributes', 15 | 'jest-preset-angular/build/serializers/ng-snapshot', 16 | 'jest-preset-angular/build/serializers/html-comment', 17 | ], 18 | transform: { '^.+\\.(ts|js|html)$': 'jest-preset-angular' }, 19 | }; 20 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/location/location-list/location-list.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; 3 | import { 4 | ApiService, 5 | Location, 6 | LocationFilter, 7 | Response, 8 | } from 'ngx-rickandmorty'; 9 | import { Observable } from 'rxjs'; 10 | 11 | @Injectable({ 12 | providedIn: 'root', 13 | }) 14 | export class LocationListResolver 15 | implements Resolve> { 16 | constructor(private api: ApiService) {} 17 | 18 | resolve( 19 | route: ActivatedRouteSnapshot 20 | ): Observable> { 21 | return this.api.getLocation(route.queryParams); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /libs/rickandmorty/utils/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'rickandmorty-utils', 3 | preset: '../../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | stringifyContentPathRegex: '\\.(html|svg)$', 8 | 9 | tsconfig: '/tsconfig.spec.json', 10 | }, 11 | }, 12 | coverageDirectory: '../../../coverage/libs/rickandmorty/utils', 13 | snapshotSerializers: [ 14 | 'jest-preset-angular/build/serializers/no-ng-attributes', 15 | 'jest-preset-angular/build/serializers/ng-snapshot', 16 | 'jest-preset-angular/build/serializers/html-comment', 17 | ], 18 | transform: { '^.+\\.(ts|js|html)$': 'jest-preset-angular' }, 19 | }; 20 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/character/character-list/character-list.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; 3 | import { 4 | ApiService, 5 | Character, 6 | CharacterFilter, 7 | Response, 8 | } from 'ngx-rickandmorty'; 9 | import { Observable } from 'rxjs'; 10 | 11 | @Injectable({ 12 | providedIn: 'root', 13 | }) 14 | export class CharacterListResolver 15 | implements Resolve> { 16 | constructor(private api: ApiService) {} 17 | 18 | resolve( 19 | route: ActivatedRouteSnapshot 20 | ): Observable> { 21 | return this.api.getCharacter(route.queryParams); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/interfaces/episode.ts: -------------------------------------------------------------------------------- 1 | import { RawEpisode } from './raw-episode'; 2 | import { extractIdsFromUrl } from '../helpers/extract-ids-from-url'; 3 | 4 | export class Episode { 5 | id: number; 6 | name: string; 7 | air_date: string; 8 | episode: string; 9 | characters: number[]; 10 | url: string; 11 | created: Date; 12 | 13 | public static fromJson({ 14 | characters, 15 | created, 16 | ...common 17 | }: RawEpisode): Episode { 18 | const episode = new Episode(); 19 | 20 | Object.assign(episode, common); 21 | 22 | episode.characters = characters.map(extractIdsFromUrl).map(([id]) => id); 23 | 24 | episode.created = new Date(created); 25 | 26 | return episode; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/interfaces/location.ts: -------------------------------------------------------------------------------- 1 | import { RawLocation } from './raw-location'; 2 | import { extractIdsFromUrl } from '../helpers/extract-ids-from-url'; 3 | 4 | export class Location { 5 | id: number; 6 | name: string; 7 | type: string; 8 | dimension: string; 9 | residents: number[]; 10 | url: string; 11 | created: Date; 12 | 13 | public static fromJson({ 14 | created, 15 | residents, 16 | ...common 17 | }: RawLocation): Location { 18 | const location = new Location(); 19 | 20 | Object.assign(location, common); 21 | 22 | location.residents = residents.map(extractIdsFromUrl).map(([id]) => id); 23 | 24 | location.created = new Date(created); 25 | 26 | return location; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /apps/rickandmorty/ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/@angular/service-worker/config/schema.json", 3 | "index": "/index.html", 4 | "assetGroups": [ 5 | { 6 | "name": "app", 7 | "installMode": "prefetch", 8 | "resources": { 9 | "files": [ 10 | "/favicon.ico", 11 | "/index.html", 12 | "/manifest.webmanifest", 13 | "/*.css", 14 | "/*.js" 15 | ] 16 | } 17 | }, 18 | { 19 | "name": "assets", 20 | "installMode": "lazy", 21 | "updateMode": "prefetch", 22 | "resources": { 23 | "files": [ 24 | "/assets/**", 25 | "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)" 26 | ] 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/episode/episode/episode.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { EpisodeComponent } from './episode.component'; 4 | 5 | describe('EpisodeComponent', () => { 6 | let component: EpisodeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [EpisodeComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(EpisodeComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | xit('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/rickandmorty/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 18 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/location/location/location.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LocationComponent } from './location.component'; 4 | 5 | describe('LocationComponent', () => { 6 | let component: LocationComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [LocationComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(LocationComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | xit('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/character/character/character.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CharacterComponent } from './character.component'; 4 | 5 | describe('CharacterComponent', () => { 6 | let component: CharacterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [CharacterComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(CharacterComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | xit('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/episode/episode-list/episode-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { EpisodeListComponent } from './episode-list.component'; 4 | 5 | describe('EpisodeComponent', () => { 6 | let component: EpisodeListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [EpisodeListComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(EpisodeListComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | xit('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/location/location-list/location-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LocationListComponent } from './location-list.component'; 4 | 5 | describe('LocationComponent', () => { 6 | let component: LocationListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [LocationListComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(LocationListComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | xit('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: IKatsuba 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. macOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/character/character-list/character-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CharacterListComponent } from './character-list.component'; 4 | 5 | describe('CharacterComponent', () => { 6 | let component: CharacterListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [CharacterListComponent], 12 | }).compileComponents(); 13 | }); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(CharacterListComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | xit('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": [ 3 | { 4 | "target": "rickandmorty", 5 | "public": "dist/apps/rickandmorty/dist/apps/rickandmorty/browser", 6 | "ignore": [ 7 | "**/.*" 8 | ], 9 | "headers": [ 10 | { 11 | "source": "*.[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f].+(css|js)", 12 | "headers": [ 13 | { 14 | "key": "Cache-Control", 15 | "value": "public,max-age=31536000,immutable" 16 | } 17 | ] 18 | } 19 | ], 20 | "rewrites": [ 21 | { 22 | "source": "**", 23 | "function": "ssr" 24 | } 25 | ] 26 | } 27 | ], 28 | "functions": { 29 | "source": "dist/apps/rickandmorty" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /libs/ngx-ssr/platform/README.md: -------------------------------------------------------------------------------- 1 | # @ngx-ssr/platform 2 | 3 | Install package 4 | 5 | ```bash 6 | npm i @ngx-ssr/platform 7 | ``` 8 | 9 | To determine the platform, use the tokens `IS_SERVER_PLATFORM` and `IS_BROWSER_PLATFORM` 10 | 11 | ```ts 12 | @Directive({ 13 | selector: '[some-directive]', 14 | }) 15 | export class SomeDirective { 16 | constructor( 17 | @Inject(IS_SERVER_PLATFORM) isServer: boolean, 18 | ) { 19 | if (isServer) { 20 | viewContainer.createEmbeddedView(templateRef); 21 | } 22 | } 23 | } 24 | ``` 25 | 26 | Use the `ifIsServer` and ` ifIsBrowser` structural directives in your template for rendering contents depending on the 27 | platform: 28 | 29 | ```ts 30 | @Component({ 31 | selector: 'ram-root', 32 | template: '', 33 | styleUrls: ['./app.component.less'], 34 | }) 35 | export class AppComponent { 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nrwl/nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nrwl/nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": [], 14 | "depConstraints": [ 15 | { 16 | "sourceTag": "*", 17 | "onlyDependOnLibsWithTags": ["*"] 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | }, 24 | { 25 | "files": ["*.ts", "*.tsx"], 26 | "extends": ["plugin:@nrwl/nx/typescript"], 27 | "rules": {} 28 | }, 29 | { 30 | "files": ["*.js", "*.jsx"], 31 | "extends": ["plugin:@nrwl/nx/javascript"], 32 | "rules": {} 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/episode/episode.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { EpisodeRoutingModule } from './episode-routing.module'; 5 | import { EpisodeListComponent } from './episode-list/episode-list.component'; 6 | import { EpisodeComponent } from './episode/episode.component'; 7 | import { ReactiveFormsModule } from '@angular/forms'; 8 | import { TuiInputModule } from '@taiga-ui/kit'; 9 | import { 10 | TuiButtonModule, 11 | TuiDataListModule, 12 | TuiLinkModule, 13 | } from '@taiga-ui/core'; 14 | 15 | @NgModule({ 16 | declarations: [EpisodeListComponent, EpisodeComponent], 17 | imports: [ 18 | CommonModule, 19 | EpisodeRoutingModule, 20 | ReactiveFormsModule, 21 | TuiInputModule, 22 | TuiButtonModule, 23 | TuiDataListModule, 24 | TuiLinkModule, 25 | ], 26 | }) 27 | export class EpisodeModule {} 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - next 8 | - beta 9 | 10 | jobs: 11 | release: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2.3.4 15 | - name: Use Node.js 14.x 16 | uses: actions/setup-node@v2.1.5 17 | with: 18 | node-version: 14.x 19 | - name: npm install 20 | run: | 21 | npm ci 22 | env: 23 | CI: true 24 | CYPRESS_INSTALL_BINARY: 0 25 | - name: Build 26 | run: | 27 | npm run build 28 | env: 29 | CI: true 30 | - name: Release 31 | run: | 32 | npm run deploy 33 | env: 34 | CI: true 35 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} 37 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 38 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/location/location.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { LocationRoutingModule } from './location-routing.module'; 5 | import { LocationListComponent } from './location-list/location-list.component'; 6 | import { LocationComponent } from './location/location.component'; 7 | import { ReactiveFormsModule } from '@angular/forms'; 8 | import { TuiInputModule } from '@taiga-ui/kit'; 9 | import { 10 | TuiButtonModule, 11 | TuiDataListModule, 12 | TuiLinkModule, 13 | } from '@taiga-ui/core'; 14 | 15 | @NgModule({ 16 | declarations: [LocationListComponent, LocationComponent], 17 | imports: [ 18 | CommonModule, 19 | LocationRoutingModule, 20 | ReactiveFormsModule, 21 | TuiInputModule, 22 | TuiButtonModule, 23 | TuiDataListModule, 24 | TuiLinkModule, 25 | ], 26 | }) 27 | export class LocationModule {} 28 | -------------------------------------------------------------------------------- /apps/rickandmorty-e2e/src/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | const { preprocessTypescript } = require('@nrwl/cypress/plugins/preprocessor'); 15 | 16 | module.exports = (on, config) => { 17 | // `on` is used to hook into various events Cypress emits 18 | // `config` is the resolved Cypress config 19 | 20 | // Preprocess Typescript file using Nx helper 21 | on('file:preprocessor', preprocessTypescript(config)); 22 | }; 23 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/episode/episode/episode.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { ApiService } from 'ngx-rickandmorty'; 4 | import { ActivatedRoute } from '@angular/router'; 5 | import { map, tap } from 'rxjs/operators'; 6 | import { ExtendedEpisode } from '@ngx-ssr/rickandmorty/utils'; 7 | import { Title } from '@angular/platform-browser'; 8 | 9 | @Component({ 10 | selector: 'ram-episode', 11 | templateUrl: './episode.component.html', 12 | styleUrls: ['./episode.component.scss'], 13 | }) 14 | export class EpisodeComponent { 15 | episode$: Observable; 16 | 17 | constructor(apiService: ApiService, route: ActivatedRoute, title: Title) { 18 | this.episode$ = route.data.pipe( 19 | map((data) => data.episode), 20 | tap((episode) => { 21 | title.setTitle(`${episode.name} | Episodes | Rick and Morty`); 22 | }) 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | export const routes: Routes = [ 5 | { 6 | path: 'episode', 7 | loadChildren: () => 8 | import('./episode/episode.module').then((m) => m.EpisodeModule), 9 | }, 10 | { 11 | path: 'location', 12 | loadChildren: () => 13 | import('./location/location.module').then((m) => m.LocationModule), 14 | }, 15 | { 16 | path: 'character', 17 | loadChildren: () => 18 | import('./character/character.module').then((m) => m.CharacterModule), 19 | }, 20 | { 21 | path: '**', 22 | redirectTo: 'character', 23 | }, 24 | ]; 25 | 26 | @NgModule({ 27 | imports: [ 28 | RouterModule.forRoot(routes, { 29 | initialNavigation: 'enabled', 30 | relativeLinkResolution: 'legacy' 31 | }), 32 | ], 33 | exports: [RouterModule], 34 | }) 35 | export class AppRoutingModule {} 36 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/episode/episode-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { EpisodeListComponent } from './episode-list/episode-list.component'; 5 | import { EpisodeComponent } from './episode/episode.component'; 6 | import { EpisodeListResolver } from './episode-list/episode-list.resolver'; 7 | import { EpisodeResolver } from './episode/episode.resolver'; 8 | 9 | const routes: Routes = [ 10 | { 11 | path: '', 12 | component: EpisodeListComponent, 13 | runGuardsAndResolvers: 'paramsOrQueryParamsChange', 14 | resolve: { episodeResponse: EpisodeListResolver }, 15 | }, 16 | { 17 | path: ':id', 18 | component: EpisodeComponent, 19 | resolve: { episode: EpisodeResolver }, 20 | }, 21 | ]; 22 | 23 | @NgModule({ 24 | imports: [RouterModule.forChild(routes)], 25 | exports: [RouterModule], 26 | }) 27 | export class EpisodeRoutingModule {} 28 | -------------------------------------------------------------------------------- /apps/rickandmorty/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": "ram", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "ram", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nrwl/nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/location/location/location.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ApiService } from 'ngx-rickandmorty'; 3 | import { ActivatedRoute } from '@angular/router'; 4 | import { map, tap } from 'rxjs/operators'; 5 | import { Observable } from 'rxjs'; 6 | import { ExtendedLocation } from '@ngx-ssr/rickandmorty/utils'; 7 | import { Title } from '@angular/platform-browser'; 8 | 9 | @Component({ 10 | selector: 'ram-location', 11 | templateUrl: './location.component.html', 12 | styleUrls: ['./location.component.scss'], 13 | }) 14 | export class LocationComponent { 15 | location$: Observable; 16 | 17 | constructor(apiService: ApiService, route: ActivatedRoute, title: Title) { 18 | this.location$ = route.data.pipe( 19 | map((data) => data.location), 20 | tap((location) => { 21 | title.setTitle(`${location.name} | Locations | Rick and Morty`); 22 | }) 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /libs/ngx-ssr/timeout/src/lib/ngx-ssr-timeout.module.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders, NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { HTTP_INTERCEPTORS } from '@angular/common/http'; 4 | import { 5 | TIMEOUT_OPTIONS, 6 | TimeoutInterceptor, 7 | TimeoutOptions, 8 | } from './timeout.interceptor'; 9 | 10 | @NgModule({ 11 | imports: [CommonModule], 12 | }) 13 | export class NgxSsrTimeoutModule { 14 | static forRoot( 15 | options?: TimeoutOptions 16 | ): ModuleWithProviders { 17 | return { 18 | ngModule: NgxSsrTimeoutModule, 19 | providers: [ 20 | { 21 | provide: HTTP_INTERCEPTORS, 22 | useClass: TimeoutInterceptor, 23 | multi: true, 24 | }, 25 | options 26 | ? { 27 | provide: TIMEOUT_OPTIONS, 28 | useValue: options, 29 | } 30 | : [], 31 | ], 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /libs/ngx-ssr/cache/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": "ngx-ssr", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "ngx-ssr", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nrwl/nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "lib": ["es2017", "dom"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "@ngx-ssr/cache": ["libs/ngx-ssr/cache/src/index.ts"], 19 | "@ngx-ssr/cache/*": ["libs/ngx-ssr/cache/*"], 20 | "@ngx-ssr/platform": ["libs/ngx-ssr/platform/src/index.ts"], 21 | "@ngx-ssr/rickandmorty/utils": ["libs/rickandmorty/utils/src/index.ts"], 22 | "@ngx-ssr/timeout": ["libs/ngx-ssr/timeout/src/index.ts"], 23 | "ngx-rickandmorty": ["libs/rickandmorty/api/src/index.ts"] 24 | } 25 | }, 26 | "exclude": ["node_modules", "tmp"] 27 | } 28 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/location/location-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { LocationListComponent } from './location-list/location-list.component'; 5 | import { LocationComponent } from './location/location.component'; 6 | import { LocationResolver } from './location/location.resolver'; 7 | import { LocationListResolver } from './location-list/location-list.resolver'; 8 | 9 | const routes: Routes = [ 10 | { 11 | path: '', 12 | component: LocationListComponent, 13 | runGuardsAndResolvers: 'paramsOrQueryParamsChange', 14 | resolve: { locationResponse: LocationListResolver }, 15 | }, 16 | { 17 | path: ':id', 18 | component: LocationComponent, 19 | resolve: { location: LocationResolver }, 20 | }, 21 | ]; 22 | 23 | @NgModule({ 24 | imports: [RouterModule.forChild(routes)], 25 | exports: [RouterModule], 26 | }) 27 | export class LocationRoutingModule {} 28 | -------------------------------------------------------------------------------- /libs/ngx-ssr/platform/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": "ngxSsr", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "ngx-ssr", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nrwl/nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /libs/ngx-ssr/timeout/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": "ngx-ssr", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "ngx-ssr", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nrwl/nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": "ngx-ssr", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "ngx-ssr", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nrwl/nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /libs/rickandmorty/utils/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": "ngx-ssr", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "ngx-ssr", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nrwl/nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/character/character/character.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ApiService } from 'ngx-rickandmorty'; 3 | import { ActivatedRoute } from '@angular/router'; 4 | import { map, tap } from 'rxjs/operators'; 5 | import { Observable } from 'rxjs'; 6 | import { ExtendedCharacter } from '@ngx-ssr/rickandmorty/utils'; 7 | import { Title } from '@angular/platform-browser'; 8 | 9 | @Component({ 10 | selector: 'ram-character', 11 | templateUrl: './character.component.html', 12 | styleUrls: ['./character.component.scss'], 13 | }) 14 | export class CharacterComponent { 15 | public character$: Observable; 16 | 17 | constructor(apiService: ApiService, route: ActivatedRoute, title: Title) { 18 | this.character$ = route.data.pipe( 19 | map((data) => data.character), 20 | tap((character) => { 21 | title.setTitle(`${character.name} | Characters | Rick and Morty`); 22 | }) 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/location/location/location.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; 3 | import { ApiService } from 'ngx-rickandmorty'; 4 | import { ExtendedLocation } from '@ngx-ssr/rickandmorty/utils'; 5 | import { Observable } from 'rxjs'; 6 | import { map, switchMap } from 'rxjs/operators'; 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class LocationResolver implements Resolve { 12 | constructor(private apiService: ApiService) {} 13 | 14 | resolve(route: ActivatedRouteSnapshot): Observable { 15 | return this.apiService 16 | .getLocation(parseInt(route.paramMap.get('id'), 10)) 17 | .pipe( 18 | switchMap(([location]) => 19 | this.apiService 20 | .getCharacter(location.residents) 21 | .pipe(map((residents) => ({ ...location, residents }))) 22 | ) 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/character/character-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { CharacterListComponent } from './character-list/character-list.component'; 5 | import { CharacterComponent } from './character/character.component'; 6 | import { CharacterResolver } from './character/character.resolver'; 7 | import { CharacterListResolver } from './character-list/character-list.resolver'; 8 | 9 | const routes: Routes = [ 10 | { 11 | path: '', 12 | component: CharacterListComponent, 13 | runGuardsAndResolvers: 'paramsOrQueryParamsChange', 14 | resolve: { characterResponse: CharacterListResolver }, 15 | }, 16 | { 17 | path: ':id', 18 | component: CharacterComponent, 19 | resolve: { character: CharacterResolver }, 20 | }, 21 | ]; 22 | 23 | @NgModule({ 24 | imports: [RouterModule.forChild(routes)], 25 | exports: [RouterModule], 26 | }) 27 | export class CharacterRoutingModule {} 28 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/character/character/character.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; 3 | import { ApiService, Episode } from 'ngx-rickandmorty'; 4 | import { Observable } from 'rxjs'; 5 | import { map, switchMap } from 'rxjs/operators'; 6 | import { ExtendedCharacter } from '@ngx-ssr/rickandmorty/utils'; 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class CharacterResolver implements Resolve { 12 | constructor(private api: ApiService) {} 13 | 14 | resolve(route: ActivatedRouteSnapshot): Observable { 15 | return this.api.getCharacter(parseInt(route.paramMap.get('id'), 10)).pipe( 16 | switchMap(([character]) => 17 | this.api.getEpisode(character.episode).pipe( 18 | map((episode: Episode[]) => ({ 19 | ...character, 20 | episode, 21 | })) 22 | ) 23 | ) 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/enums/character-gender'; 2 | export * from './lib/enums/character-status'; 3 | 4 | export * from './lib/interfaces/api-options'; 5 | export * from './lib/interfaces/character'; 6 | export * from './lib/interfaces/character-filter'; 7 | export * from './lib/interfaces/episode'; 8 | export * from './lib/interfaces/episode-filter'; 9 | export * from './lib/interfaces/filter'; 10 | export * from './lib/interfaces/location'; 11 | export * from './lib/interfaces/location-filter'; 12 | export * from './lib/interfaces/pagination'; 13 | export * from './lib/interfaces/ping'; 14 | export * from './lib/interfaces/raw-character'; 15 | export * from './lib/interfaces/raw-episode'; 16 | export * from './lib/interfaces/raw-location'; 17 | export * from './lib/interfaces/raw-pagination'; 18 | export * from './lib/interfaces/raw-response'; 19 | export * from './lib/interfaces/response'; 20 | 21 | export * from './lib/api.module'; 22 | export * from './lib/api.service'; 23 | export * from './lib/tokens'; 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ngx-ssr 2 | 3 | `ngx-ssr` is a set of utilities for working with Angular Universal. 4 | 5 | The project contains: 6 | 7 | 1. Three publishable libraries: 8 | - [@ngx-ssr/cache](./libs/ngx-ssr/cache/README.md) - in-memory implementation of the cache for GET requests and HTML. It is possible to change the 9 | storage. 10 | - [@ngx-ssr/timeout](./libs/ngx-ssr/timeout/README.md) — implementation of timeout for requests 11 | - [@ngx-ssr/platform](./libs/ngx-ssr/platform/README.md) — utilities for convenient work with platform-specific data 12 | 2. One side publishable library: 13 | - `ngx-rickandmorty` 14 | 3. [The Rick and Morty application](https://ng-rickandmorty.web.app/character) based on the Rick and Morty API. The 15 | application build artifact is deployed to Firebase Hosting. Using Firebase Function and Angular Universal, the 16 | application is rendered on the server. 17 | 18 | All developed libraries are used in the application. 19 | 20 | [Taiga UI](https://taiga-ui.dev/) is used as a UI framework. 21 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/episode/episode/episode.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; 3 | import { ApiService } from 'ngx-rickandmorty'; 4 | import { Observable } from 'rxjs'; 5 | import { map, switchMap } from 'rxjs/operators'; 6 | import { ExtendedEpisode } from '@ngx-ssr/rickandmorty/utils'; 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class EpisodeResolver implements Resolve { 12 | constructor(private apiService: ApiService) {} 13 | 14 | resolve( 15 | route: ActivatedRouteSnapshot 16 | ): Observable | Promise | ExtendedEpisode { 17 | return this.apiService 18 | .getEpisode(parseInt(route.paramMap.get('id'), 10)) 19 | .pipe( 20 | switchMap(([episode]) => 21 | this.apiService 22 | .getCharacter(episode.characters) 23 | .pipe(map((characters) => ({ ...episode, characters }))) 24 | ) 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Characters 10 | Episodes 11 | Locations 12 | 13 | 14 |
15 |
16 |
19 | 20 |
21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | -------------------------------------------------------------------------------- /libs/ngx-ssr/platform/src/lib/if-is-server.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { IfIsServerDirective } from './if-is-server.directive'; 2 | import { render, screen } from '@testing-library/angular'; 3 | import { IS_SERVER_PLATFORM } from './tokens'; 4 | 5 | describe('IsServerDirective', () => { 6 | it('should create an element', async () => { 7 | await render(IfIsServerDirective, { 8 | template: '
Some text
', 9 | providers: [ 10 | { 11 | provide: IS_SERVER_PLATFORM, 12 | useValue: true, 13 | }, 14 | ], 15 | }); 16 | 17 | expect(screen.getByText('Some text').outerHTML).toStrictEqual( 18 | '
Some text
' 19 | ); 20 | }); 21 | 22 | it('should not create an element', async () => { 23 | await render(IfIsServerDirective, { 24 | template: '
Some text
', 25 | providers: [ 26 | { 27 | provide: IS_SERVER_PLATFORM, 28 | useValue: false, 29 | }, 30 | ], 31 | }); 32 | 33 | expect(() => screen.getByText('Some text')).toThrowError(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /libs/ngx-ssr/platform/src/lib/if-is-browser.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { IfIsBrowserDirective } from './if-is-browser.directive'; 2 | import { render, screen } from '@testing-library/angular'; 3 | import { IS_BROWSER_PLATFORM } from './tokens'; 4 | 5 | describe('IsBrowserDirective', () => { 6 | it('should create an element', async () => { 7 | await render(IfIsBrowserDirective, { 8 | template: '
Some text
', 9 | providers: [ 10 | { 11 | provide: IS_BROWSER_PLATFORM, 12 | useValue: true, 13 | }, 14 | ], 15 | }); 16 | 17 | expect(screen.getByText('Some text').outerHTML).toStrictEqual( 18 | '
Some text
' 19 | ); 20 | }); 21 | 22 | it('should not create an element', async () => { 23 | await render(IfIsBrowserDirective, { 24 | template: '
Some text
', 25 | providers: [ 26 | { 27 | provide: IS_BROWSER_PLATFORM, 28 | useValue: false, 29 | }, 30 | ], 31 | }); 32 | 33 | expect(() => screen.getByText('Some text')).toThrowError(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Igor Katsuba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | ## PR Checklist 2 | Please check if your PR fulfills the following requirements: 3 | 4 | - [ ] The commit message follows [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0-beta.4/) 5 | - [ ] Tests for the changes have been added (for bug fixes / features) 6 | - [ ] Docs have been added / updated (for bug fixes / features) 7 | 8 | 9 | ## PR Type 10 | What kind of change does this PR introduce? 11 | 12 | 13 | 14 | - [ ] Bugfix 15 | - [ ] Feature 16 | - [ ] Refactoring (no functional changes, no api changes) 17 | - [ ] Other... Please describe: 18 | 19 | 20 | ## What is the current behavior? 21 | 22 | 23 | Issue Number: N/A 24 | 25 | 26 | ## What is the new behavior? 27 | 28 | 29 | ## Does this PR introduce a breaking change? 30 | 31 | - [ ] Yes 32 | - [ ] No 33 | 34 | 35 | 36 | 37 | 38 | ## Other information 39 | -------------------------------------------------------------------------------- /libs/ngx-ssr/cache/src/lib/ngx-ssr-cache.module.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders, NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { CacheController } from './cache-controller'; 4 | import { LRU_CACHE_OPTIONS, lruCacheFactory } from './lru-cache.factory'; 5 | import { LRUCacheOptions } from './lru-cache'; 6 | import { HTTP_INTERCEPTORS } from '@angular/common/http'; 7 | import { CacheInterceptor } from './cache.interceptor'; 8 | 9 | @NgModule({ 10 | imports: [CommonModule], 11 | }) 12 | export class NgxSsrCacheModule { 13 | static configLruCache( 14 | options?: LRUCacheOptions 15 | ): ModuleWithProviders { 16 | return { 17 | ngModule: NgxSsrCacheModule, 18 | providers: [ 19 | { 20 | provide: CacheController, 21 | useFactory: lruCacheFactory, 22 | }, 23 | { 24 | provide: HTTP_INTERCEPTORS, 25 | useClass: CacheInterceptor, 26 | multi: true, 27 | }, 28 | options 29 | ? { 30 | provide: LRU_CACHE_OPTIONS, 31 | useValue: options, 32 | } 33 | : [], 34 | ], 35 | }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /libs/ngx-ssr/cache/README.md: -------------------------------------------------------------------------------- 1 | # @ngx-ssr/cache 2 | 3 | Install package 4 | 5 | ```bash 6 | npm i @ngx-ssr/cache 7 | ``` 8 | 9 | Import the `NgxSsrCacheModule` module to cache all GET requests 10 | 11 | ```ts 12 | import { BrowserModule } from '@angular/platform-browser'; 13 | import { NgModule } from '@angular/core'; 14 | import { AppComponent } from './app.component'; 15 | import { NgxSsrCacheModule } from '@ngx-ssr/cache'; 16 | import { environment } from '../environments/environment'; 17 | 18 | @NgModule({ 19 | declarations: [AppComponent], 20 | imports: [ 21 | BrowserModule, 22 | NgxSsrCacheModule.configLruCache({ maxAge: 10 * 60_000, maxSize: 50 }), 23 | ], 24 | bootstrap: [AppComponent], 25 | }) 26 | export class AppModule { 27 | } 28 | ``` 29 | 30 | HTML caching is also available for express 31 | 32 | ```ts 33 | import { ngExpressEngine } from '@nguniversal/express-engine'; 34 | import { LRUCache } from '@ngx-ssr/cache'; 35 | import { withCache } from '@ngx-ssr/cache/express'; 36 | 37 | server.engine( 38 | 'html', 39 | withCache( 40 | new LRUCache({ maxAge: 10 * 60_000, maxSize: 100 }), 41 | ngExpressEngine({ 42 | bootstrap: AppServerModule, 43 | }) 44 | ) 45 | ); 46 | ``` 47 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/character/character.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { CharacterRoutingModule } from './character-routing.module'; 5 | import { CharacterListComponent } from './character-list/character-list.component'; 6 | import { CharacterComponent } from './character/character.component'; 7 | import { ReactiveFormsModule } from '@angular/forms'; 8 | import { 9 | TuiAvatarModule, 10 | TuiBadgedContentModule, 11 | TuiInputModule, 12 | TuiIslandModule, 13 | TuiLineClampModule, 14 | TuiSelectModule, 15 | } from '@taiga-ui/kit'; 16 | import { 17 | TuiButtonModule, 18 | TuiDataListModule, 19 | TuiLinkModule, 20 | TuiTextfieldControllerModule, 21 | } from '@taiga-ui/core'; 22 | 23 | @NgModule({ 24 | declarations: [CharacterListComponent, CharacterComponent], 25 | imports: [ 26 | CommonModule, 27 | CharacterRoutingModule, 28 | ReactiveFormsModule, 29 | TuiInputModule, 30 | TuiSelectModule, 31 | TuiButtonModule, 32 | TuiAvatarModule, 33 | TuiLinkModule, 34 | TuiTextfieldControllerModule, 35 | TuiBadgedContentModule, 36 | TuiIslandModule, 37 | TuiLineClampModule, 38 | TuiDataListModule, 39 | ], 40 | }) 41 | export class CharacterModule {} 42 | -------------------------------------------------------------------------------- /apps/rickandmorty-e2e/src/support/commands.ts: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | 11 | // eslint-disable-next-line @typescript-eslint/no-namespace 12 | declare namespace Cypress { 13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 14 | interface Chainable { 15 | login(email: string, password: string): void; 16 | } 17 | } 18 | // 19 | // -- This is a parent command -- 20 | Cypress.Commands.add('login', (email, password) => { 21 | console.log('Custom command example: Login', email, password); 22 | }); 23 | // 24 | // -- This is a child command -- 25 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 26 | // 27 | // 28 | // -- This is a dual command -- 29 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 30 | // 31 | // 32 | // -- This will overwrite an existing command -- 33 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 34 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/episode/episode-list/episode-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Name 6 | Episode 9 |
10 |
11 | 12 |
13 |
14 | Prev 24 | Next 34 |
35 | 36 | 37 | 42 | {{ episode.name }} 43 | 44 | 45 |
46 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/character/character/character.component.html: -------------------------------------------------------------------------------- 1 |
2 | 14 | 15 | 16 | 17 |

{{ character.name }}

18 |

19 | Location: 20 | {{ 21 | character.location.name 22 | }} 23 |

24 |

25 | Origin: 26 | {{ 27 | character.origin.name 28 | }} 29 |

30 |

Type: {{ character.type }}

31 |

Status: {{ character.status }}

32 |

Species: {{ character.species }}

33 |

Gender: {{ character.gender }}

34 |

35 | Episodes: 36 | {{ episode.name }}, 41 | 42 |

43 |
44 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/location/location-list/location-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { Location, LocationFilter, Response } from 'ngx-rickandmorty'; 4 | import { FilterService } from '@ngx-ssr/rickandmorty/utils'; 5 | import { FormGroup } from '@angular/forms'; 6 | import { ActivatedRoute } from '@angular/router'; 7 | import { map } from 'rxjs/operators'; 8 | import { Title } from '@angular/platform-browser'; 9 | import { TuiDestroyService } from '@taiga-ui/cdk'; 10 | 11 | @Component({ 12 | selector: 'ram-location', 13 | templateUrl: './location-list.component.html', 14 | styleUrls: ['./location-list.component.scss'], 15 | providers: [TuiDestroyService, FilterService], 16 | }) 17 | export class LocationListComponent { 18 | readonly locationResponse$: Observable>; 19 | readonly form: FormGroup; 20 | 21 | constructor(filter: FilterService, route: ActivatedRoute, title: Title) { 22 | title.setTitle('Locations | Rick and Morty'); 23 | 24 | this.form = filter.createForm({ 25 | name: [null], 26 | type: [null], 27 | dimension: [null], 28 | }); 29 | 30 | this.locationResponse$ = route.data.pipe( 31 | map((data) => data.locationResponse) 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/episode/episode-list/episode-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { Episode, EpisodeFilter, Response } from 'ngx-rickandmorty'; 4 | import { FormGroup } from '@angular/forms'; 5 | import { FilterService } from '@ngx-ssr/rickandmorty/utils'; 6 | import { ActivatedRoute } from '@angular/router'; 7 | import { map } from 'rxjs/operators'; 8 | import { Title } from '@angular/platform-browser'; 9 | import { TuiDestroyService } from '@taiga-ui/cdk'; 10 | 11 | @Component({ 12 | selector: 'ram-episode', 13 | templateUrl: './episode-list.component.html', 14 | styleUrls: ['./episode-list.component.scss'], 15 | providers: [TuiDestroyService, FilterService], 16 | }) 17 | export class EpisodeListComponent { 18 | readonly episodeResponse$: Observable>; 19 | readonly form: FormGroup; 20 | 21 | constructor( 22 | private filter: FilterService, 23 | private route: ActivatedRoute, 24 | title: Title 25 | ) { 26 | title.setTitle('Episodes | Rick and Morty'); 27 | 28 | this.form = filter.createForm({ 29 | name: [null], 30 | episode: [null], 31 | }); 32 | 33 | this.episodeResponse$ = route.data.pipe( 34 | map((data) => data.episodeResponse) 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | > Thank you for considering contributing to our project. Your help if very welcome! 4 | 5 | When contributing, it's better to first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. 6 | 7 | All members of our community are expected to follow our [Code of Conduct](CODE_OF_CONDUCT.md). Please make sure you are welcoming and friendly in all of our spaces. 8 | 9 | ## Getting started 10 | 11 | In order to make your contribution please make a fork of the repository. After you've pulled the code, follow these steps to kick start the development: 12 | 13 | 1. Run `npm i` to install dependencies 14 | 2. Run `npm start` to launch demo project where you could test your changes 15 | 3. Use following commands to ensure code quality 16 | 17 | ```text 18 | npm run lint 19 | npm run build 20 | npm run test 21 | npm run lint 22 | ``` 23 | 24 | ## Pull Request Process 25 | 26 | 1. We follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0-beta.4/) 27 | 28 | in our commit messages, i.e. `feat(core): improve typing` 29 | 30 | 2. Update [README.md](./README.md) to reflect changes related to public API and everything relevant 31 | 3. Make sure you cover all code changes with unit tests 32 | 4. When you are ready, create Pull Request of your fork into the original repository 33 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/interfaces/character.ts: -------------------------------------------------------------------------------- 1 | import { CharacterGender } from '../enums/character-gender'; 2 | import { RawCharacter } from './raw-character'; 3 | import { extractIdsFromUrl } from '../helpers/extract-ids-from-url'; 4 | import { CharacterStatus } from '../enums/character-status'; 5 | 6 | export class Character { 7 | id: number; 8 | name: string; 9 | status: CharacterStatus; 10 | species: string; 11 | type: string; 12 | gender: CharacterGender; 13 | origin: { 14 | name: string; 15 | id: number; 16 | }; 17 | location: { 18 | name: string; 19 | id: number; 20 | }; 21 | image: string; 22 | episode: number[]; 23 | url: string; 24 | created: Date; 25 | 26 | public static fromJson({ 27 | origin, 28 | location, 29 | episode, 30 | created, 31 | ...common 32 | }: RawCharacter): Character { 33 | const character = new Character(); 34 | 35 | Object.assign(character, common); 36 | 37 | character.origin = { 38 | name: origin.name, 39 | id: extractIdsFromUrl(origin.url)[0], 40 | }; 41 | 42 | character.location = { 43 | name: location.name, 44 | id: extractIdsFromUrl(location.url)[0], 45 | }; 46 | 47 | character.episode = episode.map(extractIdsFromUrl).map(([id]) => id); 48 | 49 | character.created = new Date(created); 50 | 51 | return character; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/location/location-list/location-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Name 6 | Type 9 |
10 | Dimension 13 |
14 | 15 |
16 |
17 | Prev 27 | Next 37 |
38 | 39 | 40 | 45 | {{ location.name }} 46 | 47 | 48 |
49 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/character/character-list/character-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Character, CharacterFilter, Response } from 'ngx-rickandmorty'; 3 | import { Observable } from 'rxjs'; 4 | import { ActivatedRoute } from '@angular/router'; 5 | import { FormGroup } from '@angular/forms'; 6 | import { FilterService } from '@ngx-ssr/rickandmorty/utils'; 7 | import { map } from 'rxjs/operators'; 8 | import { Title } from '@angular/platform-browser'; 9 | import { TuiDestroyService } from '@taiga-ui/cdk'; 10 | 11 | @Component({ 12 | selector: 'ram-character', 13 | templateUrl: './character-list.component.html', 14 | styleUrls: ['./character-list.component.scss'], 15 | providers: [TuiDestroyService, FilterService], 16 | }) 17 | export class CharacterListComponent { 18 | readonly characterResponse$: Observable>; 19 | readonly form: FormGroup; 20 | 21 | constructor( 22 | private filter: FilterService, 23 | route: ActivatedRoute, 24 | title: Title 25 | ) { 26 | title.setTitle('Characters | Rick and Morty'); 27 | 28 | this.form = filter.createForm({ 29 | name: [null], 30 | status: [null], 31 | species: [null], 32 | type: [null], 33 | gender: [null], 34 | }); 35 | 36 | this.characterResponse$ = route.data.pipe( 37 | map((data) => data.characterResponse) 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmScope": "ngx-ssr", 3 | "affected": { 4 | "defaultBase": "master" 5 | }, 6 | "implicitDependencies": { 7 | "angular.json": "*", 8 | "package.json": { 9 | "dependencies": "*", 10 | "devDependencies": "*" 11 | }, 12 | "tsconfig.base.json": "*", 13 | "tslint.json": "*", 14 | ".eslintrc.json": "*", 15 | "nx.json": "*" 16 | }, 17 | "tasksRunnerOptions": { 18 | "default": { 19 | "runner": "@nrwl/nx-cloud", 20 | "options": { 21 | "accessToken": "NDc0Nzc2NTgtNjExYy00YzYwLWIzY2QtYmQyOGRlNWMwY2IzfHJlYWQtd3JpdGU=", 22 | "cacheableOperations": ["build", "test", "lint", "e2e"], 23 | "canTrackAnalytics": false, 24 | "showUsageWarnings": true, 25 | "scan": true 26 | } 27 | } 28 | }, 29 | "projects": { 30 | "ngx-ssr-cache": { 31 | "tags": [] 32 | }, 33 | "ngx-ssr-platform": { 34 | "tags": [] 35 | }, 36 | "ngx-ssr-timeout": { 37 | "tags": [] 38 | }, 39 | "rickandmorty": { 40 | "tags": [] 41 | }, 42 | "rickandmorty-api": { 43 | "tags": [] 44 | }, 45 | "rickandmorty-e2e": { 46 | "tags": [], 47 | "implicitDependencies": ["rickandmorty"] 48 | }, 49 | "rickandmorty-utils": { 50 | "tags": [] 51 | } 52 | }, 53 | "targetDependencies": { 54 | "build": [ 55 | { 56 | "target": "build", 57 | "projects": "dependencies" 58 | } 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | - next 8 | - beta 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2.3.4 15 | - name: Use Node.js 14.x 16 | uses: actions/setup-node@v2.1.5 17 | with: 18 | node-version: 14.x 19 | - name: Install 20 | run: | 21 | npm ci 22 | env: 23 | CI: true 24 | - name: Build 25 | run: | 26 | npm run build 27 | env: 28 | CI: true 29 | lint: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v2.3.4 33 | - name: Use Node.js 12.x 34 | uses: actions/setup-node@v2.1.5 35 | with: 36 | node-version: 12.x 37 | - name: Install 38 | run: | 39 | npm ci 40 | env: 41 | CI: true 42 | - name: Lint 43 | run: | 44 | npm run lint 45 | env: 46 | CI: true 47 | test: 48 | runs-on: ubuntu-latest 49 | steps: 50 | - uses: actions/checkout@v2.3.4 51 | - name: Use Node.js 12.x 52 | uses: actions/setup-node@v2.1.5 53 | with: 54 | node-version: 12.x 55 | - name: Install 56 | run: | 57 | npm ci 58 | env: 59 | CI: true 60 | - name: Test 61 | run: | 62 | npm test 63 | npm run e2e 64 | env: 65 | CI: true 66 | -------------------------------------------------------------------------------- /libs/ngx-ssr/cache/express/index.ts: -------------------------------------------------------------------------------- 1 | import { CacheController } from '@ngx-ssr/cache'; 2 | 3 | export type ExpressRenderEngine = ( 4 | filePath: string, 5 | options: { req: { originalUrl: string }; [key: string]: any }, 6 | callback: (err?: Error | null | undefined, html?: string | undefined) => void 7 | ) => void; 8 | 9 | export function withCache( 10 | cache: CacheController, 11 | engine: ExpressRenderEngine 12 | ): ExpressRenderEngine { 13 | return ( 14 | filePath: string, 15 | options: { req: { originalUrl: string }; [key: string]: any }, 16 | callback: ( 17 | err?: Error | null | undefined, 18 | html?: string | undefined 19 | ) => void 20 | ) => { 21 | function runEngine() { 22 | engine(filePath, options, async (err, html) => { 23 | if (!err && originalUrl) { 24 | try { 25 | await cache.set(originalUrl, html).toPromise(); 26 | } catch (e) { 27 | console.error(`Setting cache for url ${originalUrl} is failed`, e); 28 | } 29 | } 30 | 31 | callback(err, html); 32 | }); 33 | } 34 | 35 | const originalUrl = options.req?.originalUrl; 36 | 37 | if (!originalUrl) { 38 | runEngine(); 39 | return; 40 | } 41 | 42 | cache 43 | .get(originalUrl) 44 | .toPromise() 45 | .then((fromCache) => { 46 | if (fromCache) { 47 | callback(null, fromCache); 48 | } else { 49 | runEngine(); 50 | } 51 | }) 52 | .catch(runEngine); 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /libs/ngx-ssr/timeout/src/lib/timeout.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, InjectionToken } from '@angular/core'; 2 | import { 3 | HttpErrorResponse, 4 | HttpEvent, 5 | HttpEventType, 6 | HttpHandler, 7 | HttpInterceptor, 8 | HttpRequest, 9 | } from '@angular/common/http'; 10 | import { NEVER, Observable, of, throwError } from 'rxjs'; 11 | import { catchError, startWith, switchMap, timeout } from 'rxjs/operators'; 12 | 13 | export interface TimeoutOptions { 14 | timeout: number; 15 | } 16 | 17 | export const TIMEOUT_OPTIONS = new InjectionToken( 18 | 'Timeout Options', 19 | { 20 | factory() { 21 | return { timeout: null }; 22 | }, 23 | } 24 | ); 25 | 26 | @Injectable() 27 | export class TimeoutInterceptor implements HttpInterceptor { 28 | constructor( 29 | @Inject(TIMEOUT_OPTIONS) private timeoutOptions: TimeoutOptions 30 | ) {} 31 | 32 | intercept( 33 | request: HttpRequest, 34 | next: HttpHandler 35 | ): Observable> { 36 | return next.handle(request).pipe( 37 | switchMap((event) => { 38 | if (event.type === HttpEventType.Sent && this.timeoutOptions.timeout) { 39 | return NEVER.pipe( 40 | startWith(event), 41 | timeout(this.timeoutOptions.timeout) 42 | ); 43 | } 44 | 45 | return of(event); 46 | }), 47 | catchError((error) => { 48 | if (error && error.name === 'TimeoutError') { 49 | return throwError( 50 | new HttpErrorResponse({ error, url: request.urlWithParams }) 51 | ); 52 | } 53 | 54 | return throwError(error); 55 | }) 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule, Sanitizer } from '@angular/core'; 3 | import { HttpClientModule } from '@angular/common/http'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { TransferHttpCacheModule } from '@nguniversal/common'; 6 | 7 | import { AppComponent } from './app.component'; 8 | import { AppRoutingModule } from './app-routing.module'; 9 | import { 10 | iconsPathFactory, 11 | TUI_ICONS_PATH, 12 | TuiRootModule, 13 | } from '@taiga-ui/core'; 14 | import { TUI_SANITIZER } from '@taiga-ui/cdk'; 15 | import { TuiTabsModule } from '@taiga-ui/kit'; 16 | import { NgxSsrCacheModule } from '@ngx-ssr/cache'; 17 | import { ServiceWorkerModule } from '@angular/service-worker'; 18 | import { environment } from '../environments/environment'; 19 | 20 | const unsafeSanitizer: Sanitizer = { 21 | sanitize: (_: any, value: any) => value, 22 | }; 23 | 24 | @NgModule({ 25 | declarations: [AppComponent], 26 | imports: [ 27 | BrowserModule.withServerTransition({ appId: 'serverApp' }), 28 | TransferHttpCacheModule, 29 | BrowserAnimationsModule, 30 | HttpClientModule, 31 | AppRoutingModule, 32 | TuiRootModule, 33 | TuiTabsModule, 34 | NgxSsrCacheModule.configLruCache({ maxAge: 10 * 60_000, maxSize: 50 }), 35 | ServiceWorkerModule.register('safety-worker.js', { 36 | enabled: environment.production, 37 | }), 38 | ], 39 | bootstrap: [AppComponent], 40 | providers: [ 41 | { 42 | provide: TUI_SANITIZER, 43 | useValue: unsafeSanitizer, 44 | }, 45 | { 46 | provide: TUI_ICONS_PATH, 47 | useValue: iconsPathFactory('assets/taiga-ui/icons/'), 48 | }, 49 | ], 50 | }) 51 | export class AppModule {} 52 | -------------------------------------------------------------------------------- /libs/rickandmorty/utils/src/lib/filter.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { debounceTime, takeUntil } from 'rxjs/operators'; 4 | import { FormBuilder, FormGroup } from '@angular/forms'; 5 | import { TuiDestroyService } from '@taiga-ui/cdk'; 6 | 7 | @Injectable() 8 | export class FilterService { 9 | constructor( 10 | private route: ActivatedRoute, 11 | private router: Router, 12 | private destroy: TuiDestroyService, 13 | private fb: FormBuilder 14 | ) {} 15 | 16 | createForm( 17 | controlsConfig: Parameters[0], 18 | options?: Parameters[1] 19 | ): FormGroup { 20 | const { queryParams } = this.route.snapshot; 21 | 22 | const form = this.fb.group( 23 | Object.keys(controlsConfig).reduce((current, key) => { 24 | if (!controlsConfig[key]) { 25 | return current; 26 | } 27 | 28 | if (queryParams[key] === undefined) { 29 | return { 30 | ...current, 31 | [key]: controlsConfig[key], 32 | }; 33 | } 34 | 35 | const controlValue = queryParams[key]; 36 | const [, ...controlConfig] = controlsConfig[key]; 37 | 38 | return { 39 | ...current, 40 | [key]: [controlValue, ...controlConfig], 41 | }; 42 | }, {}), 43 | options 44 | ); 45 | 46 | form.valueChanges 47 | .pipe(takeUntil(this.destroy), debounceTime(600)) 48 | .subscribe((filters) => { 49 | this.router.navigate([], { 50 | relativeTo: this.route, 51 | queryParams: { ...filters, page: null }, 52 | queryParamsHandling: 'merge', 53 | }); 54 | }); 55 | 56 | return form; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /libs/ngx-ssr/cache/src/lib/lru-cache.spec.ts: -------------------------------------------------------------------------------- 1 | import { LRUCache } from './lru-cache'; 2 | import { from, timer } from 'rxjs'; 3 | import { concatMap } from 'rxjs/operators'; 4 | 5 | describe('LRUCache', () => { 6 | let lruCache: LRUCache; 7 | 8 | beforeEach(() => { 9 | lruCache = new LRUCache(); 10 | }); 11 | 12 | it('should set an item', async () => { 13 | await lruCache.set('key', {}).toPromise(); 14 | 15 | expect(await lruCache.size().toPromise()).toEqual(1); 16 | }); 17 | 18 | it('should get an item', async () => { 19 | const value = { someProperty: '' }; 20 | 21 | await lruCache.set('key', value).toPromise(); 22 | 23 | expect(await lruCache.get('key').toPromise()).toStrictEqual(value); 24 | }); 25 | 26 | describe('maxSize', () => { 27 | beforeEach(async () => { 28 | lruCache = new LRUCache({ maxSize: 3 }); 29 | 30 | await from([1, 2, 3]) 31 | .pipe(concatMap((value) => lruCache.set(value.toString(), value))) 32 | .toPromise(); 33 | }); 34 | 35 | it('should push out the first element', async () => { 36 | expect(await lruCache.get('1').toPromise()).toEqual(1); 37 | await lruCache.set('4', 4).toPromise(); 38 | expect(await lruCache.get('1').toPromise()).toEqual(null); 39 | }); 40 | }); 41 | 42 | describe('maxAge', () => { 43 | beforeEach(async () => { 44 | lruCache = new LRUCache({ maxAge: 100 }); 45 | 46 | await from([1, 2, 3]) 47 | .pipe(concatMap((value) => lruCache.set(value.toString(), value))) 48 | .toPromise(); 49 | }); 50 | 51 | it('should return null', async () => { 52 | await timer(100).toPromise(); 53 | 54 | expect(await lruCache.get('1').toPromise()).toEqual(null); 55 | }); 56 | 57 | it('should override a default maxAge', async () => { 58 | await lruCache.set('1', 1, Infinity).toPromise(); 59 | await timer(100).toPromise(); 60 | expect(await lruCache.get('1').toPromise()).toEqual(1); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /libs/ngx-ssr/cache/src/lib/cache.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { ErrorHandler, Inject, Injectable } from '@angular/core'; 2 | import { 3 | HttpEvent, 4 | HttpEventType, 5 | HttpHandler, 6 | HttpInterceptor, 7 | HttpRequest, 8 | HttpResponse, 9 | } from '@angular/common/http'; 10 | import { Observable, of } from 'rxjs'; 11 | import { CacheController } from './cache-controller'; 12 | import { catchError, mapTo, switchMap } from 'rxjs/operators'; 13 | import { DOCUMENT } from '@angular/common'; 14 | 15 | @Injectable() 16 | export class CacheInterceptor implements HttpInterceptor { 17 | private doc: Document; 18 | 19 | constructor( 20 | private cacheController: CacheController, 21 | private errorHandler: ErrorHandler, 22 | @Inject(DOCUMENT) doc: any 23 | ) { 24 | this.doc = doc; 25 | } 26 | 27 | intercept( 28 | request: HttpRequest, 29 | next: HttpHandler 30 | ): Observable> { 31 | const { method, urlWithParams } = request; 32 | 33 | if (method !== 'GET') { 34 | return next.handle(request); 35 | } 36 | 37 | return this.cacheController.get(urlWithParams).pipe( 38 | switchMap((cachedResponse) => { 39 | if (cachedResponse) { 40 | return of(cachedResponse); 41 | } 42 | 43 | return next.handle(request).pipe( 44 | switchMap((response: HttpResponse) => { 45 | if ( 46 | response.type === HttpEventType.Response && 47 | response instanceof HttpResponse && 48 | response.status >= 200 && 49 | response.status < 400 50 | ) { 51 | return this.cacheController.set(urlWithParams, response).pipe( 52 | catchError((err) => { 53 | this.errorHandler.handleError(err); 54 | 55 | return of(undefined); 56 | }), 57 | mapTo(response) 58 | ); 59 | } 60 | 61 | return of(response); 62 | }) 63 | ); 64 | }) 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /libs/ngx-ssr/cache/src/lib/lru-cache.ts: -------------------------------------------------------------------------------- 1 | import { CacheController } from './cache-controller'; 2 | import { defer, Observable, of } from 'rxjs'; 3 | 4 | export interface LRUCacheOptions { 5 | maxAge: number; 6 | maxSize: number; 7 | } 8 | 9 | export class LRUCache extends CacheController { 10 | private readonly maxAge: number; 11 | private readonly maxSize: number; 12 | private readonly _cache: Map = new Map(); 13 | 14 | constructor({ maxAge, maxSize }: Partial = {}) { 15 | super(); 16 | 17 | this.maxAge = maxAge ?? Infinity; 18 | this.maxSize = maxSize ?? Infinity; 19 | } 20 | 21 | get(key: string, notFoundValue: T = null): Observable { 22 | return defer(() => { 23 | let value = this._cache.get(key)?.getValue(); 24 | 25 | if (value == null) { 26 | this.delete(key); 27 | value = notFoundValue; 28 | } 29 | 30 | return Promise.resolve(value); 31 | }); 32 | } 33 | 34 | set( 35 | key: string, 36 | value: T, 37 | maxAge: number = this.maxAge 38 | ): Observable { 39 | return defer(() => { 40 | this.deleteFirstKeyIfHaveTo(); 41 | 42 | this._cache.set(key, new LRUCacheEntity(key, value, maxAge)); 43 | 44 | return Promise.resolve(); 45 | }); 46 | } 47 | 48 | size(): Observable { 49 | return of(this.sizeSync()); 50 | } 51 | 52 | private delete(key: string): void { 53 | this._cache.delete(key); 54 | } 55 | 56 | private sizeSync(): number { 57 | return this._cache.size; 58 | } 59 | 60 | private deleteFirstKeyIfHaveTo(): void { 61 | if (this.sizeSync() >= this.maxSize) { 62 | const { value: firstKey } = this._cache.keys().next(); 63 | 64 | if (firstKey) { 65 | this.delete(firstKey); 66 | } 67 | } 68 | } 69 | } 70 | 71 | class LRUCacheEntity { 72 | private readonly _createdDate: number; 73 | 74 | constructor( 75 | public readonly key: string, 76 | private _value: T, 77 | public readonly maxAge = Infinity 78 | ) { 79 | this._createdDate = Date.now(); 80 | } 81 | 82 | getValue(): T | null { 83 | if (this._value == null || this._createdDate + this.maxAge <= Date.now()) { 84 | this._value = null; 85 | } 86 | 87 | return this._value; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /libs/ngx-ssr/timeout/src/lib/timeout.interceptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { 3 | HttpClientTestingModule, 4 | HttpTestingController, 5 | } from '@angular/common/http/testing'; 6 | import { 7 | HTTP_INTERCEPTORS, 8 | HttpClient, 9 | HttpErrorResponse, 10 | } from '@angular/common/http'; 11 | import { Injectable } from '@angular/core'; 12 | import { TIMEOUT_OPTIONS, TimeoutInterceptor } from './timeout.interceptor'; 13 | import { TimeoutError, timer } from 'rxjs'; 14 | 15 | @Injectable() 16 | class ApiService { 17 | constructor(private httpClient: HttpClient) {} 18 | 19 | get() { 20 | return this.httpClient.get('/api/v1/getResources'); 21 | } 22 | 23 | post() { 24 | return this.httpClient.post('/api/v1/getResources', { 25 | params: { 26 | query: '123', 27 | }, 28 | }); 29 | } 30 | } 31 | 32 | describe('TimeoutInterceptor', () => { 33 | let service: ApiService; 34 | let httpController: HttpTestingController; 35 | 36 | beforeEach(() => { 37 | TestBed.configureTestingModule({ 38 | imports: [HttpClientTestingModule], 39 | providers: [ 40 | ApiService, 41 | { 42 | provide: TIMEOUT_OPTIONS, 43 | useValue: { timeout: 1_000 }, 44 | }, 45 | { 46 | provide: HTTP_INTERCEPTORS, 47 | useClass: TimeoutInterceptor, 48 | multi: true, 49 | }, 50 | ], 51 | }); 52 | 53 | service = TestBed.inject(ApiService); 54 | httpController = TestBed.inject(HttpTestingController); 55 | }); 56 | 57 | it('Если превышается лимит таймаута запрос обрывается', async () => { 58 | const response = service 59 | .get() 60 | .toPromise() 61 | .catch((res) => res); 62 | 63 | httpController.expectOne(`/api/v1/getResources`); 64 | 65 | await timer(2_000).toPromise(); 66 | 67 | expect(await response).toEqual( 68 | new HttpErrorResponse({ 69 | error: expect.any(TimeoutError), 70 | url: '/api/v1/getResources', 71 | }) 72 | ); 73 | }); 74 | 75 | it('Если лимит НЕ превышается запрос работает в штатном режиме', async () => { 76 | service.get().subscribe(); 77 | 78 | const req = httpController.expectOne(`/api/v1/getResources`); 79 | 80 | await timer(100).toPromise(); 81 | 82 | req.flush(123); 83 | }); 84 | 85 | afterEach(() => { 86 | httpController.verify(); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /apps/rickandmorty/server.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js/dist/zone-node'; 2 | import '@ng-web-apis/universal/mocks'; 3 | 4 | import { ngExpressEngine } from '@nguniversal/express-engine'; 5 | import * as express from 'express'; 6 | import { join } from 'path'; 7 | 8 | import { AppServerModule } from './src/main.server'; 9 | import { APP_BASE_HREF } from '@angular/common'; 10 | import { existsSync } from 'fs'; 11 | import { LRUCache } from '@ngx-ssr/cache'; 12 | import { withCache } from '@ngx-ssr/cache/express'; 13 | 14 | // The Express app is exported so that it can be used by serverless Functions. 15 | export function app(): express.Express { 16 | const server = express(); 17 | const distFolder = join(process.cwd(), 'dist/apps/rickandmorty/browser'); 18 | const indexHtml = existsSync(join(distFolder, 'index.original.html')) 19 | ? 'index.original.html' 20 | : 'index'; 21 | 22 | // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine) 23 | server.engine( 24 | 'html', 25 | withCache( 26 | new LRUCache({ maxAge: 10 * 60_000, maxSize: 100 }), 27 | ngExpressEngine({ 28 | bootstrap: AppServerModule, 29 | }) 30 | ) 31 | ); 32 | 33 | server.set('view engine', 'html'); 34 | server.set('views', distFolder); 35 | 36 | // Example Express Rest API endpoints 37 | // server.get('/api/**', (req, res) => { }); 38 | // Serve static files from /browsera 39 | server.get( 40 | '*.*', 41 | express.static(distFolder, { 42 | maxAge: '1y', 43 | }) 44 | ); 45 | 46 | // All regular routes use the Universal engine 47 | server.get('*', (req, res) => { 48 | res.render(indexHtml, { 49 | req, 50 | providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }], 51 | }); 52 | }); 53 | 54 | return server; 55 | } 56 | 57 | function run(): void { 58 | const port = process.env.PORT || 4000; 59 | 60 | // Start up the Node server 61 | const server = app(); 62 | server.listen(port, () => { 63 | console.log(`Node Express server listening on http://localhost:${port}`); 64 | }); 65 | } 66 | 67 | // Webpack will replace 'require' with '__webpack_require__' 68 | // '__non_webpack_require__' is a proxy to Node 'require' 69 | // The below code is to ensure that the server is run only when not requiring the bundle. 70 | declare const __non_webpack_require__: NodeRequire; 71 | const mainModule = __non_webpack_require__.main; 72 | const moduleFilename = (mainModule && mainModule.filename) || ''; 73 | if (moduleFilename === __filename || moduleFilename.includes('iisnode')) { 74 | run(); 75 | } 76 | 77 | export * from './src/main.server'; 78 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: 17 | - master 18 | - next 19 | - beta 20 | pull_request: 21 | # The branches below must be a subset of the branches above 22 | branches: 23 | - master 24 | - next 25 | - beta 26 | schedule: 27 | - cron: '44 17 * * 0' 28 | 29 | jobs: 30 | analyze: 31 | name: Analyze 32 | runs-on: ubuntu-latest 33 | 34 | strategy: 35 | fail-fast: false 36 | matrix: 37 | language: [ 'javascript' ] 38 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 39 | # Learn more: 40 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 41 | 42 | steps: 43 | - name: Checkout repository 44 | uses: actions/checkout@v2.3.4 45 | 46 | # Initializes the CodeQL tools for scanning. 47 | - name: Initialize CodeQL 48 | uses: github/codeql-action/init@v1 49 | with: 50 | languages: ${{ matrix.language }} 51 | # If you wish to specify custom queries, you can do so here or in a config file. 52 | # By default, queries listed here will override any specified in a config file. 53 | # Prefix the list here with "+" to use these queries and those in the config file. 54 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v1 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 https://git.io/JvXDl 63 | 64 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 65 | # and modify them (or add more) to build your code if your project 66 | # uses a compiled language 67 | 68 | #- run: | 69 | # make bootstrap 70 | # make release 71 | 72 | - name: Perform CodeQL Analysis 73 | uses: github/codeql-action/analyze@v1 74 | -------------------------------------------------------------------------------- /decorate-angular-cli.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file decorates the Angular CLI with the Nx CLI to enable features such as computation caching 3 | * and faster execution of tasks. 4 | * 5 | * It does this by: 6 | * 7 | * - Patching the Angular CLI to warn you in case you accidentally use the undecorated ng command. 8 | * - Symlinking the ng to nx command, so all commands run through the Nx CLI 9 | * - Updating the package.json postinstall script to give you control over this script 10 | * 11 | * The Nx CLI decorates the Angular CLI, so the Nx CLI is fully compatible with it. 12 | * Every command you run should work the same when using the Nx CLI, except faster. 13 | * 14 | * Because of symlinking you can still type `ng build/test/lint` in the terminal. The ng command, in this case, 15 | * will point to nx, which will perform optimizations before invoking ng. So the Angular CLI is always invoked. 16 | * The Nx CLI simply does some optimizations before invoking the Angular CLI. 17 | * 18 | * To opt out of this patch: 19 | * - Replace occurrences of nx with ng in your package.json 20 | * - Remove the script from your postinstall script in your package.json 21 | * - Delete and reinstall your node_modules 22 | */ 23 | 24 | const fs = require("fs"); 25 | const os = require("os"); 26 | const cp = require("child_process"); 27 | const isWindows = os.platform() === "win32"; 28 | let output; 29 | try { 30 | output = require("@nrwl/workspace").output; 31 | } catch (e) { 32 | console.warn( 33 | "Angular CLI could not be decorated to enable computation caching. Please ensure @nrwl/workspace is installed." 34 | ); 35 | process.exit(0); 36 | } 37 | 38 | /** 39 | * Symlink of ng to nx, so you can keep using `ng build/test/lint` and still 40 | * invoke the Nx CLI and get the benefits of computation caching. 41 | */ 42 | function symlinkNgCLItoNxCLI() { 43 | try { 44 | const ngPath = "./node_modules/.bin/ng"; 45 | const nxPath = "./node_modules/.bin/nx"; 46 | if (isWindows) { 47 | /** 48 | * This is the most reliable way to create symlink-like behavior on Windows. 49 | * Such that it works in all shells and works with npx. 50 | */ 51 | ["", ".cmd", ".ps1"].forEach((ext) => { 52 | if (fs.existsSync(nxPath + ext)) 53 | fs.writeFileSync(ngPath + ext, fs.readFileSync(nxPath + ext)); 54 | }); 55 | } else { 56 | // If unix-based, symlink 57 | cp.execSync(`ln -sf ./nx ${ngPath}`); 58 | } 59 | } catch (e) { 60 | output.error({ 61 | title: 62 | "Unable to create a symlink from the Angular CLI to the Nx CLI:" + 63 | e.message, 64 | }); 65 | throw e; 66 | } 67 | } 68 | 69 | try { 70 | symlinkNgCLItoNxCLI(); 71 | require("@nrwl/cli/lib/decorate-cli").decorateCli(); 72 | output.log({ 73 | title: "Angular CLI has been decorated to enable computation caching.", 74 | }); 75 | } catch (e) { 76 | output.error({ 77 | title: "Decoration of the Angular CLI did not complete successfully", 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | -------------------------------------------------------------------------------- /libs/ngx-ssr/cache/src/lib/cache.interceptor.spec.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js'; 2 | import 'zone.js/testing'; 3 | import { fakeAsync, flush, TestBed } from '@angular/core/testing'; 4 | 5 | import { CacheInterceptor } from './cache.interceptor'; 6 | import { CacheController } from './cache-controller'; 7 | import { 8 | HttpClientTestingModule, 9 | HttpTestingController, 10 | } from '@angular/common/http/testing'; 11 | import { 12 | HTTP_INTERCEPTORS, 13 | HttpClient, 14 | HttpResponse, 15 | } from '@angular/common/http'; 16 | import { LRUCache } from './lru-cache'; 17 | import { 18 | BrowserDynamicTestingModule, 19 | platformBrowserDynamicTesting, 20 | } from '@angular/platform-browser-dynamic/testing'; 21 | 22 | describe('CacheInterceptor', () => { 23 | let cacheController: CacheController; 24 | let httpController: HttpTestingController; 25 | let httpClient: HttpClient; 26 | 27 | beforeEach(() => { 28 | TestBed.resetTestEnvironment(); 29 | TestBed.initTestEnvironment( 30 | BrowserDynamicTestingModule, 31 | platformBrowserDynamicTesting() 32 | ); 33 | 34 | TestBed.configureTestingModule({ 35 | imports: [HttpClientTestingModule], 36 | providers: [ 37 | { 38 | provide: HTTP_INTERCEPTORS, 39 | useClass: CacheInterceptor, 40 | multi: true, 41 | }, 42 | { provide: CacheController, useClass: LRUCache }, 43 | ], 44 | }); 45 | 46 | cacheController = TestBed.inject(CacheController); 47 | httpController = TestBed.inject(HttpTestingController); 48 | httpClient = TestBed.inject(HttpClient); 49 | }); 50 | 51 | it('should put to cache the GET response ', fakeAsync(async () => { 52 | const setSpy = jest.spyOn(cacheController, 'set'); 53 | 54 | httpClient.get('/some/url').subscribe(); 55 | 56 | flush(); 57 | 58 | const req = httpController.expectOne('/some/url'); 59 | 60 | req.flush({}); 61 | 62 | expect(setSpy).toBeCalledTimes(1); 63 | expect(await cacheController.get('/some/url').toPromise()).toEqual( 64 | new HttpResponse({ body: {}, url: '/some/url' }) 65 | ); 66 | })); 67 | 68 | it('should get a response from cache', fakeAsync(async () => { 69 | const body = {}; 70 | const response = new HttpResponse({ body, url: '/some/url' }); 71 | await cacheController.set('/some/url', response).toPromise(); 72 | 73 | const setSpy = jest.spyOn(cacheController, 'set'); 74 | const getSpy = jest.spyOn(cacheController, 'get'); 75 | 76 | const responsePromise = httpClient.get('/some/url').toPromise(); 77 | 78 | flush(); 79 | 80 | httpController.expectNone('/some/url'); 81 | 82 | expect(setSpy).not.toBeCalled(); 83 | expect(getSpy).toBeCalledTimes(1); 84 | expect(await responsePromise).toStrictEqual(body); 85 | })); 86 | 87 | it('should skip setting the POST response to cache', fakeAsync(async () => { 88 | const setSpy = jest.spyOn(cacheController, 'set'); 89 | 90 | httpClient.post('/some/url', {}).subscribe(); 91 | 92 | flush(); 93 | 94 | httpController.expectOne('/some/url').flush({}); 95 | 96 | expect(setSpy).not.toBeCalled(); 97 | })); 98 | }); 99 | -------------------------------------------------------------------------------- /apps/rickandmorty/src/app/character/character-list/character-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Name 5 | 6 | 7 | 12 | Status 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 |
22 | Type 24 | 25 | 26 | Species 28 | 29 |
30 | 31 | 36 | Gender 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 | 46 |
47 |
48 | Prev 58 | Next 68 |
69 | 70 | 74 |
75 |
76 | 88 | 93 | 94 |
95 | 96 |
97 |
{{ character.name }}
98 | 99 | 104 |
105 |
106 |
107 |
108 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at katsuba.igor@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-ssr", 3 | "version": "0.0.0-development", 4 | "private": true, 5 | "license": "MIT", 6 | "scripts": { 7 | "affected": "nx affected", 8 | "affected:apps": "nx affected:apps", 9 | "affected:build": "nx affected:build", 10 | "affected:dep-graph": "nx affected:dep-graph", 11 | "affected:e2e": "nx affected:e2e", 12 | "affected:libs": "nx affected:libs", 13 | "affected:lint": "nx affected:lint", 14 | "affected:test": "nx affected:test", 15 | "build": "nx run-many --target build --prod --all", 16 | "deploy": "nx run-many --target deploy --all", 17 | "dep-graph": "nx dep-graph", 18 | "dev:ssr": "nx run rickandmorty:serve-ssr", 19 | "e2e": "nx run-many --target e2e --all", 20 | "format": "nx format:write", 21 | "format:check": "nx format:check", 22 | "format:write": "nx format:write", 23 | "help": "nx help", 24 | "lint": "nx workspace-lint && nx run-many --target lint --all", 25 | "ng": "ng", 26 | "nx": "nx", 27 | "start": "nx serve", 28 | "test": "nx run-many --target test --all", 29 | "update": "nx migrate latest", 30 | "workspace-generator": "nx workspace-generator" 31 | }, 32 | "dependencies": { 33 | "@angular/animations": "12.2.8", 34 | "@angular/common": "12.2.8", 35 | "@angular/compiler": "12.2.8", 36 | "@angular/core": "12.2.8", 37 | "@angular/fire": "^6.1.4", 38 | "@angular/forms": "12.2.8", 39 | "@angular/platform-browser": "12.2.8", 40 | "@angular/platform-browser-dynamic": "12.2.8", 41 | "@angular/platform-server": "12.2.8", 42 | "@angular/router": "12.2.8", 43 | "@angular/service-worker": "12.2.8", 44 | "@ng-web-apis/common": "^1.12.0", 45 | "@ng-web-apis/universal": "^1.12.0", 46 | "@nguniversal/express-engine": "12.1.1", 47 | "@nrwl/angular": "12.9.0", 48 | "@taiga-ui/cdk": "2.19.0", 49 | "@taiga-ui/core": "2.19.0", 50 | "@taiga-ui/icons": "2.19.0", 51 | "@taiga-ui/kit": "2.19.0", 52 | "express": "^4.17.1", 53 | "firebase": "^8.5.0", 54 | "normalize.css": "^8.0.1", 55 | "rxjs": "~6.6.7", 56 | "tslib": "^2.0.0", 57 | "zone.js": "0.11.4" 58 | }, 59 | "devDependencies": { 60 | "@angular-devkit/build-angular": "12.2.8", 61 | "@angular-eslint/eslint-plugin": "12.3.1", 62 | "@angular-eslint/eslint-plugin-template": "12.3.1", 63 | "@angular-eslint/template-parser": "12.3.1", 64 | "@angular/cli": "12.2.8", 65 | "@angular/compiler-cli": "12.2.8", 66 | "@angular/language-service": "12.2.8", 67 | "@ng-builders/semrel": "^1.2.0", 68 | "@nguniversal/builders": "12.1.1", 69 | "@nrwl/cli": "12.9.0", 70 | "@nrwl/cypress": "12.9.0", 71 | "@nrwl/eslint-plugin-nx": "12.9.0", 72 | "@nrwl/jest": "12.9.0", 73 | "@nrwl/nx-cloud": "12.3.13", 74 | "@nrwl/tao": "12.9.0", 75 | "@nrwl/workspace": "12.9.0", 76 | "@testing-library/angular": "^10.7.1", 77 | "@types/jest": "26.0.22", 78 | "@types/node": "14.14.33", 79 | "@typescript-eslint/eslint-plugin": "4.28.5", 80 | "@typescript-eslint/parser": "4.28.5", 81 | "codelyzer": "^6.0.2", 82 | "cypress": "^6.8.0", 83 | "dotenv": "10.0.0", 84 | "eslint": "7.22.0", 85 | "eslint-config-prettier": "8.1.0", 86 | "eslint-plugin-cypress": "^2.11.3", 87 | "firebase-admin": "^9.8.0", 88 | "firebase-functions": "^3.14.1", 89 | "firebase-functions-test": "^0.2.3", 90 | "firebase-tools": "^9.10.2", 91 | "jest": "27.0.3", 92 | "jest-preset-angular": "9.0.7", 93 | "ng-packagr": "12.2.2", 94 | "prettier": "2.4.1", 95 | "ts-jest": "27.0.3", 96 | "ts-mockito": "^2.6.1", 97 | "ts-node": "~9.1.1", 98 | "tslint": "~6.1.0", 99 | "typescript": "4.3.5" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/api.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core'; 2 | import { BASE_URL } from './tokens'; 3 | import { HttpClient, HttpParams } from '@angular/common/http'; 4 | import { Observable, of } from 'rxjs'; 5 | import { Location as NgLocation } from '@angular/common'; 6 | import { PingResponse } from './interfaces/ping'; 7 | import { RawResponse } from './interfaces/raw-response'; 8 | import { EpisodeFilter } from './interfaces/episode-filter'; 9 | import { RawEpisode } from './interfaces/raw-episode'; 10 | import { LocationFilter } from './interfaces/location-filter'; 11 | import { RawLocation } from './interfaces/raw-location'; 12 | import { Location } from './interfaces/location'; 13 | import { CharacterFilter } from './interfaces/character-filter'; 14 | import { RawCharacter } from './interfaces/raw-character'; 15 | import { Episode } from './interfaces/episode'; 16 | import { catchError, map } from 'rxjs/operators'; 17 | import { responseFactory } from './helpers/response-factory'; 18 | import { Response } from './interfaces/response'; 19 | import { Character } from './interfaces/character'; 20 | import { coerceArray } from './helpers/coerce-array'; 21 | 22 | @Injectable({ 23 | providedIn: 'root', 24 | }) 25 | export class ApiService { 26 | private baseUrl = inject(BASE_URL); 27 | private http = inject(HttpClient); 28 | 29 | ping(): Observable { 30 | return this.http.get(this.baseUrl); 31 | } 32 | 33 | getEpisode(): Observable>; 34 | getEpisode(id: number | number[]): Observable; 35 | getEpisode( 36 | filter: LocationFilter 37 | ): Observable>; 38 | getEpisode( 39 | id?: number | number[] | EpisodeFilter 40 | ): Observable> { 41 | return this.getEntity('episode', id).pipe( 42 | map((rawResponse) => responseFactory(rawResponse, Episode.fromJson)) 43 | ); 44 | } 45 | 46 | getLocation(): Observable>; 47 | getLocation(id: number | number[]): Observable; 48 | getLocation( 49 | filter: LocationFilter 50 | ): Observable>; 51 | getLocation( 52 | id?: number | number[] | CharacterFilter 53 | ): Observable> { 54 | return this.getEntity('location', id).pipe( 55 | map((rawResponse) => responseFactory(rawResponse, Location.fromJson)) 56 | ); 57 | } 58 | 59 | getCharacter(): Observable>; 60 | getCharacter(id: number | number[]): Observable; 61 | getCharacter( 62 | filter: CharacterFilter 63 | ): Observable>; 64 | getCharacter( 65 | id?: number | number[] | CharacterFilter 66 | ): Observable> { 67 | return this.getEntity('character', id).pipe( 68 | map((rawResponse) => responseFactory(rawResponse, Character.fromJson)) 69 | ); 70 | } 71 | 72 | private getEntity( 73 | url: string, 74 | id?: number | number[] | Partial 75 | ): Observable> { 76 | const [ids, params] = [ 77 | this.isId(id) 78 | ? coerceArray(id).filter((i) => (i ?? false) !== false) 79 | : null, 80 | this.isId(id) ? null : id, 81 | ] as [number[], Partial]; 82 | 83 | return this.http 84 | .get>( 85 | NgLocation.joinWithSlash( 86 | NgLocation.joinWithSlash(this.baseUrl, url), 87 | ids?.join(',') ?? '' 88 | ), 89 | { params: new HttpParams({ fromObject: params as any }) } 90 | ) 91 | .pipe( 92 | catchError(() => { 93 | if (typeof id === 'number' || Array.isArray(id)) { 94 | return of([]); 95 | } 96 | 97 | return of({ 98 | info: { pages: 0, count: 0, prev: null, next: null }, 99 | results: [], 100 | }); 101 | }) 102 | ); 103 | } 104 | 105 | private isId(id: number | number[] | any) { 106 | return typeof id === 'number' || Array.isArray(id); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /migrations.json: -------------------------------------------------------------------------------- 1 | { 2 | "migrations": [ 3 | { 4 | "version": "12.5.0-beta.1", 5 | "description": "Rename the workspace-schematic script into workspace-generator script", 6 | "factory": "./src/migrations/update-12-5-0/add-target-dependencies", 7 | "cli": "nx", 8 | "package": "@nrwl/workspace", 9 | "name": "add-target-dependencies" 10 | }, 11 | { 12 | "version": "12.0.0-beta.0", 13 | "description": "adjusts the ngcc postinstall command to just leave 'ngcc' in there. This fixes Ivy in Jest tests and Storybooks", 14 | "factory": "./src/migrations/update-12-0-0/update-ngcc-postinstall", 15 | "package": "@nrwl/angular", 16 | "name": "update-ngcc-postinstall" 17 | }, 18 | { 19 | "cli": "nx", 20 | "version": "12.3.1", 21 | "description": "Remove deprecated options and update others according to new defaults. It syncs with the v12 migration of Angular builders.", 22 | "factory": "./src/migrations/update-12-3-0/update-webpack-browser-config", 23 | "package": "@nrwl/angular", 24 | "name": "update-webpack-browser-config" 25 | }, 26 | { 27 | "cli": "nx", 28 | "version": "12.3.1", 29 | "description": "Updates storybook configurations to support webpack 5", 30 | "factory": "./src/migrations/update-12-3-0/update-storybook", 31 | "package": "@nrwl/angular", 32 | "name": "update-storybook" 33 | }, 34 | { 35 | "cli": "nx", 36 | "version": "12.3.1", 37 | "description": "Migrates some rules that have changed in Angular EsLint", 38 | "factory": "./src/migrations/update-12-3-0/update-angular-eslint-rules", 39 | "package": "@nrwl/angular", 40 | "name": "update-angular-eslint-rules" 41 | }, 42 | { 43 | "cli": "nx", 44 | "version": "12.3.5-beta.0", 45 | "description": "Convert targets using @nrwl/angular:webpack-browser with the buildTarget option set to use the @nrwl/angular:delegate-build executor instead.", 46 | "factory": "./src/migrations/update-12-3-0/convert-webpack-browser-build-target-to-delegate-build", 47 | "package": "@nrwl/angular", 48 | "name": "convert-webpack-browser-build-target-to-delegate-build" 49 | }, 50 | { 51 | "cli": "nx", 52 | "version": "12.9.0", 53 | "description": "Fixes invalid importPaths for buildable and publishable libs.", 54 | "factory": "./src/migrations/update-12-9-0/update-invalid-import-paths", 55 | "package": "@nrwl/angular", 56 | "name": "update-invalid-import-paths" 57 | }, 58 | { 59 | "version": "11.1.0-beta", 60 | "description": "Removes `canActivate` from a `Route` config when `redirectTo` is also present", 61 | "factory": "./migrations/can-activate-with-redirect-to/index", 62 | "package": "@angular/core", 63 | "name": "migration-v11.1-can-activate-with-redirect-to" 64 | }, 65 | { 66 | "version": "12.0.0-beta", 67 | "description": "In Angular version 12, the type of ActivatedRouteSnapshot.fragment is nullable. This migration automatically adds non-null assertions to it.", 68 | "factory": "./migrations/activated-route-snapshot-fragment/index", 69 | "package": "@angular/core", 70 | "name": "migration-v12-activated-route-snapshot-fragment" 71 | }, 72 | { 73 | "version": "12.0.0-next.6", 74 | "description": "`XhrFactory` has been moved from `@angular/common/http` to `@angular/common`.", 75 | "factory": "./migrations/xhr-factory/index", 76 | "package": "@angular/core", 77 | "name": "migration-v12-xhr-factory" 78 | }, 79 | { 80 | "version": "12.0.2", 81 | "description": "Automatically migrates shadow-piercing selector from `/deep/` to the recommended alternative `::ng-deep`.", 82 | "factory": "./migrations/deep-shadow-piercing-selector/index", 83 | "package": "@angular/core", 84 | "name": "migration-v12-deep-shadow-piercing-selector" 85 | }, 86 | { 87 | "cli": "nx", 88 | "version": "12.8.0-beta.0", 89 | "description": "Remove Typescript Preprocessor Plugin", 90 | "factory": "./src/migrations/update-12-8-0/remove-typescript-plugin", 91 | "package": "@nrwl/cypress", 92 | "name": "remove-typescript-plugin" 93 | }, 94 | { 95 | "version": "12.1.0-beta.1", 96 | "cli": "nx", 97 | "description": "Update jest-preset-angular to version 8.4.0", 98 | "factory": "./src/migrations/update-12-1-2/update-jest-preset-angular", 99 | "package": "@nrwl/jest", 100 | "name": "update-jest-preset-angular-8-4-0" 101 | }, 102 | { 103 | "version": "12.1.2-beta.1", 104 | "cli": "nx", 105 | "description": "Replace tsConfig with tsconfig for ts-jest in jest.config.js", 106 | "factory": "./src/migrations/update-12-1-2/update-ts-jest", 107 | "package": "@nrwl/jest", 108 | "name": "update-ts-jest-6-5-5" 109 | }, 110 | { 111 | "version": "12.4.0-beta.1", 112 | "cli": "nx", 113 | "description": "Add testEnvironment: 'jsdom' in web apps + libraries", 114 | "factory": "./src/migrations/update-12-4-0/add-test-environment-for-node", 115 | "package": "@nrwl/jest", 116 | "name": "support-jest-27" 117 | }, 118 | { 119 | "version": "12.4.0-beta.1", 120 | "cli": "nx", 121 | "description": "Support for Jest 27 via updating ts-jest + jest-preset-angular", 122 | "factory": "./src/migrations/update-12-4-0/update-jest-preset-angular", 123 | "package": "@nrwl/jest", 124 | "name": "update-ts-jest-and-jest-preset-angular" 125 | }, 126 | { 127 | "version": "12.6.0-beta.0", 128 | "cli": "nx", 129 | "description": "Uses `getJestProjects()` to populate projects array in root level `jest.config.js` file.", 130 | "factory": "./src/migrations/update-12-6-0/update-base-jest-config", 131 | "package": "@nrwl/jest", 132 | "name": "update-jest-config-to-use-util" 133 | }, 134 | { 135 | "cli": "nx", 136 | "version": "11.5.0-beta.0", 137 | "description": "Update project .eslintrc.json files to always use project level tsconfigs", 138 | "factory": "./src/migrations/update-11-5-0/always-use-project-level-tsconfigs-with-eslint", 139 | "package": "@nrwl/linter", 140 | "name": "always-use-project-level-tsconfigs-with-eslint" 141 | }, 142 | { 143 | "cli": "nx", 144 | "version": "12.4.0-beta.0", 145 | "description": "Remove ESLint parserOptions.project config if no rules requiring type-checking are in use", 146 | "factory": "./src/migrations/update-12-4-0/remove-eslint-project-config-if-no-type-checking-rules", 147 | "package": "@nrwl/linter", 148 | "name": "remove-eslint-project-config-if-no-type-checking-rules" 149 | }, 150 | { 151 | "cli": "nx", 152 | "version": "12.9.0-beta.0", 153 | "description": "Add outputs for caching", 154 | "factory": "./src/migrations/update-12-9-0/add-outputs", 155 | "package": "@nrwl/linter", 156 | "name": "add-outputs" 157 | }, 158 | { 159 | "cli": "nx", 160 | "version": "12.9.0-beta.0", 161 | "description": "Remove ESLint parserOptions.project config if no rules requiring type-checking are in use", 162 | "factory": "./src/migrations/update-12-4-0/remove-eslint-project-config-if-no-type-checking-rules", 163 | "package": "@nrwl/linter", 164 | "name": "remove-eslint-project-config-if-no-type-checking-rules-again" 165 | } 166 | ] 167 | } -------------------------------------------------------------------------------- /libs/rickandmorty/api/src/lib/api.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ApiService } from './api.service'; 4 | import { 5 | HttpClientTestingModule, 6 | HttpTestingController, 7 | } from '@angular/common/http/testing'; 8 | import { BASE_URL } from './tokens'; 9 | import { Location } from '@angular/common'; 10 | 11 | describe('RamApiService', () => { 12 | let service: ApiService; 13 | let httpController: HttpTestingController; 14 | let baseUrl: string; 15 | 16 | beforeEach(() => { 17 | TestBed.configureTestingModule({ 18 | imports: [HttpClientTestingModule], 19 | }); 20 | service = TestBed.inject(ApiService); 21 | httpController = TestBed.inject(HttpTestingController); 22 | baseUrl = TestBed.inject(BASE_URL); 23 | }); 24 | 25 | it('should be created', () => { 26 | expect(service).toBeTruthy(); 27 | }); 28 | 29 | it('should return an one character', async () => { 30 | const response = service.getCharacter(1).toPromise(); 31 | 32 | const req = httpController.expectOne( 33 | Location.joinWithSlash(baseUrl, 'character/1') 34 | ); 35 | 36 | req.flush({ 37 | id: 1, 38 | name: 'Rick Sanchez', 39 | status: 'Alive', 40 | species: 'Human', 41 | type: '', 42 | gender: 'Male', 43 | origin: { 44 | name: 'Earth', 45 | url: 'https://rickandmortyapi.com/api/location/1', 46 | }, 47 | location: { 48 | name: 'Earth', 49 | url: 'https://rickandmortyapi.com/api/location/20', 50 | }, 51 | image: 'https://rickandmortyapi.com/api/character/avatar/1.jpeg', 52 | episode: [ 53 | 'https://rickandmortyapi.com/api/episode/1', 54 | 'https://rickandmortyapi.com/api/episode/2', 55 | // ... 56 | ], 57 | url: 'https://rickandmortyapi.com/api/character/1', 58 | created: '2017-11-04T18:48:46.250Z', 59 | }); 60 | 61 | expect(await response).toEqual([ 62 | { 63 | created: expect.any(Date), 64 | episode: [1, 2], 65 | gender: 'Male', 66 | id: 1, 67 | image: 'https://rickandmortyapi.com/api/character/avatar/1.jpeg', 68 | location: { 69 | id: 20, 70 | name: 'Earth', 71 | }, 72 | name: 'Rick Sanchez', 73 | origin: { 74 | id: 1, 75 | name: 'Earth', 76 | }, 77 | species: 'Human', 78 | status: 'Alive', 79 | type: '', 80 | url: 'https://rickandmortyapi.com/api/character/1', 81 | }, 82 | ]); 83 | }); 84 | 85 | it('should return many characters', async () => { 86 | const response = service.getCharacter([1, 2]).toPromise(); 87 | 88 | const req = httpController.expectOne( 89 | Location.joinWithSlash(baseUrl, 'character/1,2') 90 | ); 91 | 92 | req.flush([ 93 | { 94 | id: 1, 95 | name: 'Rick Sanchez', 96 | status: 'Alive', 97 | species: 'Human', 98 | type: '', 99 | gender: 'Male', 100 | origin: { 101 | name: 'Earth (C-137)', 102 | url: 'https://rickandmortyapi.com/api/location/1', 103 | }, 104 | location: { 105 | name: 'Earth (Replacement Dimension)', 106 | url: 'https://rickandmortyapi.com/api/location/20', 107 | }, 108 | image: 'https://rickandmortyapi.com/api/character/avatar/1.jpeg', 109 | episode: [ 110 | 'https://rickandmortyapi.com/api/episode/1', 111 | 'https://rickandmortyapi.com/api/episode/2', 112 | // ... 113 | ], 114 | url: 'https://rickandmortyapi.com/api/character/1', 115 | created: '2017-11-04T18:48:46.250Z', 116 | }, 117 | { 118 | id: 183, 119 | name: 'Johnny Depp', 120 | status: 'Alive', 121 | species: 'Human', 122 | type: '', 123 | gender: 'Male', 124 | origin: { 125 | name: 'Earth (C-500A)', 126 | url: 'https://rickandmortyapi.com/api/location/23', 127 | }, 128 | location: { 129 | name: 'Earth (C-500A)', 130 | url: 'https://rickandmortyapi.com/api/location/23', 131 | }, 132 | image: 'https://rickandmortyapi.com/api/character/avatar/183.jpeg', 133 | episode: ['https://rickandmortyapi.com/api/episode/8'], 134 | url: 'https://rickandmortyapi.com/api/character/183', 135 | created: '2017-12-29T18:51:29.693Z', 136 | }, 137 | ]); 138 | 139 | expect(await response).toEqual([ 140 | { 141 | created: expect.any(Date), 142 | episode: [1, 2], 143 | gender: 'Male', 144 | id: 1, 145 | image: 'https://rickandmortyapi.com/api/character/avatar/1.jpeg', 146 | location: { 147 | id: 20, 148 | name: 'Earth (Replacement Dimension)', 149 | }, 150 | name: 'Rick Sanchez', 151 | origin: { 152 | id: 1, 153 | name: 'Earth (C-137)', 154 | }, 155 | species: 'Human', 156 | status: 'Alive', 157 | type: '', 158 | url: 'https://rickandmortyapi.com/api/character/1', 159 | }, 160 | { 161 | created: expect.any(Date), 162 | episode: [8], 163 | gender: 'Male', 164 | id: 183, 165 | image: 'https://rickandmortyapi.com/api/character/avatar/183.jpeg', 166 | location: { 167 | id: 23, 168 | name: 'Earth (C-500A)', 169 | }, 170 | name: 'Johnny Depp', 171 | origin: { 172 | id: 23, 173 | name: 'Earth (C-500A)', 174 | }, 175 | species: 'Human', 176 | status: 'Alive', 177 | type: '', 178 | url: 'https://rickandmortyapi.com/api/character/183', 179 | }, 180 | ]); 181 | }); 182 | 183 | it('should return a response with pagination', async () => { 184 | const response = service.getCharacter().toPromise(); 185 | 186 | const req = httpController.expectOne( 187 | Location.joinWithSlash(baseUrl, 'character') 188 | ); 189 | 190 | req.flush({ 191 | info: { 192 | count: 591, 193 | pages: 20, 194 | next: 'https://rickandmortyapi.com/api/character/?page=2', 195 | prev: null, 196 | }, 197 | results: [ 198 | { 199 | id: 1, 200 | name: 'Rick Sanchez', 201 | status: 'Alive', 202 | species: 'Human', 203 | type: '', 204 | gender: 'Male', 205 | origin: { 206 | name: 'Earth', 207 | url: 'https://rickandmortyapi.com/api/location/1', 208 | }, 209 | location: { 210 | name: 'Earth', 211 | url: 'https://rickandmortyapi.com/api/location/20', 212 | }, 213 | image: 'https://rickandmortyapi.com/api/character/avatar/1.jpeg', 214 | episode: [ 215 | 'https://rickandmortyapi.com/api/episode/1', 216 | 'https://rickandmortyapi.com/api/episode/2', 217 | // ... 218 | ], 219 | url: 'https://rickandmortyapi.com/api/character/1', 220 | created: '2017-11-04T18:48:46.250Z', 221 | }, 222 | ], 223 | }); 224 | 225 | expect(await response).toEqual({ 226 | info: { 227 | count: 591, 228 | next: { 229 | page: '2', 230 | }, 231 | pages: 20, 232 | prev: null, 233 | }, 234 | results: [ 235 | { 236 | created: expect.any(Date), 237 | episode: [1, 2], 238 | gender: 'Male', 239 | id: 1, 240 | image: 'https://rickandmortyapi.com/api/character/avatar/1.jpeg', 241 | location: { 242 | id: 20, 243 | name: 'Earth', 244 | }, 245 | name: 'Rick Sanchez', 246 | origin: { 247 | id: 1, 248 | name: 'Earth', 249 | }, 250 | species: 'Human', 251 | status: 'Alive', 252 | type: '', 253 | url: 'https://rickandmortyapi.com/api/character/1', 254 | }, 255 | ], 256 | }); 257 | }); 258 | 259 | afterEach(() => { 260 | httpController.verify(); 261 | }); 262 | }); 263 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "projects": { 4 | "ngx-ssr-cache": { 5 | "projectType": "library", 6 | "root": "libs/ngx-ssr/cache", 7 | "prefix": "ngx-ssr", 8 | "sourceRoot": "libs/ngx-ssr/cache/src", 9 | "architect": { 10 | "build": { 11 | "builder": "@nrwl/angular:package", 12 | "options": { 13 | "tsConfig": "libs/ngx-ssr/cache/tsconfig.lib.json", 14 | "project": "libs/ngx-ssr/cache/ng-package.json", 15 | "updateBuildableProjectDepsInPackageJson": false 16 | }, 17 | "configurations": { 18 | "production": { 19 | "tsConfig": "libs/ngx-ssr/cache/tsconfig.lib.prod.json" 20 | } 21 | } 22 | }, 23 | "lint": { 24 | "builder": "@nrwl/linter:eslint", 25 | "options": { 26 | "lintFilePatterns": [ 27 | "libs/ngx-ssr/cache/src/**/*.ts", 28 | "libs/ngx-ssr/cache/src/**/*.html" 29 | ] 30 | }, 31 | "outputs": ["{options.outputFile}"] 32 | }, 33 | "test": { 34 | "builder": "@nrwl/jest:jest", 35 | "outputs": ["coverage/libs/ngx-ssr/cache"], 36 | "options": { 37 | "jestConfig": "libs/ngx-ssr/cache/jest.config.js", 38 | "passWithNoTests": true 39 | } 40 | }, 41 | "deploy": { 42 | "builder": "@ng-builders/semrel:release", 43 | "options": { 44 | "npm": { 45 | "pkgRoot": "dist/libs/ngx-ssr/cache" 46 | } 47 | } 48 | } 49 | } 50 | }, 51 | "ngx-ssr-platform": { 52 | "projectType": "library", 53 | "root": "libs/ngx-ssr/platform", 54 | "sourceRoot": "libs/ngx-ssr/platform/src", 55 | "prefix": "ngx-ssr", 56 | "architect": { 57 | "build": { 58 | "builder": "@nrwl/angular:package", 59 | "options": { 60 | "tsConfig": "libs/ngx-ssr/platform/tsconfig.lib.json", 61 | "project": "libs/ngx-ssr/platform/ng-package.json", 62 | "updateBuildableProjectDepsInPackageJson": false 63 | }, 64 | "configurations": { 65 | "production": { 66 | "tsConfig": "libs/ngx-ssr/platform/tsconfig.lib.prod.json" 67 | } 68 | } 69 | }, 70 | "lint": { 71 | "builder": "@nrwl/linter:eslint", 72 | "options": { 73 | "lintFilePatterns": [ 74 | "libs/ngx-ssr/platform/src/**/*.ts", 75 | "libs/ngx-ssr/platform/src/**/*.html" 76 | ] 77 | }, 78 | "outputs": ["{options.outputFile}"] 79 | }, 80 | "test": { 81 | "builder": "@nrwl/jest:jest", 82 | "outputs": ["coverage/libs/ngx-ssr/platform"], 83 | "options": { 84 | "jestConfig": "libs/ngx-ssr/platform/jest.config.js", 85 | "passWithNoTests": true 86 | } 87 | }, 88 | "deploy": { 89 | "builder": "@ng-builders/semrel:release", 90 | "options": { 91 | "npm": { 92 | "pkgRoot": "dist/libs/ngx-ssr/platform" 93 | } 94 | } 95 | } 96 | } 97 | }, 98 | "ngx-ssr-timeout": { 99 | "projectType": "library", 100 | "root": "libs/ngx-ssr/timeout", 101 | "sourceRoot": "libs/ngx-ssr/timeout/src", 102 | "prefix": "ngx-ssr", 103 | "architect": { 104 | "build": { 105 | "builder": "@nrwl/angular:package", 106 | "options": { 107 | "tsConfig": "libs/ngx-ssr/timeout/tsconfig.lib.json", 108 | "project": "libs/ngx-ssr/timeout/ng-package.json", 109 | "updateBuildableProjectDepsInPackageJson": false 110 | }, 111 | "configurations": { 112 | "production": { 113 | "tsConfig": "libs/ngx-ssr/timeout/tsconfig.lib.prod.json" 114 | } 115 | } 116 | }, 117 | "lint": { 118 | "builder": "@nrwl/linter:eslint", 119 | "options": { 120 | "lintFilePatterns": [ 121 | "libs/ngx-ssr/timeout/src/**/*.ts", 122 | "libs/ngx-ssr/timeout/src/**/*.html" 123 | ] 124 | }, 125 | "outputs": ["{options.outputFile}"] 126 | }, 127 | "test": { 128 | "builder": "@nrwl/jest:jest", 129 | "outputs": ["coverage/libs/ngx-ssr/timeout"], 130 | "options": { 131 | "jestConfig": "libs/ngx-ssr/timeout/jest.config.js", 132 | "passWithNoTests": true 133 | } 134 | }, 135 | "deploy": { 136 | "builder": "@ng-builders/semrel:release", 137 | "options": { 138 | "npm": { 139 | "pkgRoot": "dist/libs/ngx-ssr/timeout" 140 | } 141 | } 142 | } 143 | } 144 | }, 145 | "rickandmorty": { 146 | "projectType": "application", 147 | "root": "apps/rickandmorty", 148 | "sourceRoot": "apps/rickandmorty/src", 149 | "prefix": "ngx-ssr", 150 | "architect": { 151 | "build": { 152 | "builder": "@angular-devkit/build-angular:browser", 153 | "options": { 154 | "outputPath": "dist/apps/rickandmorty/browser", 155 | "index": "apps/rickandmorty/src/index.html", 156 | "main": "apps/rickandmorty/src/main.ts", 157 | "polyfills": "apps/rickandmorty/src/polyfills.ts", 158 | "tsConfig": "apps/rickandmorty/tsconfig.app.json", 159 | "aot": true, 160 | "optimization": false, 161 | "assets": [ 162 | "apps/rickandmorty/src/favicon.ico", 163 | "apps/rickandmorty/src/assets", 164 | { 165 | "glob": "**/*", 166 | "input": "node_modules/@taiga-ui/icons/src", 167 | "output": "assets/taiga-ui/icons" 168 | } 169 | ], 170 | "styles": ["apps/rickandmorty/src/styles.less"], 171 | "scripts": [] 172 | }, 173 | "configurations": { 174 | "production": { 175 | "fileReplacements": [ 176 | { 177 | "replace": "apps/rickandmorty/src/environments/environment.ts", 178 | "with": "apps/rickandmorty/src/environments/environment.prod.ts" 179 | } 180 | ], 181 | "optimization": { 182 | "scripts": true, 183 | "styles": { 184 | "minify": true, 185 | "inlineCritical": false 186 | }, 187 | "fonts": true 188 | }, 189 | "outputHashing": "all", 190 | "sourceMap": false, 191 | "namedChunks": false, 192 | "extractLicenses": true, 193 | "vendorChunk": false, 194 | "buildOptimizer": true, 195 | "budgets": [ 196 | { 197 | "type": "initial", 198 | "maximumWarning": "2mb", 199 | "maximumError": "5mb" 200 | }, 201 | { 202 | "type": "anyComponentStyle", 203 | "maximumWarning": "6kb", 204 | "maximumError": "10kb" 205 | } 206 | ], 207 | "serviceWorker": true, 208 | "ngswConfigPath": "apps/rickandmorty/ngsw-config.json" 209 | } 210 | } 211 | }, 212 | "serve": { 213 | "builder": "@angular-devkit/build-angular:dev-server", 214 | "options": { 215 | "browserTarget": "rickandmorty:build" 216 | }, 217 | "configurations": { 218 | "production": { 219 | "browserTarget": "rickandmorty:build:production" 220 | } 221 | } 222 | }, 223 | "extract-i18n": { 224 | "builder": "@angular-devkit/build-angular:extract-i18n", 225 | "options": { 226 | "browserTarget": "rickandmorty:build" 227 | } 228 | }, 229 | "lint": { 230 | "builder": "@nrwl/linter:eslint", 231 | "options": { 232 | "lintFilePatterns": [ 233 | "apps/rickandmorty/src/**/*.ts", 234 | "apps/rickandmorty/src/**/*.html" 235 | ] 236 | }, 237 | "outputs": ["{options.outputFile}"] 238 | }, 239 | "test": { 240 | "builder": "@nrwl/jest:jest", 241 | "outputs": ["coverage/apps/rickandmorty"], 242 | "options": { 243 | "jestConfig": "apps/rickandmorty/jest.config.js", 244 | "passWithNoTests": true 245 | } 246 | }, 247 | "server": { 248 | "builder": "@angular-devkit/build-angular:server", 249 | "options": { 250 | "outputPath": "dist/apps/rickandmorty/server", 251 | "main": "apps/rickandmorty/server.ts", 252 | "tsConfig": "apps/rickandmorty/tsconfig.server.json", 253 | "optimization": false, 254 | "externalDependencies": [ 255 | "firebase", 256 | "@firebase/app", 257 | "@firebase/analytics", 258 | "@firebase/auth", 259 | "@firebase/component", 260 | "@firebase/database", 261 | "@firebase/firestore", 262 | "@firebase/functions", 263 | "@firebase/installations", 264 | "@firebase/messaging", 265 | "@firebase/storage", 266 | "@firebase/performance", 267 | "@firebase/remote-config", 268 | "@firebase/util" 269 | ] 270 | }, 271 | "configurations": { 272 | "production": { 273 | "outputHashing": "media", 274 | "fileReplacements": [ 275 | { 276 | "replace": "apps/rickandmorty/src/environments/environment.ts", 277 | "with": "apps/rickandmorty/src/environments/environment.prod.ts" 278 | } 279 | ], 280 | "sourceMap": false, 281 | "optimization": { 282 | "scripts": true, 283 | "styles": { 284 | "minify": true, 285 | "inlineCritical": false 286 | }, 287 | "fonts": true 288 | } 289 | } 290 | } 291 | }, 292 | "serve-ssr": { 293 | "builder": "@nguniversal/builders:ssr-dev-server", 294 | "options": { 295 | "browserTarget": "rickandmorty:build", 296 | "serverTarget": "rickandmorty:server" 297 | }, 298 | "configurations": { 299 | "production": { 300 | "browserTarget": "rickandmorty:build:production", 301 | "serverTarget": "rickandmorty:server:production" 302 | } 303 | } 304 | }, 305 | "deploy": { 306 | "builder": "@angular/fire:deploy", 307 | "options": { 308 | "ssr": true, 309 | "functionsNodeVersion": 12 310 | } 311 | } 312 | } 313 | }, 314 | "rickandmorty-api": { 315 | "projectType": "library", 316 | "root": "libs/rickandmorty/api", 317 | "sourceRoot": "libs/rickandmorty/api/src", 318 | "prefix": "ngx-ssr", 319 | "architect": { 320 | "build": { 321 | "builder": "@nrwl/angular:package", 322 | "options": { 323 | "tsConfig": "libs/rickandmorty/api/tsconfig.lib.json", 324 | "project": "libs/rickandmorty/api/ng-package.json" 325 | }, 326 | "configurations": { 327 | "production": { 328 | "tsConfig": "libs/rickandmorty/api/tsconfig.lib.prod.json" 329 | } 330 | } 331 | }, 332 | "lint": { 333 | "builder": "@nrwl/linter:eslint", 334 | "options": { 335 | "lintFilePatterns": [ 336 | "libs/rickandmorty/api/src/**/*.ts", 337 | "libs/rickandmorty/api/src/**/*.html" 338 | ] 339 | }, 340 | "outputs": ["{options.outputFile}"] 341 | }, 342 | "test": { 343 | "builder": "@nrwl/jest:jest", 344 | "outputs": ["coverage/libs/rickandmorty/api"], 345 | "options": { 346 | "jestConfig": "libs/rickandmorty/api/jest.config.js", 347 | "passWithNoTests": true 348 | } 349 | }, 350 | "deploy": { 351 | "builder": "@ng-builders/semrel:release", 352 | "options": { 353 | "npm": { 354 | "pkgRoot": "dist/libs/rickandmorty/api" 355 | } 356 | } 357 | } 358 | } 359 | }, 360 | "rickandmorty-e2e": { 361 | "root": "apps/rickandmorty-e2e", 362 | "sourceRoot": "apps/rickandmorty-e2e/src", 363 | "projectType": "application", 364 | "architect": { 365 | "e2e": { 366 | "builder": "@nrwl/cypress:cypress", 367 | "options": { 368 | "cypressConfig": "apps/rickandmorty-e2e/cypress.json", 369 | "tsConfig": "apps/rickandmorty-e2e/tsconfig.e2e.json", 370 | "devServerTarget": "rickandmorty:serve" 371 | }, 372 | "configurations": { 373 | "production": { 374 | "devServerTarget": "rickandmorty:serve:production" 375 | } 376 | } 377 | }, 378 | "lint": { 379 | "builder": "@nrwl/linter:eslint", 380 | "options": { 381 | "lintFilePatterns": ["apps/rickandmorty-e2e/**/*.{js,ts}"] 382 | }, 383 | "outputs": ["{options.outputFile}"] 384 | } 385 | } 386 | }, 387 | "rickandmorty-utils": { 388 | "projectType": "library", 389 | "root": "libs/rickandmorty/utils", 390 | "sourceRoot": "libs/rickandmorty/utils/src", 391 | "prefix": "ngx-ssr", 392 | "architect": { 393 | "lint": { 394 | "builder": "@nrwl/linter:eslint", 395 | "options": { 396 | "lintFilePatterns": [ 397 | "libs/rickandmorty/utils/src/**/*.ts", 398 | "libs/rickandmorty/utils/src/**/*.html" 399 | ] 400 | }, 401 | "outputs": ["{options.outputFile}"] 402 | }, 403 | "test": { 404 | "builder": "@nrwl/jest:jest", 405 | "outputs": ["coverage/libs/rickandmorty/utils"], 406 | "options": { 407 | "jestConfig": "libs/rickandmorty/utils/jest.config.js", 408 | "passWithNoTests": true 409 | } 410 | } 411 | } 412 | } 413 | }, 414 | "cli": { 415 | "defaultCollection": "@nrwl/angular" 416 | }, 417 | "schematics": { 418 | "@nrwl/angular": { 419 | "application": { 420 | "linter": "eslint" 421 | }, 422 | "library": { 423 | "linter": "eslint" 424 | }, 425 | "storybook-configuration": { 426 | "linter": "eslint" 427 | } 428 | }, 429 | "@nrwl/angular:application": { 430 | "unitTestRunner": "jest", 431 | "e2eTestRunner": "cypress" 432 | }, 433 | "@nrwl/angular:library": { 434 | "unitTestRunner": "jest" 435 | }, 436 | "@nrwl/angular:component": { 437 | "style": "less" 438 | } 439 | }, 440 | "defaultProject": "rickandmorty" 441 | } 442 | --------------------------------------------------------------------------------