├── .npmrc ├── .eslintignore ├── apps ├── ng │ ├── host │ │ ├── src │ │ │ ├── assets │ │ │ │ ├── .gitkeep │ │ │ │ └── module-federation.manifest.json │ │ │ ├── app │ │ │ │ ├── app.component.css │ │ │ │ ├── app.routes.ts │ │ │ │ ├── app.component.html │ │ │ │ ├── app.config.ts │ │ │ │ └── app.component.ts │ │ │ ├── favicon.ico │ │ │ ├── styles.css │ │ │ ├── bootstrap.ts │ │ │ ├── main.ts │ │ │ ├── test-setup.ts │ │ │ └── index.html │ │ ├── webpack.config.ts │ │ ├── tsconfig.editor.json │ │ ├── tsconfig.app.json │ │ ├── tsconfig.spec.json │ │ ├── module-federation.config.ts │ │ ├── jest.config.ts │ │ ├── tsconfig.json │ │ ├── .eslintrc.json │ │ └── project.json │ ├── remote │ │ ├── src │ │ │ ├── assets │ │ │ │ └── .gitkeep │ │ │ ├── main.ts │ │ │ ├── favicon.ico │ │ │ ├── styles.css │ │ │ ├── app │ │ │ │ ├── remote-entry │ │ │ │ │ ├── entry.routes.ts │ │ │ │ │ └── entry.component.ts │ │ │ │ ├── app.routes.ts │ │ │ │ └── app.config.ts │ │ │ ├── bootstrap.ts │ │ │ ├── test-setup.ts │ │ │ ├── index.html │ │ │ └── bootstrap-remote.ts │ │ ├── webpack.config.ts │ │ ├── tsconfig.editor.json │ │ ├── module-federation.config.ts │ │ ├── tsconfig.spec.json │ │ ├── tsconfig.app.json │ │ ├── webpack.prod.config.ts │ │ ├── jest.config.ts │ │ ├── tsconfig.json │ │ ├── .eslintrc.json │ │ └── project.json │ ├── host-e2e │ │ ├── src │ │ │ └── example.spec.ts │ │ ├── project.json │ │ ├── .eslintrc.json │ │ ├── tsconfig.json │ │ └── playwright.config.ts │ └── remote-e2e │ │ ├── src │ │ └── example.spec.ts │ │ ├── project.json │ │ ├── .eslintrc.json │ │ ├── tsconfig.json │ │ └── playwright.config.ts └── react │ ├── host │ ├── public │ │ ├── .gitkeep │ │ └── favicon.ico │ ├── src │ │ └── app │ │ │ ├── global.css │ │ │ ├── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── mexo-demo.tsx │ │ │ └── providers.tsx │ ├── index.d.ts │ ├── next-env.d.ts │ ├── specs │ │ └── index.spec.tsx │ ├── jest.config.ts │ ├── project.json │ ├── tsconfig.spec.json │ ├── next.config.js │ ├── .eslintrc.json │ └── tsconfig.json │ ├── remote │ ├── public │ │ ├── .gitkeep │ │ └── favicon.ico │ ├── src │ │ └── app │ │ │ ├── page.module.css │ │ │ ├── api │ │ │ └── hello │ │ │ │ └── route.ts │ │ │ ├── layout.tsx │ │ │ ├── global.css │ │ │ └── page.tsx │ ├── index.d.ts │ ├── next-env.d.ts │ ├── specs │ │ └── index.spec.tsx │ ├── jest.config.ts │ ├── project.json │ ├── tsconfig.spec.json │ ├── next.config.js │ ├── .eslintrc.json │ └── tsconfig.json │ ├── host-e2e │ ├── src │ │ └── example.spec.ts │ ├── project.json │ ├── tsconfig.json │ ├── .eslintrc.json │ └── playwright.config.ts │ └── remote-e2e │ ├── src │ └── example.spec.ts │ ├── project.json │ ├── tsconfig.json │ ├── .eslintrc.json │ └── playwright.config.ts ├── libs ├── react │ ├── src │ │ └── index.ts │ ├── remote │ │ ├── index.ts │ │ └── lib │ │ │ └── createApp.tsx │ ├── .babelrc │ ├── host │ │ ├── index.ts │ │ └── src │ │ │ ├── mexo-host-provider.tsx │ │ │ └── mexo-app.tsx │ ├── jest.config.ts │ ├── .eslintrc.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ ├── tsconfig.lib.json │ ├── package.json │ └── project.json ├── angular │ ├── src │ │ ├── index.ts │ │ └── test-setup.ts │ ├── host │ │ ├── ng-package.json │ │ ├── src │ │ │ ├── mexo-app │ │ │ │ ├── mexo-app.module.ts │ │ │ │ ├── mexo-app.directive.spec.ts │ │ │ │ └── mexo-app.directive.ts │ │ │ ├── tokens │ │ │ │ └── mexo-apps.ts │ │ │ ├── operators │ │ │ │ └── complete.ts │ │ │ ├── services │ │ │ │ ├── registry.service.ts │ │ │ │ └── registry.service.spec.ts │ │ │ └── mexo-host.module.ts │ │ └── index.ts │ ├── remote │ │ ├── ng-package.json │ │ ├── index.ts │ │ └── src │ │ │ ├── enable-prod-mode.ts │ │ │ └── create-app.ts │ ├── ng-package.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── jest.config.ts │ ├── package.json │ ├── .eslintrc.json │ └── project.json └── core │ ├── testing │ └── src │ │ ├── index.ts │ │ ├── public_api.ts │ │ └── application.mock.ts │ ├── src │ ├── test-setup.ts │ ├── lib │ │ ├── models │ │ │ ├── default-props-type.ts │ │ │ ├── entity.ts │ │ │ ├── registration-options.ts │ │ │ ├── events.ts │ │ │ ├── lifecycle.ts │ │ │ └── application.ts │ │ ├── helpers │ │ │ ├── get-entity.ts │ │ │ ├── bootstrap-app.ts │ │ │ ├── get-app.ts │ │ │ ├── register-app.ts │ │ │ ├── preload-entity.ts │ │ │ ├── replace-apps.ts │ │ │ ├── register-entity.ts │ │ │ ├── construct-and-bootstrap-app.ts │ │ │ ├── load-app.spec.ts │ │ │ ├── register-app.spec.ts │ │ │ ├── construct-app.ts │ │ │ ├── get-app.spec.ts │ │ │ ├── load-app.ts │ │ │ ├── get-entity.spec.ts │ │ │ ├── replace-apps.spec.ts │ │ │ ├── register-entity.spec.ts │ │ │ ├── load-entity.ts │ │ │ ├── load-entity.spec.ts │ │ │ └── construct-and-bootstrap-app.spec.ts │ │ └── registry.ts │ └── index.ts │ ├── .babelrc │ ├── ng-package.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ ├── package.json │ ├── tsconfig.lib.json │ ├── jest.config.ts │ ├── .eslintrc.json │ └── project.json ├── babel.config.json ├── commitlint.config.js ├── jest.preset.js ├── .prettierignore ├── jest.config.ts ├── .vscode └── extensions.json ├── .editorconfig ├── tools └── tsconfig.tools.json ├── project.json ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── pr-checks.yml │ ├── publish.yml │ └── ci.yml ├── .gitignore ├── .verdaccio └── config.yml ├── scripts └── sync-readmes.js ├── .prettierrc.js ├── tsconfig.base.json ├── .eslintrc.json ├── CONTRIBUTING.md ├── nx.json ├── CODE_OF_CONDUCT.md ├── README.md ├── package.json └── LICENSE /.npmrc: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /apps/ng/host/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/react/host/public/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/react/remote/public/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/ng/host/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/ng/remote/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/react/host/src/app/global.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libs/react/src/index.ts: -------------------------------------------------------------------------------- 1 | export const _a = 1; 2 | -------------------------------------------------------------------------------- /libs/angular/src/index.ts: -------------------------------------------------------------------------------- 1 | export const _a = ''; 2 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "babelrcRoots": ["*"] 3 | } 4 | -------------------------------------------------------------------------------- /apps/react/remote/src/app/page.module.css: -------------------------------------------------------------------------------- 1 | .page { 2 | } 3 | -------------------------------------------------------------------------------- /libs/react/remote/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/createApp'; 2 | -------------------------------------------------------------------------------- /libs/core/testing/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './public_api'; 2 | -------------------------------------------------------------------------------- /libs/core/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /libs/angular/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /libs/core/testing/src/public_api.ts: -------------------------------------------------------------------------------- 1 | export * from './application.mock'; 2 | -------------------------------------------------------------------------------- /apps/ng/remote/src/main.ts: -------------------------------------------------------------------------------- 1 | import('./bootstrap').catch((err) => console.error(err)); 2 | -------------------------------------------------------------------------------- /libs/core/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@nx/react/babel"], 3 | "plugins": [] 4 | } 5 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /libs/react/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@nx/react/babel"], 3 | "plugins": [] 4 | } 5 | -------------------------------------------------------------------------------- /libs/angular/host/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "lib": { 3 | "entryFile": "index.ts" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /libs/angular/remote/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "lib": { 3 | "entryFile": "index.ts" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /apps/ng/host/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/platacard/mexo/HEAD/apps/ng/host/src/favicon.ico -------------------------------------------------------------------------------- /libs/core/src/lib/models/default-props-type.ts: -------------------------------------------------------------------------------- 1 | export type DefaultPropsType = Record; 2 | -------------------------------------------------------------------------------- /libs/react/host/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src/mexo-app'; 2 | export * from './src/mexo-host-provider'; 3 | -------------------------------------------------------------------------------- /apps/ng/host/src/assets/module-federation.manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "ng-remote": "http://localhost:4202" 3 | } 4 | -------------------------------------------------------------------------------- /apps/ng/host/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /apps/ng/remote/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/platacard/mexo/HEAD/apps/ng/remote/src/favicon.ico -------------------------------------------------------------------------------- /apps/ng/remote/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /libs/angular/remote/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src/enable-prod-mode'; 2 | export * from './src/create-app'; 3 | -------------------------------------------------------------------------------- /apps/react/host/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/platacard/mexo/HEAD/apps/react/host/public/favicon.ico -------------------------------------------------------------------------------- /apps/react/remote/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/platacard/mexo/HEAD/apps/react/remote/public/favicon.ico -------------------------------------------------------------------------------- /apps/ng/host/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Route } from '@angular/router'; 2 | 3 | export const appRoutes: Route[] = []; 4 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nx/jest/preset').default; 2 | 3 | module.exports = { 4 | ...nxPreset, 5 | }; 6 | -------------------------------------------------------------------------------- /libs/core/src/lib/models/entity.ts: -------------------------------------------------------------------------------- 1 | export interface EntityConstructor { 2 | new (name: string, props?: T): K; 3 | } 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .angular 2 | dist 3 | **/coverage/** 4 | **/node_modules/** 5 | **/schematics/*/files/**/* 6 | package-lock.json 7 | 8 | /.nx/cache -------------------------------------------------------------------------------- /apps/ng/remote/src/app/remote-entry/entry.routes.ts: -------------------------------------------------------------------------------- 1 | import { Route } from '@angular/router'; 2 | 3 | export const remoteRoutes: Route[] = []; 4 | -------------------------------------------------------------------------------- /apps/react/remote/src/app/api/hello/route.ts: -------------------------------------------------------------------------------- 1 | export async function GET(request: Request) { 2 | return new Response('Hello, from API!'); 3 | } 4 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest'; 2 | 3 | import { getJestProjects } from '@nx/jest'; 4 | 5 | const config: Config = { projects: getJestProjects() }; 6 | 7 | export default config; 8 | -------------------------------------------------------------------------------- /libs/core/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/libs/core", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/ng/host/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import { withModuleFederation } from '@nx/angular/module-federation'; 2 | 3 | import config from './module-federation.config'; 4 | 5 | export default withModuleFederation(config); 6 | -------------------------------------------------------------------------------- /apps/ng/remote/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import { withModuleFederation } from '@nx/angular/module-federation'; 2 | 3 | import config from './module-federation.config'; 4 | 5 | export default withModuleFederation(config); 6 | -------------------------------------------------------------------------------- /apps/react/host/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | declare module '*.svg' { 3 | const content: any; 4 | export const ReactComponent: any; 5 | export default content; 6 | } 7 | -------------------------------------------------------------------------------- /apps/ng/host/tsconfig.editor.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*.ts"], 4 | "compilerOptions": {}, 5 | "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"] 6 | } 7 | -------------------------------------------------------------------------------- /apps/ng/remote/tsconfig.editor.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*.ts"], 4 | "compilerOptions": {}, 5 | "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"] 6 | } 7 | -------------------------------------------------------------------------------- /apps/react/remote/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | declare module '*.svg' { 3 | const content: any; 4 | export const ReactComponent: any; 5 | export default content; 6 | } 7 | -------------------------------------------------------------------------------- /apps/react/host/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/react/remote/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /libs/angular/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/libs/angular", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | }, 7 | "allowedNonPeerDependencies": ["@mexo/core"] 8 | } 9 | -------------------------------------------------------------------------------- /apps/ng/remote/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Route } from '@angular/router'; 2 | 3 | export const appRoutes: Route[] = [ 4 | { 5 | path: '', 6 | loadChildren: () => 7 | import('./remote-entry/entry.routes').then((m) => m.remoteRoutes), 8 | }, 9 | ]; 10 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "angular.ng-template", 5 | "esbenp.prettier-vscode", 6 | "firsttris.vscode-jest-runner", 7 | "dbaeumer.vscode-eslint", 8 | "ms-playwright.playwright" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /apps/ng/host-e2e/src/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test('has title', async ({ page }) => { 4 | await page.goto('/'); 5 | 6 | // Expect h1 to contain a substring. 7 | expect(await page.locator('h1').innerText()).toContain('Welcome'); 8 | }); 9 | -------------------------------------------------------------------------------- /apps/ng/remote-e2e/src/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test('has title', async ({ page }) => { 4 | await page.goto('/'); 5 | 6 | // Expect h1 to contain a substring. 7 | expect(await page.locator('h1').innerText()).toContain('Welcome'); 8 | }); 9 | -------------------------------------------------------------------------------- /apps/react/host-e2e/src/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test('has title', async ({ page }) => { 4 | await page.goto('/'); 5 | 6 | // Expect h1 to contain a substring. 7 | expect(await page.locator('h1').innerText()).toContain('Welcome'); 8 | }); 9 | -------------------------------------------------------------------------------- /apps/ng/remote/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig } from '@angular/core'; 2 | import { provideRouter } from '@angular/router'; 3 | 4 | import { appRoutes } from './app.routes'; 5 | 6 | export const appConfig: ApplicationConfig = { 7 | providers: [provideRouter(appRoutes)], 8 | }; 9 | -------------------------------------------------------------------------------- /apps/react/remote-e2e/src/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test('has title', async ({ page }) => { 4 | await page.goto('/'); 5 | 6 | // Expect h1 to contain a substring. 7 | expect(await page.locator('h1').innerText()).toContain('Welcome'); 8 | }); 9 | -------------------------------------------------------------------------------- /libs/angular/host/src/mexo-app/mexo-app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { MexoAppDirective } from './mexo-app.directive'; 4 | 5 | @NgModule({ 6 | declarations: [MexoAppDirective], 7 | exports: [MexoAppDirective], 8 | }) 9 | export class MexoAppModule {} 10 | -------------------------------------------------------------------------------- /apps/ng/host/src/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | 3 | import { AppComponent } from './app/app.component'; 4 | import { appConfig } from './app/app.config'; 5 | 6 | bootstrapApplication(AppComponent, appConfig).catch((err) => 7 | console.error(err), 8 | ); 9 | -------------------------------------------------------------------------------- /apps/ng/remote/module-federation.config.ts: -------------------------------------------------------------------------------- 1 | import { ModuleFederationConfig } from '@nx/webpack'; 2 | 3 | const config: ModuleFederationConfig = { 4 | name: 'ng-remote', 5 | exposes: { 6 | './App': 'apps/ng/remote/src/bootstrap-remote.ts', 7 | }, 8 | }; 9 | 10 | export default config; 11 | -------------------------------------------------------------------------------- /apps/react/host/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { MexoDemo } from './mexo-demo'; 2 | import { Providers } from './providers'; 3 | 4 | export default function Index() { 5 | return ( 6 | 7 |

Welcome to react-host!

