diff --git a/libs/native-federation/collection.json b/libs/native-federation/collection.json index f5b16fac..247d56b1 100644 --- a/libs/native-federation/collection.json +++ b/libs/native-federation/collection.json @@ -1,17 +1,22 @@ { "$schema": "../../node_modules/@angular-devkit/schematics/collection-schema.json", - "name": "module-federation", + "name": "native-federation", "version": "0.0.1", "schematics": { "ng-add": { "factory": "./src/schematics/init/schematic", "schema": "./src/schematics/init/schema.json", - "description": "Initialize an angular project for webpack module federation" + "description": "Initialize an angular project for native federation" }, "init": { "factory": "./src/schematics/init/schematic", "schema": "./src/schematics/init/schema.json", - "description": "Initialize an angular project for webpack module federation" + "description": "Initialize an angular project for native federation" + }, + "remove": { + "factory": "./src/schematics/remove/schematic", + "schema": "./src/schematics/remove/schema.json", + "description": "Removes native federation" } } } diff --git a/libs/native-federation/src/schematics/init/schematic.ts b/libs/native-federation/src/schematics/init/schematic.ts index 654ebf8f..7994620f 100644 --- a/libs/native-federation/src/schematics/init/schematic.ts +++ b/libs/native-federation/src/schematics/init/schematic.ts @@ -7,6 +7,7 @@ import { mergeWith, template, move, + noop, } from '@angular-devkit/schematics'; import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; @@ -51,16 +52,22 @@ export default function config(options: MfSchematicSchema): Rule { const remoteMap = await generateRemoteMap(workspace, projectName); - if (options.type === 'dynamic-host') { + if (options.type === 'dynamic-host' && !tree.exists(manifestPath)) { tree.create(manifestPath, JSON.stringify(remoteMap, null, '\t')); } - const generateRule = await generateFederationConfig( - remoteMap, - projectRoot, - projectSourceRoot, - options - ); + const federationConfigPath = path.join(projectRoot, 'federation.config.js'); + + const exists = tree.exists(federationConfigPath); + + const generateRule = !exists + ? await generateFederationConfig( + remoteMap, + projectRoot, + projectSourceRoot, + options + ) + : noop; updateWorkspaceConfig(tree, normalized, workspace, workspaceFileName); @@ -104,9 +111,17 @@ function updateWorkspaceConfig( projectConfig.architect.build = { builder: '@angular-architects/native-federation:build', - options: { - target: `${projectName}:esbuild:production`, + options: {}, + configurations: { + production: { + target: `${projectName}:esbuild:production`, + }, + development: { + target: `${projectName}:esbuild:development`, + dev: true, + }, }, + defaultConfiguration: 'production', }; projectConfig.architect['serve-original'] = projectConfig.architect.serve; @@ -121,6 +136,15 @@ function updateWorkspaceConfig( }, }; + const serveSsr = projectConfig.architect['serve-ssr']; + if (serveSsr && !serveSsr.options) { + serveSsr.options = {}; + } + + if (serveSsr) { + serveSsr.options.port = port; + } + // projectConfig.architect.serve.builder = serveBuilder; // TODO: Register further builders when ready tree.overwrite(workspaceFileName, JSON.stringify(workspace, null, '\t')); diff --git a/libs/native-federation/src/schematics/remove/schema.d.ts b/libs/native-federation/src/schematics/remove/schema.d.ts new file mode 100644 index 00000000..7e964f90 --- /dev/null +++ b/libs/native-federation/src/schematics/remove/schema.d.ts @@ -0,0 +1,3 @@ +export interface MfSchematicSchema { + project: string; +} diff --git a/libs/native-federation/src/schematics/remove/schema.json b/libs/native-federation/src/schematics/remove/schema.json new file mode 100644 index 00000000..19dd98af --- /dev/null +++ b/libs/native-federation/src/schematics/remove/schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "mf", + "title": "", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The project to add module federation", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "Project name (press enter for default project)" + } + } +} diff --git a/libs/native-federation/src/schematics/remove/schematic.ts b/libs/native-federation/src/schematics/remove/schematic.ts new file mode 100644 index 00000000..591fb6f9 --- /dev/null +++ b/libs/native-federation/src/schematics/remove/schematic.ts @@ -0,0 +1,179 @@ +import { Rule, Tree, noop } from '@angular-devkit/schematics'; + +import { MfSchematicSchema } from './schema'; + +import * as path from 'path'; + +type NormalizedOptions = { + polyfills: string; + projectName: string; + projectRoot: string; + projectSourceRoot: string; + manifestPath: string; + projectConfig: any; + main: string; +}; + +export default function remove(options: MfSchematicSchema): Rule { + return async function (tree /*, context*/) { + const workspaceFileName = getWorkspaceFileName(tree); + const workspace = JSON.parse(tree.read(workspaceFileName).toString('utf8')); + + const normalized = normalizeOptions(options, workspace); + + const { polyfills, projectRoot } = normalized; + + const bootstrapPath = path.join(projectRoot, 'src/bootstrap.ts'); + const mainPath = path.join(projectRoot, 'src/main.ts'); + + makeMainSync(tree, bootstrapPath, mainPath); + updatePolyfills(tree, polyfills); + updateWorkspaceConfig(tree, normalized, workspace, workspaceFileName); + }; +} + +function makeMainSync(tree, bootstrapPath: string, mainPath: string) { + if (tree.exists(bootstrapPath) && tree.exists(mainPath)) { + tree.delete(mainPath); + tree.rename(bootstrapPath, mainPath); + } +} + +function updateWorkspaceConfig( + tree: Tree, + options: NormalizedOptions, + workspace: any, + workspaceFileName: string +) { + const { projectConfig } = options; + + if (!projectConfig?.architect?.build || !projectConfig?.architect?.serve) { + throw new Error( + `The project doen't have a build or serve target in angular.json!` + ); + } + + if (projectConfig.architect.esbuild) { + projectConfig.architect.build = projectConfig.architect.esbuild; + delete projectConfig.architect.esbuild; + } + + if (projectConfig.architect['serve-original']) { + projectConfig.architect.serve = projectConfig.architect['serve-original']; + delete projectConfig.architect['serve-original']; + } + + if (projectConfig.architect.serve) { + const conf = projectConfig.architect.serve.configurations; + conf.production.browserTarget = conf.production.browserTarget.replace( + ':esbuild:', + ':build:' + ); + conf.development.browserTarget = conf.development.browserTarget.replace( + ':esbuild:', + ':build:' + ); + } + + tree.overwrite(workspaceFileName, JSON.stringify(workspace, null, '\t')); +} + +function normalizeOptions( + options: MfSchematicSchema, + workspace: any +): NormalizedOptions { + if (!options.project) { + options.project = workspace.defaultProject; + } + + const projects = Object.keys(workspace.projects); + + if (!options.project && projects.length === 0) { + throw new Error( + `No default project found. Please specifiy a project name!` + ); + } + + if (!options.project) { + console.log( + 'Using first configured project as default project: ' + projects[0] + ); + options.project = projects[0]; + } + + const projectName = options.project; + const projectConfig = workspace.projects[projectName]; + + if (!projectConfig) { + throw new Error(`Project ${projectName} not found!`); + } + + const projectRoot: string = projectConfig.root?.replace(/\\/g, '/'); + const projectSourceRoot: string = projectConfig.sourceRoot?.replace( + /\\/g, + '/' + ); + + const manifestPath = path + .join(projectRoot, 'src/assets/federation.manifest.json') + .replace(/\\/g, '/'); + + const main = projectConfig.architect.build.options.main; + + if (!projectConfig.architect.build.options.polyfills) { + projectConfig.architect.build.options.polyfills = []; + } + + const polyfills = projectConfig.architect.build.options.polyfills; + return { + polyfills, + projectName, + projectRoot, + projectSourceRoot, + manifestPath, + projectConfig, + main, + }; +} + +function updatePolyfills(tree, polyfills: any) { + if (typeof polyfills === 'string') { + updatePolyfillsFile(tree, polyfills); + } else { + updatePolyfillsArray(tree, polyfills); + } +} + +function updatePolyfillsFile(tree, polyfills: any) { + let polyfillsContent = tree.readText(polyfills); + if (polyfillsContent.includes('es-module-shims')) { + polyfillsContent = polyfillsContent.replace( + `import 'es-module-shims';`, + '' + ); + tree.overwrite(polyfills, polyfillsContent); + } +} + +function updatePolyfillsArray(tree, polyfills: any) { + const polyfillsConfig = polyfills as string[]; + + const index = polyfillsConfig.findIndex((p) => p === 'es-module-shims'); + if (index === -1) { + return; + } + + polyfillsConfig.splice(index, 1); +} + +export function getWorkspaceFileName(tree: Tree): string { + if (tree.exists('angular.json')) { + return 'angular.json'; + } + if (tree.exists('workspace.json')) { + return 'workspace.json'; + } + throw new Error( + "angular.json or workspace.json expected! Did you call this in your project's root?" + ); +}