8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /apps/ng/host/src/main.ts: -------------------------------------------------------------------------------- 1 | import { setRemoteDefinitions } from '@nx/angular/mf'; 2 | 3 | fetch('/assets/module-federation.manifest.json') 4 | .then((res) => res.json()) 5 | .then((definitions) => setRemoteDefinitions(definitions)) 6 | .then(() => import('./bootstrap').catch((err) => console.error(err))); 7 | -------------------------------------------------------------------------------- /libs/angular/host/src/tokens/mexo-apps.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | import { AppRegistrationOptions } from '@mexo/core'; 4 | 5 | export const MEXO_APPS = new InjectionToken( 6 | 'Mexo apps', 7 | { 8 | factory: () => [], 9 | }, 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/angular/remote/src/enable-prod-mode.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode as angularEnableProdMode } from '@angular/core'; 2 | 3 | /** 4 | * Disable Angular's development mode safely 5 | */ 6 | export function enableProdMode() { 7 | try { 8 | angularEnableProdMode(); 9 | } catch (e) { 10 | // 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /libs/react/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'react', 4 | preset: '../../jest.preset.js', 5 | transform: { 6 | '^.+\\.[tj]sx?$': 'babel-jest', 7 | }, 8 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 9 | coverageDirectory: '../../coverage/libs/react', 10 | }; 11 | -------------------------------------------------------------------------------- /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 | "importHelpers": false 10 | }, 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /libs/core/src/lib/helpers/get-entity.ts: -------------------------------------------------------------------------------- 1 | import { EntityConstructor } from '../models/entity'; 2 | import { loadedEntityRegistry } from '../registry'; 3 | 4 | export async function getEntity( 5 | entityName: string, 6 | ): Promise | null> { 7 | return loadedEntityRegistry.get(entityName) || null; 8 | } 9 | -------------------------------------------------------------------------------- /libs/core/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "declarationMap": false, 5 | "target": "ES2022", 6 | "useDefineForClassFields": false 7 | }, 8 | "angularCompilerOptions": { 9 | "enableIvy": false 10 | }, 11 | "exclude": ["jest.config.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /apps/ng/remote/src/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | 3 | import { appConfig } from './app/app.config'; 4 | import { RemoteEntryComponent } from './app/remote-entry/entry.component'; 5 | 6 | bootstrapApplication(RemoteEntryComponent, appConfig).catch((err) => 7 | console.error(err), 8 | ); 9 | -------------------------------------------------------------------------------- /libs/angular/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "declarationMap": false, 5 | "target": "ES2022", 6 | "useDefineForClassFields": false 7 | }, 8 | "angularCompilerOptions": { 9 | "enableIvy": false 10 | }, 11 | "exclude": ["jest.config.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /apps/ng/host/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | 3 | // @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment 4 | globalThis.ngJest = { 5 | testEnvironmentOptions: { 6 | errorOnUnknownElements: true, 7 | errorOnUnknownProperties: true, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /apps/ng/remote/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | 3 | // @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment 4 | globalThis.ngJest = { 5 | testEnvironmentOptions: { 6 | errorOnUnknownElements: true, 7 | errorOnUnknownProperties: true, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /libs/angular/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", "**/*.test.ts", "**/*.d.ts", "jest.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /libs/core/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/ng/host/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc", 5 | "types": [], 6 | "target": "ES2020" 7 | }, 8 | "files": ["src/main.ts"], 9 | "include": ["src/**/*.d.ts"], 10 | "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /libs/angular/host/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src/mexo-app/mexo-app.directive'; 2 | export * from './src/mexo-app/mexo-app.module'; 3 | 4 | export * from './src/mexo-host.module'; 5 | 6 | export * from './src/services/registry.service'; 7 | 8 | export * from './src/tokens/mexo-apps'; 9 | 10 | export { AppRegistrationOptions } from '@mexo/core'; 11 | -------------------------------------------------------------------------------- /apps/ng/host-e2e/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-host-e2e", 3 | "$schema": "../../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "application", 5 | "sourceRoot": "apps/ng/host-e2e/src", 6 | "implicitDependencies": ["ng-host"], 7 | "// targets": "to see all targets run: nx show project ng-host-e2e --web", 8 | "targets": {} 9 | } 10 | -------------------------------------------------------------------------------- /apps/react/host-e2e/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-host-e2e", 3 | "$schema": "../../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "apps/react/host-e2e/src", 5 | "tags": [], 6 | "implicitDependencies": ["react-host"], 7 | "// targets": "to see all targets run: nx show project react-host-e2e --web", 8 | "targets": {} 9 | } 10 | -------------------------------------------------------------------------------- /apps/react/host/specs/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { render } from '@testing-library/react'; 4 | 5 | import Page from '../src/app/page'; 6 | 7 | describe('Page', () => { 8 | it('should render successfully', () => { 9 | const { baseElement } = render(); 10 | expect(baseElement).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /apps/react/remote/specs/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { render } from '@testing-library/react'; 4 | 5 | import Page from '../src/app/page'; 6 | 7 | describe('Page', () => { 8 | it('should render successfully', () => { 9 | const { baseElement } = render(); 10 | expect(baseElement).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /libs/core/src/lib/helpers/bootstrap-app.ts: -------------------------------------------------------------------------------- 1 | import { Application } from '../models/application'; 2 | 3 | export async function bootstrapApp>( 4 | app: Application, 5 | selector: string | Element, 6 | props?: T, 7 | ): Promise> { 8 | await app.bootstrap(selector, props); 9 | 10 | return app; 11 | } 12 | -------------------------------------------------------------------------------- /apps/ng/remote-e2e/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-remote-e2e", 3 | "$schema": "../../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "application", 5 | "sourceRoot": "apps/ng/remote-e2e/src", 6 | "implicitDependencies": ["ng-remote"], 7 | "// targets": "to see all targets run: nx show project ng-remote-e2e --web", 8 | "targets": {} 9 | } 10 | -------------------------------------------------------------------------------- /apps/react/remote-e2e/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-remote-e2e", 3 | "$schema": "../../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "apps/react/remote-e2e/src", 5 | "tags": [], 6 | "implicitDependencies": ["react-remote"], 7 | "// targets": "to see all targets run: nx show project react-remote-e2e --web", 8 | "targets": {} 9 | } 10 | -------------------------------------------------------------------------------- /libs/angular/host/src/operators/complete.ts: -------------------------------------------------------------------------------- 1 | import { finalize, startWith, switchMap } from 'rxjs/operators'; 2 | 3 | import { NEVER } from 'rxjs'; 4 | 5 | export const complete = (fn: (value: T) => void) => 6 | switchMap((value: T) => 7 | NEVER.pipe( 8 | startWith(value), 9 | finalize(() => { 10 | fn(value); 11 | }), 12 | ), 13 | ); 14 | -------------------------------------------------------------------------------- /project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mexo", 3 | "$schema": "node_modules/nx/schemas/project-schema.json", 4 | "targets": { 5 | "local-registry": { 6 | "executor": "@nx/js:verdaccio", 7 | "options": { 8 | "port": 4873, 9 | "config": ".verdaccio/config.yml", 10 | "storage": "tmp/local-registry/storage" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # ================================================================================== 2 | # platacard/mexo codeowners 3 | # ================================================================================== 4 | # More info: https://help.github.com/articles/about-codeowners/ 5 | # 6 | 7 | * @IKatsuba 8 | # will be requested for review when someone opens a pull request 9 | -------------------------------------------------------------------------------- /apps/ng/host/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ng-host 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /libs/core/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": [ 10 | "**/*.spec.ts", 11 | "**/*.test.ts", 12 | "**/*.mock.ts", 13 | "**/*.d.ts", 14 | "jest.config.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /libs/angular/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 | "compilerOptions": { 17 | "target": "es2020" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /libs/core/src/lib/helpers/get-app.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConstructor } from '../models/application'; 2 | import { DefaultPropsType } from '../models/default-props-type'; 3 | import { getEntity } from './get-entity'; 4 | 5 | export async function getApp( 6 | appName: string, 7 | ): Promise | null> { 8 | return getEntity(appName); 9 | } 10 | -------------------------------------------------------------------------------- /libs/core/src/lib/helpers/register-app.ts: -------------------------------------------------------------------------------- 1 | import { DefaultPropsType } from '../models/default-props-type'; 2 | import { AppRegistrationOptions } from '../models/registration-options'; 3 | import { registerEntity } from './register-entity'; 4 | 5 | export function registerApp( 6 | options: AppRegistrationOptions, 7 | ) { 8 | registerEntity(options); 9 | } 10 | -------------------------------------------------------------------------------- /apps/react/host/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './global.css'; 2 | 3 | export const metadata = { 4 | title: 'Welcome to react-host', 5 | description: 'Generated by create-nx-workspace', 6 | }; 7 | 8 | export default function RootLayout({ 9 | children, 10 | }: { 11 | children: React.ReactNode; 12 | }) { 13 | return ( 14 | 15 | {children} 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /apps/ng/host/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc", 5 | "module": "commonjs", 6 | "target": "es2016", 7 | "types": ["jest", "node"] 8 | }, 9 | "files": ["src/test-setup.ts"], 10 | "include": [ 11 | "jest.config.ts", 12 | "src/**/*.test.ts", 13 | "src/**/*.spec.ts", 14 | "src/**/*.d.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /apps/react/remote/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './global.css'; 2 | 3 | export const metadata = { 4 | title: 'Welcome to react-remote', 5 | description: 'Generated by create-nx-workspace', 6 | }; 7 | 8 | export default function RootLayout({ 9 | children, 10 | }: { 11 | children: React.ReactNode; 12 | }) { 13 | return ( 14 | 15 | {children} 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /apps/ng/remote/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ng-remote 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/ng/remote/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc", 5 | "module": "commonjs", 6 | "target": "es2016", 7 | "types": ["jest", "node"] 8 | }, 9 | "files": ["src/test-setup.ts"], 10 | "include": [ 11 | "jest.config.ts", 12 | "src/**/*.test.ts", 13 | "src/**/*.spec.ts", 14 | "src/**/*.d.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /libs/core/src/lib/helpers/preload-entity.ts: -------------------------------------------------------------------------------- 1 | import { loadEntity } from './load-entity'; 2 | 3 | export function preloadEntity(name: string) { 4 | if (!window) { 5 | return; 6 | } 7 | 8 | const loadEntityFn = () => { 9 | loadEntity(name); 10 | }; 11 | 12 | if ('requestIdleCallback' in window) { 13 | requestIdleCallback(loadEntityFn); 14 | } else { 15 | setTimeout(loadEntityFn); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /libs/react/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:@nx/react", "../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /apps/ng/host/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |

Host App

2 | 3 |
4 |

5 | Apps (mexoApp) 6 | 7 |

8 | 9 | @for (app of apps$ | async; track app) { 10 |
16 | } 17 |
18 | -------------------------------------------------------------------------------- /apps/react/host/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'react-host', 4 | preset: '../../../jest.preset.js', 5 | transform: { 6 | '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest', 7 | '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/next/babel'] }], 8 | }, 9 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 10 | coverageDirectory: '../../../coverage/apps/react/host', 11 | }; 12 | -------------------------------------------------------------------------------- /libs/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "allowJs": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "files": [], 10 | "include": [], 11 | "references": [ 12 | { 13 | "path": "./tsconfig.lib.json" 14 | }, 15 | { 16 | "path": "./tsconfig.spec.json" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /apps/ng/remote/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc", 5 | "types": [], 6 | "target": "ES2020" 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/bootstrap-remote.ts", 11 | "src/app/remote-entry/entry.routes.ts" 12 | ], 13 | "include": ["src/**/*.d.ts"], 14 | "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /apps/react/remote/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'react-remote', 4 | preset: '../../../jest.preset.js', 5 | transform: { 6 | '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest', 7 | '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/next/babel'] }], 8 | }, 9 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 10 | coverageDirectory: '../../../coverage/apps/react/remote', 11 | }; 12 | -------------------------------------------------------------------------------- /apps/ng/remote/src/bootstrap-remote.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js'; 2 | 3 | import { bootstrapApplication } from '@angular/platform-browser'; 4 | 5 | import { createApp } from '@mexo/angular/remote'; 6 | 7 | import { appConfig } from './app/app.config'; 8 | import { RemoteEntryComponent } from './app/remote-entry/entry.component'; 9 | 10 | export const remoteBootstrap = createApp( 11 | () => bootstrapApplication(RemoteEntryComponent, appConfig), 12 | 'app-ng-remote-entry', 13 | ); 14 | -------------------------------------------------------------------------------- /apps/react/host/src/app/mexo-demo.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | 5 | import { MexoApp } from '@mexo/react/host'; 6 | 7 | export const MexoDemo = () => { 8 | const [visible, setVisible] = React.useState(false); 9 | 10 | return ( 11 | <> 12 | 15 | {visible && } 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /apps/react/host-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "outDir": "../../../dist/out-tsc", 6 | "module": "commonjs", 7 | "sourceMap": false 8 | }, 9 | "include": [ 10 | "**/*.ts", 11 | "**/*.js", 12 | "playwright.config.ts", 13 | "src/**/*.spec.ts", 14 | "src/**/*.spec.js", 15 | "src/**/*.test.ts", 16 | "src/**/*.test.js", 17 | "src/**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /libs/react/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "**/*.spec.ts", 10 | "**/*.test.ts", 11 | "**/*.spec.tsx", 12 | "**/*.test.tsx", 13 | "**/*.spec.js", 14 | "**/*.test.js", 15 | "**/*.spec.jsx", 16 | "**/*.test.jsx", 17 | "**/*.d.ts", 18 | "jest.config.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /apps/react/host/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-host", 3 | "$schema": "../../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "apps/react/host", 5 | "projectType": "application", 6 | "tags": [], 7 | "targets": { 8 | "todo:test": { 9 | "executor": "@nx/jest:jest", 10 | "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 11 | "options": { 12 | "jestConfig": "apps/react/host/jest.config.ts" 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/react/remote-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "outDir": "../../../dist/out-tsc", 6 | "module": "commonjs", 7 | "sourceMap": false 8 | }, 9 | "include": [ 10 | "**/*.ts", 11 | "**/*.js", 12 | "playwright.config.ts", 13 | "src/**/*.spec.ts", 14 | "src/**/*.spec.js", 15 | "src/**/*.test.ts", 16 | "src/**/*.test.js", 17 | "src/**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /apps/react/remote/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-remote", 3 | "$schema": "../../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "apps/react/remote", 5 | "projectType": "application", 6 | "tags": [], 7 | "targets": { 8 | "test": { 9 | "executor": "@nx/jest:jest", 10 | "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 11 | "options": { 12 | "jestConfig": "apps/react/remote/jest.config.ts" 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /libs/angular/host/src/services/registry.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { AppRegistrationOptions, registerEntity } from '@mexo/core'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class RegistryService { 9 | registerMany(apps: ReadonlyArray) { 10 | apps.forEach((app) => this.register(app)); 11 | } 12 | 13 | register(options: AppRegistrationOptions) { 14 | registerEntity(options); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /libs/core/src/lib/models/registration-options.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConstructor } from './application'; 2 | import { DefaultPropsType } from './default-props-type'; 3 | 4 | export interface EntityRegistrationOptions { 5 | name: string; 6 | load: () => PromiseLike; 7 | preload?: boolean; 8 | } 9 | 10 | export interface AppRegistrationOptions< 11 | T extends DefaultPropsType = DefaultPropsType, 12 | > extends EntityRegistrationOptions> { 13 | props?: T; 14 | } 15 | -------------------------------------------------------------------------------- /libs/react/host/src/mexo-host-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useEffect } from 'react'; 4 | 5 | import { AppRegistrationOptions, registerApp } from '@mexo/core'; 6 | 7 | export const MexoHostProvider = ({ 8 | apps, 9 | children, 10 | }: { 11 | apps: AppRegistrationOptions[]; 12 | children: React.ReactNode; 13 | }) => { 14 | useEffect(() => { 15 | apps.forEach((app) => { 16 | registerApp(app); 17 | }); 18 | }, [apps]); 19 | 20 | return children; 21 | }; 22 | -------------------------------------------------------------------------------- /libs/core/src/lib/helpers/replace-apps.ts: -------------------------------------------------------------------------------- 1 | import { Application } from '../models/application'; 2 | import { DefaultPropsType } from '../models/default-props-type'; 3 | import { constructAndBootstrapApp } from './construct-and-bootstrap-app'; 4 | 5 | export async function replaceApps< 6 | T extends DefaultPropsType = DefaultPropsType, 7 | >(from: Application, to: string, props?: T): Promise> { 8 | from.destroy(); 9 | 10 | return constructAndBootstrapApp(to, from.container, props); 11 | } 12 | -------------------------------------------------------------------------------- /libs/core/src/lib/helpers/register-entity.ts: -------------------------------------------------------------------------------- 1 | import { EntityRegistrationOptions } from '../models/registration-options'; 2 | import { entityOptionsRegistry } from '../registry'; 3 | import { preloadEntity } from './preload-entity'; 4 | 5 | export function registerEntity< 6 | T, 7 | K extends EntityRegistrationOptions = EntityRegistrationOptions, 8 | >(options: K): void { 9 | entityOptionsRegistry.set(options.name, options); 10 | 11 | if (options.preload) { 12 | preloadEntity(options.name); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /apps/ng/remote/webpack.prod.config.ts: -------------------------------------------------------------------------------- 1 | import { withModuleFederation } from '@nx/angular/module-federation'; 2 | 3 | import config from './module-federation.config'; 4 | 5 | export default withModuleFederation({ 6 | ...config, 7 | /* 8 | * Remote overrides for production. 9 | * Each entry is a pair of a unique name and the URL where it is deployed. 10 | * 11 | * e.g. 12 | * remotes: [ 13 | * ['app1', 'https://app1.example.com'], 14 | * ['app2', 'https://app2.example.com'], 15 | * ] 16 | */ 17 | }); 18 | -------------------------------------------------------------------------------- /apps/ng/host-e2e/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:playwright/recommended", "../../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | }, 17 | { 18 | "files": ["src/**/*.{ts,js,tsx,jsx}"], 19 | "rules": {} 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /apps/ng/remote-e2e/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:playwright/recommended", "../../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | }, 17 | { 18 | "files": ["src/**/*.{ts,js,tsx,jsx}"], 19 | "rules": {} 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /apps/ng/remote/src/app/remote-entry/entry.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { Component, signal } from '@angular/core'; 3 | 4 | @Component({ 5 | standalone: true, 6 | imports: [CommonModule], 7 | selector: 'app-ng-remote-entry', 8 | template: ` 9 |

Ng Remote Entry

10 |
11 | {{ count() }} 12 | 13 | 14 |
15 | `, 16 | }) 17 | export class RemoteEntryComponent { 18 | count = signal(0); 19 | } 20 | -------------------------------------------------------------------------------- /apps/react/host-e2e/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:playwright/recommended", "../../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | }, 17 | { 18 | "files": ["src/**/*.{ts,js,tsx,jsx}"], 19 | "rules": {} 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /apps/react/remote-e2e/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:playwright/recommended", "../../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | }, 17 | { 18 | "files": ["src/**/*.{ts,js,tsx,jsx}"], 19 | "rules": {} 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /libs/core/testing/src/application.mock.ts: -------------------------------------------------------------------------------- 1 | import { Application } from '../../src/lib/models/application'; 2 | import { MexoMessageEvent } from '../../src/lib/models/events'; 3 | import { MexoLifecycleEvent } from '../../src/lib/models/lifecycle'; 4 | 5 | export class ApplicationMock extends Application { 6 | navigate(_url: string, _props?: unknown): Promise { 7 | return Promise.resolve(undefined); 8 | } 9 | 10 | send(_msg: string | MexoMessageEvent): Promise { 11 | return Promise.resolve(undefined); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/react/host/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"], 7 | "jsx": "react" 8 | }, 9 | "include": [ 10 | "jest.config.ts", 11 | "src/**/*.test.ts", 12 | "src/**/*.spec.ts", 13 | "src/**/*.test.tsx", 14 | "src/**/*.spec.tsx", 15 | "src/**/*.test.js", 16 | "src/**/*.spec.js", 17 | "src/**/*.test.jsx", 18 | "src/**/*.spec.jsx", 19 | "src/**/*.d.ts" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /apps/react/remote/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"], 7 | "jsx": "react" 8 | }, 9 | "include": [ 10 | "jest.config.ts", 11 | "src/**/*.test.ts", 12 | "src/**/*.spec.ts", 13 | "src/**/*.test.tsx", 14 | "src/**/*.spec.tsx", 15 | "src/**/*.test.js", 16 | "src/**/*.spec.js", 17 | "src/**/*.test.jsx", 18 | "src/**/*.spec.jsx", 19 | "src/**/*.d.ts" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /libs/react/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node"], 6 | "declaration": true 7 | }, 8 | "files": [ 9 | "../../node_modules/@nx/react/typings/cssmodule.d.ts", 10 | "../../node_modules/@nx/react/typings/image.d.ts" 11 | ], 12 | "exclude": [ 13 | "**/*.spec.ts", 14 | "**/*.test.ts", 15 | "**/*.spec.tsx", 16 | "**/*.test.tsx", 17 | "jest.config.ts" 18 | ], 19 | "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] 20 | } 21 | -------------------------------------------------------------------------------- /libs/angular/host/src/services/registry.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 2 | import { TestBed } from '@angular/core/testing'; 3 | 4 | import { RegistryService } from './registry.service'; 5 | 6 | describe('RegistryService', () => { 7 | let service: RegistryService; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | imports: [HttpClientTestingModule], 12 | }); 13 | service = TestBed.inject(RegistryService); 14 | }); 15 | 16 | it('should be created', () => { 17 | expect(service).toBeTruthy(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /libs/core/src/lib/helpers/construct-and-bootstrap-app.ts: -------------------------------------------------------------------------------- 1 | import { Application } from '../models/application'; 2 | import { DefaultPropsType } from '../models/default-props-type'; 3 | import { bootstrapApp } from './bootstrap-app'; 4 | import { constructApp } from './construct-app'; 5 | 6 | export async function constructAndBootstrapApp< 7 | T extends DefaultPropsType = DefaultPropsType, 8 | >( 9 | appName: string, 10 | selector: string | Element, 11 | props?: T, 12 | ): Promise> { 13 | const app = await constructApp(appName); 14 | 15 | return bootstrapApp(app, selector, props); 16 | } 17 | -------------------------------------------------------------------------------- /apps/react/host/next.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-var-requires 4 | const { composePlugins, withNx } = require('@nx/next'); 5 | 6 | /** 7 | * @type {import('@nx/next/plugins/with-nx').WithNxOptions} 8 | **/ 9 | const nextConfig = { 10 | nx: { 11 | // Set this to true if you would like to use SVGR 12 | // See: https://github.com/gregberge/svgr 13 | svgr: false, 14 | }, 15 | }; 16 | 17 | const plugins = [ 18 | // Add more Next.js plugins to this list if needed. 19 | withNx, 20 | ]; 21 | 22 | module.exports = composePlugins(...plugins)(nextConfig); 23 | -------------------------------------------------------------------------------- /apps/react/remote/next.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-var-requires 4 | const { composePlugins, withNx } = require('@nx/next'); 5 | 6 | /** 7 | * @type {import('@nx/next/plugins/with-nx').WithNxOptions} 8 | **/ 9 | const nextConfig = { 10 | nx: { 11 | // Set this to true if you would like to use SVGR 12 | // See: https://github.com/gregberge/svgr 13 | svgr: false, 14 | }, 15 | }; 16 | 17 | const plugins = [ 18 | // Add more Next.js plugins to this list if needed. 19 | withNx, 20 | ]; 21 | 22 | module.exports = composePlugins(...plugins)(nextConfig); 23 | -------------------------------------------------------------------------------- /libs/core/src/lib/helpers/load-app.spec.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationMock } from '@mexo/core/testing'; 2 | 3 | import { loadApp } from './load-app'; 4 | import { registerApp } from './register-app'; 5 | 6 | describe('loadApp', () => { 7 | beforeEach(async () => { 8 | registerApp({ 9 | name: 'appMock1', 10 | async load() { 11 | return ApplicationMock; 12 | }, 13 | }); 14 | }); 15 | 16 | it('should load an app constructor', async () => { 17 | expect.assertions(1); 18 | 19 | await loadApp('appMock1'); 20 | 21 | expect(await loadApp('appMock1')).toStrictEqual(ApplicationMock); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /libs/core/src/lib/helpers/register-app.spec.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationMock } from '@mexo/core/testing'; 2 | 3 | import { entityOptionsRegistry } from '../registry'; 4 | import { registerApp } from './register-app'; 5 | 6 | describe('registerApp', () => { 7 | beforeEach(async () => { 8 | registerApp({ 9 | name: 'appMock', 10 | async load() { 11 | return ApplicationMock; 12 | }, 13 | }); 14 | }); 15 | 16 | it('should register an app', async () => { 17 | expect.assertions(1); 18 | 19 | expect(entityOptionsRegistry.get('appMock')).toEqual({ 20 | name: 'appMock', 21 | load: expect.any(Function), 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /libs/core/src/lib/helpers/construct-app.ts: -------------------------------------------------------------------------------- 1 | import { Application } from '../models/application'; 2 | import { DefaultPropsType } from '../models/default-props-type'; 3 | import { AppRegistrationOptions } from '../models/registration-options'; 4 | import { entityOptionsRegistry } from '../registry'; 5 | import { loadApp } from './load-app'; 6 | 7 | export async function constructApp< 8 | T extends DefaultPropsType = DefaultPropsType, 9 | >(appName: string): Promise> { 10 | const AppConstructor = await loadApp(appName); 11 | 12 | return new AppConstructor( 13 | appName, 14 | entityOptionsRegistry.get>(appName)?.props, 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /apps/ng/host/module-federation.config.ts: -------------------------------------------------------------------------------- 1 | import { ModuleFederationConfig } from '@nx/webpack'; 2 | 3 | const config: ModuleFederationConfig = { 4 | name: 'ng-host', 5 | /** 6 | * To use a remote that does not exist in your current Nx Workspace 7 | * You can use the tuple-syntax to define your remote 8 | * 9 | * remotes: [['my-external-remote', 'https://nx-angular-remote.netlify.app']] 10 | * 11 | * You _may_ need to add a `remotes.d.ts` file to your `src/` folder declaring the external remote for tsc, with the 12 | * following content: 13 | * 14 | * declare module 'my-external-remote'; 15 | * 16 | */ 17 | remotes: [], 18 | }; 19 | 20 | export default config; 21 | -------------------------------------------------------------------------------- /apps/ng/host-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "outDir": "../../../dist/out-tsc", 6 | "module": "commonjs", 7 | "sourceMap": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "noImplicitOverride": true, 11 | "noPropertyAccessFromIndexSignature": true, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true 14 | }, 15 | "include": [ 16 | "**/*.ts", 17 | "**/*.js", 18 | "playwright.config.ts", 19 | "src/**/*.spec.ts", 20 | "src/**/*.spec.js", 21 | "src/**/*.test.ts", 22 | "src/**/*.test.js", 23 | "src/**/*.d.ts" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /apps/ng/remote-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "outDir": "../../../dist/out-tsc", 6 | "module": "commonjs", 7 | "sourceMap": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "noImplicitOverride": true, 11 | "noPropertyAccessFromIndexSignature": true, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true 14 | }, 15 | "include": [ 16 | "**/*.ts", 17 | "**/*.js", 18 | "playwright.config.ts", 19 | "src/**/*.spec.ts", 20 | "src/**/*.spec.js", 21 | "src/**/*.test.ts", 22 | "src/**/*.test.js", 23 | "src/**/*.d.ts" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /libs/angular/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "target": "ES2022", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"], 11 | "useDefineForClassFields": false 12 | }, 13 | "angularCompilerOptions": { 14 | "compilationMode": "partial", 15 | "skipTemplateCodegen": true, 16 | "strictMetadataEmit": true, 17 | "enableResourceInlining": true 18 | }, 19 | "exclude": [ 20 | "src/test-setup.ts", 21 | "**/*.spec.ts", 22 | "**/*.test.ts", 23 | "jest.config.ts" 24 | ], 25 | "include": ["**/*.ts"] 26 | } 27 | -------------------------------------------------------------------------------- /libs/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mexo/core", 3 | "version": "1.2.0", 4 | "keywords": [ 5 | "microfrontends", 6 | "single-page-application", 7 | "framework", 8 | "react", 9 | "angular", 10 | "routing", 11 | "microservices" 12 | ], 13 | "homepage": "https://github.com/platacard/mexo", 14 | "repository": "https://github.com/platacard/mexo", 15 | "license": "Apache-2.0", 16 | "contributors": [ 17 | "Igor Katsuba " 18 | ], 19 | "dependencies": { 20 | "tslib": "2.6.2" 21 | }, 22 | "peerDependencies": {}, 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "authors": [ 27 | "Igor Katsuba " 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /libs/core/src/lib/helpers/get-app.spec.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationMock } from '@mexo/core/testing'; 2 | 3 | import { getApp } from './get-app'; 4 | import { loadApp } from './load-app'; 5 | import { registerApp } from './register-app'; 6 | 7 | describe('getApp', () => { 8 | beforeEach(async () => { 9 | registerApp({ 10 | name: 'appMock', 11 | async load() { 12 | return ApplicationMock; 13 | }, 14 | }); 15 | 16 | await loadApp('appMock'); 17 | }); 18 | 19 | it('should return an app constructor', async () => { 20 | expect.assertions(1); 21 | 22 | const appConstructor = await getApp('appMock'); 23 | 24 | expect(appConstructor).toStrictEqual(ApplicationMock); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /libs/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mexo/react", 3 | "version": "1.2.0", 4 | "keywords": [ 5 | "microfrontends", 6 | "single-page-application", 7 | "framework", 8 | "angular", 9 | "react", 10 | "routing", 11 | "microservices" 12 | ], 13 | "homepage": "https://github.com/platacard/mexo", 14 | "repository": "https://github.com/platacard/mexo", 15 | "license": "Apache-2.0", 16 | "contributors": [ 17 | "Igor Katsuba " 18 | ], 19 | "dependencies": { 20 | "tslib": "2.6.2" 21 | }, 22 | "peerDependencies": {}, 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "authors": [ 27 | "Igor Katsuba " 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /libs/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/helpers/construct-and-bootstrap-app'; 2 | export * from './lib/helpers/bootstrap-app'; 3 | export * from './lib/helpers/construct-app'; 4 | export * from './lib/helpers/get-app'; 5 | export * from './lib/helpers/get-entity'; 6 | export * from './lib/helpers/load-app'; 7 | export * from './lib/helpers/load-entity'; 8 | export * from './lib/helpers/register-app'; 9 | export * from './lib/helpers/register-entity'; 10 | export * from './lib/helpers/replace-apps'; 11 | 12 | export * from './lib/models/application'; 13 | export * from './lib/models/entity'; 14 | export * from './lib/models/events'; 15 | export * from './lib/models/lifecycle'; 16 | export * from './lib/models/registration-options'; 17 | -------------------------------------------------------------------------------- /libs/core/src/lib/helpers/load-app.ts: -------------------------------------------------------------------------------- 1 | import { Application, ApplicationConstructor } from '../models/application'; 2 | import { DefaultPropsType } from '../models/default-props-type'; 3 | import { loadedEntityRegistry } from '../registry'; 4 | import { getApp } from './get-app'; 5 | import { loadEntity } from './load-entity'; 6 | 7 | export async function loadApp( 8 | appName: string, 9 | ): Promise> { 10 | const appConstructor = await getApp(appName); 11 | 12 | if (appConstructor) { 13 | loadedEntityRegistry.set(appName, appConstructor); 14 | 15 | return appConstructor; 16 | } 17 | 18 | return loadEntity>(appName); 19 | } 20 | -------------------------------------------------------------------------------- /libs/core/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "target": "ES2022", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"], 11 | "useDefineForClassFields": false 12 | }, 13 | "angularCompilerOptions": { 14 | "compilationMode": "partial", 15 | "skipTemplateCodegen": true, 16 | "strictMetadataEmit": true, 17 | "enableResourceInlining": true 18 | }, 19 | "exclude": [ 20 | "src/test-setup.ts", 21 | "**/*.spec.ts", 22 | "**/*.test.ts", 23 | "**/*.mock.ts", 24 | "jest.config.ts" 25 | ], 26 | "include": ["**/*.ts"] 27 | } 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "\U0001F31F" 5 | labels: enhancement, needverification 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 | -------------------------------------------------------------------------------- /.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 | /.angular 37 | 38 | # System Files 39 | .DS_Store 40 | Thumbs.db 41 | 42 | .angular 43 | .nx 44 | 45 | # Next.js 46 | .next 47 | out -------------------------------------------------------------------------------- /.verdaccio/config.yml: -------------------------------------------------------------------------------- 1 | # path to a directory with all packages 2 | storage: ../tmp/local-registry/storage 3 | 4 | # a list of other known repositories we can talk to 5 | uplinks: 6 | npmjs: 7 | url: https://registry.npmjs.org/ 8 | maxage: 60m 9 | 10 | packages: 11 | '**': 12 | # give all users (including non-authenticated users) full access 13 | # because it is a local registry 14 | access: $all 15 | publish: $all 16 | unpublish: $all 17 | 18 | # if package is not available locally, proxy requests to npm registry 19 | proxy: npmjs 20 | 21 | # log settings 22 | logs: 23 | type: stdout 24 | format: pretty 25 | level: warn 26 | 27 | publish: 28 | allow_offline: true # set offline to true to allow publish offline 29 | -------------------------------------------------------------------------------- /libs/core/src/lib/helpers/get-entity.spec.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationMock } from '@mexo/core/testing'; 2 | 3 | import { getEntity } from './get-entity'; 4 | import { loadEntity } from './load-entity'; 5 | import { registerEntity } from './register-entity'; 6 | 7 | describe('getEntity', () => { 8 | beforeEach(async () => { 9 | registerEntity({ 10 | name: 'appMock', 11 | async load() { 12 | return ApplicationMock; 13 | }, 14 | }); 15 | 16 | await loadEntity('appMock'); 17 | }); 18 | 19 | it('should return an app constructor', async () => { 20 | expect.assertions(1); 21 | 22 | const appConstructor = await getEntity('appMock'); 23 | 24 | expect(appConstructor).toStrictEqual(ApplicationMock); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /apps/react/host/src/app/providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { loadRemoteModule, setRemoteDefinitions } from '@nx/angular/mf'; 4 | 5 | import { MexoHostProvider } from '@mexo/react/host'; 6 | 7 | setRemoteDefinitions({ 8 | 'ng-remote': 'http://localhost:4202/remoteEntry.mjs', 9 | }); 10 | 11 | export function Providers({ children }: { children: React.ReactNode }) { 12 | return ( 13 | 18 | loadRemoteModule('ng-remote', './App').then( 19 | (m) => m.remoteBootstrap, 20 | ), 21 | preload: true, 22 | }, 23 | ]} 24 | > 25 | {children} 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /scripts/sync-readmes.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { argv } = require('process'); 3 | 4 | const packageName = argv.length >= 2 ? argv[2] : ''; 5 | 6 | if (!packageName) { 7 | throw 'Название пакета не передано в sync-readmes'; 8 | } 9 | 10 | const README_PATH = 'README.md'; 11 | 12 | copyExtraFiles(); 13 | 14 | function copyExtraFiles() { 15 | if (!fs.existsSync(README_PATH)) { 16 | throw new Error('README.md does not exist'); 17 | } else { 18 | copyReadmeIntoLibFolder(README_PATH, packageName); 19 | } 20 | } 21 | 22 | function copyReadmeIntoLibFolder(srcPath, lib) { 23 | try { 24 | const fileBody = fs.readFileSync(srcPath).toString(); 25 | 26 | fs.writeFileSync(`dist/libs/${lib}/${README_PATH}`, fileBody); 27 | } catch (_) {} 28 | } 29 | -------------------------------------------------------------------------------- /apps/react/host/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:@nx/react-typescript", 4 | "next", 5 | "next/core-web-vitals", 6 | "../../../.eslintrc.json" 7 | ], 8 | "ignorePatterns": ["!**/*", ".next/**/*"], 9 | "overrides": [ 10 | { 11 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 12 | "rules": { 13 | "@next/next/no-html-link-for-pages": ["error", "apps/react/host/pages"] 14 | } 15 | }, 16 | { 17 | "files": ["*.ts", "*.tsx"], 18 | "rules": {} 19 | }, 20 | { 21 | "files": ["*.js", "*.jsx"], 22 | "rules": {} 23 | }, 24 | { 25 | "files": ["*.spec.ts", "*.spec.tsx", "*.spec.js", "*.spec.jsx"], 26 | "env": { 27 | "jest": true 28 | } 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /apps/ng/host/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'ng-host', 4 | preset: '../../../jest.preset.js', 5 | setupFilesAfterEnv: ['/src/test-setup.ts'], 6 | coverageDirectory: '../../../coverage/apps/ng/host', 7 | transform: { 8 | '^.+\\.(ts|mjs|js|html)$': [ 9 | 'jest-preset-angular', 10 | { 11 | tsconfig: '/tsconfig.spec.json', 12 | stringifyContentPathRegex: '\\.(html|svg)$', 13 | }, 14 | ], 15 | }, 16 | transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], 17 | snapshotSerializers: [ 18 | 'jest-preset-angular/build/serializers/no-ng-attributes', 19 | 'jest-preset-angular/build/serializers/ng-snapshot', 20 | 'jest-preset-angular/build/serializers/html-comment', 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /apps/ng/remote/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'ng-remote', 4 | preset: '../../../jest.preset.js', 5 | setupFilesAfterEnv: ['/src/test-setup.ts'], 6 | coverageDirectory: '../../../coverage/apps/ng/remote', 7 | transform: { 8 | '^.+\\.(ts|mjs|js|html)$': [ 9 | 'jest-preset-angular', 10 | { 11 | tsconfig: '/tsconfig.spec.json', 12 | stringifyContentPathRegex: '\\.(html|svg)$', 13 | }, 14 | ], 15 | }, 16 | transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], 17 | snapshotSerializers: [ 18 | 'jest-preset-angular/build/serializers/no-ng-attributes', 19 | 'jest-preset-angular/build/serializers/ng-snapshot', 20 | 'jest-preset-angular/build/serializers/html-comment', 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /apps/react/remote/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:@nx/react-typescript", 4 | "next", 5 | "next/core-web-vitals", 6 | "../../../.eslintrc.json" 7 | ], 8 | "ignorePatterns": ["!**/*", ".next/**/*"], 9 | "overrides": [ 10 | { 11 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 12 | "rules": { 13 | "@next/next/no-html-link-for-pages": [ 14 | "error", 15 | "apps/react/remote/pages" 16 | ] 17 | } 18 | }, 19 | { 20 | "files": ["*.ts", "*.tsx"], 21 | "rules": {} 22 | }, 23 | { 24 | "files": ["*.js", "*.jsx"], 25 | "rules": {} 26 | }, 27 | { 28 | "files": ["*.spec.ts", "*.spec.tsx", "*.spec.js", "*.spec.jsx"], 29 | "env": { 30 | "jest": true 31 | } 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | plugins: [ 4 | 'prettier-plugin-packagejson', 5 | '@ianvs/prettier-plugin-sort-imports', 6 | ], 7 | importOrder: [ 8 | '^react', 9 | '^(@angular/localize/(.*)|zone.js/(.*))$', 10 | '', 11 | '', 12 | '', 13 | '', 14 | '', 15 | '^(@angular/(.*)|rxjs)$', 16 | '^(@nx/(.*))$', 17 | '', 18 | `^@(mexo)/(.*)$`, 19 | '', 20 | '^@(steppe-ui|admin-hub|diftech|ngx-bridges|toolhub)/(.*)$', 21 | '', 22 | '^[./]', 23 | ], 24 | importOrderParserPlugins: ['decorators-legacy', 'typescript', 'jsx'], 25 | overrides: [ 26 | { 27 | files: ['*.md'], 28 | options: { 29 | proseWrap: 'always', 30 | printWidth: 80, 31 | }, 32 | }, 33 | ], 34 | }; 35 | -------------------------------------------------------------------------------- /libs/core/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest'; 2 | 3 | const config: Config = { 4 | displayName: 'core', 5 | preset: '../../jest.preset.js', 6 | setupFilesAfterEnv: ['/src/test-setup.ts'], 7 | coverageDirectory: '../../coverage/libs/core', 8 | transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], 9 | snapshotSerializers: [ 10 | 'jest-preset-angular/build/serializers/no-ng-attributes', 11 | 'jest-preset-angular/build/serializers/ng-snapshot', 12 | 'jest-preset-angular/build/serializers/html-comment', 13 | ], 14 | transform: { 15 | '^.+\\.(ts|js|mjs|html)$': [ 16 | 'jest-preset-angular', 17 | { 18 | stringifyContentPathRegex: '\\.(html|svg)$', 19 | 20 | tsconfig: '/tsconfig.spec.json', 21 | }, 22 | ], 23 | }, 24 | }; 25 | 26 | export default config; 27 | -------------------------------------------------------------------------------- /libs/angular/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest'; 2 | 3 | const config: Config = { 4 | displayName: 'angular', 5 | preset: '../../jest.preset.js', 6 | setupFilesAfterEnv: ['/src/test-setup.ts'], 7 | coverageDirectory: '../../coverage/libs/core', 8 | transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], 9 | snapshotSerializers: [ 10 | 'jest-preset-angular/build/serializers/no-ng-attributes', 11 | 'jest-preset-angular/build/serializers/ng-snapshot', 12 | 'jest-preset-angular/build/serializers/html-comment', 13 | ], 14 | transform: { 15 | '^.+\\.(ts|js|mjs|html)$': [ 16 | 'jest-preset-angular', 17 | { 18 | stringifyContentPathRegex: '\\.(html|svg)$', 19 | 20 | tsconfig: '/tsconfig.spec.json', 21 | }, 22 | ], 23 | }, 24 | }; 25 | 26 | export default config; 27 | -------------------------------------------------------------------------------- /libs/angular/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mexo/angular", 3 | "version": "1.2.0", 4 | "keywords": [ 5 | "microfrontends", 6 | "single-page-application", 7 | "framework", 8 | "angular", 9 | "react", 10 | "microservices", 11 | "routing" 12 | ], 13 | "homepage": "https://github.com/platacard/mexo", 14 | "repository": "https://github.com/platacard/mexo", 15 | "license": "Apache-2.0", 16 | "contributors": [ 17 | "Igor Katsuba " 18 | ], 19 | "dependencies": { 20 | "tslib": "2.6.2" 21 | }, 22 | "peerDependencies": { 23 | "@angular/common": ">=17.0.0", 24 | "@angular/core": ">=17.0.0", 25 | "@angular/router": ">=17.0.0" 26 | }, 27 | "publishConfig": { 28 | "access": "public" 29 | }, 30 | "authors": [ 31 | "Igor Katsuba " 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /libs/core/src/lib/models/events.ts: -------------------------------------------------------------------------------- 1 | import { Application } from './application'; 2 | 3 | export class MexoEvent { 4 | target: Application | null = null; 5 | constructor(public readonly type: string) {} 6 | } 7 | 8 | // todo: подумать над сообщениями 9 | export class MexoMessageEvent extends MexoEvent { 10 | static isMessageEvent(event: MexoEvent): event is MexoMessageEvent { 11 | return ( 12 | event && 13 | event.constructor && 14 | event.constructor.name === MexoMessageEvent.name 15 | ); 16 | } 17 | } 18 | 19 | // todo: подумать над ивентами роутинга 20 | export class MexoNavigationEvent extends MexoEvent { 21 | static isNavigationEvent(event: MexoEvent): event is MexoNavigationEvent { 22 | return ( 23 | event && 24 | event.constructor && 25 | event.constructor.name === MexoNavigationEvent.name 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/ng/host/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "useDefineForClassFields": false, 5 | "forceConsistentCasingInFileNames": true, 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true 11 | }, 12 | "files": [], 13 | "include": [], 14 | "references": [ 15 | { 16 | "path": "./tsconfig.editor.json" 17 | }, 18 | { 19 | "path": "./tsconfig.app.json" 20 | }, 21 | { 22 | "path": "./tsconfig.spec.json" 23 | } 24 | ], 25 | "extends": "../../../tsconfig.base.json", 26 | "angularCompilerOptions": { 27 | "enableI18nLegacyMessageIdFormat": false, 28 | "strictInjectionParameters": true, 29 | "strictInputAccessModifiers": true, 30 | "strictTemplates": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /libs/core/src/lib/registry.ts: -------------------------------------------------------------------------------- 1 | import { EntityConstructor } from './models/entity'; 2 | import { EntityRegistrationOptions } from './models/registration-options'; 3 | 4 | export class MexoRegistry { 5 | private _map = new Map(); 6 | 7 | clear(): void { 8 | this._map.clear(); 9 | } 10 | 11 | get(key: K): E | undefined { 12 | return this._map.get(key) as E; 13 | } 14 | 15 | set(key: K, value: E): void { 16 | this._map.set(key, value); 17 | } 18 | } 19 | 20 | export const entityOptionsRegistry = new MexoRegistry< 21 | string, 22 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 23 | EntityRegistrationOptions 24 | >(); 25 | 26 | export const loadedEntityRegistry = new MexoRegistry< 27 | string, 28 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 29 | EntityConstructor 30 | >(); 31 | -------------------------------------------------------------------------------- /apps/ng/remote/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "useDefineForClassFields": false, 5 | "forceConsistentCasingInFileNames": true, 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true 11 | }, 12 | "files": [], 13 | "include": [], 14 | "references": [ 15 | { 16 | "path": "./tsconfig.editor.json" 17 | }, 18 | { 19 | "path": "./tsconfig.app.json" 20 | }, 21 | { 22 | "path": "./tsconfig.spec.json" 23 | } 24 | ], 25 | "extends": "../../../tsconfig.base.json", 26 | "angularCompilerOptions": { 27 | "enableI18nLegacyMessageIdFormat": false, 28 | "strictInjectionParameters": true, 29 | "strictInputAccessModifiers": true, 30 | "strictTemplates": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /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 | "strict": true, 11 | "importHelpers": true, 12 | "target": "es2015", 13 | "module": "esnext", 14 | "lib": ["es2017", "dom"], 15 | "skipLibCheck": true, 16 | "skipDefaultLibCheck": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "@mexo/angular": ["libs/angular/src/index.ts"], 20 | "@mexo/angular/*": ["libs/angular/*"], 21 | "@mexo/core": ["libs/core/src/index.ts"], 22 | "@mexo/core/testing": ["libs/core/testing/src/index.ts"], 23 | "@mexo/react": ["libs/react/src/index.ts"], 24 | "@mexo/react/*": ["libs/react/*"] 25 | } 26 | }, 27 | "exclude": ["node_modules", "tmp"] 28 | } 29 | -------------------------------------------------------------------------------- /apps/ng/host/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig, importProvidersFrom } from '@angular/core'; 2 | import { provideRouter } from '@angular/router'; 3 | import { loadRemoteModule, setRemoteDefinitions } from '@nx/angular/mf'; 4 | 5 | import { MexoHostModule } from '@mexo/angular/host'; 6 | 7 | import { appRoutes } from './app.routes'; 8 | 9 | setRemoteDefinitions({ 10 | 'ng-remote': 'http://localhost:4202/remoteEntry.mjs', 11 | }); 12 | 13 | export const appConfig: ApplicationConfig = { 14 | providers: [ 15 | provideRouter(appRoutes), 16 | importProvidersFrom( 17 | MexoHostModule.register({ 18 | apps: [ 19 | { 20 | name: 'ng-remote', 21 | load: () => 22 | loadRemoteModule('ng-remote', './App').then( 23 | (m) => m.remoteBootstrap, 24 | ), 25 | }, 26 | ], 27 | }), 28 | ), 29 | ], 30 | }; 31 | -------------------------------------------------------------------------------- /apps/react/host/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "allowJs": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "incremental": true, 14 | "plugins": [ 15 | { 16 | "name": "next" 17 | } 18 | ], 19 | "types": ["jest", "node"] 20 | }, 21 | "include": [ 22 | "**/*.ts", 23 | "**/*.tsx", 24 | "**/*.js", 25 | "**/*.jsx", 26 | "../../../apps/react/host/.next/types/**/*.ts", 27 | "../../../dist/apps/react/host/.next/types/**/*.ts", 28 | "next-env.d.ts", 29 | ".next/types/**/*.ts" 30 | ], 31 | "exclude": ["node_modules", "jest.config.ts", "**/*.spec.ts", "**/*.test.ts"] 32 | } 33 | -------------------------------------------------------------------------------- /apps/react/remote/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "allowJs": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "incremental": true, 14 | "plugins": [ 15 | { 16 | "name": "next" 17 | } 18 | ], 19 | "types": ["jest", "node"] 20 | }, 21 | "include": [ 22 | "**/*.ts", 23 | "**/*.tsx", 24 | "**/*.js", 25 | "**/*.jsx", 26 | "../../../apps/react/remote/.next/types/**/*.ts", 27 | "../../../dist/apps/react/remote/.next/types/**/*.ts", 28 | "next-env.d.ts", 29 | ".next/types/**/*.ts" 30 | ], 31 | "exclude": ["node_modules", "jest.config.ts", "**/*.spec.ts", "**/*.test.ts"] 32 | } 33 | -------------------------------------------------------------------------------- /apps/ng/host/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@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": "app", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "app", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /apps/ng/remote/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@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": "app", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "app", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /libs/core/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@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": "mexo", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "mexo", 25 | "style": "kebab-case" 26 | } 27 | ], 28 | "quotes": "off" 29 | } 30 | }, 31 | { 32 | "files": ["*.html"], 33 | "extends": ["plugin:@nx/angular-template"], 34 | "rules": {} 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /libs/angular/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@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": "", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "", 25 | "style": "kebab-case" 26 | } 27 | ], 28 | "@angular-eslint/no-input-rename": 0 29 | } 30 | }, 31 | { 32 | "files": ["*.html"], 33 | "extends": ["plugin:@nx/angular-template"], 34 | "rules": {} 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /libs/core/src/lib/models/lifecycle.ts: -------------------------------------------------------------------------------- 1 | import { MexoEvent } from './events'; 2 | 3 | export enum LifecycleEventTypes { 4 | bootstrapped = 'bootstrapped', 5 | destroyed = 'destroyed', 6 | } 7 | 8 | export class MexoLifecycleEvent extends MexoEvent { 9 | static isLifecycleEvent(event: MexoEvent): event is MexoLifecycleEvent { 10 | return ( 11 | event && 12 | event.constructor && 13 | event.constructor.name === MexoLifecycleEvent.name 14 | ); 15 | } 16 | 17 | static bootstrapped(): MexoLifecycleEvent { 18 | return new MexoLifecycleEvent(LifecycleEventTypes.bootstrapped); 19 | } 20 | 21 | static isBootstrappedEvent(event: MexoLifecycleEvent): boolean { 22 | return event.type === LifecycleEventTypes.bootstrapped; 23 | } 24 | 25 | static destroyed(): MexoLifecycleEvent { 26 | return new MexoLifecycleEvent(LifecycleEventTypes.destroyed); 27 | } 28 | 29 | static isDestroyedEvent(event: MexoLifecycleEvent): boolean { 30 | return event.type === LifecycleEventTypes.destroyed; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/pr-checks.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | 6 | permissions: 7 | actions: read 8 | contents: read 9 | 10 | jobs: 11 | main: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | # Connect your workspace on nx.app and uncomment this to enable task distribution. 19 | # The "--stop-agents-after" is optional, but allows idle agents to shut down once the "e2e-ci" targets have been requested 20 | # - run: npx nx-cloud start-ci-run --distribute-on="5 linux-medium-js" --stop-agents-after="e2e-ci" 21 | 22 | # Cache node_modules 23 | - uses: actions/setup-node@v3 24 | with: 25 | node-version: 20 26 | cache: 'npm' 27 | - run: npm ci 28 | - uses: nrwl/nx-set-shas@v4 29 | 30 | # Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud 31 | # - run: npx nx-cloud record -- echo Hello World 32 | - run: npx nx affected -t lint test build 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "\U0001F41B" 5 | labels: bug, needverification 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. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /apps/ng/host/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { AsyncPipe } from '@angular/common'; 2 | import { ChangeDetectorRef, Component } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | import { BehaviorSubject } from 'rxjs'; 5 | 6 | import { MexoHostModule } from '@mexo/angular/host'; 7 | import { Application, MexoLifecycleEvent } from '@mexo/core'; 8 | 9 | @Component({ 10 | standalone: true, 11 | imports: [RouterModule, AsyncPipe, MexoHostModule], 12 | selector: 'app-root', 13 | templateUrl: './app.component.html', 14 | styleUrl: './app.component.css', 15 | }) 16 | export class AppComponent { 17 | apps$ = new BehaviorSubject(['ng-remote']); 18 | 19 | constructor(private cdr: ChangeDetectorRef) {} 20 | 21 | toggleApps(): void { 22 | this.apps$.next(this.apps$.value.length ? [] : ['ng-remote']); 23 | } 24 | 25 | appLoaded(event: Application> | null): void { 26 | console.log(`${event?.name}: loaded`); 27 | } 28 | 29 | appHook(event: MexoLifecycleEvent) { 30 | console.log(`${event.target?.name}: ${event.type}`); 31 | 32 | this.cdr.detectChanges(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /libs/angular/host/src/mexo-host.module.ts: -------------------------------------------------------------------------------- 1 | import { Inject, ModuleWithProviders, NgModule } from '@angular/core'; 2 | 3 | import { AppRegistrationOptions } from '@mexo/core'; 4 | 5 | import { MexoAppModule } from './mexo-app/mexo-app.module'; 6 | import { RegistryService } from './services/registry.service'; 7 | import { MEXO_APPS } from './tokens/mexo-apps'; 8 | 9 | export interface MexoHostModuleOptions { 10 | apps?: AppRegistrationOptions[]; 11 | } 12 | 13 | @NgModule({ 14 | exports: [MexoAppModule], 15 | }) 16 | export class MexoHostModule { 17 | constructor( 18 | @Inject(MEXO_APPS) allApps: AppRegistrationOptions[][], 19 | registry: RegistryService, 20 | ) { 21 | allApps.forEach((apps) => registry.registerMany(apps)); 22 | } 23 | 24 | static register({ 25 | apps, 26 | }: MexoHostModuleOptions): ModuleWithProviders { 27 | return { 28 | ngModule: MexoHostModule, 29 | providers: [ 30 | apps 31 | ? { 32 | provide: MEXO_APPS, 33 | useValue: apps, 34 | multi: true, 35 | } 36 | : [], 37 | ], 38 | }; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | 8 | jobs: 9 | test: 10 | name: Publish 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | id-token: write # needed for provenance data generation 15 | timeout-minutes: 10 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Install Node 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: 20 26 | registry-url: https://registry.npmjs.org/ 27 | 28 | - name: Install dependencies 29 | run: npm install 30 | shell: bash 31 | 32 | - name: Print Environment Info 33 | run: npx nx report 34 | shell: bash 35 | 36 | - run: git config --global user.email "igor@katsuba.dev" 37 | - run: git config --global user.name "IKatsuba" 38 | 39 | - name: Publish packages 40 | run: npx nx release publish 41 | shell: bash 42 | env: 43 | NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} 44 | NPM_CONFIG_PROVENANCE: true 45 | -------------------------------------------------------------------------------- /libs/core/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "core", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "library", 5 | "sourceRoot": "libs/core/src", 6 | "prefix": "", 7 | "targets": { 8 | "build": { 9 | "executor": "@nx/angular:package", 10 | "options": { 11 | "tsConfig": "libs/core/tsconfig.lib.json", 12 | "project": "libs/core/ng-package.json" 13 | }, 14 | "configurations": { 15 | "production": { 16 | "tsConfig": "libs/core/tsconfig.lib.prod.json" 17 | } 18 | } 19 | }, 20 | "lint": { 21 | "executor": "@nx/eslint:lint", 22 | "options": { 23 | "lintFilePatterns": [ 24 | "libs/core/src/**/*.ts", 25 | "libs/core/src/**/*.html", 26 | "libs/core/package.json" 27 | ] 28 | }, 29 | "outputs": ["{options.outputFile}"] 30 | }, 31 | "test": { 32 | "executor": "@nx/jest:jest", 33 | "outputs": ["{workspaceRoot}/coverage/core"], 34 | "options": { 35 | "jestConfig": "libs/core/jest.config.ts", 36 | "coverageDirectory": "coverage/core" 37 | } 38 | } 39 | }, 40 | "tags": [] 41 | } 42 | -------------------------------------------------------------------------------- /libs/angular/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "library", 5 | "sourceRoot": "libs/angular/src", 6 | "prefix": "", 7 | "targets": { 8 | "build": { 9 | "executor": "@nx/angular:package", 10 | "options": { 11 | "tsConfig": "libs/angular/tsconfig.lib.json", 12 | "project": "libs/angular/ng-package.json" 13 | }, 14 | "configurations": { 15 | "production": { 16 | "tsConfig": "libs/angular/tsconfig.lib.prod.json" 17 | } 18 | } 19 | }, 20 | "lint": { 21 | "executor": "@nx/eslint:lint", 22 | "options": { 23 | "lintFilePatterns": [ 24 | "libs/angular/src/**/*.ts", 25 | "libs/angular/src/**/*.html", 26 | "lib/angular/package.json" 27 | ] 28 | }, 29 | "outputs": ["{options.outputFile}"] 30 | }, 31 | "test": { 32 | "executor": "@nx/jest:jest", 33 | "outputs": ["{workspaceRoot}/coverage/angular"], 34 | "options": { 35 | "jestConfig": "libs/angular/jest.config.ts", 36 | "coverageDirectory": "coverage/angular" 37 | } 38 | } 39 | }, 40 | "tags": [] 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: write-all 9 | 10 | jobs: 11 | main: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | # Connect your workspace on nx.app and uncomment this to enable task distribution. 19 | # The "--stop-agents-after" is optional, but allows idle agents to shut down once the "e2e-ci" targets have been requested 20 | # - run: npx nx-cloud start-ci-run --distribute-on="5 linux-medium-js" --stop-agents-after="e2e-ci" 21 | 22 | # Cache node_modules 23 | - uses: actions/setup-node@v3 24 | with: 25 | node-version: 20 26 | cache: 'npm' 27 | - run: npm ci 28 | - uses: nrwl/nx-set-shas@v4 29 | 30 | # Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud 31 | # - run: npx nx-cloud record -- echo Hello World 32 | - run: npx nx affected -t lint test build 33 | - run: git config --global user.email "igor@katsuba.dev" 34 | - run: git config --global user.name "IKatsuba" 35 | - run: npx nx release --skip-publish 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} 38 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": ["@mexo/core/testing"], 14 | "depConstraints": [ 15 | { 16 | "sourceTag": "*", 17 | "onlyDependOnLibsWithTags": ["*"] 18 | } 19 | ] 20 | } 21 | ], 22 | "@typescript-eslint/no-unused-vars": [ 23 | "error", 24 | { 25 | "argsIgnorePattern": "^_" 26 | } 27 | ], 28 | "quotes": "off" 29 | } 30 | }, 31 | { 32 | "files": ["*.ts", "*.tsx"], 33 | "extends": ["plugin:@nx/typescript"], 34 | "rules": {} 35 | }, 36 | { 37 | "files": ["*.js", "*.jsx"], 38 | "extends": ["plugin:@nx/javascript"], 39 | "rules": {} 40 | }, 41 | { 42 | "files": ["*.json"], 43 | "parser": "jsonc-eslint-parser", 44 | "rules": { 45 | "@nx/dependency-checks": [ 46 | "error", 47 | { 48 | "ignoredDependencies": ["tslib"] 49 | } 50 | ] 51 | } 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /libs/core/src/lib/helpers/replace-apps.spec.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationMock } from '@mexo/core/testing'; 2 | 3 | import { constructAndBootstrapApp } from './construct-and-bootstrap-app'; 4 | import { registerApp } from './register-app'; 5 | import { replaceApps } from './replace-apps'; 6 | 7 | describe('replaceApps', () => { 8 | beforeEach(async () => { 9 | registerApp({ 10 | name: 'appMock1', 11 | async load() { 12 | return ApplicationMock; 13 | }, 14 | }); 15 | 16 | registerApp({ 17 | name: 'appMock2', 18 | async load() { 19 | return ApplicationMock; 20 | }, 21 | }); 22 | }); 23 | 24 | it('should destroy the appMock1 app', async () => { 25 | expect.assertions(1); 26 | 27 | const app1 = await constructAndBootstrapApp('appMock1', '#'); 28 | if (!app1) { 29 | throw new Error('No app found'); 30 | } 31 | 32 | await replaceApps(app1, 'appMock2'); 33 | 34 | expect(app1?.isDestroyed).toStrictEqual(true); 35 | }); 36 | 37 | it('should bootstrap the appMock2 app', async () => { 38 | expect.assertions(1); 39 | 40 | const app1 = await constructAndBootstrapApp('appMock1', '#'); 41 | if (!app1) { 42 | throw new Error('No app found'); 43 | } 44 | const app2 = await replaceApps(app1, 'appMock2'); 45 | 46 | expect(app2?.isBootstrapped).toStrictEqual(true); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /libs/react/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "libs/react/src", 5 | "projectType": "library", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/js:tsc", 9 | "outputs": ["{options.outputPath}"], 10 | "options": { 11 | "outputPath": "dist/libs/react", 12 | "tsConfig": "libs/react/tsconfig.lib.json", 13 | "project": "libs/react/package.json", 14 | "main": "libs/react/src/index.ts", 15 | "additionalEntryPoints": [ 16 | "libs/react/remote/index.ts", 17 | "libs/react/host/index.ts" 18 | ], 19 | "generateExportsField": true, 20 | "assets": [ 21 | { 22 | "glob": "README.md", 23 | "input": ".", 24 | "output": "." 25 | } 26 | ] 27 | } 28 | }, 29 | "lint": { 30 | "executor": "@nx/eslint:lint", 31 | "options": { 32 | "lintFilePatterns": ["libs/react/**/*.{ts,tsx,js,jsx}"] 33 | }, 34 | "outputs": ["{options.outputFile}"] 35 | }, 36 | "test": { 37 | "executor": "@nx/jest:jest", 38 | "outputs": ["{workspaceRoot}/coverage/react"], 39 | "options": { 40 | "jestConfig": "libs/react/jest.config.ts", 41 | "coverageDirectory": "coverage/react" 42 | } 43 | } 44 | }, 45 | "tags": [] 46 | } 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | > Thank you for considering contributing to our project. Your help is very much 4 | > appreciated! 5 | 6 | When contributing, it's better to first discuss the change you wish to make via 7 | issue or discussion, or any other method with the owners of this repository 8 | before making a change. 9 | 10 | All members of our community are expected to follow our 11 | [Code of Conduct](CODE_OF_CONDUCT.md). Please make sure you are welcoming and 12 | friendly in all of our spaces. 13 | 14 | ## Getting started 15 | 16 | In order to make your contribution please make a fork of the repository. After 17 | you've pulled the code, follow these steps to kick start the development: 18 | 19 | 1. Run `npm ci` to install dependencies 20 | 2. Run `npm run start` to launch demo project where you could see and test your 21 | changes 22 | 3. You can also run `npm run test` command for testing 23 | 24 | ## Pull Request Process 25 | 26 | 1. We follow 27 | [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0-beta.4/) 28 | in our commit messages, i.e. `feat(core): add cool things`. 29 | - `feat: ...` will trigger release of all packages after merge 30 | - `feat(core): ...` will trigger `@mexo/core` release after merge 31 | - `feat(unknown-project): ...` will trigger no release 32 | 2. Make sure you cover all code changes with unit tests 33 | 3. When you are ready, create Pull Request of your fork into original repository 34 | -------------------------------------------------------------------------------- /libs/react/host/src/mexo-app.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { forwardRef, HtmlHTMLAttributes, useEffect, useRef } from 'react'; 4 | 5 | import { Application, constructApp } from '@mexo/core'; 6 | 7 | interface MicrofrontAppProps extends HtmlHTMLAttributes { 8 | name: string; 9 | } 10 | 11 | export const MexoApp = forwardRef( 12 | ({ name, ...props }, ref) => { 13 | const containerRef = useRef(null); 14 | const appRef = useRef(null); 15 | 16 | const setReferences = (node: HTMLDivElement) => { 17 | containerRef.current = node; 18 | if (ref) { 19 | if (typeof ref === 'function') { 20 | ref(node); 21 | } else { 22 | ref.current = node; 23 | } 24 | } 25 | }; 26 | 27 | useEffect(() => { 28 | let ignore = false; 29 | 30 | const construct = async (name: string, container: HTMLDivElement) => { 31 | const app = await constructApp(name); 32 | if (!ignore) { 33 | appRef.current?.destroy(); 34 | app.bootstrap(container); 35 | appRef.current = app; 36 | } 37 | }; 38 | 39 | if (name && containerRef.current) { 40 | construct(name, containerRef.current); 41 | } 42 | 43 | return () => { 44 | ignore = true; 45 | appRef.current?.destroy(); 46 | }; 47 | }, [name]); 48 | 49 | return
; 50 | }, 51 | ); 52 | -------------------------------------------------------------------------------- /libs/core/src/lib/helpers/register-entity.spec.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationMock } from '@mexo/core/testing'; 2 | 3 | import { entityOptionsRegistry } from '../registry'; 4 | import { preloadEntity } from './preload-entity'; 5 | import { registerEntity } from './register-entity'; 6 | 7 | jest.mock('./preload-entity', () => ({ 8 | preloadEntity: jest.fn(), 9 | })); 10 | 11 | describe('registerEntity', () => { 12 | it('should register an app', async () => { 13 | expect.assertions(1); 14 | 15 | registerEntity({ 16 | name: 'appMock', 17 | async load() { 18 | return ApplicationMock; 19 | }, 20 | }); 21 | 22 | expect(entityOptionsRegistry.get('appMock')).toEqual({ 23 | name: 'appMock', 24 | load: expect.any(Function), 25 | }); 26 | }); 27 | 28 | it('should run preloadEntity when preload is true', async () => { 29 | expect.assertions(1); 30 | 31 | registerEntity({ 32 | name: 'appMock', 33 | async load() { 34 | return ApplicationMock; 35 | }, 36 | preload: true, 37 | }); 38 | 39 | expect(preloadEntity).toHaveBeenCalledWith('appMock'); 40 | }); 41 | 42 | it('should not run preloadEntity when preload is false', async () => { 43 | expect.assertions(1); 44 | 45 | registerEntity({ 46 | name: 'appMock', 47 | async load() { 48 | return ApplicationMock; 49 | }, 50 | preload: false, 51 | }); 52 | 53 | expect(preloadEntity).not.toHaveBeenCalled(); 54 | }); 55 | 56 | afterEach(() => { 57 | jest.clearAllMocks(); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /libs/core/src/lib/helpers/load-entity.ts: -------------------------------------------------------------------------------- 1 | import { EntityConstructor } from '../models/entity'; 2 | import { EntityRegistrationOptions } from '../models/registration-options'; 3 | import { entityOptionsRegistry, loadedEntityRegistry } from '../registry'; 4 | import { getEntity } from './get-entity'; 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | const entities = new Map>>(); 8 | 9 | export async function loadEntity( 10 | entityName: string, 11 | ): Promise> { 12 | const entity = await getEntity(entityName); 13 | 14 | if (entity) { 15 | return entity; 16 | } 17 | 18 | const options = 19 | entityOptionsRegistry.get< 20 | EntityRegistrationOptions> 21 | >(entityName); 22 | 23 | if (!options) { 24 | throw `Mexo entity "${entityName}" has not been registered. Check the spelling or register an app.`; 25 | } 26 | 27 | if (!options.load) { 28 | throw `Mexo entity "${entityName}" is registered but it has no "load" function. Please, provide it`; 29 | } 30 | 31 | const result = await loadEntityInternal(options); 32 | 33 | if (result) { 34 | loadedEntityRegistry.set(entityName, result); 35 | } 36 | 37 | return result; 38 | } 39 | 40 | function loadEntityInternal({ 41 | name, 42 | load, 43 | }: EntityRegistrationOptions>): Promise< 44 | EntityConstructor 45 | > { 46 | if (entities.get(name)) { 47 | return entities.get(name) as Promise>; 48 | } 49 | 50 | const entity = load() as Promise>; 51 | entities.set(name, entity); 52 | 53 | return entity; 54 | } 55 | -------------------------------------------------------------------------------- /libs/react/remote/lib/createApp.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import { createRoot, Root } from 'react-dom/client'; 3 | 4 | import { 5 | Application, 6 | ApplicationConstructor, 7 | MexoLifecycleEvent, 8 | MexoMessageEvent, 9 | } from '@mexo/core'; 10 | 11 | export function createApp

( 12 | name: string, 13 | element: ReactElement

, 14 | ): ApplicationConstructor { 15 | class ReactApplication extends Application { 16 | private root: Root | undefined; 17 | async bootstrap( 18 | container: string | Element, 19 | props?: Record, 20 | ) { 21 | container = 22 | typeof container === 'string' 23 | ? document.querySelector(container)! 24 | : container; 25 | 26 | this.root = createRoot(container); 27 | await super.bootstrap(container, props); 28 | 29 | this.root.render(element); 30 | //https://github.com/reactwg/react-18/discussions/5#discussioncomment-796012 31 | requestIdleCallback(() => { 32 | this.emitHook(MexoLifecycleEvent.bootstrapped()); 33 | }); 34 | } 35 | 36 | destroy() { 37 | super.destroy(); 38 | 39 | if (this.root) { 40 | this.root.unmount(); 41 | } 42 | this.container = ''; 43 | 44 | this.emitHook(MexoLifecycleEvent.destroyed()); 45 | } 46 | 47 | async navigate(_url: string, _props: unknown | undefined): Promise { 48 | return undefined; 49 | } 50 | 51 | async send(_msg: string | MexoMessageEvent): Promise { 52 | return undefined; 53 | } 54 | } 55 | 56 | document.dispatchEvent( 57 | new CustomEvent('mexo:load', { 58 | detail: { name, appConstructor: ReactApplication }, 59 | }), 60 | ); 61 | 62 | return ReactApplication; 63 | } 64 | -------------------------------------------------------------------------------- /apps/ng/host-e2e/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | import { workspaceRoot } from '@nx/devkit'; 4 | import { nxE2EPreset } from '@nx/playwright/preset'; 5 | 6 | // For CI, you may want to set BASE_URL to the deployed application. 7 | const baseURL = process.env['BASE_URL'] || 'http://localhost:4200'; 8 | 9 | /** 10 | * Read environment variables from file. 11 | * https://github.com/motdotla/dotenv 12 | */ 13 | // require('dotenv').config(); 14 | 15 | /** 16 | * See https://playwright.dev/docs/test-configuration. 17 | */ 18 | export default defineConfig({ 19 | ...nxE2EPreset(__filename, { testDir: './src' }), 20 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 21 | use: { 22 | baseURL, 23 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 24 | trace: 'on-first-retry', 25 | }, 26 | /* Run your local dev server before starting the tests */ 27 | webServer: { 28 | command: 'npx nx serve ng-host', 29 | url: 'http://localhost:4200', 30 | reuseExistingServer: !process.env.CI, 31 | cwd: workspaceRoot, 32 | }, 33 | projects: [ 34 | { 35 | name: 'chromium', 36 | use: { ...devices['Desktop Chrome'] }, 37 | }, 38 | 39 | { 40 | name: 'firefox', 41 | use: { ...devices['Desktop Firefox'] }, 42 | }, 43 | 44 | { 45 | name: 'webkit', 46 | use: { ...devices['Desktop Safari'] }, 47 | }, 48 | 49 | // Uncomment for mobile browsers support 50 | /* { 51 | name: 'Mobile Chrome', 52 | use: { ...devices['Pixel 5'] }, 53 | }, 54 | { 55 | name: 'Mobile Safari', 56 | use: { ...devices['iPhone 12'] }, 57 | }, */ 58 | 59 | // Uncomment for branded browsers 60 | /* { 61 | name: 'Microsoft Edge', 62 | use: { ...devices['Desktop Edge'], channel: 'msedge' }, 63 | }, 64 | { 65 | name: 'Google Chrome', 66 | use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 67 | } */ 68 | ], 69 | }); 70 | -------------------------------------------------------------------------------- /apps/ng/remote-e2e/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | import { workspaceRoot } from '@nx/devkit'; 4 | import { nxE2EPreset } from '@nx/playwright/preset'; 5 | 6 | // For CI, you may want to set BASE_URL to the deployed application. 7 | const baseURL = process.env['BASE_URL'] || 'http://localhost:4202'; 8 | 9 | /** 10 | * Read environment variables from file. 11 | * https://github.com/motdotla/dotenv 12 | */ 13 | // require('dotenv').config(); 14 | 15 | /** 16 | * See https://playwright.dev/docs/test-configuration. 17 | */ 18 | export default defineConfig({ 19 | ...nxE2EPreset(__filename, { testDir: './src' }), 20 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 21 | use: { 22 | baseURL, 23 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 24 | trace: 'on-first-retry', 25 | }, 26 | /* Run your local dev server before starting the tests */ 27 | webServer: { 28 | command: 'npx nx serve ng-remote', 29 | url: 'http://localhost:4202', 30 | reuseExistingServer: !process.env.CI, 31 | cwd: workspaceRoot, 32 | }, 33 | projects: [ 34 | { 35 | name: 'chromium', 36 | use: { ...devices['Desktop Chrome'] }, 37 | }, 38 | 39 | { 40 | name: 'firefox', 41 | use: { ...devices['Desktop Firefox'] }, 42 | }, 43 | 44 | { 45 | name: 'webkit', 46 | use: { ...devices['Desktop Safari'] }, 47 | }, 48 | 49 | // Uncomment for mobile browsers support 50 | /* { 51 | name: 'Mobile Chrome', 52 | use: { ...devices['Pixel 5'] }, 53 | }, 54 | { 55 | name: 'Mobile Safari', 56 | use: { ...devices['iPhone 12'] }, 57 | }, */ 58 | 59 | // Uncomment for branded browsers 60 | /* { 61 | name: 'Microsoft Edge', 62 | use: { ...devices['Desktop Edge'], channel: 'msedge' }, 63 | }, 64 | { 65 | name: 'Google Chrome', 66 | use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 67 | } */ 68 | ], 69 | }); 70 | -------------------------------------------------------------------------------- /apps/react/host-e2e/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | import { workspaceRoot } from '@nx/devkit'; 4 | import { nxE2EPreset } from '@nx/playwright/preset'; 5 | 6 | // For CI, you may want to set BASE_URL to the deployed application. 7 | const baseURL = process.env['BASE_URL'] || 'http://127.0.0.1:3000'; 8 | 9 | /** 10 | * Read environment variables from file. 11 | * https://github.com/motdotla/dotenv 12 | */ 13 | // require('dotenv').config(); 14 | 15 | /** 16 | * See https://playwright.dev/docs/test-configuration. 17 | */ 18 | export default defineConfig({ 19 | ...nxE2EPreset(__filename, { testDir: './src' }), 20 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 21 | use: { 22 | baseURL, 23 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 24 | trace: 'on-first-retry', 25 | }, 26 | /* Run your local dev server before starting the tests */ 27 | webServer: { 28 | command: 'npx nx start react-host', 29 | url: 'http://127.0.0.1:3000', 30 | reuseExistingServer: !process.env.CI, 31 | cwd: workspaceRoot, 32 | }, 33 | projects: [ 34 | { 35 | name: 'chromium', 36 | use: { ...devices['Desktop Chrome'] }, 37 | }, 38 | 39 | { 40 | name: 'firefox', 41 | use: { ...devices['Desktop Firefox'] }, 42 | }, 43 | 44 | { 45 | name: 'webkit', 46 | use: { ...devices['Desktop Safari'] }, 47 | }, 48 | 49 | // Uncomment for mobile browsers support 50 | /* { 51 | name: 'Mobile Chrome', 52 | use: { ...devices['Pixel 5'] }, 53 | }, 54 | { 55 | name: 'Mobile Safari', 56 | use: { ...devices['iPhone 12'] }, 57 | }, */ 58 | 59 | // Uncomment for branded browsers 60 | /* { 61 | name: 'Microsoft Edge', 62 | use: { ...devices['Desktop Edge'], channel: 'msedge' }, 63 | }, 64 | { 65 | name: 'Google Chrome', 66 | use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 67 | } */ 68 | ], 69 | }); 70 | -------------------------------------------------------------------------------- /apps/react/remote-e2e/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | import { workspaceRoot } from '@nx/devkit'; 4 | import { nxE2EPreset } from '@nx/playwright/preset'; 5 | 6 | // For CI, you may want to set BASE_URL to the deployed application. 7 | const baseURL = process.env['BASE_URL'] || 'http://127.0.0.1:3000'; 8 | 9 | /** 10 | * Read environment variables from file. 11 | * https://github.com/motdotla/dotenv 12 | */ 13 | // require('dotenv').config(); 14 | 15 | /** 16 | * See https://playwright.dev/docs/test-configuration. 17 | */ 18 | export default defineConfig({ 19 | ...nxE2EPreset(__filename, { testDir: './src' }), 20 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 21 | use: { 22 | baseURL, 23 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 24 | trace: 'on-first-retry', 25 | }, 26 | /* Run your local dev server before starting the tests */ 27 | webServer: { 28 | command: 'npx nx start react-remote', 29 | url: 'http://127.0.0.1:3000', 30 | reuseExistingServer: !process.env.CI, 31 | cwd: workspaceRoot, 32 | }, 33 | projects: [ 34 | { 35 | name: 'chromium', 36 | use: { ...devices['Desktop Chrome'] }, 37 | }, 38 | 39 | { 40 | name: 'firefox', 41 | use: { ...devices['Desktop Firefox'] }, 42 | }, 43 | 44 | { 45 | name: 'webkit', 46 | use: { ...devices['Desktop Safari'] }, 47 | }, 48 | 49 | // Uncomment for mobile browsers support 50 | /* { 51 | name: 'Mobile Chrome', 52 | use: { ...devices['Pixel 5'] }, 53 | }, 54 | { 55 | name: 'Mobile Safari', 56 | use: { ...devices['iPhone 12'] }, 57 | }, */ 58 | 59 | // Uncomment for branded browsers 60 | /* { 61 | name: 'Microsoft Edge', 62 | use: { ...devices['Desktop Edge'], channel: 'msedge' }, 63 | }, 64 | { 65 | name: 'Google Chrome', 66 | use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 67 | } */ 68 | ], 69 | }); 70 | -------------------------------------------------------------------------------- /libs/angular/host/src/mexo-app/mexo-app.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator'; 2 | import { first } from 'rxjs/operators'; 3 | 4 | import { asapScheduler } from 'rxjs'; 5 | 6 | import { Application, MexoLifecycleEvent } from '@mexo/core'; 7 | import { ApplicationMock } from '@mexo/core/testing'; 8 | 9 | import { MexoHostModule } from '../mexo-host.module'; 10 | import { MexoAppDirective } from './mexo-app.directive'; 11 | 12 | describe('MexoAppDirective', () => { 13 | let spectator: SpectatorDirective; 14 | const createDirective = createDirectiveFactory({ 15 | directive: MexoAppDirective, 16 | imports: [ 17 | MexoHostModule.register({ 18 | apps: [ 19 | { 20 | name: 'name', 21 | load: async () => ApplicationMock, 22 | }, 23 | ], 24 | }), 25 | ], 26 | }); 27 | 28 | beforeEach(async () => { 29 | spectator = createDirective(`

`, { 30 | hostProps: { 31 | name: 'name', 32 | }, 33 | }); 34 | }); 35 | 36 | it('should create', () => { 37 | expect(spectator.directive).toBeTruthy(); 38 | }); 39 | 40 | it('should emit an application', async () => { 41 | expect.assertions(1); 42 | 43 | expect( 44 | await spectator.directive.application.pipe(first()).toPromise(), 45 | ).toBeInstanceOf(Application); 46 | }); 47 | 48 | it('should emit an destroy event on app name change', async () => { 49 | expect.assertions(1); 50 | 51 | const event$ = spectator.directive.hook.pipe(first()).toPromise(); 52 | spectator.setHostInput('name', ''); 53 | 54 | const event = MexoLifecycleEvent.destroyed(); 55 | event.target = expect.any(Application); 56 | 57 | const result = await event$; 58 | 59 | expect(result).toEqual(event); 60 | }); 61 | 62 | it('should complete the subject when the directive is destroyed', async () => { 63 | expect.assertions(1); 64 | 65 | const app$ = spectator.directive.application.toPromise(); 66 | 67 | asapScheduler.schedule(() => { 68 | spectator.directive.ngOnDestroy(); 69 | }); 70 | 71 | expect(await app$).toEqual(expect.any(Application)); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /libs/core/src/lib/helpers/load-entity.spec.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationMock } from '@mexo/core/testing'; 2 | 3 | import { EntityRegistrationOptions } from '../models/registration-options'; 4 | import { loadEntity } from './load-entity'; 5 | import { registerEntity } from './register-entity'; 6 | 7 | describe('loadEntity', () => { 8 | beforeEach(async () => { 9 | registerEntity({ 10 | name: 'appMock1', 11 | async load() { 12 | return ApplicationMock; 13 | }, 14 | }); 15 | 16 | registerEntity({ 17 | name: 'appMock2', 18 | load: undefined, 19 | } as unknown as EntityRegistrationOptions); 20 | 21 | registerEntity({ 22 | name: 'appMock3', 23 | async load() { 24 | return ApplicationMock; 25 | }, 26 | }); 27 | 28 | registerEntity({ 29 | name: 'appMock4', 30 | load() { 31 | return Promise.resolve(ApplicationMock); 32 | }, 33 | }); 34 | }); 35 | 36 | it('should load an entity constructor', async () => { 37 | expect.assertions(3); 38 | 39 | await loadEntity('appMock1'); 40 | 41 | expect(await loadEntity('appMock1')).toStrictEqual(ApplicationMock); 42 | expect(await loadEntity('appMock3')).toStrictEqual(ApplicationMock); 43 | expect(await loadEntity('appMock4')).toStrictEqual(ApplicationMock); 44 | }); 45 | 46 | it('should throw an error when trying to load an unregistered entity', async () => { 47 | expect.assertions(1); 48 | 49 | await expect(loadEntity('unregistered')).rejects.toEqual( 50 | `Mexo entity "unregistered" has not been registered. Check the spelling or register an app.`, 51 | ); 52 | }); 53 | 54 | it('should throw an error when options does not have the load function', async () => { 55 | expect.assertions(1); 56 | 57 | await expect(loadEntity('appMock2')).rejects.toEqual( 58 | `Mexo entity "appMock2" is registered but it has no "load" function. Please, provide it`, 59 | ); 60 | }); 61 | 62 | it('should run the load function only once', async () => { 63 | expect.assertions(1); 64 | 65 | const load = jest.fn(() => Promise.resolve(ApplicationMock)); 66 | 67 | registerEntity({ 68 | name: 'appMock5', 69 | load, 70 | }); 71 | 72 | await loadEntity('appMock5'); 73 | await loadEntity('appMock5'); 74 | 75 | expect(load).toHaveBeenCalledTimes(1); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /libs/angular/remote/src/create-app.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationRef, CompilerOptions, InjectionToken } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | 4 | import { 5 | Application, 6 | ApplicationConstructor, 7 | MexoLifecycleEvent, 8 | MexoMessageEvent, 9 | } from '@mexo/core'; 10 | 11 | export const APP_NAME = new InjectionToken('App name'); 12 | export const ROOT_SELECTOR = new InjectionToken('Root selector'); 13 | 14 | // todo: очень грубая имплементация 15 | export function createApp< 16 | TModule, 17 | Props extends Record = Record, 18 | >( 19 | bootstrapFn: (props?: CompilerOptions) => Promise, 20 | rootSelector: string, 21 | ): ApplicationConstructor { 22 | // todo: не хватает имплементации хуков, сообщений и навигации 23 | class AngularApp< 24 | T extends Record = Props, 25 | > extends Application { 26 | private router: Router | null = null; 27 | private appRef: ApplicationRef | null = null; 28 | 29 | override destroy() { 30 | super.destroy(); 31 | 32 | if (this.appRef) { 33 | this.appRef.destroy(); 34 | this.appRef = null; 35 | } 36 | 37 | this.emitHook(MexoLifecycleEvent.destroyed()); 38 | } 39 | 40 | override async bootstrap( 41 | container: string | Element, 42 | props?: T, 43 | ): Promise { 44 | const containerElement = 45 | typeof container === 'string' 46 | ? document.querySelector(container) 47 | : container; 48 | 49 | if (!containerElement) { 50 | throw new Error(`No container found for ${container}`); 51 | } 52 | 53 | const rootElement = document.createElement(rootSelector); 54 | 55 | containerElement.appendChild(rootElement); 56 | 57 | this.appRef = await bootstrapFn(props); 58 | 59 | this.router = this.appRef.injector.get(Router, null, { 60 | optional: true, 61 | }); 62 | 63 | await super.bootstrap(container, props); 64 | 65 | this.emitHook(MexoLifecycleEvent.bootstrapped()); 66 | } 67 | 68 | async navigate(url: string, _props: unknown | undefined): Promise { 69 | if (this.router) { 70 | await this.router.navigateByUrl(url); 71 | } 72 | } 73 | 74 | async send(_msg: string | MexoMessageEvent): Promise { 75 | // 76 | } 77 | } 78 | 79 | return AngularApp; 80 | } 81 | -------------------------------------------------------------------------------- /libs/core/src/lib/helpers/construct-and-bootstrap-app.spec.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationMock } from '@mexo/core/testing'; 2 | 3 | import { Application } from '../models/application'; 4 | import { AppRegistrationOptions } from '../models/registration-options'; 5 | import { entityOptionsRegistry, loadedEntityRegistry } from '../registry'; 6 | import { constructAndBootstrapApp } from './construct-and-bootstrap-app'; 7 | import { loadApp } from './load-app'; 8 | import { registerApp } from './register-app'; 9 | 10 | function clearRegistry() { 11 | loadedEntityRegistry.clear(); 12 | entityOptionsRegistry.clear(); 13 | } 14 | 15 | describe('constructAndBootstrapApp', () => { 16 | let options: AppRegistrationOptions; 17 | let loadFn: jest.SpiedFunction; 18 | 19 | beforeEach(async () => { 20 | options = { 21 | name: 'appMock', 22 | async load() { 23 | return ApplicationMock; 24 | }, 25 | }; 26 | 27 | loadFn = jest.spyOn(options, 'load'); 28 | 29 | registerApp(options); 30 | }); 31 | 32 | describe('An app is already loaded', () => { 33 | beforeEach(async () => { 34 | await loadApp('appMock'); 35 | }); 36 | 37 | it("shouldn't load an app again", async () => { 38 | expect.assertions(1); 39 | 40 | await constructAndBootstrapApp('appMock', '#container'); 41 | 42 | expect(loadFn).toHaveBeenCalledTimes(1); 43 | }); 44 | 45 | it('should bootstrap an app', async () => { 46 | expect.assertions(1); 47 | 48 | const app = await constructAndBootstrapApp('appMock', '#container'); 49 | 50 | expect(app).toBeInstanceOf(ApplicationMock); 51 | }); 52 | }); 53 | 54 | describe('An app is NOT loaded', () => { 55 | it('should load an app', async () => { 56 | const load = jest.fn(() => Promise.resolve(ApplicationMock)); 57 | const options = { 58 | name: 'appMock2', 59 | load, 60 | }; 61 | 62 | registerApp(options); 63 | 64 | expect.assertions(1); 65 | 66 | await constructAndBootstrapApp('appMock2', '#container'); 67 | 68 | expect(load).toHaveBeenCalledTimes(1); 69 | }); 70 | 71 | it('should bootstrap an app', async () => { 72 | expect.assertions(1); 73 | 74 | const app = await constructAndBootstrapApp('appMock', '#container'); 75 | 76 | expect(app).toBeInstanceOf(Application); 77 | }); 78 | }); 79 | 80 | afterEach(() => { 81 | clearRegistry(); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /libs/angular/host/src/mexo-app/mexo-app.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | catchError, 3 | shareReplay, 4 | switchMap, 5 | takeUntil, 6 | tap, 7 | } from 'rxjs/operators'; 8 | 9 | import { 10 | Directive, 11 | ElementRef, 12 | ErrorHandler, 13 | Input, 14 | NgZone, 15 | OnDestroy, 16 | Output, 17 | } from '@angular/core'; 18 | import { defer, NEVER, Observable, of, Subject } from 'rxjs'; 19 | 20 | import { 21 | Application, 22 | bootstrapApp, 23 | constructApp, 24 | MexoLifecycleEvent, 25 | } from '@mexo/core'; 26 | 27 | import { complete } from '../operators/complete'; 28 | 29 | @Directive({ 30 | selector: '[mexoApp]:not(ng-container)', 31 | }) 32 | export class MexoAppDirective implements OnDestroy { 33 | @Output() 34 | hook: Observable; 35 | 36 | @Output() 37 | application: Observable; 38 | 39 | @Input('mexoApp') 40 | set name(appName: string) { 41 | this.ngZone.runOutsideAngular(() => this.name$.next(appName)); 42 | } 43 | 44 | private destroy$ = new Subject(); 45 | private name$ = new Subject(); 46 | 47 | constructor( 48 | private elementRef: ElementRef, 49 | private ngZone: NgZone, 50 | private errorHandler: ErrorHandler, 51 | ) { 52 | const app$ = this.name$.pipe( 53 | tap(() => NgZone.assertNotInAngularZone()), 54 | switchMap((name) => (name ? constructApp(name) : of(null))), 55 | catchError((error) => this.handleError(error)), 56 | takeUntil(this.destroy$), 57 | shareReplay(1), 58 | ); 59 | 60 | this.application = app$.pipe( 61 | complete((app) => { 62 | app?.destroy(); 63 | }), 64 | switchMap((name) => 65 | name 66 | ? defer(() => bootstrapApp(name, this.elementRef.nativeElement)).pipe( 67 | catchError((error) => this.handleError(error)), 68 | ) 69 | : of(null), 70 | ), 71 | takeUntil(this.destroy$), 72 | shareReplay(1), 73 | ); 74 | 75 | this.hook = app$.pipe( 76 | switchMap((app) => 77 | app 78 | ? new Observable((subscriber) => 79 | app.onHook((event) => subscriber.next(event)), 80 | ) 81 | : NEVER, 82 | ), 83 | ); 84 | 85 | this.application.subscribe(); 86 | } 87 | 88 | ngOnDestroy() { 89 | this.destroy$.next(); 90 | this.destroy$.complete(); 91 | } 92 | 93 | private handleError(error: unknown): Observable { 94 | this.errorHandler.handleError(error); 95 | return of(null); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /apps/ng/host/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-host", 3 | "$schema": "../../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "application", 5 | "prefix": "app", 6 | "sourceRoot": "apps/ng/host/src", 7 | "tags": [], 8 | "targets": { 9 | "build": { 10 | "executor": "@nx/angular:webpack-browser", 11 | "outputs": ["{options.outputPath}"], 12 | "options": { 13 | "outputPath": "dist/apps/ng/host", 14 | "index": "apps/ng/host/src/index.html", 15 | "main": "apps/ng/host/src/main.ts", 16 | "polyfills": ["zone.js"], 17 | "tsConfig": "apps/ng/host/tsconfig.app.json", 18 | "assets": ["apps/ng/host/src/favicon.ico", "apps/ng/host/src/assets"], 19 | "styles": ["apps/ng/host/src/styles.css"], 20 | "scripts": [], 21 | "customWebpackConfig": { 22 | "path": "apps/ng/host/webpack.config.ts" 23 | } 24 | }, 25 | "configurations": { 26 | "production": { 27 | "budgets": [ 28 | { 29 | "type": "initial", 30 | "maximumWarning": "500kb", 31 | "maximumError": "1mb" 32 | }, 33 | { 34 | "type": "anyComponentStyle", 35 | "maximumWarning": "2kb", 36 | "maximumError": "4kb" 37 | } 38 | ], 39 | "outputHashing": "all" 40 | }, 41 | "development": { 42 | "buildOptimizer": false, 43 | "optimization": false, 44 | "vendorChunk": true, 45 | "extractLicenses": false, 46 | "sourceMap": true, 47 | "namedChunks": true 48 | } 49 | }, 50 | "defaultConfiguration": "production" 51 | }, 52 | "serve": { 53 | "executor": "@nx/angular:module-federation-dev-server", 54 | "options": { 55 | "port": 4200, 56 | "publicHost": "http://localhost:4200" 57 | }, 58 | "configurations": { 59 | "production": { 60 | "buildTarget": "ng-host:build:production" 61 | }, 62 | "development": { 63 | "buildTarget": "ng-host:build:development" 64 | } 65 | }, 66 | "defaultConfiguration": "development" 67 | }, 68 | "extract-i18n": { 69 | "executor": "@angular-devkit/build-angular:extract-i18n", 70 | "options": { 71 | "buildTarget": "ng-host:build" 72 | } 73 | }, 74 | "lint": { 75 | "executor": "@nx/eslint:lint" 76 | }, 77 | "test": { 78 | "executor": "@nx/jest:jest", 79 | "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 80 | "options": { 81 | "jestConfig": "apps/ng/host/jest.config.ts" 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /libs/core/src/lib/models/application.ts: -------------------------------------------------------------------------------- 1 | import { DefaultPropsType } from './default-props-type'; 2 | import { EntityConstructor } from './entity'; 3 | import { MexoEvent, MexoMessageEvent, MexoNavigationEvent } from './events'; 4 | import { MexoLifecycleEvent } from './lifecycle'; 5 | 6 | export type Listener = (event: T) => void; 7 | 8 | export abstract class Application< 9 | T extends DefaultPropsType = DefaultPropsType, 10 | > { 11 | isBootstrapped = false; 12 | isDestroyed = true; 13 | container: Element | string = ''; 14 | 15 | protected readonly hook = new Set>(); 16 | protected readonly message = new Set>(); 17 | protected readonly navigationEvent = new Set>(); 18 | 19 | constructor( 20 | public readonly name: string, 21 | public props?: T, 22 | ) {} 23 | 24 | onMessage(fn: Listener): () => void { 25 | this.message.add(fn); 26 | 27 | return () => { 28 | this.message.delete(fn); 29 | }; 30 | } 31 | 32 | emitMessage(event: MexoMessageEvent) { 33 | this.callListeners(this.message, event); 34 | } 35 | 36 | onRouteChange(fn: Listener): () => void { 37 | this.navigationEvent.add(fn); 38 | 39 | return () => { 40 | this.navigationEvent.delete(fn); 41 | }; 42 | } 43 | 44 | emitRouteChange(event: MexoNavigationEvent) { 45 | this.callListeners(this.navigationEvent, event); 46 | } 47 | 48 | onHook(fn: Listener): () => void { 49 | this.hook.add(fn); 50 | 51 | return () => { 52 | this.hook.delete(fn); 53 | }; 54 | } 55 | 56 | emitHook(event: MexoLifecycleEvent) { 57 | this.callListeners(this.hook, event); 58 | } 59 | 60 | destroy() { 61 | this.emitHook(MexoLifecycleEvent.destroyed()); 62 | 63 | this.hook.clear(); // todo: в этот поток перед комплитом нужен евент дестроя 64 | this.message.clear(); 65 | this.navigationEvent.clear(); 66 | 67 | this.isBootstrapped = false; 68 | this.isDestroyed = true; 69 | } 70 | 71 | async bootstrap(container: string | Element, _props?: T): Promise { 72 | this.isBootstrapped = true; 73 | this.container = container; 74 | } 75 | 76 | abstract send(msg: string | MexoMessageEvent): Promise; 77 | 78 | // todo: шо за пропс? Надо придумать 79 | abstract navigate(url: string, props?: unknown): Promise; 80 | 81 | protected callListeners( 82 | listeners: Set>, 83 | event: K, 84 | ) { 85 | event.target = this; 86 | 87 | [...listeners].forEach((fn) => { 88 | fn(event); 89 | }); 90 | } 91 | } 92 | 93 | export type ApplicationConstructor< 94 | T extends Record = Record, 95 | > = EntityConstructor>; 96 | -------------------------------------------------------------------------------- /apps/ng/remote/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-remote", 3 | "$schema": "../../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "application", 5 | "prefix": "app", 6 | "sourceRoot": "apps/ng/remote/src", 7 | "tags": [], 8 | "targets": { 9 | "build": { 10 | "executor": "@nx/angular:webpack-browser", 11 | "outputs": ["{options.outputPath}"], 12 | "options": { 13 | "outputPath": "dist/apps/ng/remote", 14 | "index": "apps/ng/remote/src/index.html", 15 | "main": "apps/ng/remote/src/main.ts", 16 | "polyfills": ["zone.js"], 17 | "tsConfig": "apps/ng/remote/tsconfig.app.json", 18 | "assets": [ 19 | "apps/ng/remote/src/favicon.ico", 20 | "apps/ng/remote/src/assets" 21 | ], 22 | "styles": ["apps/ng/remote/src/styles.css"], 23 | "scripts": [], 24 | "customWebpackConfig": { 25 | "path": "apps/ng/remote/webpack.config.ts" 26 | } 27 | }, 28 | "configurations": { 29 | "production": { 30 | "budgets": [ 31 | { 32 | "type": "initial", 33 | "maximumWarning": "500kb", 34 | "maximumError": "1mb" 35 | }, 36 | { 37 | "type": "anyComponentStyle", 38 | "maximumWarning": "2kb", 39 | "maximumError": "4kb" 40 | } 41 | ], 42 | "outputHashing": "all", 43 | "customWebpackConfig": { 44 | "path": "apps/ng/remote/webpack.prod.config.ts" 45 | } 46 | }, 47 | "development": { 48 | "buildOptimizer": false, 49 | "optimization": false, 50 | "vendorChunk": true, 51 | "extractLicenses": false, 52 | "sourceMap": true, 53 | "namedChunks": true 54 | } 55 | }, 56 | "defaultConfiguration": "production" 57 | }, 58 | "serve": { 59 | "executor": "@nx/angular:dev-server", 60 | "options": { 61 | "port": 4202, 62 | "publicHost": "http://localhost:4202" 63 | }, 64 | "configurations": { 65 | "production": { 66 | "buildTarget": "ng-remote:build:production" 67 | }, 68 | "development": { 69 | "buildTarget": "ng-remote:build:development" 70 | } 71 | }, 72 | "defaultConfiguration": "development" 73 | }, 74 | "extract-i18n": { 75 | "executor": "@angular-devkit/build-angular:extract-i18n", 76 | "options": { 77 | "buildTarget": "ng-remote:build" 78 | } 79 | }, 80 | "lint": { 81 | "executor": "@nx/eslint:lint" 82 | }, 83 | "test": { 84 | "executor": "@nx/jest:jest", 85 | "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 86 | "options": { 87 | "jestConfig": "apps/ng/remote/jest.config.ts" 88 | } 89 | }, 90 | "serve-static": { 91 | "executor": "@nx/web:file-server", 92 | "defaultConfiguration": "production", 93 | "options": { 94 | "buildTarget": "ng-remote:build", 95 | "port": 4202, 96 | "watch": false 97 | }, 98 | "configurations": { 99 | "development": { 100 | "buildTarget": "ng-remote:build:development" 101 | }, 102 | "production": { 103 | "buildTarget": "ng-remote:build:production" 104 | } 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 3 | "tasksRunnerOptions": { 4 | "default": { 5 | "options": { 6 | "canTrackAnalytics": false, 7 | "showUsageWarnings": true 8 | } 9 | } 10 | }, 11 | "affected": { 12 | "defaultBase": "origin/main" 13 | }, 14 | "targetDefaults": { 15 | "build": { 16 | "dependsOn": ["^build"], 17 | "inputs": ["production", "^production"], 18 | "cache": true 19 | }, 20 | "lint": { 21 | "inputs": ["default", "{workspaceRoot}/.eslintrc.json"], 22 | "cache": true 23 | }, 24 | "@nx/jest:jest": { 25 | "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"], 26 | "cache": true, 27 | "options": { 28 | "passWithNoTests": true 29 | }, 30 | "configurations": { 31 | "ci": { 32 | "ci": true, 33 | "codeCoverage": true 34 | } 35 | } 36 | }, 37 | "@angular-devkit/build-angular:browser": { 38 | "cache": true, 39 | "dependsOn": ["^build"], 40 | "inputs": ["production", "^production"] 41 | }, 42 | "@nx/angular:webpack-browser": { 43 | "inputs": [ 44 | "production", 45 | "^production", 46 | { 47 | "env": "NX_MF_DEV_SERVER_STATIC_REMOTES" 48 | } 49 | ] 50 | }, 51 | "nx-release-publish": { 52 | "dependsOn": ["build"], 53 | "options": { 54 | "packageRoot": "dist/libs/{projectName}" 55 | } 56 | } 57 | }, 58 | "namedInputs": { 59 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 60 | "sharedGlobals": [ 61 | "{workspaceRoot}/angular.json", 62 | "{workspaceRoot}/tsconfig.base.json", 63 | "{workspaceRoot}/nx.json", 64 | "{workspaceRoot}/babel.config.json" 65 | ], 66 | "production": [ 67 | "default", 68 | "!{projectRoot}/.eslintrc.json", 69 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", 70 | "!{projectRoot}/tsconfig.spec.json", 71 | "!{projectRoot}/jest.config.[jt]s", 72 | "!{projectRoot}/src/test-setup.[jt]s" 73 | ] 74 | }, 75 | "plugins": [ 76 | { 77 | "plugin": "@nx/next/plugin", 78 | "options": { 79 | "startTargetName": "start", 80 | "buildTargetName": "build", 81 | "devTargetName": "dev", 82 | "serveStaticTargetName": "serve-static" 83 | } 84 | }, 85 | { 86 | "plugin": "@nx/playwright/plugin", 87 | "options": { 88 | "targetName": "e2e" 89 | } 90 | }, 91 | { 92 | "plugin": "@nx/eslint/plugin", 93 | "options": { 94 | "targetName": "eslint:lint", 95 | "extensions": ["ts", "tsx", "js", "jsx", "html", "vue"] 96 | } 97 | } 98 | ], 99 | "release": { 100 | "projects": ["libs/*"], 101 | "version": { 102 | "conventionalCommits": true 103 | }, 104 | "changelog": { 105 | "workspaceChangelog": { 106 | "file": false, 107 | "createRelease": "github" 108 | } 109 | } 110 | }, 111 | "generators": { 112 | "@nx/next": { 113 | "application": { 114 | "style": "css", 115 | "linter": "eslint" 116 | } 117 | }, 118 | "@nx/angular:application": { 119 | "e2eTestRunner": "playwright", 120 | "linter": "eslint", 121 | "style": "css", 122 | "unitTestRunner": "jest" 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /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 9 | expression, level of experience, education, socio-economic status, nationality, 10 | personal 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 reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | 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 at igor.katsuba@dif.tech. All complaints will 59 | be reviewed and investigated and will result in a response that is deemed 60 | necessary and appropriate to the circumstances. The project team is obligated to 61 | maintain confidentiality with regard to the reporter of an incident. Further 62 | 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], 71 | version 1.4, available at 72 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 73 | 74 | [homepage]: https://www.contributor-covenant.org 75 | 76 | For answers to common questions about this code of conduct, see 77 | https://www.contributor-covenant.org/faq 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Mexo Project is a fork of the 2 | [Microzord](https://github.com/taiga-family/microzord) project. The main goal of 3 | the fork is to make the project more stable and to add new features. Mexo 4 | Project license is Apache 2.0, the same as the original project. 5 | 6 | # Mexo 7 | 8 | ## What is it? 9 | 10 | It is a small tool that allows you to add and render multiple apps on one page. 11 | It has simple ideomatic APIs for all modern frameworks and builds bridges 12 | between them. 13 | 14 | 🧙 **Framework agnostic**. You can have an Angular application with a React app 15 | header and a Vue app footer. Each app can have endless amount of nested apps 16 | inside. There are also wrappers for every modern framework with simple API. 17 | 18 | 🧩 **Modular**. You can insert another app in your app in any place as an 19 | ordinary component. Use the same methods to bind data and to handle its events. 20 | 21 | 🐝 **Tiny as a bee and works as well**. No new code in the bundle of child 22 | application and a bit more than 1 KB library for host application to get all the 23 | benefits! 24 | 25 | ## Installation and usage 26 | 27 | An extensive demo is coming soon... 28 | 29 | ### An example of usage external apps in Angular app 30 | 31 | app.module.ts 32 | 33 | ```ts 34 | @NgModule({ 35 | imports: [ 36 | MexoHostModule.register({ 37 | apps: [ 38 | { 39 | name: 'react-menu', 40 | }, 41 | { 42 | name: 'vue-footer-app', 43 | }, 44 | ], 45 | }), 46 | ], 47 | }) 48 | export class AppModule {} 49 | ``` 50 | 51 | Usage in application: 52 | 53 | ```html 54 |
55 | 56 |
Any content
57 | 58 |
62 | ``` 63 | 64 | ## Core team 65 | 66 | 67 | 68 | 100 | 101 | 102 |
69 | Igor Katsuba
Igor Katsuba
77 |
78 | 86 | 91 | 98 |
99 |
103 | 104 | ## License 105 | 106 | 🆓 Feel free to use our library in your commercial and private applications 107 | 108 | All mexo packages are covered by [Apache 2.0](/LICENSE) 109 | 110 | Read more about this license 111 | [here](https://choosealicense.com/licenses/apache-2.0/) 112 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mexo", 3 | "version": "0.0.0", 4 | "private": true, 5 | "keywords": [ 6 | "microfrontends", 7 | "single-page-application", 8 | "framework", 9 | "angular", 10 | "react", 11 | "routing", 12 | "microservices" 13 | ], 14 | "homepage": "https://github.com/platacard/mexo", 15 | "repository": "https://github.com/platacard/mexo", 16 | "license": "Apache-2.0", 17 | "contributors": [ 18 | "Igor Katsuba " 19 | ], 20 | "scripts": {}, 21 | "husky": { 22 | "hooks": { 23 | "pre-commit": "lint-staged", 24 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 25 | } 26 | }, 27 | "lint-staged": { 28 | "*.{js,jsx,ts,tsx,html,md,less,scss,json}": [ 29 | "npm run format:write" 30 | ], 31 | "*.{js,jsx,ts,tsx}": [ 32 | "eslint --fix" 33 | ] 34 | }, 35 | "dependencies": { 36 | "@angular-architects/module-federation": "17.0.8", 37 | "@angular/animations": "17.3.9", 38 | "@angular/common": "17.3.9", 39 | "@angular/compiler": "17.3.9", 40 | "@angular/core": "17.3.9", 41 | "@angular/forms": "17.3.9", 42 | "@angular/platform-browser": "17.3.9", 43 | "@angular/platform-browser-dynamic": "17.3.9", 44 | "@angular/router": "17.3.9", 45 | "@nx/angular": "19.0.5", 46 | "@swc/helpers": "0.5.11", 47 | "core-js": "3.37.1", 48 | "next": "14.0.4", 49 | "ngx-highlightjs": "10.0.0", 50 | "react": "18.3.1", 51 | "react-dom": "18.3.1", 52 | "react-router-dom": "6.23.1", 53 | "rxjs": "7.8.1", 54 | "tslib": "2.6.2", 55 | "zone.js": "0.14.6" 56 | }, 57 | "devDependencies": { 58 | "@angular-devkit/build-angular": "17.3.7", 59 | "@angular-devkit/core": "17.3.7", 60 | "@angular-devkit/schematics": "17.3.7", 61 | "@angular-eslint/eslint-plugin": "17.4.1", 62 | "@angular-eslint/eslint-plugin-template": "17.4.1", 63 | "@angular-eslint/template-parser": "17.4.1", 64 | "@angular/cdk": "17.3.9", 65 | "@angular/cli": "17.3.7", 66 | "@angular/compiler-cli": "17.3.9", 67 | "@babel/core": "7.24.5", 68 | "@babel/preset-env": "7.24.5", 69 | "@babel/preset-react": "7.24.1", 70 | "@babel/preset-typescript": "7.24.1", 71 | "@commitlint/config-conventional": "19.2.2", 72 | "@ianvs/prettier-plugin-sort-imports": "^4.2.1", 73 | "@ngneat/spectator": "18.0.2", 74 | "@nx/cypress": "19.0.5", 75 | "@nx/devkit": "19.0.5", 76 | "@nx/eslint": "19.0.5", 77 | "@nx/eslint-plugin": "19.0.5", 78 | "@nx/jest": "19.0.5", 79 | "@nx/js": "19.0.5", 80 | "@nx/next": "19.0.5", 81 | "@nx/node": "19.0.5", 82 | "@nx/playwright": "19.0.5", 83 | "@nx/react": "19.0.5", 84 | "@nx/rollup": "19.0.5", 85 | "@nx/web": "19.0.5", 86 | "@nx/webpack": "19.0.5", 87 | "@nx/workspace": "19.0.5", 88 | "@playwright/test": "^1.36.0", 89 | "@pmmmwh/react-refresh-webpack-plugin": "0.5.13", 90 | "@schematics/angular": "17.3.7", 91 | "@svgr/webpack": "8.1.0", 92 | "@swc-node/register": "1.9.1", 93 | "@swc/cli": "0.3.12", 94 | "@swc/core": "1.5.7", 95 | "@testing-library/react": "15.0.7", 96 | "@types/jest": "29.5.12", 97 | "@types/node": "20.12.12", 98 | "@types/react": "18.3.2", 99 | "@types/react-dom": "18.3.0", 100 | "@types/react-router-dom": "5.3.3", 101 | "@types/webpack": "5.28.5", 102 | "@typescript-eslint/eslint-plugin": "7.10.0", 103 | "@typescript-eslint/parser": "7.10.0", 104 | "babel-core": "7.0.0-bridge.0", 105 | "babel-jest": "29.7.0", 106 | "commitlint": "19.3.0", 107 | "css-loader": "7.1.1", 108 | "cypress": "13.9.0", 109 | "dotenv": "16.4.5", 110 | "eslint": "8.57.0", 111 | "eslint-config-next": "14.0.4", 112 | "eslint-config-prettier": "9.1.0", 113 | "eslint-plugin-cypress": "3.2.0", 114 | "eslint-plugin-import": "2.29.1", 115 | "eslint-plugin-jsx-a11y": "6.8.0", 116 | "eslint-plugin-playwright": "^0.15.3", 117 | "eslint-plugin-react": "7.34.1", 118 | "eslint-plugin-react-hooks": "4.6.2", 119 | "husky": "9.0.11", 120 | "jest": "29.7.0", 121 | "jest-environment-jsdom": "29.7.0", 122 | "jest-preset-angular": "14.1.0", 123 | "lint-staged": "15.2.2", 124 | "ng-packagr": "17.3.0", 125 | "nx": "19.0.5", 126 | "postcss": "8.4.38", 127 | "postcss-preset-env": "9.5.13", 128 | "prettier": "^3.2.5", 129 | "prettier-plugin-packagejson": "^2.5.0", 130 | "react-refresh": "0.14.2", 131 | "style-loader": "4.0.0", 132 | "stylus": "0.63.0", 133 | "stylus-loader": "8.1.0", 134 | "ts-jest": "29.1.3", 135 | "ts-node": "10.9.2", 136 | "tsconfig-paths": "4.2.0", 137 | "typescript": "5.2.2", 138 | "url-loader": "4.1.1", 139 | "verdaccio": "^5.0.4", 140 | "webpack": "5.91.0" 141 | }, 142 | "engines": { 143 | "node": ">= 18.x.x" 144 | }, 145 | "authors": [ 146 | "Igor Katsuba " 147 | ], 148 | "nx": { 149 | "includedScripts": [] 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2024 Acpekt 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. -------------------------------------------------------------------------------- /apps/react/remote/src/app/global.css: -------------------------------------------------------------------------------- 1 | html { 2 | -webkit-text-size-adjust: 100%; 3 | font-family: 4 | ui-sans-serif, 5 | system-ui, 6 | -apple-system, 7 | BlinkMacSystemFont, 8 | Segoe UI, 9 | Roboto, 10 | Helvetica Neue, 11 | Arial, 12 | Noto Sans, 13 | sans-serif, 14 | Apple Color Emoji, 15 | Segoe UI Emoji, 16 | Segoe UI Symbol, 17 | Noto Color Emoji; 18 | line-height: 1.5; 19 | tab-size: 4; 20 | scroll-behavior: smooth; 21 | } 22 | body { 23 | font-family: inherit; 24 | line-height: inherit; 25 | margin: 0; 26 | } 27 | h1, 28 | h2, 29 | p, 30 | pre { 31 | margin: 0; 32 | } 33 | *, 34 | ::before, 35 | ::after { 36 | box-sizing: border-box; 37 | border-width: 0; 38 | border-style: solid; 39 | border-color: currentColor; 40 | } 41 | h1, 42 | h2 { 43 | font-size: inherit; 44 | font-weight: inherit; 45 | } 46 | a { 47 | color: inherit; 48 | text-decoration: inherit; 49 | } 50 | pre { 51 | font-family: 52 | ui-monospace, 53 | SFMono-Regular, 54 | Menlo, 55 | Monaco, 56 | Consolas, 57 | Liberation Mono, 58 | Courier New, 59 | monospace; 60 | } 61 | svg { 62 | display: block; 63 | vertical-align: middle; 64 | shape-rendering: auto; 65 | text-rendering: optimizeLegibility; 66 | } 67 | pre { 68 | background-color: rgba(55, 65, 81, 1); 69 | border-radius: 0.25rem; 70 | color: rgba(229, 231, 235, 1); 71 | font-family: 72 | ui-monospace, 73 | SFMono-Regular, 74 | Menlo, 75 | Monaco, 76 | Consolas, 77 | Liberation Mono, 78 | Courier New, 79 | monospace; 80 | overflow: scroll; 81 | padding: 0.5rem 0.75rem; 82 | } 83 | 84 | .shadow { 85 | box-shadow: 86 | 0 0 #0000, 87 | 0 0 #0000, 88 | 0 10px 15px -3px rgba(0, 0, 0, 0.1), 89 | 0 4px 6px -2px rgba(0, 0, 0, 0.05); 90 | } 91 | .rounded { 92 | border-radius: 1.5rem; 93 | } 94 | .wrapper { 95 | width: 100%; 96 | } 97 | .container { 98 | margin-left: auto; 99 | margin-right: auto; 100 | max-width: 768px; 101 | padding-bottom: 3rem; 102 | padding-left: 1rem; 103 | padding-right: 1rem; 104 | color: rgba(55, 65, 81, 1); 105 | width: 100%; 106 | } 107 | #welcome { 108 | margin-top: 2.5rem; 109 | } 110 | #welcome h1 { 111 | font-size: 3rem; 112 | font-weight: 500; 113 | letter-spacing: -0.025em; 114 | line-height: 1; 115 | } 116 | #welcome span { 117 | display: block; 118 | font-size: 1.875rem; 119 | font-weight: 300; 120 | line-height: 2.25rem; 121 | margin-bottom: 0.5rem; 122 | } 123 | #hero { 124 | align-items: center; 125 | background-color: hsla(214, 62%, 21%, 1); 126 | border: none; 127 | box-sizing: border-box; 128 | color: rgba(55, 65, 81, 1); 129 | display: grid; 130 | grid-template-columns: 1fr; 131 | margin-top: 3.5rem; 132 | } 133 | #hero .text-container { 134 | color: rgba(255, 255, 255, 1); 135 | padding: 3rem 2rem; 136 | } 137 | #hero .text-container h2 { 138 | font-size: 1.5rem; 139 | line-height: 2rem; 140 | position: relative; 141 | } 142 | #hero .text-container h2 svg { 143 | color: hsla(162, 47%, 50%, 1); 144 | height: 2rem; 145 | left: -0.25rem; 146 | position: absolute; 147 | top: 0; 148 | width: 2rem; 149 | } 150 | #hero .text-container h2 span { 151 | margin-left: 2.5rem; 152 | } 153 | #hero .text-container a { 154 | background-color: rgba(255, 255, 255, 1); 155 | border-radius: 0.75rem; 156 | color: rgba(55, 65, 81, 1); 157 | display: inline-block; 158 | margin-top: 1.5rem; 159 | padding: 1rem 2rem; 160 | text-decoration: inherit; 161 | } 162 | #hero .logo-container { 163 | display: none; 164 | justify-content: center; 165 | padding-left: 2rem; 166 | padding-right: 2rem; 167 | } 168 | #hero .logo-container svg { 169 | color: rgba(255, 255, 255, 1); 170 | width: 66.666667%; 171 | } 172 | #middle-content { 173 | align-items: flex-start; 174 | display: grid; 175 | gap: 4rem; 176 | grid-template-columns: 1fr; 177 | margin-top: 3.5rem; 178 | } 179 | #learning-materials { 180 | padding: 2.5rem 2rem; 181 | } 182 | #learning-materials h2 { 183 | font-weight: 500; 184 | font-size: 1.25rem; 185 | letter-spacing: -0.025em; 186 | line-height: 1.75rem; 187 | padding-left: 1rem; 188 | padding-right: 1rem; 189 | } 190 | .list-item-link { 191 | align-items: center; 192 | border-radius: 0.75rem; 193 | display: flex; 194 | margin-top: 1rem; 195 | padding: 1rem; 196 | transition-property: 197 | background-color, 198 | border-color, 199 | color, 200 | fill, 201 | stroke, 202 | opacity, 203 | box-shadow, 204 | transform, 205 | filter, 206 | backdrop-filter, 207 | -webkit-backdrop-filter; 208 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 209 | transition-duration: 150ms; 210 | width: 100%; 211 | } 212 | .list-item-link svg:first-child { 213 | margin-right: 1rem; 214 | height: 1.5rem; 215 | transition-property: 216 | background-color, 217 | border-color, 218 | color, 219 | fill, 220 | stroke, 221 | opacity, 222 | box-shadow, 223 | transform, 224 | filter, 225 | backdrop-filter, 226 | -webkit-backdrop-filter; 227 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 228 | transition-duration: 150ms; 229 | width: 1.5rem; 230 | } 231 | .list-item-link > span { 232 | flex-grow: 1; 233 | font-weight: 400; 234 | transition-property: 235 | background-color, 236 | border-color, 237 | color, 238 | fill, 239 | stroke, 240 | opacity, 241 | box-shadow, 242 | transform, 243 | filter, 244 | backdrop-filter, 245 | -webkit-backdrop-filter; 246 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 247 | transition-duration: 150ms; 248 | } 249 | .list-item-link > span > span { 250 | color: rgba(107, 114, 128, 1); 251 | display: block; 252 | flex-grow: 1; 253 | font-size: 0.75rem; 254 | font-weight: 300; 255 | line-height: 1rem; 256 | transition-property: 257 | background-color, 258 | border-color, 259 | color, 260 | fill, 261 | stroke, 262 | opacity, 263 | box-shadow, 264 | transform, 265 | filter, 266 | backdrop-filter, 267 | -webkit-backdrop-filter; 268 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 269 | transition-duration: 150ms; 270 | } 271 | .list-item-link svg:last-child { 272 | height: 1rem; 273 | transition-property: all; 274 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 275 | transition-duration: 150ms; 276 | width: 1rem; 277 | } 278 | .list-item-link:hover { 279 | color: rgba(255, 255, 255, 1); 280 | background-color: hsla(162, 47%, 50%, 1); 281 | } 282 | .list-item-link:hover > span { 283 | } 284 | .list-item-link:hover > span > span { 285 | color: rgba(243, 244, 246, 1); 286 | } 287 | .list-item-link:hover svg:last-child { 288 | transform: translateX(0.25rem); 289 | } 290 | #other-links { 291 | } 292 | .button-pill { 293 | padding: 1.5rem 2rem; 294 | transition-duration: 300ms; 295 | transition-property: 296 | background-color, 297 | border-color, 298 | color, 299 | fill, 300 | stroke, 301 | opacity, 302 | box-shadow, 303 | transform, 304 | filter, 305 | backdrop-filter, 306 | -webkit-backdrop-filter; 307 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 308 | align-items: center; 309 | display: flex; 310 | } 311 | .button-pill svg { 312 | transition-property: 313 | background-color, 314 | border-color, 315 | color, 316 | fill, 317 | stroke, 318 | opacity, 319 | box-shadow, 320 | transform, 321 | filter, 322 | backdrop-filter, 323 | -webkit-backdrop-filter; 324 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 325 | transition-duration: 150ms; 326 | flex-shrink: 0; 327 | width: 3rem; 328 | } 329 | .button-pill > span { 330 | letter-spacing: -0.025em; 331 | font-weight: 400; 332 | font-size: 1.125rem; 333 | line-height: 1.75rem; 334 | padding-left: 1rem; 335 | padding-right: 1rem; 336 | } 337 | .button-pill span span { 338 | display: block; 339 | font-size: 0.875rem; 340 | font-weight: 300; 341 | line-height: 1.25rem; 342 | } 343 | .button-pill:hover svg, 344 | .button-pill:hover { 345 | color: rgba(255, 255, 255, 1) !important; 346 | } 347 | #nx-console:hover { 348 | background-color: rgba(0, 122, 204, 1); 349 | } 350 | #nx-console svg { 351 | color: rgba(0, 122, 204, 1); 352 | } 353 | #nx-console-jetbrains { 354 | margin-top: 2rem; 355 | } 356 | #nx-console-jetbrains:hover { 357 | background-color: rgba(255, 49, 140, 1); 358 | } 359 | #nx-console-jetbrains svg { 360 | color: rgba(255, 49, 140, 1); 361 | } 362 | #nx-repo:hover { 363 | background-color: rgba(24, 23, 23, 1); 364 | } 365 | #nx-repo svg { 366 | color: rgba(24, 23, 23, 1); 367 | } 368 | #nx-cloud { 369 | margin-bottom: 2rem; 370 | margin-top: 2rem; 371 | padding: 2.5rem 2rem; 372 | } 373 | #nx-cloud > div { 374 | align-items: center; 375 | display: flex; 376 | } 377 | #nx-cloud > div svg { 378 | border-radius: 0.375rem; 379 | flex-shrink: 0; 380 | width: 3rem; 381 | } 382 | #nx-cloud > div h2 { 383 | font-size: 1.125rem; 384 | font-weight: 400; 385 | letter-spacing: -0.025em; 386 | line-height: 1.75rem; 387 | padding-left: 1rem; 388 | padding-right: 1rem; 389 | } 390 | #nx-cloud > div h2 span { 391 | display: block; 392 | font-size: 0.875rem; 393 | font-weight: 300; 394 | line-height: 1.25rem; 395 | } 396 | #nx-cloud p { 397 | font-size: 1rem; 398 | line-height: 1.5rem; 399 | margin-top: 1rem; 400 | } 401 | #nx-cloud pre { 402 | margin-top: 1rem; 403 | } 404 | #nx-cloud a { 405 | color: rgba(107, 114, 128, 1); 406 | display: block; 407 | font-size: 0.875rem; 408 | line-height: 1.25rem; 409 | margin-top: 1.5rem; 410 | text-align: right; 411 | } 412 | #nx-cloud a:hover { 413 | text-decoration: underline; 414 | } 415 | #commands { 416 | padding: 2.5rem 2rem; 417 | margin-top: 3.5rem; 418 | } 419 | #commands h2 { 420 | font-size: 1.25rem; 421 | font-weight: 400; 422 | letter-spacing: -0.025em; 423 | line-height: 1.75rem; 424 | padding-left: 1rem; 425 | padding-right: 1rem; 426 | } 427 | #commands p { 428 | font-size: 1rem; 429 | font-weight: 300; 430 | line-height: 1.5rem; 431 | margin-top: 1rem; 432 | padding-left: 1rem; 433 | padding-right: 1rem; 434 | } 435 | details { 436 | align-items: center; 437 | display: flex; 438 | margin-top: 1rem; 439 | padding-left: 1rem; 440 | padding-right: 1rem; 441 | width: 100%; 442 | } 443 | details pre > span { 444 | color: rgba(181, 181, 181, 1); 445 | display: block; 446 | } 447 | summary { 448 | border-radius: 0.5rem; 449 | display: flex; 450 | font-weight: 400; 451 | padding: 0.5rem; 452 | cursor: pointer; 453 | transition-property: 454 | background-color, 455 | border-color, 456 | color, 457 | fill, 458 | stroke, 459 | opacity, 460 | box-shadow, 461 | transform, 462 | filter, 463 | backdrop-filter, 464 | -webkit-backdrop-filter; 465 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 466 | transition-duration: 150ms; 467 | } 468 | summary:hover { 469 | background-color: rgba(243, 244, 246, 1); 470 | } 471 | summary svg { 472 | height: 1.5rem; 473 | margin-right: 1rem; 474 | width: 1.5rem; 475 | } 476 | #love { 477 | color: rgba(107, 114, 128, 1); 478 | font-size: 0.875rem; 479 | line-height: 1.25rem; 480 | margin-top: 3.5rem; 481 | opacity: 0.6; 482 | text-align: center; 483 | } 484 | #love svg { 485 | color: rgba(252, 165, 165, 1); 486 | width: 1.25rem; 487 | height: 1.25rem; 488 | display: inline; 489 | margin-top: -0.25rem; 490 | } 491 | @media screen and (min-width: 768px) { 492 | #hero { 493 | grid-template-columns: repeat(2, minmax(0, 1fr)); 494 | } 495 | #hero .logo-container { 496 | display: flex; 497 | } 498 | #middle-content { 499 | grid-template-columns: repeat(2, minmax(0, 1fr)); 500 | } 501 | } 502 | -------------------------------------------------------------------------------- /apps/react/remote/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import styles from './page.module.css'; 2 | 3 | export default function Index() { 4 | /* 5 | * Replace the elements below with your own. 6 | * 7 | * Note: The corresponding styles are in the ./index.css file. 8 | */ 9 | return ( 10 |
11 |
12 |
13 |
14 |

15 | Hello there, 16 | Welcome react-remote 👋 17 |

18 |
19 | 20 |
21 |
22 |

23 | 29 | 35 | 36 | You're up and running 37 |

38 | What's next? 39 |
40 |
41 | 47 | 48 | 49 |
50 |
51 | 52 | 355 | 356 |
357 |

Next steps

358 |

Here are some things you can do with Nx:

359 |
360 | 361 | 367 | 373 | 374 | Add UI library 375 | 376 |
377 |                 # Generate UI lib
378 |                 nx g @nx/next:library ui
379 |                 # Add a component
380 |                 nx g @nx/next:component ui/src/lib/button
381 |               
382 |
383 |
384 | 385 | 391 | 397 | 398 | View project details 399 | 400 |
nx show project react-remote --web
401 |
402 |
403 | 404 | 410 | 416 | 417 | View interactive project graph 418 | 419 |
nx graph
420 |
421 |
422 | 423 | 429 | 435 | 436 | Run affected commands 437 | 438 |
439 |                 # see what's been affected by changes
440 |                 nx affected:graph
441 |                 # run tests for current changes
442 |                 nx affected:test
443 |                 # run e2e tests for current changes
444 |                 nx affected:e2e
445 |               
446 |
447 |
448 | 449 |

450 | Carefully crafted with 451 | 457 | 463 | 464 |

465 |
466 |
467 |
468 | ); 469 | } 470 | --------------------------------------------------------------------------------