diff --git a/.gitignore b/.gitignore index 016df259..7c8239ff 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,7 @@ dist-legacy/ flow-typed/ eslint-report.html types/* +.nitro #distrubution distribution/ diff --git a/angular/app-types/angular-app-type/angular-app-options.ts b/angular/app-types/angular-app-type/angular-app-options.ts index b0b9466a..69e36ab9 100644 --- a/angular/app-types/angular-app-type/angular-app-options.ts +++ b/angular/app-types/angular-app-type/angular-app-options.ts @@ -1,9 +1,12 @@ -import { BrowserOptions, DevServerOptions } from '@bitdev/angular.dev-services.common'; +import { + ApplicationOptions, + BrowserOptions, + DevServerOptions +} from '@bitdev/angular.dev-services.common'; import { Bundler } from '@teambit/bundler'; import { WebpackConfigTransformer } from '@teambit/webpack'; import { AngularDeployContext } from './deploy-context'; - export type AngularAppOptions = { /** * Name of the application. @@ -16,9 +19,9 @@ export type AngularAppOptions = { sourceRoot: string; /** - * Instance of bundler to use. default is Webpack. + * Instance of bundler to use, default is esbuild after v17 and webpack before that. */ - bundler?: Bundler | string; + bundler?: Bundler; /** * Set webpack build transformers @@ -43,10 +46,10 @@ export type AngularAppOptions = { /** * Angular options for `bit build` */ - angularBuildOptions: BrowserOptions; + angularBuildOptions: BrowserOptions | ApplicationOptions; /** * Angular options for `bit run` */ - angularServeOptions: BrowserOptions & DevServerOptions; + angularServeOptions: (BrowserOptions & DevServerOptions) | (ApplicationOptions & DevServerOptions); }; diff --git a/angular/app-types/angular-app-type/angular.app-type.ts b/angular/app-types/angular-app-type/angular.app-type.ts index 9e3f0351..ee45cea0 100644 --- a/angular/app-types/angular-app-type/angular.app-type.ts +++ b/angular/app-types/angular-app-type/angular.app-type.ts @@ -2,6 +2,7 @@ import { GenericAngularEnv, getWorkspace, NG_APP_NAME } from '@bitdev/angular.de import { Application, ApplicationType } from '@teambit/application'; import { DependencyResolverAspect, DependencyResolverMain } from '@teambit/dependency-resolver'; import { EnvContext, EnvHandler } from '@teambit/envs'; +import { Logger } from '@teambit/logger'; import { Workspace } from '@teambit/workspace'; import { AngularAppOptions } from './angular-app-options'; import { AngularApp } from './angular.application'; @@ -12,7 +13,8 @@ interface AngularAppTypeOptions { } export class AngularAppType implements ApplicationType { - constructor(readonly name: string, private angularEnv: GenericAngularEnv, private context: EnvContext, private depsResolver: DependencyResolverMain, private workspace?: Workspace) {} + constructor(readonly name: string, private angularEnv: GenericAngularEnv, private context: EnvContext, private depsResolver: DependencyResolverMain, private logger: Logger, private workspace?: Workspace) { + } createApp(options: AngularAppOptions): Application { return new AngularApp( @@ -20,7 +22,8 @@ export class AngularAppType implements ApplicationType { this.context, options, this.depsResolver, - this.workspace, + this.logger, + this.workspace ); } @@ -29,7 +32,8 @@ export class AngularAppType implements ApplicationType { const name = options.name || NG_APP_NAME; const depsResolver = context.getAspect(DependencyResolverAspect.id); const workspace = getWorkspace(context); - return new AngularAppType(name, options.angularEnv, context, depsResolver, workspace); + const logger = context.createLogger(name); + return new AngularAppType(name, options.angularEnv, context, depsResolver, logger, workspace); }; } } diff --git a/angular/app-types/angular-app-type/angular.application.ts b/angular/app-types/angular-app-type/angular.application.ts index 2fe6c398..95e3acac 100644 --- a/angular/app-types/angular-app-type/angular.application.ts +++ b/angular/app-types/angular-app-type/angular.application.ts @@ -1,23 +1,35 @@ -import { GenericAngularEnv } from '@bitdev/angular.dev-services.common'; -import { AngularPreview, BundlerProvider, DevServerProvider } from '@bitdev/angular.dev-services.preview.preview'; +import { VERSION } from '@angular/cli'; +import { + ApplicationOptions, + GenericAngularEnv, + normalizePath +} from '@bitdev/angular.dev-services.common'; +import { + AngularPreview, + BundlerProvider, + DevServerProvider +} from '@bitdev/angular.dev-services.preview.preview'; import { AppBuildContext, AppContext, Application } from '@teambit/application'; import { Bundler, BundlerContext, DevServer, DevServerContext } from '@teambit/bundler'; import { Component } from '@teambit/component'; import { DependencyResolverMain } from '@teambit/dependency-resolver'; import { EnvContext, EnvHandler } from '@teambit/envs'; import { CACHE_ROOT } from '@teambit/legacy/dist/constants'; -import { pathNormalizeToLinux } from '@teambit/legacy/dist/utils'; +import { Logger } from '@teambit/logger'; import { Preview } from '@teambit/preview'; import { Port } from '@teambit/toolbox.network.get-port'; import { Workspace } from '@teambit/workspace'; -import { existsSync, mkdirSync, writeFileSync } from 'fs-extra'; +import assert from 'assert'; +import { existsSync, mkdirSync, outputJsonSync } from 'fs-extra'; import { cloneDeep } from 'lodash'; import objectHash from 'object-hash'; import { join } from 'path'; import { readConfigFile, sys } from 'typescript'; import { AngularAppOptions } from './angular-app-options'; import { AngularAppBuildResult } from './angular-build-result'; -import { expandIncludeExclude } from './utils'; +import { buildApplication } from './application.bundler'; +import { serveApplication } from './application.dev-server'; +import { expandIncludeExclude, JsonObject } from './utils'; const writeHash = new Map(); @@ -35,17 +47,18 @@ export class AngularApp implements Application { private envContext: EnvContext, readonly options: AngularAppOptions, private depsResolver: DependencyResolverMain, + private logger: Logger, private workspace?: Workspace ) { this.name = options.name; - const idName = `bitdev.angular/${this.name}`; + const idName = `bitdev.angular/${ this.name }`; this.tempFolder = workspace?.getTempDir(idName) || join(CACHE_ROOT, idName); if (!existsSync(this.tempFolder)) { mkdirSync(this.tempFolder, { recursive: true }); } - this.tsconfigPath = pathNormalizeToLinux(join(this.tempFolder, `__tsconfig-${Date.now()}.json`)); + this.tsconfigPath = normalizePath(join(this.tempFolder, `tsconfig/tsconfig-${ Date.now() }.json`)); this.preview = this.getPreview(); } @@ -55,11 +68,12 @@ export class AngularApp implements Application { return join(artifactsDir, this.name); } - private getDevServerContext(context: AppContext): DevServerContext { + private getDevServerContext(context: AppContext, appRootPath: string): DevServerContext { + // const ngEnvOptions = this.angularEnv.getNgEnvOptions(); return Object.assign(cloneDeep(context), { entry: [], - rootPath: '', - publicPath: `${this.publicDir}/${this.options.name}`, + rootPath: /*ngEnvOptions.devServer === 'vite' ? appRootPath : */'', + publicPath: `${ this.publicDir }/${ this.options.name }`, title: this.options.name }); } @@ -67,7 +81,7 @@ export class AngularApp implements Application { private getBundlerContext(context: AppBuildContext): BundlerContext { const { capsule, artifactsDir } = context; const publicDir = this.getPublicDir(artifactsDir); - const outputPath = pathNormalizeToLinux(join(capsule.path, publicDir)); + const outputPath = normalizePath(join(capsule.path, publicDir)); return Object.assign(cloneDeep(context), { targets: [{ @@ -76,7 +90,7 @@ export class AngularApp implements Application { outputPath }], entry: [], - rootPath: '/', + rootPath: '.', appName: this.options.name }); } @@ -97,73 +111,81 @@ export class AngularApp implements Application { }); } - private generateTsConfig(bitCmps: Component[], appRootPath: string, tsconfigPath: string): string { - const tsconfigJSON = readConfigFile(tsconfigPath, sys.readFile).config; + private generateTsConfig(bitCmps: Component[], appRootPath: string, tsconfigPath: string, serverEntry?: string): void { + const tsconfigJSON: JsonObject = readConfigFile(tsconfigPath, sys.readFile).config; // Add the paths to tsconfig to remap bit components to local folders tsconfigJSON.compilerOptions.paths = tsconfigJSON.compilerOptions.paths || {}; bitCmps.forEach((dep: Component) => { - let componentDir = this.workspace?.componentDir(dep.id, { - ignoreVersion: true - }); - if (componentDir) { - componentDir = pathNormalizeToLinux(componentDir); - const pkgName = this.depsResolver.getPackageName(dep); - // TODO we should find a way to use the real entry file based on the component config because people can change it - tsconfigJSON.compilerOptions.paths[pkgName] = [`${componentDir}/public-api.ts`, `${componentDir}`]; - tsconfigJSON.compilerOptions.paths[`${pkgName}/*`] = [`${componentDir}/*`]; - } + let componentDir = this.workspace?.componentDir(dep.id, { + ignoreVersion: true + }); + if (componentDir) { + componentDir = normalizePath(componentDir); + const pkgName = this.depsResolver.getPackageName(dep); + // TODO we should find a way to use the real entry file based on the component config because people can change it + tsconfigJSON.compilerOptions.paths[pkgName] = [`${ componentDir }/public-api.ts`, `${ componentDir }`]; + tsconfigJSON.compilerOptions.paths[`${ pkgName }/*`] = [`${ componentDir }/*`]; + } }); + if (serverEntry) { + tsconfigJSON.files.push(serverEntry); + } + const tsconfigContent = expandIncludeExclude(tsconfigJSON, this.tsconfigPath, [appRootPath]); const hash = objectHash(tsconfigContent); - // write only if link has changed (prevents triggering fs watches) if (writeHash.get(this.tsconfigPath) !== hash) { - writeFileSync(this.tsconfigPath, tsconfigContent); + outputJsonSync(this.tsconfigPath, tsconfigContent, { spaces: 2 }); writeHash.set(this.tsconfigPath, hash); } + } + + async getDevServer(context: AppContext, appRootPath: string): Promise { + const devServerContext = this.getDevServerContext(context, appRootPath); + const preview = this.preview(this.envContext); - return tsconfigContent; + return preview.getDevServer(devServerContext)(this.envContext); } - async getDevServer(context: AppContext): Promise { - if(!this.workspace) { - throw new Error('workspace is not defined'); - } + // TODO: fix return type once bit has a new stable version + async run(context: AppContext): Promise { + assert(this.workspace, 'Workspace is not defined'); + const port = context.port || (await Port.getPortFromRange(this.options.portRange || [3000, 4000])); const appRootPath = this.workspace.componentDir(context.appComponent.id, { ignoreVersion: true - }) || ''; + }); const tsconfigPath = join(appRootPath, this.options.angularServeOptions.tsConfig); const workspaceCmpsIDs = await this.workspace.listIds(); const bitCmps = await this.workspace.getMany(workspaceCmpsIDs); this.generateTsConfig(bitCmps, appRootPath, tsconfigPath); - const devServerContext = this.getDevServerContext(context); - const preview = this.preview(this.envContext); - return preview.getDevServer(devServerContext)(this.envContext); - } + if (Number(VERSION.major) >= 16) { + await serveApplication({ + angularOptions: { + ...this.options.angularBuildOptions as ApplicationOptions, + tsConfig: this.tsconfigPath + }, + sourceRoot: this.options.sourceRoot || 'src', + workspaceRoot: appRootPath, + port, + logger: this.logger, + tempFolder: this.tempFolder + }); + return port; + } - async run(context: AppContext): Promise { - const port = context.port || (await Port.getPortFromRange(this.options.portRange || [3000, 4000])); - const devServer = await this.getDevServer(context); + const devServer = await this.getDevServer(context, appRootPath); await devServer.listen(port); return port; } async getBundler(context: AppBuildContext): Promise { - if (this.options.bundler && typeof this.options.bundler !== 'string') { - return this.options.bundler as Bundler; + if (this.options.bundler) { + return this.options.bundler; } - if (this.options.bundler === 'vite') { - throw new Error('implement vite bundler'); - } - - const { capsule } = context; - const appRootPath = capsule.path; - const tsconfigPath = join(appRootPath, this.options.angularBuildOptions.tsConfig); - this.generateTsConfig([capsule.component], appRootPath, tsconfigPath); const bundlerContext = this.getBundlerContext(context); const preview = this.preview(this.envContext); @@ -171,10 +193,33 @@ export class AngularApp implements Application { } async build(context: AppBuildContext): Promise { - const bundler = await this.getBundler(context); - await bundler.run(); + const { capsule } = context; + const outputPath = this.getPublicDir(context.artifactsDir); + const appRootPath = capsule.path; + const tsconfigPath = join(appRootPath, this.options.angularBuildOptions.tsConfig); + const appOptions = this.options.angularBuildOptions as ApplicationOptions; + const entryServer = appOptions.ssr && Number(VERSION.major) >= 17 ? './entry.server.ts' : undefined; + this.generateTsConfig([capsule.component], appRootPath, tsconfigPath, entryServer); + + if (!this.options.bundler && Number(VERSION.major) >= 16) { + await buildApplication({ + angularOptions: { + ...appOptions, + tsConfig: this.tsconfigPath + }, + outputPath, + sourceRoot: this.options.sourceRoot || 'src', + workspaceRoot: context.capsule.path, + logger: this.logger, + tempFolder: this.tempFolder, + entryServer + }); + } else { + const bundler = await this.getBundler(context); + await bundler.run(); + } return { - publicDir: `${this.getPublicDir(context.artifactsDir)}/${this.publicDir}` + publicDir: outputPath }; } } diff --git a/angular/app-types/angular-app-type/application.bundler.ts b/angular/app-types/angular-app-type/application.bundler.ts new file mode 100644 index 00000000..f9c3a5d8 --- /dev/null +++ b/angular/app-types/angular-app-type/application.bundler.ts @@ -0,0 +1,261 @@ +/* eslint-disable no-param-reassign */ +import { OutputHashing } from '@angular-devkit/build-angular'; +import type { JsonObject } from '@angular-devkit/core'; +import { VERSION } from '@angular/cli'; +import { + ApplicationOptions, + dedupPaths, + getLoggerApi, + loadEsmModule, + normalizePath +} from '@bitdev/angular.dev-services.common'; +import { buildApplicationInternal } from '@bitdev/angular.dev-services.ng-compat'; +import { Logger } from '@teambit/logger'; +import assert from 'assert'; +import { outputFileSync } from 'fs-extra'; +// @ts-ignore +import type { NitroConfig } from 'nitropack'; +import { basename, extname, join, posix, relative, resolve } from 'path'; +import { getIndexInputFile } from './utils'; + +export type BuildApplicationOptions = { + angularOptions: Partial; + outputPath: string; + sourceRoot: string; + workspaceRoot: string; + logger: Logger; + tempFolder: string; + entryServer?: string; +} + +export async function buildApplication(options: BuildApplicationOptions): Promise { + const { angularOptions: { tsConfig, ssr } } = options; + const isSsr = !!ssr && Number(VERSION.major) >= 17; + + assert(tsConfig, 'tsConfig option is required'); + + if(isSsr) { + addEntryServer(options); + } + + const appOptions = getAppOptions(options, isSsr); + const builderContext = getBuilderContext(options); + const builderPlugins: any[] = []; + + for await (const result of buildApplicationInternal( + appOptions as any, + builderContext, + { write: true }, + builderPlugins + )) { + if (!result.success && result.errors) { + throw new Error(result.errors.map((err: any) => err.text).join('\n')); + } + } + + if (isSsr) { + await buildNitro(options); + } +} + +function addEntryServer(options: BuildApplicationOptions): void { + const { entryServer, angularOptions: { ssr, server } } = options; + if (ssr && entryServer) { + const fileContent = `import type { ApplicationRef } from '@angular/core'; +import { renderApplication, renderModule } from '@angular/platform-server'; +import bootstrap from '${ server?.replace(extname(server), '') }'; + +function isBootstrapFn(value: unknown): value is () => Promise { + // We can differentiate between a module and a bootstrap function by reading compiler-generated "ɵmod" static property: + return typeof value === 'function' && !('ɵmod' in value); +} + +export default async function render(url: string, document: string) { + let html: string; + if (isBootstrapFn(bootstrap)) { + html = await renderApplication(bootstrap, { + document, + url + }); + } else { + html = await renderModule(bootstrap, { + document, + url + }); + } + + return html; +}`; + outputFileSync(resolve(options.workspaceRoot, entryServer), fileContent); + } +} + +function getAppOptions(options: BuildApplicationOptions, isSsr: boolean): any { + const { entryServer, angularOptions, outputPath, sourceRoot, workspaceRoot } = options; + + // declare constants for all reusable values here + const normalizedIndex = `./${ join(sourceRoot, 'index.html') }`; + const normalizedBrowser = `./${ join(sourceRoot, 'main.ts') }`; + + const dedupedAssets = dedupPaths([posix.join(sourceRoot, `assets/**/*`), ...(angularOptions.assets ?? [])]); + const dedupedStyles = dedupPaths([posix.join(sourceRoot, `styles.${angularOptions.inlineStyleLanguage}`), ...(angularOptions.styles ?? [])]); + + return { + ...angularOptions, + baseHref: angularOptions.baseHref ?? './', + preserveSymlinks: false, + outputPath, + index: angularOptions.index ?? normalizedIndex, + browser: angularOptions.browser ?? normalizedBrowser, + tsConfig: relative(workspaceRoot, angularOptions.tsConfig!), + assets: dedupedAssets, + styles: dedupedStyles, + scripts: angularOptions.scripts, + namedChunks: angularOptions.namedChunks ?? true, + optimization: angularOptions.optimization ?? true, + aot: true, + deleteOutputPath: true, + sourceMap: angularOptions.sourceMap ?? true, + outputHashing: angularOptions.outputHashing ?? OutputHashing.All, + watch: false, + server: isSsr ? angularOptions.server : undefined, + prerender: isSsr ? (angularOptions.prerender ?? !!angularOptions.server) : undefined, + ssr: isSsr ? { + entry: entryServer + } : undefined + }; +} + + +function getBuilderContext(options: BuildApplicationOptions): any { + const { workspaceRoot, sourceRoot, tempFolder } = options; + return { + id: 1, + builder: { + builderName: '@bitdev/build-angular:application', + description: 'Bit Angular Application Builder', + optionSchema: {} + }, + logger: getLoggerApi(options.logger), + workspaceRoot: workspaceRoot, + currentDirectory: '', + // doesn't matter, just needs to exist + target: { + project: 'bit-ng-app-builder', + target: 'build' + }, + getProjectMetadata: function(projectName: string): Promise { + return Promise.resolve({ + root: '', + sourceRoot, + cli: { cache: { enabled: true, path: resolve(tempFolder, 'angular/cache') } } + }); + } + }; +} + +async function getNitroConfig(options: BuildApplicationOptions): Promise { + const { + workspaceRoot, + tempFolder, + outputPath, + angularOptions: { ssr, index, prerender } + } = options; + + const outputDir = normalizePath(join(workspaceRoot, outputPath)); + const browserDir = normalizePath(resolve(outputDir, 'browser')); + const serverDir = normalizePath(resolve(outputDir, 'server')); + const nitroDir = normalizePath(resolve(outputDir, 'nitro')); + const indexPath = getIndexInputFile(index!); + + const prerenderedRoutes = prerender ? (await import(`${ outputDir }/prerendered-routes.json`)).default : undefined; + + return { + rootDir: workspaceRoot, + logLevel: 1, // TODO reset this to 3 or 2 https://github.com/unjs/consola/#log-level + srcDir: normalizePath(`${ workspaceRoot }/src/server`), + scanDirs: [normalizePath(`${ workspaceRoot }/src/server`)], + buildDir: resolve(tempFolder, 'nitro'), + + alias: ssr ? { + '#alias/entry.server': normalizePath(join(serverDir, 'server.mjs')), + '#alias/index': normalizePath(join(serverDir, `${ basename(indexPath, extname(indexPath)) }.server.html`)) + } : {}, + serverAssets: ssr ? [{ + baseName: 'public', + dir: browserDir + }] : [], + publicAssets: ssr ? [{ + dir: browserDir + }] : [], + output: ssr ? { + dir: nitroDir, + publicDir: posix.join(nitroDir, 'public') + } : {}, + externals: { + external: [ + 'rxjs', + 'node-fetch-native/dist/polyfill' + ] + }, + moduleSideEffects: [ + 'zone.js/node', + 'zone.js/fesm2015/zone-node' + ], + renderer: ssr ? normalizePath(require.resolve('./runtime/renderer')) : undefined, + // handlers: ssr ? [{ + // handler: normalizePath(require.resolve('./runtime/api-middleware')), + // middleware: true + // }] : [], + prerender: prerenderedRoutes, + typescript: { + generateTsConfig: false + } + // TODO allow customizing this + // runtimeConfig: { ...nitroOptions?.runtimeConfig }, + }; +} + +async function buildNitro(options: BuildApplicationOptions): Promise { + const logger = options.logger.createLongProcessLogger('Building nitro server', options.angularOptions.prerender ? 2 : 1); + const nitroConfig = await getNitroConfig(options); + + const { + createNitro, + build, + prepare, + copyPublicAssets, + prerender + } = await loadEsmModule('nitropack'); + + + const nitro = await createNitro({ + dev: false, + ...nitroConfig + }); + + await prepare(nitro); + await copyPublicAssets(nitro); + + // const indexOutput = `${ nitroConfig?.output?.publicDir }/index.html`; + // if ( + // nitroConfig?.prerender?.routes + // && nitroConfig?.prerender?.routes.find((route: string) => route === '/') + // && existsSync(indexOutput) + // ) { + // // Remove the root index.html so it can be replaced with the prerendered version + // unlinkSync(`${ nitroConfig?.output?.publicDir }/index.html`); + // } + + + if (options.angularOptions.prerender) { + logger.logProgress(`Prerendering static pages`); + await prerender(nitro); + } + + logger.logProgress('Building Server files'); + await build(nitro); + + await nitro.close(); + logger.end(); +} diff --git a/angular/app-types/angular-app-type/application.dev-server.ts b/angular/app-types/angular-app-type/application.dev-server.ts new file mode 100644 index 00000000..97cbb42b --- /dev/null +++ b/angular/app-types/angular-app-type/application.dev-server.ts @@ -0,0 +1,183 @@ +/* eslint-disable no-param-reassign */ +import { executeDevServerBuilder, OutputHashing } from '@angular-devkit/build-angular'; +import type { JsonObject } from '@angular-devkit/core'; +import { VERSION } from '@angular/cli'; +import { + dedupPaths, + getLoggerApi, + loadEsmModule, + normalizePath +} from '@bitdev/angular.dev-services.common'; +import { + type ApplicationBuilderOptions, + type DevServerBuilderOptions +} from '@bitdev/angular.dev-services.ng-compat'; +import { Logger } from '@teambit/logger'; +import assert from 'assert'; +import { createEvent } from 'h3'; +// @ts-ignore +import type { NitroConfig } from 'nitropack'; +import { join, posix, relative, resolve } from 'path'; +// @ts-ignore +import type { Connect } from 'vite'; + +export type ServeApplicationOptions = { + angularOptions: Partial; + sourceRoot: string; + workspaceRoot: string; + logger: Logger; + port: number; + tempFolder: string; +} + +// TODO allow customizing this +const API_ROUTE = '/api'; +const OUTPUT_PATH = 'dist'; +const BUILDER_NAME = '@angular-devkit/build-angular:application'; +const CACHE_PATH = 'angular/cache'; + +export async function serveApplication(options: ServeApplicationOptions): Promise { + const { + angularOptions + } = options; + const isSsr = !!angularOptions.server && Number(VERSION.major) >= 17; + + assert(angularOptions.tsConfig, 'tsConfig option is required'); + + const appOptions = getAppOptions(options, isSsr); + const builderContext = getBuilderContext(options, appOptions); + // intercept SIGINT and exit cleanly, otherwise the process will not exit properly on Ctrl+C + process.on('SIGINT', () => process.exit(1)); + + const devServerOptions = isSsr ? { + middleware: [await createNitroApiMiddleware(options)] + } : undefined; + + // @ts-ignore only v17+ has 4 arguments, previous versions only have 3 + const res = await executeDevServerBuilder(appOptions, builderContext as any, undefined, devServerOptions).toPromise(); + console.log(res); +} + +function getAppOptions(options: ServeApplicationOptions, isSsr: boolean): ApplicationBuilderOptions & DevServerBuilderOptions { + const { angularOptions, port, sourceRoot, workspaceRoot } = options; + // declare constants for all reusable values here + const normalizedIndex = `./${ join(sourceRoot, 'index.html') }`; + const normalizedBrowser = `./${ join(sourceRoot, 'main.ts') }`; + const serverPath = `./${ join(sourceRoot, 'main.server.ts') }`; + + const dedupedAssets = dedupPaths([posix.join(sourceRoot, `assets/**/*`), ...(angularOptions.assets ?? [])]); + const dedupedStyles = dedupPaths([posix.join(sourceRoot, `styles.${ angularOptions.inlineStyleLanguage }`), ...(angularOptions.styles ?? [])]); + + return { + ...angularOptions, + baseHref: angularOptions.baseHref ?? './', + preserveSymlinks: false, + outputPath: OUTPUT_PATH, + index: angularOptions.index ?? normalizedIndex, + browser: angularOptions.browser ?? normalizedBrowser, + tsConfig: relative(workspaceRoot, angularOptions.tsConfig!), + assets: dedupedAssets, + styles: dedupedStyles, + scripts: angularOptions.scripts, + namedChunks: angularOptions.namedChunks ?? true, + optimization: angularOptions.optimization ?? true, + aot: true, + deleteOutputPath: true, + sourceMap: angularOptions.sourceMap ?? true, + outputHashing: angularOptions.outputHashing ?? OutputHashing.All, + watch: true, + server: isSsr ? angularOptions.server ?? serverPath : undefined, + prerender: isSsr ? angularOptions.prerender ?? !!angularOptions.server : false, + ssr: isSsr ? (angularOptions.ssr ?? !!angularOptions.server) : false, + port, + browserTarget: BUILDER_NAME, + // @ts-ignore Angular 17+ + buildTarget: BUILDER_NAME + }; +} + +function getBuilderContext(options: ServeApplicationOptions, appOptions: ApplicationBuilderOptions & DevServerBuilderOptions) { + const { workspaceRoot } = options; + return { + id: 1, + builder: { + builderName: BUILDER_NAME, + description: 'Bit Angular Application Builder', + optionSchema: {} + }, + logger: getLoggerApi(options.logger), + workspaceRoot: workspaceRoot, + currentDirectory: '', + // doesn't matter, just needs to exist + target: { + project: 'bit-ng-app-builder', + target: 'development' + }, + getProjectMetadata: getProjectMetadata(options), + addTeardown: (teardown: () => Promise | void) => { + teardown(); + return; + }, + getBuilderNameForTarget: () => Promise.resolve(BUILDER_NAME), + getTargetOptions: () => Promise.resolve(appOptions as any as JsonObject), + validateOptions: () => Promise.resolve(appOptions as any) + }; +} + +function getProjectMetadata(options: ServeApplicationOptions) { + const { sourceRoot, tempFolder } = options; + return function(projectName: string): Promise { + return Promise.resolve({ + root: '', + sourceRoot, + cli: { + cache: { + enabled: true, + path: resolve(tempFolder, CACHE_PATH) + } + } + }); + }; +} + +function getNitroConfig(options: ServeApplicationOptions): NitroConfig { + const { workspaceRoot, tempFolder } = options; + const rootDir = workspaceRoot; + return { + rootDir, + logLevel: 2, + srcDir: normalizePath(`${ rootDir }/src/server`), + scanDirs: [normalizePath(`${ rootDir }/src/server`)], + buildDir: resolve(tempFolder, 'nitro') + }; +} + +async function createNitroApiMiddleware(options: ServeApplicationOptions): Promise { + const nitroConfig = getNitroConfig(options); + + const { + createNitro, + createDevServer, + build + } = await loadEsmModule('nitropack'); + + const nitro = await createNitro({ + dev: true, + ...nitroConfig + }); + + const server = createDevServer(nitro); + await build(nitro); + + return async( + req: any, + res: any, + next: any + ) => { + if (req.originalUrl.startsWith(API_ROUTE)) { + await server.app.handler(createEvent(req, res)); + return; + } + next(); + }; +} diff --git a/angular/app-types/angular-app-type/component.json b/angular/app-types/angular-app-type/component.json index e02010e0..8f065332 100644 --- a/angular/app-types/angular-app-type/component.json +++ b/angular/app-types/angular-app-type/component.json @@ -8,12 +8,17 @@ "teambit.dependencies/dependency-resolver": { "policy": { "dependencies": { + "h3": "^1.9.0", + "nitropack": "^2.8.0", "@angular-devkit/build-angular": "-", - "typescript": "-" + "typescript": "-", + "#alias": "-" }, "peerDependencies": { "@angular-devkit/build-angular": ">= 0.0.1", - "typescript": ">= 3.5.3" + "@angular/cli": ">= 13.0.0", + "typescript": ">= 3.5.3", + "vite": "^4.5.0 || ^5.0.0" } } } diff --git a/angular/app-types/angular-app-type/runtime/api-middleware.js b/angular/app-types/angular-app-type/runtime/api-middleware.js new file mode 100644 index 00000000..b350311a --- /dev/null +++ b/angular/app-types/angular-app-type/runtime/api-middleware.js @@ -0,0 +1,29 @@ +/** + * This file is written in JavaScript + * because it is used by Nitro to build + * the renderer for SSR. + * + * The package is shipped as commonjs + * which won't be parsed by Nitro correctly. + */ +import { eventHandler, proxyRequest } from 'h3'; + +export default eventHandler(async (event) => { + const apiPrefix = `/${import.meta.env.RUNTIME_CONFIG?.apiPrefix ?? 'api'}`; + if (event.node.req.url?.startsWith(apiPrefix)) { + const reqUrl = event.node.req.url?.replace(apiPrefix, ''); + + if ( + event.node.req.method === 'GET' && + // in the case of XML routes, we want to proxy the request so that nitro gets the correct headers + // and can render the XML correctly as a static asset + !event.node.req.url?.endsWith('.xml') + ) { + return $fetch(reqUrl); + } + + return proxyRequest(event, reqUrl, { + fetch: $fetch.native, + }); + } +}); diff --git a/angular/app-types/angular-app-type/runtime/renderer.js b/angular/app-types/angular-app-type/runtime/renderer.js new file mode 100644 index 00000000..345dcfb4 --- /dev/null +++ b/angular/app-types/angular-app-type/runtime/renderer.js @@ -0,0 +1,18 @@ +/** + * This file is written in JavaScript + * because it is used by Nitro to build + * the renderer for SSR. + * + * The package is shipped as commonjs + * which won't be parsed by Nitro correctly. + */ +import { eventHandler } from 'h3'; + +import renderer from '#alias/entry.server'; +import template from '#alias/index'; + +export default eventHandler(async (event) => { + const html = await renderer(event.node.req.url, template); + + return html; +}); diff --git a/angular/app-types/angular-app-type/utils.ts b/angular/app-types/angular-app-type/utils.ts index e4fc58b8..d6c2b1f3 100644 --- a/angular/app-types/angular-app-type/utils.ts +++ b/angular/app-types/angular-app-type/utils.ts @@ -1,6 +1,11 @@ -import { pathNormalizeToLinux } from "@teambit/legacy/dist/utils"; +import { ApplicationOptions, normalizePath } from '@bitdev/angular.dev-services.common'; +import assert from 'assert'; import { flatten } from 'lodash'; -import { relative, dirname } from 'path'; +import { dirname, relative } from 'path'; + +export interface JsonObject { + [prop: string]: any; +} /** * Takes a tsconfig.json file, a list of component directories, and returns a new tsconfig.json file with the include @@ -10,7 +15,7 @@ import { relative, dirname } from 'path'; * @param {string[]} compDirs - An array of paths to the component directories. * @returns the tsConfig object. */ -export function expandIncludeExclude(tsconfigJSON: any, targetPath: string, compDirs: string[]): string { +export function expandIncludeExclude(tsconfigJSON: JsonObject, targetPath: string, compDirs: string[]): JsonObject { // eslint-disable-next-line no-param-reassign targetPath = dirname(targetPath); @@ -19,7 +24,7 @@ export function expandIncludeExclude(tsconfigJSON: any, targetPath: string, comp tsconfigJSON.include = flatten( tsconfigJSON.include.map((includedPath: string) => { return compDirs.map((compDir: string) => { - const compDirRelative = pathNormalizeToLinux(relative(targetPath, compDir)); + const compDirRelative = normalizePath(relative(targetPath, compDir)); return `${compDirRelative}/${includedPath}`; }); }) @@ -30,7 +35,7 @@ export function expandIncludeExclude(tsconfigJSON: any, targetPath: string, comp tsconfigJSON.exclude = flatten( tsconfigJSON.exclude.map((excludedPath: string) => { return compDirs.map((compDir: string) => { - const compDirRelative = pathNormalizeToLinux(relative(targetPath, compDir)); + const compDirRelative = normalizePath(relative(targetPath, compDir)); return `${compDirRelative}/${excludedPath}`; }); }) @@ -41,12 +46,20 @@ export function expandIncludeExclude(tsconfigJSON: any, targetPath: string, comp tsconfigJSON.files = flatten( tsconfigJSON.files.map((filesPath: string) => { return compDirs.map((compDir: string) => { - const compDirRelative = pathNormalizeToLinux(relative(targetPath, compDir)); + const compDirRelative = normalizePath(relative(targetPath, compDir)); return `${compDirRelative}/${filesPath}`; }); }) ); } - return JSON.stringify(tsconfigJSON, undefined, 2); + return tsconfigJSON; +} + +export function getIndexInputFile(index: ApplicationOptions['index']): string { + assert(index, 'No index file provided'); + if (typeof index === 'string') { + return index; + } + return (index as any).input; } diff --git a/angular/devkit/common/component.json b/angular/devkit/common/component.json index c3fb7ede..32603cbb 100644 --- a/angular/devkit/common/component.json +++ b/angular/devkit/common/component.json @@ -9,7 +9,8 @@ "policy": { "dependencies": { "@angular-devkit/build-angular": "-", - "typescript": "-" + "typescript": "-", + "normalize-path": "^3.0.0" }, "peerDependencies": { "@angular-devkit/build-angular": ">= 0.0.1", diff --git a/angular/devkit/common/env-options.ts b/angular/devkit/common/env-options.ts index 49db2c51..6ab1300b 100644 --- a/angular/devkit/common/env-options.ts +++ b/angular/devkit/common/env-options.ts @@ -22,7 +22,7 @@ export type AngularEnvOptions = { * Vite only works for apps, not preview yet. * @default 'webpack' */ - devServer?: 'webpack' | 'vite'; + // devServer?: 'webpack' | 'vite'; /** * The bundler to use: webpack or vite. * Vite only works for apps, not preview yet. diff --git a/angular/devkit/common/generic-angular-env.ts b/angular/devkit/common/generic-angular-env.ts index 668c7c7c..c1181c56 100644 --- a/angular/devkit/common/generic-angular-env.ts +++ b/angular/devkit/common/generic-angular-env.ts @@ -1,4 +1,8 @@ -import type { BrowserBuilderOptions, DevServerBuilderOptions } from '@angular-devkit/build-angular'; +import type { + ApplicationBuilderOptions, + BrowserBuilderOptions, + DevServerBuilderOptions +} from '@bitdev/angular.dev-services.ng-compat'; import { AppsEnv } from '@teambit/application'; import { BuilderEnv } from '@teambit/builder'; import { Bundler, BundlerContext, DevServer, DevServerContext } from '@teambit/bundler'; @@ -8,8 +12,9 @@ import { PreviewEnv } from '@teambit/preview'; import { Configuration, WebpackConfigTransformer, WebpackConfigWithDevServer } from '@teambit/webpack'; import { AngularEnvOptions } from './env-options'; -export type BrowserOptions = Omit; -export type DevServerOptions = Omit; +export type BrowserOptions = Omit & {inlineStyleLanguage?: "css" | "less" | "sass" | "scss"}; +export type DevServerOptions = Omit; +export type ApplicationOptions = Omit export interface GenericAngularEnv extends AppsEnv, @@ -22,7 +27,7 @@ export interface GenericAngularEnv devServerContext: DevServerContext, ngEnvOptions: AngularEnvOptions, transformers?: WebpackConfigTransformer[], - angularOptions?: Partial, + angularOptions?: Partial<(BrowserOptions | ApplicationOptions) & DevServerOptions>, webpackOptions?: Partial, sourceRoot?: string ): AsyncEnvHandler; @@ -31,7 +36,7 @@ export interface GenericAngularEnv bundlerContext: BundlerContext, ngEnvOptions: AngularEnvOptions, transformers?: WebpackConfigTransformer[], - angularOptions?: Partial, + angularOptions?: Partial<(BrowserOptions | ApplicationOptions) & DevServerOptions>, webpackOptions?: Partial, sourceRoot?: string ): AsyncEnvHandler diff --git a/angular/devkit/common/index.ts b/angular/devkit/common/index.ts index 5b9be709..4be2731c 100644 --- a/angular/devkit/common/index.ts +++ b/angular/devkit/common/index.ts @@ -1,4 +1,4 @@ export type { AngularEnvOptions, WebpackConfigFactory } from './env-options'; -export { GenericAngularEnv, DevServerOptions, BrowserOptions } from './generic-angular-env'; +export * from './generic-angular-env'; export * from './utils'; export { AngularComponentTemplateOptions } from './templates'; diff --git a/angular/devkit/common/utils.ts b/angular/devkit/common/utils.ts index 6a45640a..48a85357 100644 --- a/angular/devkit/common/utils.ts +++ b/angular/devkit/common/utils.ts @@ -4,11 +4,13 @@ import { Component, ComponentID } from '@teambit/component'; import { DevFilesMain } from '@teambit/dev-files'; import { EnvContext } from '@teambit/envs'; import { IsolatorMain } from '@teambit/isolator'; -import { pathNormalizeToLinux } from '@teambit/legacy/dist/utils'; +import { Logger } from '@teambit/logger'; import { PkgMain } from '@teambit/pkg'; import TesterAspect from '@teambit/tester'; import WorkspaceAspect, { Workspace } from '@teambit/workspace'; -import { existsSync, mkdirSync, writeFileSync } from 'fs-extra'; +import { outputFileSync } from 'fs-extra'; +// @ts-ignore +import normalize from 'normalize-path'; import objectHash from 'object-hash'; import { dirname, join, posix, resolve } from 'path'; import { readConfigFile, sys } from 'typescript'; @@ -116,13 +118,15 @@ export function isBuildContext(context: DevServerContext | BundlerContext): cont return (context as BundlerContext).capsuleNetwork !== undefined; } -export function isAppDevContext(context: DevServerContext | AppContext): context is DevServerContext & AppContext { +export function isAppDevContext(context: any): context is DevServerContext & AppContext { return (context as any).appName !== undefined; } -export function isAppBuildContext( - context: BundlerContext | AppBuildContext -): context is BundlerContext & AppBuildContext { +export function isAppBuildContext(context: any): context is BundlerContext & AppBuildContext { + return (context as any).appName !== undefined; +} + +export function isAppContext(context: any): context is T { return (context as any).appName !== undefined; } @@ -141,7 +145,7 @@ export function generateTsConfig( ): string { const tsconfigPath = join(appPath, 'tsconfig.app.json'); const tsconfigJSON = readConfigFile(tsconfigPath, sys.readFile).config; - const pAppPath = pathNormalizeToLinux(appPath); + const pAppPath = normalizePath(appPath); // tsconfigJSON.config.angularCompilerOptions.enableIvy = this.enableIvy; tsconfigJSON.files = tsconfigJSON.files.map((file: string) => posix.join(pAppPath, file)); @@ -174,9 +178,6 @@ export function writeTsconfig( const includePaths = new Set(); const excludePaths = new Set(); const dirPath = join(tempFolder, context.id); - if (!existsSync(dirPath)) { - mkdirSync(dirPath, { recursive: true }); - } // get the list of files for existing component compositions to include in the compilation context.components.forEach((component: Component) => { @@ -192,9 +193,9 @@ export function writeTsconfig( if (!capsule) { throw new Error(`No capsule found for ${component.id} in network graph`); } - outputPath = pathNormalizeToLinux(capsule.path); + outputPath = normalizePath(capsule.path); } else { - outputPath = pathNormalizeToLinux(workspace?.componentDir(component.id, { + outputPath = normalizePath(workspace?.componentDir(component.id, { ignoreVersion: true }) || ''); } @@ -214,15 +215,15 @@ export function writeTsconfig( const content = generateTsConfig(rootPath, Array.from(includePaths), Array.from(excludePaths), tsPaths); const hash = objectHash(content); - const targetPath = join(dirPath, `__tsconfig-${timestamp}.json`); + const targetPath = join(dirPath, `tsconfig/tsconfig-${timestamp}.json`); // write only if the link has changed (prevents triggering fs watches) if (writeHash.get(targetPath) !== hash) { - writeFileSync(targetPath, content); + outputFileSync(targetPath, content); writeHash.set(targetPath, hash); } - return pathNormalizeToLinux(targetPath); + return normalizePath(targetPath); } export function dedupPaths(paths: (string | any)[]): string[] { @@ -239,3 +240,22 @@ export function dedupPaths(paths: (string | any)[]): string[] { export function packagePath(packageName: string, path = ''): string { return join(dirname(require.resolve(`${packageName}/package.json`)), path); } + +/** + * Normalize slashes in a file path to be posix/unix-like forward slashes. + * Also condenses repeat slashes to a single slash and removes and trailing slashes, unless disabled. + */ +export function normalizePath(path: string, removeTrailingSlashes = false): string { + return normalize(path, removeTrailingSlashes); +} + +export function getLoggerApi(logger: Logger) { + return { + error: (m: string) => logger.consoleFailure(m), + log: (m: string) => logger.console(m), + warn: (m: string) => logger.consoleWarning(m), + info: (m: string) => logger.console(m), + colorMessage: (m: string) => logger.console(m), + createChild: () => logger + } as any; +} diff --git a/angular/devkit/ng-compat/build-angular/browser-schema.ts b/angular/devkit/ng-compat/build-angular/browser-schema.ts deleted file mode 100644 index b1211ddc..00000000 --- a/angular/devkit/ng-compat/build-angular/browser-schema.ts +++ /dev/null @@ -1,3 +0,0 @@ -/* eslint-disable */ - -export let BrowserBuilderSchema = require('@angular-devkit/build-angular/src/builders/browser/schema').Schema; diff --git a/angular/devkit/ng-compat/build-angular/builder-options.ts b/angular/devkit/ng-compat/build-angular/builder-options.ts new file mode 100644 index 00000000..1d0bf512 --- /dev/null +++ b/angular/devkit/ng-compat/build-angular/builder-options.ts @@ -0,0 +1,2 @@ +/* eslint-disable */ +export type { BrowserBuilderOptions, DevServerBuilderOptions } from '@angular-devkit/build-angular'; diff --git a/angular/devkit/ng-compat/build-angular/builders/application.ts b/angular/devkit/ng-compat/build-angular/builders/application.ts new file mode 100644 index 00000000..a3d2aef2 --- /dev/null +++ b/angular/devkit/ng-compat/build-angular/builders/application.ts @@ -0,0 +1,24 @@ +import { ApplicationBuilderOptions } from '../schemas/application.schema'; +import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; +import type { Plugin, OutputFile } from 'esbuild'; +import { VERSION } from '@angular/cli'; + +export let buildApplicationInternal = ( + options: ApplicationBuilderOptions, + context: BuilderContext & { + signal?: AbortSignal; + }, infrastructureSettings?: { + write?: boolean; + }, plugins?: Plugin[] + // @ts-ignore +) => AsyncIterable; + +if (Number(VERSION.major) >= 16) { + buildApplicationInternal = require('@angular-devkit/build-angular/src/builders/application').buildApplicationInternal; +} diff --git a/angular/devkit/ng-compat/build-angular/schemas/application.schema.ts b/angular/devkit/ng-compat/build-angular/schemas/application.schema.ts new file mode 100644 index 00000000..e2c58cc1 --- /dev/null +++ b/angular/devkit/ng-compat/build-angular/schemas/application.schema.ts @@ -0,0 +1,429 @@ +/** + * Application builder target options + */ +export interface ApplicationBuilderOptions { + /** + * A list of CommonJS packages that are allowed to be used without a build time warning. + */ + allowedCommonJsDependencies?: string[]; + /** + * Build using Ahead of Time compilation. + */ + aot?: boolean; + /** + * Generates an application shell during build time. + */ + appShell?: boolean; + /** + * List of static application assets. + */ + assets?: AssetPattern[]; + /** + * Base url for the application being built. + */ + baseHref?: string; + /** + * The full path for the browser entry point to the application, relative to the current + * workspace. + */ + browser: string; + /** + * Budget thresholds to ensure parts of your application stay within boundaries which you + * set. + */ + budgets?: Budget[]; + /** + * Define the crossorigin attribute setting of elements that provide CORS support. + */ + crossOrigin?: CrossOrigin; + /** + * Delete the output path before building. + */ + deleteOutputPath?: boolean; + /** + * Exclude the listed external dependencies from being bundled into the bundle. Instead, the + * created bundle relies on these dependencies to be available during runtime. + */ + externalDependencies?: string[]; + /** + * Extract all licenses in a separate file. + */ + extractLicenses?: boolean; + /** + * Replace compilation source files with other compilation source files in the build. + */ + fileReplacements?: FileReplacement[]; + /** + * How to handle duplicate translations for i18n. + */ + i18nDuplicateTranslation?: I18NTranslation; + /** + * How to handle missing translations for i18n. + */ + i18nMissingTranslation?: I18NTranslation; + /** + * Configures the generation of the application's HTML index. + */ + index: IndexUnion; + /** + * The stylesheet language to use for the application's inline component styles. + */ + inlineStyleLanguage?: "css" | "less" | "sass" | "scss"; + /** + * Translate the bundles in one or more locales. + */ + localize?: Localize; + /** + * Use file name for lazy loaded chunks. + */ + namedChunks?: boolean; + /** + * Enables optimization of the build output. Including minification of scripts and styles, + * tree-shaking, dead-code elimination, inlining of critical CSS and fonts inlining. For + * more information, see + * https://angular.io/guide/workspace-config#optimization-configuration. + */ + optimization?: OptimizationUnion; + /** + * Define the output filename cache-busting hashing mode. + */ + outputHashing?: OutputHashing; + /** + * The full path for the new output directory, relative to the current workspace. + */ + outputPath: string; + /** + * Enable and define the file watching poll time period in milliseconds. + */ + poll?: number; + /** + * A list of polyfills to include in the build. Can be a full path for a file, relative to + * the current workspace or module specifier. Example: 'zone.js'. + */ + polyfills?: string[]; + /** + * Prerender (SSG) pages of your application during build time. + */ + prerender?: PrerenderUnion; + /** + * Do not use the real path when resolving modules. If unset then will default to `true` if + * NodeJS option --preserve-symlinks is set. + */ + preserveSymlinks?: boolean; + /** + * Log progress to the console while building. + */ + progress?: boolean; + /** + * Global scripts to be included in the build. + */ + scripts?: ScriptElement[]; + /** + * The full path for the server entry point to the application, relative to the current + * workspace. + */ + server?: string; + /** + * Generates a service worker configuration. + */ + serviceWorker?: ServiceWorker; + /** + * Output source maps for scripts and styles. For more information, see + * https://angular.io/guide/workspace-config#source-map-configuration. + */ + sourceMap?: SourceMapUnion; + /** + * Server side render (SSR) pages of your application during runtime. + */ + ssr?: boolean; + /** + * Generates a 'stats.json' file which can be analyzed with + * https://esbuild.github.io/analyze/. + */ + statsJson?: boolean; + /** + * Options to pass to style preprocessors. + */ + stylePreprocessorOptions?: StylePreprocessorOptions; + /** + * Global styles to be included in the build. + */ + styles?: StyleElement[]; + /** + * Enables the use of subresource integrity validation. + */ + subresourceIntegrity?: boolean; + /** + * The full path for the TypeScript configuration file, relative to the current workspace. + */ + tsConfig: string; + /** + * Adds more details to output logging. + */ + verbose?: boolean; + /** + * Run build when files change. + */ + watch?: boolean; + /** + * TypeScript configuration for Web Worker modules. + */ + webWorkerTsConfig?: string; +} +export type AssetPattern = AssetPatternClass | string; +export interface AssetPatternClass { + /** + * Allow glob patterns to follow symlink directories. This allows subdirectories of the + * symlink to be searched. + */ + followSymlinks?: boolean; + /** + * The pattern to match. + */ + glob: string; + /** + * An array of globs to ignore. + */ + ignore?: string[]; + /** + * The input directory path in which to apply 'glob'. Defaults to the project root. + */ + input: string; + /** + * Absolute path within the output. + */ + output: string; +} +export interface Budget { + /** + * The baseline size for comparison. + */ + baseline?: string; + /** + * The threshold for error relative to the baseline (min & max). + */ + error?: string; + /** + * The maximum threshold for error relative to the baseline. + */ + maximumError?: string; + /** + * The maximum threshold for warning relative to the baseline. + */ + maximumWarning?: string; + /** + * The minimum threshold for error relative to the baseline. + */ + minimumError?: string; + /** + * The minimum threshold for warning relative to the baseline. + */ + minimumWarning?: string; + /** + * The name of the bundle. + */ + name?: string; + /** + * The type of budget. + */ + type: Type; + /** + * The threshold for warning relative to the baseline (min & max). + */ + warning?: string; +} +/** + * The type of budget. + */ +export declare enum Type { + All = "all", + AllScript = "allScript", + Any = "any", + AnyComponentStyle = "anyComponentStyle", + AnyScript = "anyScript", + Bundle = "bundle", + Initial = "initial" +} +/** + * Define the crossorigin attribute setting of elements that provide CORS support. + */ +export declare enum CrossOrigin { + Anonymous = "anonymous", + None = "none", + UseCredentials = "use-credentials" +} +export interface FileReplacement { + replace: string; + with: string; +} +/** + * How to handle duplicate translations for i18n. + * + * How to handle missing translations for i18n. + */ +export declare enum I18NTranslation { + Error = "error", + Ignore = "ignore", + Warning = "warning" +} +/** + * Configures the generation of the application's HTML index. + */ +export type IndexUnion = boolean | IndexObject | string; +export interface IndexObject { + /** + * The path of a file to use for the application's generated HTML index. + */ + input: string; + /** + * The output path of the application's generated HTML index file. The full provided path + * will be used and will be considered relative to the application's configured output path. + */ + output?: string; + [property: string]: any; +} +/** + * Translate the bundles in one or more locales. + */ +export type Localize = string[] | boolean; +/** + * Enables optimization of the build output. Including minification of scripts and styles, + * tree-shaking, dead-code elimination, inlining of critical CSS and fonts inlining. For + * more information, see + * https://angular.io/guide/workspace-config#optimization-configuration. + */ +export type OptimizationUnion = boolean | OptimizationClass; +export interface OptimizationClass { + /** + * Enables optimization for fonts. This option requires internet access. `HTTPS_PROXY` + * environment variable can be used to specify a proxy server. + */ + fonts?: FontsUnion; + /** + * Enables optimization of the scripts output. + */ + scripts?: boolean; + /** + * Enables optimization of the styles output. + */ + styles?: StylesUnion; +} +/** + * Enables optimization for fonts. This option requires internet access. `HTTPS_PROXY` + * environment variable can be used to specify a proxy server. + */ +export type FontsUnion = boolean | FontsClass; +export interface FontsClass { + /** + * Reduce render blocking requests by inlining external Google Fonts and Adobe Fonts CSS + * definitions in the application's HTML index file. This option requires internet access. + * `HTTPS_PROXY` environment variable can be used to specify a proxy server. + */ + inline?: boolean; +} +/** + * Enables optimization of the styles output. + */ +export type StylesUnion = boolean | StylesClass; +export interface StylesClass { + /** + * Extract and inline critical CSS definitions to improve first paint time. + */ + inlineCritical?: boolean; + /** + * Minify CSS definitions by removing extraneous whitespace and comments, merging + * identifiers and minimizing values. + */ + minify?: boolean; +} +/** + * Define the output filename cache-busting hashing mode. + */ +export declare enum OutputHashing { + All = "all", + Bundles = "bundles", + Media = "media", + None = "none" +} +/** + * Prerender (SSG) pages of your application during build time. + */ +export type PrerenderUnion = boolean | PrerenderClass; +export interface PrerenderClass { + /** + * Whether the builder should process the Angular Router configuration to find all + * unparameterized routes and prerender them. + */ + discoverRoutes?: boolean; + /** + * The path to a file that contains a list of all routes to prerender, separated by + * newlines. This option is useful if you want to prerender routes with parameterized URLs. + */ + routesFile?: string; +} +export type ScriptElement = ScriptClass | string; +export interface ScriptClass { + /** + * The bundle name for this extra entry point. + */ + bundleName?: string; + /** + * If the bundle will be referenced in the HTML file. + */ + inject?: boolean; + /** + * The file to include. + */ + input: string; +} +/** + * Generates a service worker configuration. + */ +export type ServiceWorker = boolean | string; +/** + * Output source maps for scripts and styles. For more information, see + * https://angular.io/guide/workspace-config#source-map-configuration. + */ +export type SourceMapUnion = boolean | SourceMapClass; +export interface SourceMapClass { + /** + * Output source maps used for error reporting tools. + */ + hidden?: boolean; + /** + * Output source maps for all scripts. + */ + scripts?: boolean; + /** + * Output source maps for all styles. + */ + styles?: boolean; + /** + * Resolve vendor packages source maps. + */ + vendor?: boolean; +} +/** + * Options to pass to style preprocessors. + */ +export interface StylePreprocessorOptions { + /** + * Paths to include. Paths will be resolved to workspace root. + */ + includePaths?: string[]; +} +export type StyleElement = StyleClass | string; +export interface StyleClass { + /** + * The bundle name for this extra entry point. + */ + bundleName?: string; + /** + * If the bundle will be referenced in the HTML file. + */ + inject?: boolean; + /** + * The file to include. + */ + input: string; +} diff --git a/angular/devkit/ng-compat/build-angular/utils/index.ts b/angular/devkit/ng-compat/build-angular/utils/index.ts index 78823414..2b6074eb 100644 --- a/angular/devkit/ng-compat/build-angular/utils/index.ts +++ b/angular/devkit/ng-compat/build-angular/utils/index.ts @@ -1,13 +1,13 @@ /* eslint-disable */ import { json, logging } from '@angular-devkit/core'; import { VERSION } from '@angular/cli'; -import { BrowserBuilderSchema } from '../browser-schema'; +import { BrowserBuilderOptions } from '../builder-options'; export * from './normalize-cache'; export * from './package-chunk-sort'; export * from './webpack-browser-config'; -export let normalizeBrowserSchema: (workspaceRoot: string, projectRoot: string, projectSourceRoot: string | undefined, options: typeof BrowserBuilderSchema, metadata?: json.JsonObject, logger?: logging.LoggerApi) => any; +export let normalizeBrowserSchema: (workspaceRoot: string, projectRoot: string, projectSourceRoot: string | undefined, options: BrowserBuilderOptions, metadata?: json.JsonObject, logger?: logging.LoggerApi) => any; export let normalizeOptimization: (optimization?: any) => any; export let BuildBrowserFeatures: any; diff --git a/angular/devkit/ng-compat/build-angular/utils/webpack-browser-config.ts b/angular/devkit/ng-compat/build-angular/utils/webpack-browser-config.ts index c4221c56..d2d03881 100644 --- a/angular/devkit/ng-compat/build-angular/utils/webpack-browser-config.ts +++ b/angular/devkit/ng-compat/build-angular/utils/webpack-browser-config.ts @@ -2,7 +2,7 @@ import { logging } from '@angular-devkit/core'; import { VERSION } from '@angular/cli'; import { Configuration } from '@teambit/webpack'; -import { BrowserBuilderSchema } from '../browser-schema'; +import { BrowserBuilderOptions } from '../builder-options'; type wbConfigFn = ( workspaceRoot: string, @@ -26,7 +26,7 @@ type wbConfigFnV12 = ( ) => Promise; export let generateWebpackConfig: wbConfigFn & wbConfigFnV12; -export let getIndexOutputFile: (index: typeof BrowserBuilderSchema['index']) => string; +export let getIndexOutputFile: (index: BrowserBuilderOptions['index']) => string; if (VERSION.major) { const webpackBrowserConfig = require('@angular-devkit/build-angular/src/utils/webpack-browser-config'); diff --git a/angular/devkit/ng-compat/build-angular/webpack/stats.ts b/angular/devkit/ng-compat/build-angular/webpack/stats.ts index 7962341b..22311444 100644 --- a/angular/devkit/ng-compat/build-angular/webpack/stats.ts +++ b/angular/devkit/ng-compat/build-angular/webpack/stats.ts @@ -2,9 +2,9 @@ import { WebpackLoggingCallback } from '@angular-devkit/build-webpack'; import { logging } from '@angular-devkit/core'; import { VERSION } from '@angular/cli'; -import { BrowserBuilderSchema } from '../browser-schema'; +import { BrowserBuilderOptions } from '../builder-options'; -export let createWebpackLoggingCallback: (options: typeof BrowserBuilderSchema, logger: logging.LoggerApi) => WebpackLoggingCallback; +export let createWebpackLoggingCallback: (options: BrowserBuilderOptions, logger: logging.LoggerApi) => WebpackLoggingCallback; if ((VERSION.major === '16' && Number(VERSION.minor) >= 2) || Number(VERSION.major) > 16) { createWebpackLoggingCallback = require('@angular-devkit/build-angular/src/tools/webpack/utils/stats').createWebpackLoggingCallback; diff --git a/angular/devkit/ng-compat/component.json b/angular/devkit/ng-compat/component.json index c2f91fd2..2860d7fa 100644 --- a/angular/devkit/ng-compat/component.json +++ b/angular/devkit/ng-compat/component.json @@ -11,6 +11,8 @@ }, "peerDependencies": { "@angular-devkit/build-angular": ">= 0.0.1", + "@angular-devkit/architect": ">= 0.0.1", + "@angular/core": ">= 13.0.0", "@angular/cli": ">= 13.0.0", "webpack": ">= 4.44.2" } diff --git a/angular/devkit/ng-compat/index.ts b/angular/devkit/ng-compat/index.ts index ecb367c3..71efae7d 100644 --- a/angular/devkit/ng-compat/index.ts +++ b/angular/devkit/ng-compat/index.ts @@ -1,4 +1,6 @@ export * from './build-angular/index-html-webpack-plugin'; export * from './build-angular/webpack'; export * from './build-angular/utils'; -export * from './build-angular/browser-schema'; +export * from './build-angular/builder-options'; +export * from './build-angular/schemas/application.schema'; +export * from './build-angular/builders/application'; diff --git a/angular/devkit/preview/preview/angular-preview.ts b/angular/devkit/preview/preview/angular-preview.ts index b686eac4..8ed27d34 100644 --- a/angular/devkit/preview/preview/angular-preview.ts +++ b/angular/devkit/preview/preview/angular-preview.ts @@ -1,4 +1,9 @@ -import { AngularEnvOptions, BrowserOptions, DevServerOptions } from '@bitdev/angular.dev-services.common'; +import { + AngularEnvOptions, + ApplicationOptions, + BrowserOptions, + DevServerOptions +} from '@bitdev/angular.dev-services.common'; import { AppContext } from '@teambit/application'; import { Bundler, BundlerContext, DevServer, DevServerContext } from '@teambit/bundler'; import { AsyncEnvHandler, EnvHandler } from '@teambit/envs'; @@ -12,7 +17,7 @@ import 'webpack-dev-server'; export type DevServerProvider = ( context: DevServerContext | (DevServerContext & AppContext), transformers?: WebpackConfigTransformer[], - angularOptions?: Partial, + angularOptions?: Partial<(BrowserOptions | ApplicationOptions) & DevServerOptions>, webpackOptions?: Partial, sourceRoot?: string ) => AsyncEnvHandler; @@ -20,7 +25,7 @@ export type DevServerProvider = ( export type BundlerProvider = ( context: BundlerContext | (BundlerContext & AppContext), transformers?: WebpackConfigTransformer[], - angularOptions?: Partial, + angularOptions?: Partial<(BrowserOptions | ApplicationOptions) & DevServerOptions>, webpackOptions?: Partial, sourceRoot?: string ) => AsyncEnvHandler; @@ -88,7 +93,7 @@ export class AngularPreview implements Preview { getDevServer( context: DevServerContext, transformers?: WebpackConfigTransformer[], - angularOptions?: Partial, + angularOptions?: Partial<(BrowserOptions | ApplicationOptions) & DevServerOptions>, webpackOptions?: Partial, sourceRoot?: string ): AsyncEnvHandler { diff --git a/angular/devkit/vite/component.json b/angular/devkit/vite/component.json index 33bd3e89..e2480587 100644 --- a/angular/devkit/vite/component.json +++ b/angular/devkit/vite/component.json @@ -8,40 +8,24 @@ "teambit.dependencies/dependency-resolver": { "policy": { "dependencies": { - "@analogjs/vite-plugin-angular": "^0.2.0", - "@mdx-js/react": "^1.6.22", - "@originjs/vite-plugin-commonjs": "^1.0.3", + "@analogjs/platform": "-", "@teambit/node.deps-detectors.detective-es6": "0.0.1", + "@teambit/vite.vite-bundler": "*", + "@teambit/vite.vite-dev-server": "*", + "@types/express": "^4.17.17", "@types/memoizee": "0.4.5", - "@vitejs/plugin-react": "^4.0.0", "find-root": "1.1.0", "memoizee": "0.4.15", - "node-stdlib-browser": "^1.2.0", - "parse5-html-rewriting-stream": "7.0.0", - "remark-admonitions": "^1.2.1", - "remark-frontmatter": "^2.0.0", - "typescript": "-", - "unified": "^10.1.2", - "unist-util-remove": "^2.0.1", - "unist-util-visit": "^2.0.3", - "vite": "^4.4.9", - "vite-plugin-commonjs": "^0.8.2", - "vite-plugin-markdown": "2.1.0", - "vite-plugin-mdx": "^3.5.11", - "vite-plugin-node-polyfills": "^0.11.2", - "vite-plugin-require-transform": "^1.0.21", - "yaml": "^2.3.1" + "parse5-html-rewriting-stream": "7.0.0" }, "devDependencies": { "@types/find-root": "1.1.2", - "@types/jest": "26.0.20", - "@types/node": "12.20.4", - "@babel/runtime": "7.20.0" + "@types/node": "12.20.4" }, "peerDependencies": { + "@analogjs/platform": ">= 0.2.22", "@angular/common": ">= 13.0.0", - "typescript": ">= 3.5.3", - "vite": "4.4.8" + "vite": "5.0.0" } } } diff --git a/angular/devkit/vite/config.factory.ts b/angular/devkit/vite/config.factory.ts new file mode 100644 index 00000000..6ef6a3f4 --- /dev/null +++ b/angular/devkit/vite/config.factory.ts @@ -0,0 +1,182 @@ +import { default as analog } from '@analogjs/platform'; +import { isAppBuildContext, normalizePath } from '@bitdev/angular.dev-services.common'; +import { BundlerContext, DevServerContext } from '@teambit/bundler'; +import { CACHE_ROOT } from '@teambit/legacy/dist/constants'; +import assert from 'assert'; +import { basename, extname, join, posix, resolve } from 'path'; +// @ts-ignore +import type { InlineConfig } from 'vite'; +import { getIndexInputFile, htmlPlugin } from './plugins/index-html.plugin'; +import { BuildMode, NgViteOptions, ViteAspectsContext } from './utils/types'; +import { joinPathFragments } from './utils/utils'; + +type ConfigFactoryOptions = NgViteOptions & { context: DevServerContext | BundlerContext }; + + +const SSR_DIR = 'ssr'; +const BROWSER_DIR = 'browser'; + +/** + * Generate Vite configuration object based on provided options. + * + * @param {ConfigFactoryOptions} options - The options for generating the configuration. + * @param {ViteAspectsContext} aspectContext - The context object for Vite aspects. + * @return {Promise} - The generated Vite configuration object. + */ +export async function configFactory( + options: ConfigFactoryOptions, + aspectContext: ViteAspectsContext +): Promise { + const { mergeConfig } = await import('vite'); + const { + angularOptions: { tsConfig, index }, + context: { rootPath, envRuntime }, + context, + name, + outputPath + } = options; + assert(rootPath, 'rootPath is required'); + assert(outputPath, 'outputPath is required'); + const { workspace } = aspectContext; + const idName = `${ envRuntime.id }/${ name }`; + const tempFolder = workspace ? workspace.getTempDir(idName) : join(CACHE_ROOT, idName); + const indexPath = getIndexInputFile(index!); + const browserDir = normalizePath(resolve(outputPath, BROWSER_DIR)); + const ssrDir = normalizePath(resolve(outputPath, SSR_DIR)); + + assert(isAppBuildContext(context), 'Angular vite only support apps for now'); + let appRootPath: string; + const capsule = (options.context as any).capsule; + if (capsule) { + appRootPath = capsule.path; + } else { + appRootPath = workspace?.componentDir(context.appComponent.id, { + ignoreVersion: true + }) || ''; + } + const tsconfigPath = tsConfig ?? posix.join(appRootPath, 'tsconfig.app.json'); + + const baseConfig = createBaseConfig(options, tsconfigPath, tempFolder, browserDir, ssrDir, appRootPath, indexPath); + const serverConfig = await createServerConfig(rootPath); + const buildConfig = createBuildConfig(browserDir, appRootPath, indexPath); + const testConfig = createTestConfig(); + + return mergeConfig( + baseConfig, + { + build: buildConfig + //server: serverConfig, + //test: testConfig + } + ); +} + +function createBaseConfig(options: ConfigFactoryOptions, tsconfigPath: string, tempFolder: string, browserDir: string, ssrDir: string, appRootPath: string, indexPath: string) { + const { + angularOptions: { baseHref, server, ssr }, + angularOptions, + context: { rootPath }, + buildMode, + outputPath, + sourceRoot + } = options; + + assert(rootPath, 'rootPath is required'); + assert(outputPath, 'outputPath is required'); + + // TODO: fix this + const entryServer = ''; //(ssr as SsrClass)?.entry as string; + + const baseConfig: InlineConfig = { + // publicDir: 'src/assets', // todo + root: join(appRootPath, sourceRoot || 'src'), + base: baseHref ?? './', + mode: buildMode, + resolve: { + mainFields: ['module'] + }, + define: { + 'import.meta.vitest': buildMode !== BuildMode.Production + }, + plugins: [ + analog({ + vite: { + tsconfig: tsconfigPath + }, + ssr: !!server, + ssrBuildDir: ssr ? ssrDir : undefined, + entryServer: resolve(appRootPath, entryServer), + nitro: { + alias: ssr ? { + '#analog/ssr': join(ssrDir, basename(entryServer, extname(entryServer))), + '#analog/index': join(browserDir, basename(indexPath)) + } : {}, + serverAssets: ssr ? [{ + baseName: 'public', + dir: browserDir + }] : [], + publicAssets: ssr ? [{ + dir: browserDir + }] : [], + output: ssr ? { + dir: normalizePath(resolve(outputPath, 'server')), + publicDir: normalizePath(resolve(outputPath, 'server/public')) + } : {}, + buildDir: normalizePath(resolve(tempFolder, 'nitro')), + prerender: { + concurrency: 4 + } + }, + index: indexPath, + workspaceRoot: appRootPath + }) + ] + }; + + if (server) { + // When running the ssr dev server, CTRL+C doesn't seem to work anymore (bug in nitro?) + // This is a workaround to make sure the process exits when CTRL+C is pressed + process.on('SIGINT', () => process.exit(1)); + } //else { + // When we are not using ssr, we need to add the html plugin to generate the index.html file + baseConfig.plugins!.push(htmlPlugin(angularOptions, rootPath!, indexPath, !!ssr)); + //} + return baseConfig; +} + +async function createServerConfig(rootPath: string) { + const { searchForWorkspaceRoot } = await import('vite'); + return { + fs: { + allow: [ + searchForWorkspaceRoot(joinPathFragments(rootPath)), + joinPathFragments('.', 'node_modules/vite') + ] + } + }; +} + +function createBuildConfig(browserDir: string, appRootPath: string, indexPath: string) { + return { + target: ['es2020'], + outDir: browserDir, + emptyOutDir: true, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true + } + }; +} + +function createTestConfig() { + return { + globals: true, + environment: 'jsdom', + setupFiles: ['src/test.ts'], + include: ['**/*.spec.ts'] + }; +} + + + + diff --git a/angular/devkit/vite/dev-server/config.ts b/angular/devkit/vite/dev-server/config.ts deleted file mode 100644 index 7ee424f2..00000000 --- a/angular/devkit/vite/dev-server/config.ts +++ /dev/null @@ -1,124 +0,0 @@ -// @ts-nocheck -// Make sure bit recognizes the dependencies -import '@mdx-js/react'; -import 'node-stdlib-browser'; -import { relative } from 'path'; -import { defineConfig, InlineConfig } from 'vite'; -import { nodePolyfills } from 'vite-plugin-node-polyfills'; -import { ViteDevServerAspectsContext, ViteDevServerOptions } from './types'; -import { getHostAlias } from './utils'; -// import react from "@vitejs/plugin-react"; -// import mdx from "vite-plugin-mdx"; -// import { htmlPlugin } from "./html-plugin"; -// import mdxOptions from "./mdx-config"; - -/** - * Generate a Vite config for dev server. - * - * 1. alias/fallbacks/provide for Node APIs and host deps - * 2. module loaders like for MDX - * 3. root path, public path, entry files, and html - * 4. websocket protocol - */ -export async function configFactory(options: ViteDevServerOptions, aspectContext: ViteDevServerAspectsContext, port: number): Promise { - const { - devServerContext: { publicPath, entry, rootPath, envRuntime }, - define, - alias, - plugins, - transformers - } = options; - const { workspace } = aspectContext; - const entries = entry; - - const root = options.root ?? workspace.path; - const publicDir = relative(workspace.path, publicPath); - const base = options.base ?? `/${rootPath}/`; - - // TODO: for WebpackBitReporterPlugin - // id; - // pubsub; - - // e.g. teambit.vite/examples/vue/vue@07106b6c1256c0efd94804266c07572a450cf675 - const envId = envRuntime.id; - // directory to place preview.root files - // const previewRootDir = `./node_modules/.cache/bit/teambit.ui`; - // directory to place preview entries files - // e.g. ./node_modules/.cache/bit/teambit.preview/preview/{envId}/compositions-1687107103571.js - // const entriesDir = `./node_modules/.cache/bit/teambit.preview/preview/${envId}`; - // directory to place vite cache - // TODO - // const cacheDir = `./node_modules/.cache/bit/teambit.vite/vite-dev-server/${envId}`; - const cacheDir = `./node_modules/.cache/bit/bitdev.angular/vite-dev-server/${envId}`; - - // set host deps as alias - const hostAlias = getHostAlias(options.devServerContext); - - // get full component list from workspace, and then - // - ignore them from watch - // - exclude them from optimizeDeps - const components = await workspace.list(); - const packageList = components.map(c => workspace.componentPackageName(c)); - - const config: InlineConfig = defineConfig({ - configFile: false, - envFile: false, - root, - base, - publicDir, - define, - resolve: { - mainFields: ['module'], - alias: [ - { - // this is required for the SCSS modules - find: /^~(.*)$/, - replacement: '$1' - }, - ...hostAlias, - ...alias || [] - ] - }, - // apply different cache dir for each env - cacheDir, - server: { - port, - watch: { - // 1. preview root - // 2. entry files - // 3. local packages - ignored: [ - ...packageList.map(p => `!**/node_modules/${p}/**`) - ] - }, - fs: { - strict: false - } - }, - optimizeDeps: { - entries - // exclude: packageList, - }, - // TODO: make it replaceable and reusable - plugins: [ - nodePolyfills(), - // react(), - // mdx(mdxOptions), - // htmlPlugin(entries), - ...plugins || [] - ] - }); - - // apply transformers - if (transformers) { - transformers.forEach(transformer => { - transformer(config); - }); - } - - return config; -} - - - - diff --git a/angular/devkit/vite/dev-server/types.ts b/angular/devkit/vite/dev-server/types.ts deleted file mode 100644 index 44db8edc..00000000 --- a/angular/devkit/vite/dev-server/types.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { - AngularEnvOptions, - BrowserOptions, - DevServerOptions -} from '@bitdev/angular.dev-services.common'; -import type { DevServerContext } from '@teambit/bundler'; -import { Logger } from '@teambit/logger'; -import { PubsubMain } from '@teambit/pubsub'; -import type { Workspace } from '@teambit/workspace'; -// @ts-ignore -import type { Alias, InlineConfig, PluginOption } from 'vite'; - - -export type ViteConfigTransformer = (config: InlineConfig) => void; - -export type ViteDevServerAspectsContext = { - logger: Logger; - workspace: Workspace; - pubsub: PubsubMain; -}; - -export type NgViteDevServerOptions = { - angularOptions: Partial; - - /** - * context of the dev server execution. - */ - devServerContext: DevServerContext; - - /** - * name of the dev server. - */ - name?: string; - - ngEnvOptions: AngularEnvOptions; - - sourceRoot?: string; - - // TODO: fix type once we can support preview with vite - transformers?: (ViteConfigTransformer | any)[]; - - // TODO: remove this once we can support preview with vite - [key: string]: any; -}; - -export type ViteDevServerOptions = { - /** - * name of the dev server. - */ - name?: string; - - /** - * context of the dev server execution. - */ - devServerContext: DevServerContext; - - /** - * optimize entries before passing them to Vite. - */ - optimizeEntries?: (entries: string[], context: ViteDevServerAspectsContext) => string[]; - - /** - * root path of the dev server. - */ - root?: string; - - /** - * base URL to use for all relative URLs in a document - */ - base?: string; - - /** - * variables to be injected to the dev server. - */ - define?: Record; - - /** - * alias to be injected to the dev server. - */ - alias?: Alias[]; - - /** - * list of plugins to be injected to the dev server. - */ - plugins?: PluginOption[]; - - /** - * list of transformers to modify Vite config in an advanced way. - */ - transformers?: ViteConfigTransformer[]; -}; diff --git a/angular/devkit/vite/dev-server/utils/html-plugin.ts b/angular/devkit/vite/dev-server/utils/html-plugin.ts deleted file mode 100644 index f0b3968b..00000000 --- a/angular/devkit/vite/dev-server/utils/html-plugin.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { IncomingMessage, ServerResponse } from 'http'; -import type { Plugin } from 'vite'; - -const cleanUrl = (url: string) => url.replace(/#.*$/s, '').replace(/\?.*$/s, ''); - -const defaultHeadHtml = ` - - - - -` - -const defaultBodyHtml = ` -
-` - -const defaultGenScriptHtml = (entries: string[]): string => entries.map( - src => ``).join('\n'); - -const defaultGenHtml = (entries: string[]) => ` - - -${defaultHeadHtml} - -${defaultBodyHtml} -${defaultGenScriptHtml(entries)} - -`; - -export type HtmlPluginOptions = { - entries: string[]; - genHtml?: (entries: string[], context: { req: IncomingMessage, res: ServerResponse }) => string; -}; - -export const htmlPlugin = ({ entries, genHtml }: HtmlPluginOptions): Plugin => { - return { - name: 'html-plugin', - configureServer(server) { - return () => { - server.middlewares.use(async (req, res, next) => { - const url = req.url && cleanUrl(req.url) - if (url?.endsWith('.html')) { - res.statusCode = 200 - res.setHeader('Content-Type', 'text/html') - const preHtml = genHtml? genHtml(entries, { req, res }) : defaultGenHtml(entries); - const html = await server.transformIndexHtml( - url, - preHtml, - req.originalUrl - ) - res.end(html) - return - } - next() - }) - } - } - } -} diff --git a/angular/devkit/vite/dev-server/utils/index.ts b/angular/devkit/vite/dev-server/utils/index.ts deleted file mode 100644 index 3131ccc4..00000000 --- a/angular/devkit/vite/dev-server/utils/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './html-plugin'; -export * from './host-alias'; -export * from './mdx-config'; diff --git a/angular/devkit/vite/dev-server/utils/mdx-config.ts b/angular/devkit/vite/dev-server/utils/mdx-config.ts deleted file mode 100644 index 331d4765..00000000 --- a/angular/devkit/vite/dev-server/utils/mdx-config.ts +++ /dev/null @@ -1,109 +0,0 @@ -import detectiveEs6 from '@teambit/node.deps-detectors.detective-es6'; -// @ts-ignore -import remarkNotes from 'remark-admonitions'; -import detectFrontmatter from 'remark-frontmatter'; -import type { Pluggable } from 'unified'; -import remove from 'unist-util-remove'; -import visit from 'unist-util-visit'; -import type { MdxOptions } from 'vite-plugin-mdx'; -import yaml from 'yaml'; - -type ImportSpecifier = { - /** - * relative/absolute or module name. e.g. the `y` in the example of `import x from 'y';` - */ - fromModule: string; - - /** - * is default import (e.g. `import x from 'y';`) - */ - isDefault?: boolean; - - /** - * the name used to identify the module, e.g. the `x` in the example of `import x from 'y';` - */ - identifier?: string; -}; - -const DEFAULT_RENDERER = ` -// @ts-nocheck -import React from 'react' -import { mdx } from '@mdx-js/react' - -/* @jsxRuntime classic */ -/* @jsx mdx */ -`; - -function wrapWithScopeContext(): Pluggable { - return (tree, file) => { - const imports: ImportSpecifier[] = file.data?.imports || []; - const ids = imports.reduce((identifiers: string[], importSpecifier: ImportSpecifier) => { - const newIds: string[] = []; - if (importSpecifier.identifier) newIds.push(importSpecifier.identifier); - return identifiers.concat(newIds); - }, []); - - const preNode = { - type: 'jsx', - value: ``, - }; - - const postNode = { - type: 'jsx', - value: ``, - }; - - tree.children.unshift({ - type: 'import', - value: `import { MDXScopeProvider } from '@teambit/mdx.ui.mdx-scope-context';`, - }); - - tree.children.unshift(preNode); - tree.children.push(postNode); - }; -} - -function extractMetadata(): Pluggable { - return function transformer(tree, file) { - visit(tree, 'yaml', (node: any) => { - try { - // eslint-disable-next-line no-param-reassign - file.data.frontmatter = yaml.parse(node.value, { prettyErrors: true }); - } catch (err: any) { - throw new Error( - `failed extracting metadata/front-matter using Yaml lib, due to an error (please disregard the line/column): ${err.message}` - ); - } - }); - }; -} - -function extractImports(): Pluggable { - return function transformer(tree, file) { - visit(tree, 'import', (node: any) => { - const es6Import = detectiveEs6(node.value); - const imports: ImportSpecifier[] = Object.keys(es6Import).flatMap((dep) => { - if (!es6Import[dep].importSpecifiers) { - return { - fromModule: dep, - }; - } - return es6Import[dep].importSpecifiers.map((importSpecifier) => ({ - fromModule: dep, - identifier: importSpecifier.name, - isDefault: importSpecifier.isDefault, - })); - }); - // eslint-disable-next-line no-param-reassign - (file.data.imports ||= []).push(...imports); - }); - - remove(tree, 'yaml'); - }; -} - -export const mdxOptions: MdxOptions = { - remarkPlugins: [remarkNotes, [detectFrontmatter, ['yaml']], extractMetadata, extractImports], - rehypePlugins: [wrapWithScopeContext], - renderer: DEFAULT_RENDERER, -} diff --git a/angular/devkit/vite/index.ts b/angular/devkit/vite/index.ts index 025081cb..ca5e622d 100644 --- a/angular/devkit/vite/index.ts +++ b/angular/devkit/vite/index.ts @@ -1,2 +1,3 @@ -export * from './ng-vite-dev-server'; -export * from './dev-server/types'; +export * from './ng-vite.dev-server'; +export * from './ng-vite.bundler'; +export * from './utils/types'; diff --git a/angular/devkit/vite/ng-vite-dev-server.ts b/angular/devkit/vite/ng-vite-dev-server.ts deleted file mode 100644 index 75762bfd..00000000 --- a/angular/devkit/vite/ng-vite-dev-server.ts +++ /dev/null @@ -1,82 +0,0 @@ -// eslint-disable-next-line import/no-named-default -import { default as ngVitePlugin } from '@analogjs/vite-plugin-angular'; -import { isAppDevContext } from '@bitdev/angular.dev-services.common'; -import type { DevServer } from '@teambit/bundler'; -import type { AsyncEnvHandler, EnvContext } from '@teambit/envs'; -import type { Workspace } from '@teambit/workspace'; -import type { Server } from 'http'; -import { posix } from 'path'; -// @ts-ignore -import type { InlineConfig, Plugin } from 'vite'; -import { configFactory } from './dev-server/config'; -import { - NgViteDevServerOptions, - ViteDevServerAspectsContext, - ViteDevServerOptions -} from './dev-server/types'; -import { htmlPlugin } from './plugins/index-html.plugin'; - -export class NgViteDevServer { - id = 'ng-vite-dev-server'; - - constructor( - private options: ViteDevServerOptions, - private aspectContext: ViteDevServerAspectsContext - ) { - } - - async listen(port: number): Promise { - const config: InlineConfig = await configFactory(this.options, this.aspectContext, port); - const vite = await import('vite'); - const server = await vite.createServer(config); - await server.listen(port); - if (!server.httpServer) { - throw new Error('vite server failed to start'); - } - return server.httpServer; - } - - static from(options: NgViteDevServerOptions): AsyncEnvHandler { - return async(context: EnvContext): Promise => { - const {rootPath} = options.devServerContext; - const name = options.name || 'vite-dev-server'; - const logger = context.createLogger(name); - const workspace: Workspace = context.getAspect('teambit.workspace/workspace'); - const pubsub = context.getAspect('teambit.harmony/pubsub'); - - let appRootPath: string; - let tsconfigPath: string; - if (isAppDevContext(options.devServerContext)) { // When you use `bit run ` - appRootPath = workspace?.componentDir(options.devServerContext.appComponent.id, { - ignoreVersion: true - }) || ''; - tsconfigPath = options?.angularOptions?.tsConfig ?? posix.join(appRootPath, 'tsconfig.app.json'); - } else { // When you use `bit start` - // appRootPath = getPreviewRootPath(workspace); - // tsconfigPath = writeTsconfig(options.devServerContext, appRootPath, tempFolder, application, pkg, devFilesMain, workspace); - throw new Error('vite does not support preview yet'); - } - - const opts = { - devServerContext: options.devServerContext, - root: rootPath, - base: options.angularOptions.baseHref ?? './', - plugins: [ - ngVitePlugin({ - tsconfig: tsconfigPath, - workspaceRoot: rootPath - }) as Plugin[], - htmlPlugin(options.angularOptions, rootPath, options.sourceRoot) - ] - }; - - const aspectContext: ViteDevServerAspectsContext = { - logger, - workspace, - pubsub - }; - return new NgViteDevServer(opts, aspectContext); - }; - } -} - diff --git a/angular/devkit/vite/ng-vite.bundler.ts b/angular/devkit/vite/ng-vite.bundler.ts new file mode 100644 index 00000000..3586c0d8 --- /dev/null +++ b/angular/devkit/vite/ng-vite.bundler.ts @@ -0,0 +1,36 @@ +import type { Bundler, BundlerContext } from '@teambit/bundler'; +import type { AsyncEnvHandler, EnvContext } from '@teambit/envs'; +import { Logger } from '@teambit/logger'; +import { ViteBundler, ViteBundlerOptions } from '@teambit/vite.vite-bundler'; +import type { Workspace } from '@teambit/workspace'; +// @ts-ignore +import type { InlineConfig } from 'vite'; +import { configFactory } from './config.factory'; +import { NgViteOptions } from './utils/types'; + +export class NgViteBundler { + + static from(options: NgViteOptions & { context: BundlerContext }): AsyncEnvHandler { + return async(context: EnvContext): Promise => { + options.name = options.name || 'ng-vite-bundler'; + const logger: Logger = context.createLogger(options.name); + const workspace: Workspace = context.getAspect('teambit.workspace/workspace'); + + const ngConfig = await configFactory(options, { logger, workspace }); + // @ts-ignore + const transformer: any = (config: InlineConfig, vite: typeof import('vite')): InlineConfig => { + return vite.mergeConfig(config, ngConfig); + }; + + const viteConfig: ViteBundlerOptions = { + bundlerContext: options.context as BundlerContext, + name: options.name, + resolveHostAlias: true, + transformers: [transformer, ...options.transformers ?? []] + }; + + return ViteBundler.from(viteConfig)(context); + }; + } +} + diff --git a/angular/devkit/vite/ng-vite.dev-server.ts b/angular/devkit/vite/ng-vite.dev-server.ts new file mode 100644 index 00000000..9c5725f2 --- /dev/null +++ b/angular/devkit/vite/ng-vite.dev-server.ts @@ -0,0 +1,41 @@ +import type { DevServer, DevServerContext } from '@teambit/bundler'; +import type { AsyncEnvHandler, EnvContext } from '@teambit/envs'; +import { ViteDevServer, ViteDevServerOptions } from '@teambit/vite.vite-dev-server'; +import { configFactory } from './config.factory'; +import { NgViteOptions } from './utils/types'; + +// Extracted constant +const DEFAULT_SERVER_NAME = 'ng-vite-dev-server'; + +export class NgViteDevServer { + static from(options: NgViteOptions & { context: DevServerContext }): AsyncEnvHandler { + return async(context: EnvContext): Promise => { + options.name = options.name || DEFAULT_SERVER_NAME; + const logger = context.createLogger(options.name); + const workspace = context.getAspect('teambit.workspace/workspace'); + + const ngConfig = await configFactory(options, { logger, workspace }); + + // @ts-ignore + function transformer(config: any, vite: typeof import('vite')): any { + return vite.mergeConfig(config, ngConfig); + } + + const viteConfig: ViteDevServerOptions = { + define: ngConfig.define, + devServerContext: options.context as DevServerContext, + name: options.name, + optimizeEntries: options.optimizeEntries as any, + plugins: ngConfig.plugins as any, + resolveHostAlias: true, + // Improved readability for transformers array + transformers: [ + transformer, ...(options.transformers ?? []) + ] + }; + + return ViteDevServer.from(viteConfig)(context); + }; + } +} + diff --git a/angular/devkit/vite/plugins/index-file/augment-index-html.ts b/angular/devkit/vite/plugins/index-file/augment-index-html.ts index 20f907c5..d11a263f 100644 --- a/angular/devkit/vite/plugins/index-file/augment-index-html.ts +++ b/angular/devkit/vite/plugins/index-file/augment-index-html.ts @@ -7,6 +7,7 @@ */ import { loadEsmModule } from '@bitdev/angular.dev-services.common'; +import assert from 'assert'; import { extname } from 'path'; import { htmlRewritingStream } from './html-rewriting-stream'; import { EntryPointsType } from './package-chunk-sort'; @@ -124,11 +125,9 @@ export async function augmentIndexHtml( break; case '.mjs': case '.mts': - if (!isModule) { - // It would be very confusing to link an `*.mjs` file in a non-module script context, - // so we disallow it entirely. - throw new Error('`.mjs` & `.mts` files *must* set `isModule` to `true`.'); - } + // It would be very confusing to link an `*.mjs` file in a non-module script context, + // so we disallow it entirely. + assert(isModule, '`.mjs` & `.mts` files *must* set `isModule` to `true`.'); scripts.set(file, true /* isModule */); break; case '.css': diff --git a/angular/devkit/vite/plugins/index-file/package-chunk-sort.ts b/angular/devkit/vite/plugins/index-file/package-chunk-sort.ts index 5d979939..d8ca0db4 100644 --- a/angular/devkit/vite/plugins/index-file/package-chunk-sort.ts +++ b/angular/devkit/vite/plugins/index-file/package-chunk-sort.ts @@ -8,7 +8,7 @@ import {parse} from 'path'; // import { ScriptElement, StyleElement } from '@angular-devkit/build-angular/src/builders/browser/schema'; -import type { BrowserBuilderOptions } from '@angular-devkit/build-angular'; +import type { BrowserBuilderOptions } from '@bitdev/angular.dev-services.ng-compat'; export type EntryPointsType = [name: string, isModule: boolean, inject: boolean]; diff --git a/angular/devkit/vite/plugins/index-html.plugin.ts b/angular/devkit/vite/plugins/index-html.plugin.ts index 71d09e49..2a96e027 100644 --- a/angular/devkit/vite/plugins/index-html.plugin.ts +++ b/angular/devkit/vite/plugins/index-html.plugin.ts @@ -1,31 +1,34 @@ -import type { BrowserBuilderOptions } from '@angular-devkit/build-angular'; import type { ServerResponse } from 'http'; import memoize from 'memoizee'; -import { join } from 'path'; +// @ts-ignore import type { Connect, Plugin, ViteDevServer } from 'vite'; +import { ApplicationOptions } from '@bitdev/angular.dev-services.common'; import { IndexHtmlGenerator } from './index-file/index-html-generator'; import { generateEntryPoints } from './index-file/package-chunk-sort'; +import assert from 'assert'; -export function getIndexInputFile(index: BrowserBuilderOptions['index']): string { +export function getIndexInputFile(index: ApplicationOptions['index']): string { + assert(index, 'No index file provided'); if (typeof index === 'string') { return index; } - return index.input; + return (index as any).input; } const cleanUrl = (url: string) => url.replace(/#.*$/s, '').replace(/\?.*$/s, ''); -async function genHtml(options: Partial, rootPath: string, sourceRoot = 'src') { +async function genHtml(options: Partial, rootPath: string, indexPath: string) { + assert(options.browser, 'No main file provided'); const entrypoints = generateEntryPoints({ - main: options.main ?? `./${join(sourceRoot, `main.ts`)}`, - polyfills: options.polyfills ?? `./${join(sourceRoot, `polyfills.ts`)}`, + main: options.browser, + polyfills: options.polyfills ?? [], scripts: options.scripts ?? [], styles: options.styles ?? [] }); const indexHtmlGenerator = new IndexHtmlGenerator({ rootPath, - indexPath: getIndexInputFile(options.index ?? `./${join(sourceRoot, `index.html`)}`), + indexPath, entrypoints, crossOrigin: options.crossOrigin }); @@ -50,18 +53,21 @@ async function genHtml(options: Partial, rootPath: string const memoized = memoize(genHtml); -export const htmlPlugin = (options: Partial, rootPath: string, sourceRoot = 'src'): Plugin => { +export const htmlPlugin = (options: Partial, rootPath: string, indexPath: string, ssr: boolean): Plugin => { return { - name: 'html-plugin', + name: 'ng-vite-html-plugin', configureServer(server: ViteDevServer) { return (): void => { + // if(ssr) { + // return; + // } server.middlewares.use(async(req: Connect.IncomingMessage, res: ServerResponse, next: Connect.NextFunction) => { const url = req.url && cleanUrl(req.url); if (url?.endsWith('.html')) { res.statusCode = 200; res.setHeader('Content-Type', 'text/html'); - let html = await memoized(options, rootPath, sourceRoot); - html = await server.transformIndexHtml(options.index as string ?? `./${join(sourceRoot, `index.html`)}`, html, req.originalUrl); + let html = await memoized(options, rootPath, indexPath); + html = await server.transformIndexHtml(indexPath, html, req.originalUrl); res.end(html); return; } diff --git a/angular/devkit/vite/plugins/server-entry.plugin.ts b/angular/devkit/vite/plugins/server-entry.plugin.ts new file mode 100644 index 00000000..fb998fe2 --- /dev/null +++ b/angular/devkit/vite/plugins/server-entry.plugin.ts @@ -0,0 +1,113 @@ +// // SSR dev server, middleware and error page source modified from +// // https://github.com/solidjs/solid-start/blob/main/packages/start/dev/server.js +// +// import { Connect, Plugin, ViteDevServer } from 'vite'; +// import * as path from 'path'; +// import * as fs from 'fs'; +// +// interface ServerOptions { +// indexPath?: string; +// entryServer?: string; +// } +// +// export function devServerPlugin(options: ServerOptions): Plugin { +// const entryServer = options.entryServer || 'src/main.server.ts'; +// const index = options.indexPath || 'index.html'; +// +// return { +// name: 'ng-vite-server-entry-plugin', +// config() { +// return { +// resolve: { +// alias: { +// '~ng-vite/main-server': entryServer, +// }, +// }, +// }; +// }, +// configureServer(viteServer) { +// return async () => { +// remove_html_middlewares(viteServer.middlewares); +// viteServer.middlewares.use(async (req, res) => { +// let template = fs.readFileSync( +// path.resolve(viteServer.config.root, index), +// 'utf-8' +// ); +// +// template = await viteServer.transformIndexHtml( +// req.originalUrl as string, +// template +// ); +// +// console.log(template); +// +// try { +// const entryServer = ( +// await viteServer.ssrLoadModule('~ng-vite/main-server') +// )['default']; +// const result = await entryServer(req.originalUrl, template); +// console.log(result); +// +// res.setHeader('Content-Type', 'text/html'); +// res.end(result); +// } catch (e) { +// viteServer && viteServer.ssrFixStacktrace(e as Error); +// res.statusCode = 500; +// res.end(` +// +// +// +// +// Error +// +// +// +// +// +// `); +// } +// }); +// }; +// }, +// }; +// } +// +// /** +// * Removes Vite internal middleware +// * +// * @param server +// */ +// function remove_html_middlewares(server: ViteDevServer['middlewares']) { +// const html_middlewares = [ +// 'viteIndexHtmlMiddleware', +// 'vite404Middleware', +// 'viteSpaFallbackMiddleware', +// ]; +// for (let i = server.stack.length - 1; i > 0; i--) { +// // @ts-ignore +// if (html_middlewares.includes(server.stack[i].handle.name)) { +// server.stack.splice(i, 1); +// } +// } +// } +// +// /** +// * Formats error for SSR message in error overlay +// * @param req +// * @param error +// * @returns +// */ +// function prepareError(req: Connect.IncomingMessage, error: unknown) { +// const e = error as Error; +// return { +// message: `An error occured while server rendering ${req.url}:\n\n\t${ +// typeof e === 'string' ? e : e.message +// } `, +// stack: typeof e === 'string' ? '' : e.stack, +// }; +// } diff --git a/angular/devkit/vite/dev-server/utils/host-alias.ts b/angular/devkit/vite/utils/host-alias.ts similarity index 92% rename from angular/devkit/vite/dev-server/utils/host-alias.ts rename to angular/devkit/vite/utils/host-alias.ts index c42e5253..22dd5331 100644 --- a/angular/devkit/vite/dev-server/utils/host-alias.ts +++ b/angular/devkit/vite/utils/host-alias.ts @@ -2,6 +2,7 @@ import findRoot from 'find-root'; import type { Alias } from 'vite'; +import type {BundlerContext, DevServerContext} from '@teambit/bundler'; export type ViteContext = { /** @@ -30,7 +31,7 @@ export type ViteContext = { aliasHostDependencies?: boolean; }; -export function getHostAlias(context: ViteContext): Alias[] { +export function getHostAlias(context: BundlerContext | DevServerContext): [] { const alias: Alias[] = []; const { hostDependencies: deps, aliasHostDependencies, hostRootDir } = context; if (deps && aliasHostDependencies) { diff --git a/angular/devkit/vite/utils/types.ts b/angular/devkit/vite/utils/types.ts new file mode 100644 index 00000000..ac8b517e --- /dev/null +++ b/angular/devkit/vite/utils/types.ts @@ -0,0 +1,36 @@ +import { ApplicationOptions, DevServerOptions } from '@bitdev/angular.dev-services.common'; +import { Logger } from '@teambit/logger'; +import type { ViteDevServerOptions } from '@teambit/vite.vite-dev-server'; +import { Workspace } from '@teambit/workspace'; + + +export type ViteAspectsContext = { + logger: Logger; + workspace: Workspace; +}; + +export enum BuildMode { + Development = 'development', + Production = 'production', +} + +export type NgViteOptions = { + angularOptions: Partial; + + /** name of the dev server */ + name?: string; + + // TODO: fix type once we can support preview with vite + /** list of transformers to modify Vite config in an advanced way */ + transformers?: ViteDevServerOptions['transformers']; + + /** optimize entries before passing them to Vite */ + optimizeEntries?: (entries: string[], context: ViteAspectsContext) => string[]; + + /** Output folder path for build files */ + outputPath?: string; + + buildMode?: BuildMode; + + sourceRoot?: string; +}; diff --git a/angular/devkit/vite/utils/utils.ts b/angular/devkit/vite/utils/utils.ts new file mode 100644 index 00000000..1251c9fe --- /dev/null +++ b/angular/devkit/vite/utils/utils.ts @@ -0,0 +1,21 @@ +import * as path from 'path'; + +function removeWindowsDriveLetter(osSpecificPath: string): string { + return osSpecificPath.replace(/^[A-Z]:/, ''); +} + +/** + * Coverts an os specific path to a unix style path. Use this when writing paths to config files. + * This should not be used to read files on disk because of the removal of Windows drive letters. + */ +export function normalizePath(osSpecificPath: string): string { + return removeWindowsDriveLetter(osSpecificPath).split('\\').join('/'); +} + +/** + * Normalized path fragments and joins them. Use this when writing paths to config files. + * This should not be used to read files on disk because of the removal of Windows drive letters. + */ +export function joinPathFragments(...fragments: string[]): string { + return normalizePath(path.join(...fragments)); +} diff --git a/angular/devkit/webpack/ng-webpack-bundler.ts b/angular/devkit/webpack/ng-webpack-bundler.ts index e30b1b5c..7a5621cb 100644 --- a/angular/devkit/webpack/ng-webpack-bundler.ts +++ b/angular/devkit/webpack/ng-webpack-bundler.ts @@ -1,5 +1,5 @@ import { - AngularEnvOptions, + AngularEnvOptions, ApplicationOptions, BrowserOptions, BundlerSetup, DevServerOptions, @@ -25,13 +25,14 @@ import { WebpackMain } from '@teambit/webpack'; import { generateTransformers, runTransformers } from '@teambit/webpack.webpack-bundler'; +import assert from 'assert'; import { join } from 'path'; import type { Configuration, WebpackPluginInstance } from 'webpack'; import { getPreviewRootPath, WebpackConfigFactoryOpts } from './utils'; import { StatsLoggerPlugin } from './webpack-plugins/stats-logger'; export type NgWebpackBundlerOptions = { - angularOptions?: Partial; + angularOptions?: Partial<(BrowserOptions | ApplicationOptions) & DevServerOptions>; /** * context of the bundler execution. @@ -64,9 +65,7 @@ export type WebpackBuildConfigFactory = export class NgWebpackBundler { static from(options: NgWebpackBundlerOptions): AsyncEnvHandler { return async(context: EnvContext): Promise => { - if (!options.ngEnvOptions.webpackConfigFactory) { - throw new Error('ngEnvOptions.webpackConfigFactory is required to use the Webpack bundler'); - } + assert(options.ngEnvOptions.webpackConfigFactory, 'webpackConfigFactory is required to use the Webpack bundler'); const webpackBuildConfigFactory: WebpackBuildConfigFactory = options.ngEnvOptions.webpackConfigFactory; const name = options.name || 'ng-webpack-bundler'; @@ -90,11 +89,11 @@ export class NgWebpackBundler { let appRootPath: string; let tsconfigPath: string; let plugins: WebpackPluginInstance[] = []; - if (isAppBuildContext(bundlerContext)) { // When you use `bit run ` - appRootPath = bundlerContext.capsule.path;// this.workspace?.componentDir(context.appComponent.id, {ignoreVersion: true}) || ''; + if (isAppBuildContext(bundlerContext)) { // When you use `bit build` for an actual angular app + appRootPath = bundlerContext.capsule.path; tsconfigPath = join(appRootPath, 'tsconfig.app.json'); plugins = [new StatsLoggerPlugin()]; - } else { // When you use `bit build` + } else { // When you use `bit build` for the preview app appRootPath = getPreviewRootPath(workspace); tsconfigPath = writeTsconfig(bundlerContext, appRootPath, tempFolder, application, pkg, devFilesMain, workspace); } @@ -136,9 +135,7 @@ export class NgWebpackBundler { return afterMutation.raw; })); - if(!options.ngEnvOptions.webpackModulePath) { - throw new Error('ngEnvOptions.webpackModulePath is required to use the Webpack bundler'); - } + assert(options.ngEnvOptions.webpackModulePath, 'webpackModulePath is required to use the Webpack bundler'); // eslint-disable-next-line import/no-dynamic-require,global-require const webpack = require(options.ngEnvOptions.webpackModulePath); diff --git a/angular/devkit/webpack/ng-webpack-dev-server.ts b/angular/devkit/webpack/ng-webpack-dev-server.ts index 5538dbc4..3fa7fc61 100644 --- a/angular/devkit/webpack/ng-webpack-dev-server.ts +++ b/angular/devkit/webpack/ng-webpack-dev-server.ts @@ -1,5 +1,6 @@ import { AngularEnvOptions, + ApplicationOptions, BrowserOptions, BundlerSetup, DevServerOptions, @@ -27,12 +28,13 @@ import { WebpackMain } from '@teambit/webpack'; import { generateTransformers, runTransformers } from '@teambit/webpack.webpack-bundler'; +import assert from 'assert'; import { join, posix } from 'path'; import type { Configuration } from 'webpack'; import { getPreviewRootPath, WebpackConfigFactoryOpts } from './utils'; export type WebpackDevServerOptions = { - angularOptions: Partial; + angularOptions: Partial<(BrowserOptions | ApplicationOptions) & DevServerOptions>; /** * context of the dev server execution. @@ -69,9 +71,7 @@ export type WebpackServeConfigFactory = export class NgWebpackDevServer { static from(options: WebpackDevServerOptions): AsyncEnvHandler { return async(context: EnvContext): Promise => { - if (!options.ngEnvOptions.webpackConfigFactory) { - throw new Error('ngEnvOptions.webpackConfigFactory is required to use the Webpack dev server'); - } + assert(options.ngEnvOptions.webpackConfigFactory, 'ngEnvOptions.webpackConfigFactory is required to use the Webpack dev server'); const name = options.name || 'ng-webpack-dev-server'; const logger = context.createLogger(name); @@ -153,12 +153,8 @@ export class NgWebpackDevServer { transformerContext ); - if (!options.ngEnvOptions.webpackModulePath) { - throw new Error('ngEnvOptions.webpackModulePath is required to use the Webpack dev server'); - } - if (!options.ngEnvOptions.webpackDevServerModulePath) { - throw new Error('ngEnvOptions.webpackDevServerModulePath is required to use the Webpack dev server'); - } + assert(options.ngEnvOptions.webpackModulePath, 'ngEnvOptions.webpackModulePath is required to use the Webpack dev server'); + assert(options.ngEnvOptions.webpackDevServerModulePath, 'ngEnvOptions.webpackDevServerModulePath is required to use the Webpack dev server'); // eslint-disable-next-line import/no-dynamic-require,global-require const webpack = require(options.ngEnvOptions.webpackModulePath); diff --git a/angular/devkit/webpack/webpack-plugins/angular-resolver.ts b/angular/devkit/webpack/webpack-plugins/angular-resolver.ts index 19a12380..0c2bee82 100644 --- a/angular/devkit/webpack/webpack-plugins/angular-resolver.ts +++ b/angular/devkit/webpack/webpack-plugins/angular-resolver.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ // eslint-disable-next-line max-classes-per-file -import { pathNormalizeToLinux } from '@teambit/legacy/dist/utils'; +import { normalizePath } from '@bitdev/angular.dev-services.common'; import type { Compiler } from 'webpack'; import { NodeJSFileSystem } from './nodejs-file-system'; @@ -33,7 +33,7 @@ function getResourceData(resolveData: any): ResourceData { packageName: descriptionFileData?.name, packageVersion: descriptionFileData?.version, relativePath, - packagePath: pathNormalizeToLinux(descriptionFileRoot), + packagePath: normalizePath(descriptionFileRoot), resource: resolveData.createData?.resource || resolveData.resource, entryPointPackageData: descriptionFileData }; diff --git a/angular/envs/angular-env/component.json b/angular/envs/angular-env/component.json index 53b8168d..cb5d1e88 100644 --- a/angular/envs/angular-env/component.json +++ b/angular/envs/angular-env/component.json @@ -17,14 +17,16 @@ "@angular-eslint/eslint-plugin-template": "~16.1.1", "@angular-eslint/template-parser": "~16.1.1", "@angular/animations": "^17.0.0", + "@angular/cli": "^17.0.0", "@angular/common": "^17.0.0", "@angular/compiler": "^17.0.0", "@angular/compiler-cli": "^17.0.0", - "@angular/cli": "^17.0.0", "@angular/core": "^17.0.0", "@angular/elements": "^17.0.0", "@angular/platform-browser": "^17.0.0", "@angular/platform-browser-dynamic": "^17.0.0", + "@angular/platform-server": "^17.0.0", + "@angular/ssr": "^17.0.0", "@jest/globals": "^29.3.1", "@ngtools/webpack": "^17.0.0", "@types/eslint": "^8.40.0", diff --git a/angular/envs/angular-env/env.jsonc b/angular/envs/angular-env/env.jsonc index 4777eaa5..d0cf9158 100644 --- a/angular/envs/angular-env/env.jsonc +++ b/angular/envs/angular-env/env.jsonc @@ -70,6 +70,16 @@ "version": "^17.0.0", "supportedRange": "^17.0.0" }, + { + "name": "@angular/platform-server", + "version": "^17.0.0", + "supportedRange": "^17.0.0" + }, + { + "name": "@angular/router", + "version": "^17.0.0", + "supportedRange": "^17.0.0" + }, { "name": "jest", "version": "^29.5.0", diff --git a/angular/envs/angular-v13-env/env.jsonc b/angular/envs/angular-v13-env/env.jsonc index b32e0f4f..d6b87c4c 100644 --- a/angular/envs/angular-v13-env/env.jsonc +++ b/angular/envs/angular-v13-env/env.jsonc @@ -70,6 +70,11 @@ "version": "~13.2.0", "supportedRange": "^13.0.0" }, + { + "name": "@angular/router", + "version": "~13.2.0", + "supportedRange": "^13.0.0" + }, { "name": "jest", "version": "^27.4.4", diff --git a/angular/envs/angular-v13-env/webpack-config.factory.ts b/angular/envs/angular-v13-env/webpack-config.factory.ts index 13ef4227..df356acb 100644 --- a/angular/envs/angular-v13-env/webpack-config.factory.ts +++ b/angular/envs/angular-v13-env/webpack-config.factory.ts @@ -1,8 +1,11 @@ /* eslint-disable no-param-reassign */ -import type { BrowserBuilderOptions, DevServerBuilderOptions } from '@angular-devkit/build-angular'; import { OutputHashing } from '@angular-devkit/build-angular'; import { getSystemPath, normalize, tags } from '@angular-devkit/core'; -import { BundlerSetup, dedupPaths } from '@bitdev/angular.dev-services.common'; +import { BundlerSetup, dedupPaths, getLoggerApi } from '@bitdev/angular.dev-services.common'; +import type { + BrowserBuilderOptions, + DevServerBuilderOptions +} from '@bitdev/angular.dev-services.ng-compat'; import { generateEntryPoints, generateWebpackConfig, @@ -29,7 +32,8 @@ import { WebpackConfigTransformer, WebpackConfigWithDevServer } from '@teambit/webpack'; -import path, { join } from 'path'; +import assert from 'assert'; +import { join, posix, resolve } from 'path'; import { Configuration } from 'webpack'; import { webpack5BuildConfigFactory } from './webpack/webpack5.build.config'; import { webpack5ServeConfigFactory } from './webpack/webpack5.serve.config'; @@ -99,6 +103,8 @@ async function getWebpackConfig( angularOptions: Partial = {}, sourceRoot = 'src' ): Promise { + assert(!(angularOptions as any).server, "SSR is only available for Angular v16+"); + // Options from angular.json const browserOptions: BrowserBuilderOptions = { ...angularOptions, @@ -109,8 +115,8 @@ async function getWebpackConfig( main: angularOptions.main ?? join(sourceRoot, `main.ts`), polyfills: angularOptions.polyfills ?? join(sourceRoot, `polyfills.ts`), tsConfig: angularOptions.tsConfig ?? tsconfigPath, - assets: dedupPaths([path.posix.join(sourceRoot, `favicon.ico`), path.posix.join(sourceRoot, `assets`), ...(angularOptions.assets ?? [])]), - styles: dedupPaths([path.posix.join(sourceRoot, `styles.scss`), ...(angularOptions.styles ?? [])]), + assets: dedupPaths([posix.join(sourceRoot, `favicon.ico`), posix.join(sourceRoot, `assets`), ...(angularOptions.assets ?? [])]), + styles: dedupPaths([posix.join(sourceRoot, `styles.${ angularOptions.inlineStyleLanguage ?? 'scss' }`), ...(angularOptions.styles ?? [])]), scripts: angularOptions.scripts, vendorChunk: angularOptions.vendorChunk ?? true, namedChunks: angularOptions.namedChunks ?? true, @@ -123,6 +129,7 @@ async function getWebpackConfig( watch: setup === BundlerSetup.Serve, allowedCommonJsDependencies: ['dompurify', '@teambit/harmony', 'graphql', '@teambit/documenter.ng.content.copy-box', ...(angularOptions.allowedCommonJsDependencies || [])], }; + const normalizedWorkspaceRoot = normalize(workspaceRoot); // used to load component config files, such as tailwind config, ... const projectRoot = normalize(workspaceRoot); @@ -147,11 +154,7 @@ async function getWebpackConfig( } ); - const loggerApi = { - createChild: () => logger as any, - ...logger, - log: logger.console, - } as any; + const loggerApi = getLoggerApi(logger); let webpackConfig: any = await generateWebpackConfig( getSystemPath(normalizedWorkspaceRoot), @@ -187,7 +190,7 @@ async function getWebpackConfig( const cacheOptions = normalizeCacheOptions({}, workspaceRoot); webpackConfig.plugins.push( new IndexHtmlWebpackPlugin({ - indexPath: path.resolve(workspaceRoot, browserOptions.index as string), + indexPath: resolve(workspaceRoot, browserOptions.index as string), outputPath: getIndexOutputFile(normalizedIndex), baseHref: browserOptions.baseHref || '/', entrypoints, diff --git a/angular/envs/angular-v13-env/webpack/webpack5.serve.config.ts b/angular/envs/angular-v13-env/webpack/webpack5.serve.config.ts index 271130e9..1c4224a2 100644 --- a/angular/envs/angular-v13-env/webpack/webpack5.serve.config.ts +++ b/angular/envs/angular-v13-env/webpack/webpack5.serve.config.ts @@ -1,5 +1,8 @@ -import { BitDedupeModuleResolvePlugin, StatsLoggerPlugin } from '@bitdev/angular.dev-services.webpack'; -import { pathNormalizeToLinux } from '@teambit/legacy/dist/utils'; +import { normalizePath } from '@bitdev/angular.dev-services.common'; +import { + BitDedupeModuleResolvePlugin, + StatsLoggerPlugin +} from '@bitdev/angular.dev-services.webpack'; import { PubsubMain } from '@teambit/pubsub'; import { fallbacks, @@ -66,7 +69,7 @@ export function webpack5ServeConfigFactory( chunkFilename: 'static/js/[name].chunk.js', // point sourcemap entries to original disk locations (format as URL on windows) - devtoolModuleFilenameTemplate: (info: any) => pathNormalizeToLinux(resolve(info.absoluteResourcePath)), + devtoolModuleFilenameTemplate: (info: any) => normalizePath(resolve(info.absoluteResourcePath)), // this defaults to 'window', but by setting it to 'this' then // module chunks which are built will work in web workers as well. diff --git a/angular/envs/angular-v14-env/env.jsonc b/angular/envs/angular-v14-env/env.jsonc index e72572ec..b10bc0c1 100644 --- a/angular/envs/angular-v14-env/env.jsonc +++ b/angular/envs/angular-v14-env/env.jsonc @@ -70,6 +70,11 @@ "version": "~14.0.2", "supportedRange": "^14.0.0" }, + { + "name": "@angular/router", + "version": "~14.0.2", + "supportedRange": "^14.0.0" + }, { "name": "jest", "version": "^28.0.0", diff --git a/angular/envs/angular-v14-env/webpack-config.factory.ts b/angular/envs/angular-v14-env/webpack-config.factory.ts index 7a1dc939..71b3e617 100644 --- a/angular/envs/angular-v14-env/webpack-config.factory.ts +++ b/angular/envs/angular-v14-env/webpack-config.factory.ts @@ -1,8 +1,11 @@ /* eslint-disable no-param-reassign */ -import type { BrowserBuilderOptions, DevServerBuilderOptions } from '@angular-devkit/build-angular'; import { OutputHashing } from '@angular-devkit/build-angular'; import { getSystemPath, normalize, tags } from '@angular-devkit/core'; -import { BundlerSetup, dedupPaths } from '@bitdev/angular.dev-services.common'; +import { BundlerSetup, dedupPaths, getLoggerApi } from '@bitdev/angular.dev-services.common'; +import type { + BrowserBuilderOptions, + DevServerBuilderOptions +} from '@bitdev/angular.dev-services.ng-compat'; import { generateEntryPoints, generateWebpackConfig, @@ -29,7 +32,8 @@ import { WebpackConfigTransformer, WebpackConfigWithDevServer } from '@teambit/webpack'; -import path, { join } from 'path'; +import assert from 'assert'; +import { join, posix, resolve } from 'path'; import { Configuration } from 'webpack'; import { webpack5BuildConfigFactory } from './webpack/webpack5.build.config'; import { webpack5ServeConfigFactory } from './webpack/webpack5.serve.config'; @@ -99,6 +103,8 @@ async function getWebpackConfig( angularOptions: Partial = {}, sourceRoot = 'src' ): Promise { + assert(!(angularOptions as any).server, "SSR is only available for Angular v16+"); + // Options from angular.json const browserOptions: BrowserBuilderOptions = { ...angularOptions, @@ -109,8 +115,8 @@ async function getWebpackConfig( main: angularOptions.main ?? `./${join(sourceRoot, `main.ts`)}`, polyfills: angularOptions.polyfills ?? `./${join(sourceRoot, `polyfills.ts`)}`, tsConfig: angularOptions.tsConfig ?? tsconfigPath, - assets: dedupPaths([path.posix.join(sourceRoot, `assets/**/*`), ...(angularOptions.assets ?? [])]), - styles: dedupPaths([path.posix.join(sourceRoot, `styles.scss`), ...(angularOptions.styles ?? [])]), + assets: dedupPaths([posix.join(sourceRoot, `assets/**/*`), ...(angularOptions.assets ?? [])]), + styles: dedupPaths([posix.join(sourceRoot, `styles.${ angularOptions.inlineStyleLanguage ?? 'scss' }`), ...(angularOptions.styles ?? [])]), scripts: angularOptions.scripts, vendorChunk: angularOptions.vendorChunk ?? true, namedChunks: angularOptions.namedChunks ?? true, @@ -123,16 +129,12 @@ async function getWebpackConfig( watch: setup === BundlerSetup.Serve, allowedCommonJsDependencies: ['dompurify', '@teambit/harmony', 'graphql', '@teambit/documenter.ng.content.copy-box', ...(angularOptions.allowedCommonJsDependencies || [])] }; + const normalizedWorkspaceRoot = normalize(workspaceRoot); // used to load component config files, such as tailwind config, ... const projectRoot = normalize(workspaceRoot); const normalizedSourceRoot = normalize(sourceRoot); - - const loggerApi = { - createChild: () => logger as any, - ...logger, - log: logger.console - } as any; + const loggerApi = getLoggerApi(logger); const normalizedOptions = normalizeBrowserSchema( normalizedWorkspaceRoot, @@ -188,7 +190,7 @@ async function getWebpackConfig( const cacheOptions = normalizeCacheOptions({}, workspaceRoot); webpackConfig.plugins.push( new IndexHtmlWebpackPlugin({ - indexPath: path.resolve(workspaceRoot, browserOptions.index as string), + indexPath: resolve(workspaceRoot, browserOptions.index as string), outputPath: getIndexOutputFile(normalizedIndex), baseHref: browserOptions.baseHref || '/', entrypoints, diff --git a/angular/envs/angular-v14-env/webpack/webpack5.serve.config.ts b/angular/envs/angular-v14-env/webpack/webpack5.serve.config.ts index 58c46378..14ae0ea2 100644 --- a/angular/envs/angular-v14-env/webpack/webpack5.serve.config.ts +++ b/angular/envs/angular-v14-env/webpack/webpack5.serve.config.ts @@ -1,5 +1,8 @@ -import { BitDedupeModuleResolvePlugin, StatsLoggerPlugin } from '@bitdev/angular.dev-services.webpack'; -import { pathNormalizeToLinux } from '@teambit/legacy/dist/utils'; +import { normalizePath } from '@bitdev/angular.dev-services.common'; +import { + BitDedupeModuleResolvePlugin, + StatsLoggerPlugin +} from '@bitdev/angular.dev-services.webpack'; import { PubsubMain } from '@teambit/pubsub'; import { fallbacks, @@ -65,7 +68,7 @@ export function webpack5ServeConfigFactory( chunkFilename: 'static/js/[name].chunk.js', // point sourcemap entries to original disk locations (format as URL on windows) - devtoolModuleFilenameTemplate: (info: any) => pathNormalizeToLinux(resolve(info.absoluteResourcePath)) + devtoolModuleFilenameTemplate: (info: any) => normalizePath(resolve(info.absoluteResourcePath)) // this defaults to 'window', but by setting it to 'this' then // module chunks which are built will work in web workers as well. diff --git a/angular/envs/angular-v15-env/env.jsonc b/angular/envs/angular-v15-env/env.jsonc index d3cf253f..1bdbf7e7 100644 --- a/angular/envs/angular-v15-env/env.jsonc +++ b/angular/envs/angular-v15-env/env.jsonc @@ -70,6 +70,11 @@ "version": "~15.0.4", "supportedRange": "^15.0.0" }, + { + "name": "@angular/router", + "version": "~15.0.4", + "supportedRange": "^15.0.0" + }, { "name": "jest", "version": "^29.3.1", diff --git a/angular/envs/angular-v15-env/webpack-config.factory.ts b/angular/envs/angular-v15-env/webpack-config.factory.ts index 11fdacc1..33ef14c6 100644 --- a/angular/envs/angular-v15-env/webpack-config.factory.ts +++ b/angular/envs/angular-v15-env/webpack-config.factory.ts @@ -2,7 +2,7 @@ import type { BrowserBuilderOptions, DevServerBuilderOptions } from '@angular-devkit/build-angular'; import { OutputHashing } from '@angular-devkit/build-angular'; import { getSystemPath, normalize, tags } from '@angular-devkit/core'; -import { BundlerSetup, dedupPaths } from '@bitdev/angular.dev-services.common'; +import { BundlerSetup, dedupPaths, getLoggerApi } from '@bitdev/angular.dev-services.common'; import { generateEntryPoints, generateWebpackConfig, @@ -29,7 +29,8 @@ import { WebpackConfigTransformer, WebpackConfigWithDevServer } from '@teambit/webpack'; -import path, { join } from 'path'; +import assert from 'assert'; +import { join, posix, resolve } from 'path'; import { Configuration } from 'webpack'; import { webpack5BuildConfigFactory } from './webpack/webpack5.build.config'; import { webpack5ServeConfigFactory } from './webpack/webpack5.serve.config'; @@ -99,6 +100,8 @@ async function getWebpackConfig( angularOptions: Partial = {}, sourceRoot = 'src' ): Promise { + assert(!(angularOptions as any).server, "SSR is only available for Angular v16+"); + // Options from angular.json const browserOptions: BrowserBuilderOptions = { ...angularOptions, @@ -107,10 +110,10 @@ async function getWebpackConfig( outputPath: 'public', // doesn't matter because it will be deleted from the config index: angularOptions.index ?? `./${join(sourceRoot, `index.html`)}`, main: angularOptions.main ?? `./${join(sourceRoot, `main.ts`)}`, - polyfills: angularOptions.polyfills ?? `./${join(sourceRoot, `polyfills.ts`)}`, + polyfills: angularOptions.polyfills, tsConfig: angularOptions.tsConfig ?? tsconfigPath, - assets: dedupPaths([path.posix.join(sourceRoot, `assets/**/*`), ...(angularOptions.assets ?? [])]), - styles: dedupPaths([path.posix.join(sourceRoot, `styles.scss`), ...(angularOptions.styles ?? [])]), + assets: dedupPaths([posix.join(sourceRoot, `assets/**/*`), ...(angularOptions.assets ?? [])]), + styles: dedupPaths([posix.join(sourceRoot, `styles.${ angularOptions.inlineStyleLanguage ?? 'scss' }`), ...(angularOptions.styles ?? [])]), scripts: angularOptions.scripts, vendorChunk: angularOptions.vendorChunk ?? true, namedChunks: angularOptions.namedChunks ?? true, @@ -123,16 +126,12 @@ async function getWebpackConfig( watch: setup === BundlerSetup.Serve, allowedCommonJsDependencies: ['dompurify', '@teambit/harmony', 'graphql', '@teambit/documenter.ng.content.copy-box', ...(angularOptions.allowedCommonJsDependencies || [])] }; + const normalizedWorkspaceRoot = normalize(workspaceRoot); // used to load component config files, such as tailwind config, ... const projectRoot = normalize(workspaceRoot); const normalizedSourceRoot = normalize(sourceRoot); - - const loggerApi = { - createChild: () => logger as any, - ...logger, - log: logger.console - } as any; + const loggerApi = getLoggerApi(logger); const normalizedOptions = normalizeBrowserSchema( normalizedWorkspaceRoot, @@ -189,7 +188,7 @@ async function getWebpackConfig( const cacheOptions = normalizeCacheOptions({}, workspaceRoot); webpackConfig.plugins.push( new IndexHtmlWebpackPlugin({ - indexPath: path.resolve(workspaceRoot, browserOptions.index as string), + indexPath: resolve(workspaceRoot, browserOptions.index as string), outputPath: getIndexOutputFile(normalizedIndex), baseHref: browserOptions.baseHref || '/', entrypoints, diff --git a/angular/envs/angular-v15-env/webpack/webpack5.serve.config.ts b/angular/envs/angular-v15-env/webpack/webpack5.serve.config.ts index d4527652..dc2c9dd5 100644 --- a/angular/envs/angular-v15-env/webpack/webpack5.serve.config.ts +++ b/angular/envs/angular-v15-env/webpack/webpack5.serve.config.ts @@ -1,5 +1,5 @@ +import { normalizePath } from '@bitdev/angular.dev-services.common'; import { BitDedupeModuleResolvePlugin, StatsLoggerPlugin } from '@bitdev/angular.dev-services.webpack'; -import { pathNormalizeToLinux } from '@teambit/legacy/dist/utils'; import { PubsubMain } from '@teambit/pubsub'; import { fallbacks, @@ -65,7 +65,7 @@ export function webpack5ServeConfigFactory( chunkFilename: 'static/js/[name].chunk.js', // point sourcemap entries to original disk locations (format as URL on windows) - devtoolModuleFilenameTemplate: (info: any) => pathNormalizeToLinux(resolve(info.absoluteResourcePath)) + devtoolModuleFilenameTemplate: (info: any) => normalizePath(resolve(info.absoluteResourcePath)) // this defaults to 'window', but by setting it to 'this' then // module chunks which are built will work in web workers as well. diff --git a/angular/envs/angular-v16-env/angular-v16-env.bit-env.ts b/angular/envs/angular-v16-env/angular-v16-env.bit-env.ts index 288e3eb1..adecc205 100644 --- a/angular/envs/angular-v16-env/angular-v16-env.bit-env.ts +++ b/angular/envs/angular-v16-env/angular-v16-env.bit-env.ts @@ -1,19 +1,6 @@ -import { - AngularEnvOptions, - BrowserOptions, - DevServerOptions, - isAppDevContext -} from '@bitdev/angular.dev-services.common'; -import { NgViteDevServer, ViteConfigTransformer } from '@bitdev/angular.dev-services.vite'; +import { AngularEnvOptions } from '@bitdev/angular.dev-services.common'; import { AngularBaseEnv } from '@bitdev/angular.envs.base-env'; -import { DevServer, DevServerContext } from '@teambit/bundler'; -import { AsyncEnvHandler } from '@teambit/envs'; import { NativeCompileCache } from '@teambit/toolbox.performance.v8-cache'; -import { - Configuration, - WebpackConfigTransformer, - WebpackConfigWithDevServer -} from '@teambit/webpack'; import { webpackConfigFactory } from './webpack-config.factory'; // Disable v8-caching because it breaks ESM loaders @@ -39,29 +26,8 @@ export class AngularV16Env extends AngularBaseEnv { // resolving to the webpack used by angular devkit to avoid multiple instances of webpack // otherwise, if we use a different version, it would break webpackModulePath: require.resolve('webpack', { paths: [require.resolve('@angular-devkit/build-angular')] }), - devServer: 'webpack', + // devServer: 'webpack', }; - - override getDevServer( - devServerContext: DevServerContext, - ngEnvOptions: AngularEnvOptions, - transformers: (WebpackConfigTransformer | ViteConfigTransformer)[] = [], - angularOptions: Partial = {}, - webpackOptions: Partial = {}, - sourceRoot?: string - ): AsyncEnvHandler { - if (this.ngEnvOptions.devServer === 'vite' && isAppDevContext(devServerContext)) { - return NgViteDevServer.from({ - angularOptions, - devServerContext, - ngEnvOptions, - sourceRoot, - transformers, - webpackOptions - }); - } - return super.getDevServer(devServerContext, ngEnvOptions, transformers as WebpackConfigTransformer[], angularOptions, webpackOptions, sourceRoot); - } } export default new AngularV16Env(); diff --git a/angular/envs/angular-v16-env/component.json b/angular/envs/angular-v16-env/component.json index 59afd1f9..fc95fe93 100644 --- a/angular/envs/angular-v16-env/component.json +++ b/angular/envs/angular-v16-env/component.json @@ -17,14 +17,15 @@ "@angular-eslint/eslint-plugin-template": "~16.1.1", "@angular-eslint/template-parser": "~16.1.1", "@angular/animations": "~16.2.0", + "@angular/cli": "~16.2.0", "@angular/common": "~16.2.0", "@angular/compiler": "~16.2.0", "@angular/compiler-cli": "~16.2.0", - "@angular/cli": "~16.2.0", "@angular/core": "~16.2.0", "@angular/elements": "~16.2.0", "@angular/platform-browser": "~16.2.0", "@angular/platform-browser-dynamic": "~16.2.0", + "@angular/platform-server": "~16.2.0", "@jest/globals": "^29.3.1", "@ngtools/webpack": "~16.2.0", "@types/eslint": "^8.40.0", diff --git a/angular/envs/angular-v16-env/env.jsonc b/angular/envs/angular-v16-env/env.jsonc index d0bb3237..30930fd2 100644 --- a/angular/envs/angular-v16-env/env.jsonc +++ b/angular/envs/angular-v16-env/env.jsonc @@ -70,6 +70,16 @@ "version": "~16.2.0", "supportedRange": "^16.2.0" }, + { + "name": "@angular/platform-server", + "version": "^16.2.0", + "supportedRange": "^16.2.0" + }, + { + "name": "@angular/router", + "version": "^16.2.0", + "supportedRange": "^16.2.0" + }, { "name": "jest", "version": "^29.5.0", diff --git a/angular/envs/angular-v16-env/webpack-config.factory.ts b/angular/envs/angular-v16-env/webpack-config.factory.ts index 63f15c4f..09fa6897 100644 --- a/angular/envs/angular-v16-env/webpack-config.factory.ts +++ b/angular/envs/angular-v16-env/webpack-config.factory.ts @@ -1,8 +1,11 @@ /* eslint-disable no-param-reassign */ -import type { BrowserBuilderOptions, DevServerBuilderOptions } from '@angular-devkit/build-angular'; import { OutputHashing } from '@angular-devkit/build-angular'; import { getSystemPath, normalize, tags } from '@angular-devkit/core'; -import { BundlerSetup, dedupPaths } from '@bitdev/angular.dev-services.common'; +import { BundlerSetup, dedupPaths, getLoggerApi } from '@bitdev/angular.dev-services.common'; +import type { + BrowserBuilderOptions, + DevServerBuilderOptions +} from '@bitdev/angular.dev-services.ng-compat'; import { generateEntryPoints, generateWebpackConfig, @@ -29,7 +32,7 @@ import { WebpackConfigTransformer, WebpackConfigWithDevServer } from '@teambit/webpack'; -import path, { join } from 'path'; +import { join, posix, resolve } from 'path'; import type { Configuration } from 'webpack'; import { webpack5BuildConfigFactory } from './webpack/webpack5.build.config'; import { webpack5ServeConfigFactory } from './webpack/webpack5.serve.config'; @@ -107,10 +110,10 @@ async function getWebpackConfig( outputPath: 'public', // doesn't matter because it will be deleted from the config index: angularOptions.index ?? `./${join(sourceRoot, `index.html`)}`, main: angularOptions.main ?? `./${join(sourceRoot, `main.ts`)}`, - polyfills: angularOptions.polyfills ?? `./${join(sourceRoot, `polyfills.ts`)}`, + polyfills: angularOptions.polyfills, tsConfig: angularOptions.tsConfig ?? tsconfigPath, - assets: dedupPaths([path.posix.join(sourceRoot, `assets/**/*`), ...(angularOptions.assets ?? [])]), - styles: dedupPaths([path.posix.join(sourceRoot, `styles.scss`), ...(angularOptions.styles ?? [])]), + assets: dedupPaths([posix.join(sourceRoot, `assets/**/*`), ...(angularOptions.assets ?? [])]), + styles: dedupPaths([posix.join(sourceRoot, `styles.${ angularOptions.inlineStyleLanguage ?? 'scss' }`), ...(angularOptions.styles ?? [])]), scripts: angularOptions.scripts, vendorChunk: angularOptions.vendorChunk ?? true, namedChunks: angularOptions.namedChunks ?? true, @@ -127,11 +130,7 @@ async function getWebpackConfig( // used to load component config files, such as tailwind config, ... const projectRoot = normalize(workspaceRoot); const normalizedSourceRoot = normalize(sourceRoot); - const loggerApi = { - createChild: () => logger as any, - ...logger, - log: logger.console - } as any; + const loggerApi = getLoggerApi(logger); const normalizedOptions = normalizeBrowserSchema( normalizedWorkspaceRoot, @@ -188,7 +187,7 @@ async function getWebpackConfig( const cacheOptions = normalizeCacheOptions({}, workspaceRoot); webpackConfig.plugins.push( new IndexHtmlWebpackPlugin({ - indexPath: path.resolve(workspaceRoot, browserOptions.index as string), + indexPath: resolve(workspaceRoot, browserOptions.index as string), outputPath: getIndexOutputFile(normalizedIndex), baseHref: browserOptions.baseHref || '/', entrypoints, diff --git a/angular/envs/angular-v16-env/webpack/webpack5.serve.config.ts b/angular/envs/angular-v16-env/webpack/webpack5.serve.config.ts index d4527652..dc2c9dd5 100644 --- a/angular/envs/angular-v16-env/webpack/webpack5.serve.config.ts +++ b/angular/envs/angular-v16-env/webpack/webpack5.serve.config.ts @@ -1,5 +1,5 @@ +import { normalizePath } from '@bitdev/angular.dev-services.common'; import { BitDedupeModuleResolvePlugin, StatsLoggerPlugin } from '@bitdev/angular.dev-services.webpack'; -import { pathNormalizeToLinux } from '@teambit/legacy/dist/utils'; import { PubsubMain } from '@teambit/pubsub'; import { fallbacks, @@ -65,7 +65,7 @@ export function webpack5ServeConfigFactory( chunkFilename: 'static/js/[name].chunk.js', // point sourcemap entries to original disk locations (format as URL on windows) - devtoolModuleFilenameTemplate: (info: any) => pathNormalizeToLinux(resolve(info.absoluteResourcePath)) + devtoolModuleFilenameTemplate: (info: any) => normalizePath(resolve(info.absoluteResourcePath)) // this defaults to 'window', but by setting it to 'this' then // module chunks which are built will work in web workers as well. diff --git a/angular/envs/angular-v17-env/angular-v17-env.bit-env.ts b/angular/envs/angular-v17-env/angular-v17-env.bit-env.ts index b57df631..2988b5b5 100644 --- a/angular/envs/angular-v17-env/angular-v17-env.bit-env.ts +++ b/angular/envs/angular-v17-env/angular-v17-env.bit-env.ts @@ -1,19 +1,6 @@ -import { - AngularEnvOptions, - BrowserOptions, - DevServerOptions, - isAppDevContext -} from '@bitdev/angular.dev-services.common'; -import { NgViteDevServer, ViteConfigTransformer } from '@bitdev/angular.dev-services.vite'; +import { AngularEnvOptions } from '@bitdev/angular.dev-services.common'; import { AngularBaseEnv } from '@bitdev/angular.envs.base-env'; -import { DevServer, DevServerContext } from '@teambit/bundler'; -import { AsyncEnvHandler } from '@teambit/envs'; import { NativeCompileCache } from '@teambit/toolbox.performance.v8-cache'; -import { - Configuration, - WebpackConfigTransformer, - WebpackConfigWithDevServer -} from '@teambit/webpack'; import { webpackConfigFactory } from './webpack-config.factory'; // Disable v8-caching because it breaks ESM loaders @@ -39,29 +26,8 @@ export class AngularV17Env extends AngularBaseEnv { // resolving to the webpack used by angular devkit to avoid multiple instances of webpack // otherwise, if we use a different version, it would break webpackModulePath: require.resolve('webpack', { paths: [require.resolve('@angular-devkit/build-angular')] }), - devServer: 'webpack', + // devServer: 'vite', }; - - override getDevServer( - devServerContext: DevServerContext, - ngEnvOptions: AngularEnvOptions, - transformers: (WebpackConfigTransformer | ViteConfigTransformer)[] = [], - angularOptions: Partial = {}, - webpackOptions: Partial = {}, - sourceRoot?: string - ): AsyncEnvHandler { - if (this.ngEnvOptions.devServer === 'vite' && isAppDevContext(devServerContext)) { - return NgViteDevServer.from({ - angularOptions, - devServerContext, - ngEnvOptions, - sourceRoot, - transformers, - webpackOptions - }); - } - return super.getDevServer(devServerContext, ngEnvOptions, transformers as WebpackConfigTransformer[], angularOptions, webpackOptions, sourceRoot); - } } export default new AngularV17Env(); diff --git a/angular/envs/angular-v17-env/component.json b/angular/envs/angular-v17-env/component.json index e726de23..0fefd6c0 100644 --- a/angular/envs/angular-v17-env/component.json +++ b/angular/envs/angular-v17-env/component.json @@ -17,14 +17,16 @@ "@angular-eslint/eslint-plugin-template": "~16.1.1", "@angular-eslint/template-parser": "~16.1.1", "@angular/animations": "^17.0.0", + "@angular/cli": "^17.0.0", "@angular/common": "^17.0.0", "@angular/compiler": "^17.0.0", "@angular/compiler-cli": "^17.0.0", - "@angular/cli": "^17.0.0", "@angular/core": "^17.0.0", "@angular/elements": "^17.0.0", "@angular/platform-browser": "^17.0.0", "@angular/platform-browser-dynamic": "^17.0.0", + "@angular/platform-server": "^17.0.0", + "@angular/ssr": "^17.0.0", "@jest/globals": "^29.3.1", "@ngtools/webpack": "^17.0.0", "@types/eslint": "^8.40.0", diff --git a/angular/envs/angular-v17-env/env.jsonc b/angular/envs/angular-v17-env/env.jsonc index 4777eaa5..d0cf9158 100644 --- a/angular/envs/angular-v17-env/env.jsonc +++ b/angular/envs/angular-v17-env/env.jsonc @@ -70,6 +70,16 @@ "version": "^17.0.0", "supportedRange": "^17.0.0" }, + { + "name": "@angular/platform-server", + "version": "^17.0.0", + "supportedRange": "^17.0.0" + }, + { + "name": "@angular/router", + "version": "^17.0.0", + "supportedRange": "^17.0.0" + }, { "name": "jest", "version": "^29.5.0", diff --git a/angular/envs/angular-v17-env/webpack-config.factory.ts b/angular/envs/angular-v17-env/webpack-config.factory.ts index 2d4c0937..7c133669 100644 --- a/angular/envs/angular-v17-env/webpack-config.factory.ts +++ b/angular/envs/angular-v17-env/webpack-config.factory.ts @@ -1,8 +1,11 @@ /* eslint-disable no-param-reassign */ -import type { BrowserBuilderOptions, DevServerBuilderOptions } from '@angular-devkit/build-angular'; import { OutputHashing } from '@angular-devkit/build-angular'; import { getSystemPath, normalize, tags } from '@angular-devkit/core'; -import { BundlerSetup, dedupPaths } from '@bitdev/angular.dev-services.common'; +import { BundlerSetup, dedupPaths, getLoggerApi } from '@bitdev/angular.dev-services.common'; +import type { + BrowserBuilderOptions, + DevServerBuilderOptions +} from '@bitdev/angular.dev-services.ng-compat'; import { generateEntryPoints, generateWebpackConfig, @@ -29,7 +32,7 @@ import { WebpackConfigTransformer, WebpackConfigWithDevServer } from '@teambit/webpack'; -import path, { join } from 'path'; +import { join, posix, resolve } from 'path'; import type { Configuration } from 'webpack'; import { webpack5BuildConfigFactory } from './webpack/webpack5.build.config'; import { webpack5ServeConfigFactory } from './webpack/webpack5.serve.config'; @@ -96,7 +99,7 @@ async function getWebpackConfig( logger: Logger, setup: BundlerSetup, webpackOptions: Partial = {}, - angularOptions: Partial = {}, + angularOptions: Partial = {}, sourceRoot = 'src' ): Promise { // Options from angular.json @@ -106,11 +109,11 @@ async function getWebpackConfig( preserveSymlinks: false, outputPath: 'public', // doesn't matter because it will be deleted from the config index: angularOptions.index ?? `./${join(sourceRoot, `index.html`)}`, - main: angularOptions.main ?? `./${join(sourceRoot, `main.ts`)}`, - polyfills: angularOptions.polyfills ?? `./${join(sourceRoot, `polyfills.ts`)}`, + main: angularOptions.browser ?? `./${join(sourceRoot, `main.ts`)}`, + polyfills: angularOptions.polyfills, tsConfig: angularOptions.tsConfig ?? tsconfigPath, - assets: dedupPaths([path.posix.join(sourceRoot, `assets/**/*`), ...(angularOptions.assets ?? [])]), - styles: dedupPaths([path.posix.join(sourceRoot, `styles.scss`), ...(angularOptions.styles ?? [])]), + assets: dedupPaths([posix.join(sourceRoot, `assets/**/*`), ...(angularOptions.assets ?? [])]), + styles: dedupPaths([posix.join(sourceRoot, `styles.${ angularOptions.inlineStyleLanguage ?? 'scss' }`), ...(angularOptions.styles ?? [])]), scripts: angularOptions.scripts, vendorChunk: angularOptions.vendorChunk ?? true, namedChunks: angularOptions.namedChunks ?? true, @@ -127,11 +130,7 @@ async function getWebpackConfig( // used to load component config files, such as tailwind config, ... const projectRoot = normalize(workspaceRoot); const normalizedSourceRoot = normalize(sourceRoot); - const loggerApi = { - createChild: () => logger as any, - ...logger, - log: logger.console - } as any; + const loggerApi = getLoggerApi(logger); const normalizedOptions = normalizeBrowserSchema( normalizedWorkspaceRoot, projectRoot, @@ -187,7 +186,7 @@ async function getWebpackConfig( const cacheOptions = normalizeCacheOptions({}, workspaceRoot); webpackConfig.plugins.push( new IndexHtmlWebpackPlugin({ - indexPath: path.resolve(workspaceRoot, browserOptions.index as string), + indexPath: resolve(workspaceRoot, browserOptions.index as string), outputPath: getIndexOutputFile(normalizedIndex), baseHref: browserOptions.baseHref || '/', entrypoints, diff --git a/angular/envs/angular-v17-env/webpack/webpack5.serve.config.ts b/angular/envs/angular-v17-env/webpack/webpack5.serve.config.ts index d4527652..dc2c9dd5 100644 --- a/angular/envs/angular-v17-env/webpack/webpack5.serve.config.ts +++ b/angular/envs/angular-v17-env/webpack/webpack5.serve.config.ts @@ -1,5 +1,5 @@ +import { normalizePath } from '@bitdev/angular.dev-services.common'; import { BitDedupeModuleResolvePlugin, StatsLoggerPlugin } from '@bitdev/angular.dev-services.webpack'; -import { pathNormalizeToLinux } from '@teambit/legacy/dist/utils'; import { PubsubMain } from '@teambit/pubsub'; import { fallbacks, @@ -65,7 +65,7 @@ export function webpack5ServeConfigFactory( chunkFilename: 'static/js/[name].chunk.js', // point sourcemap entries to original disk locations (format as URL on windows) - devtoolModuleFilenameTemplate: (info: any) => pathNormalizeToLinux(resolve(info.absoluteResourcePath)) + devtoolModuleFilenameTemplate: (info: any) => normalizePath(resolve(info.absoluteResourcePath)) // this defaults to 'window', but by setting it to 'this' then // module chunks which are built will work in web workers as well. diff --git a/angular/envs/base-env/angular-base-env.bit-env.ts b/angular/envs/base-env/angular-base-env.bit-env.ts index dc582e65..495f1521 100644 --- a/angular/envs/base-env/angular-base-env.bit-env.ts +++ b/angular/envs/base-env/angular-base-env.bit-env.ts @@ -1,6 +1,7 @@ import { AngularAppType } from '@bitdev/angular.app-types.angular-app-type'; import { AngularEnvOptions, + ApplicationOptions, BrowserOptions, DevServerOptions, NG_APP_NAME @@ -76,9 +77,9 @@ export abstract class AngularBaseEnv implements AngularEnvInterface { */ public setNgEnvOptions(...ngEnvOptions: Partial[]): void { this.ngEnvOptions = merge(this.ngEnvOptions || {}, ...ngEnvOptions); - if (this.ngEnvOptions.devServer === 'vite' && this.angularVersion < 16) { + /*if (this.ngEnvOptions.devServer === 'vite' && this.angularVersion < 16) { throw new Error(`Vite dev server is only supported for Angular 16+`); - } + }*/ } /** @@ -148,7 +149,7 @@ export abstract class AngularBaseEnv implements AngularEnvInterface { devServerContext: DevServerContext, ngEnvOptions: AngularEnvOptions, transformers: WebpackConfigTransformer[] = [], - angularOptions: Partial = {}, + angularOptions: Partial<(BrowserOptions | ApplicationOptions) & DevServerOptions> = {}, webpackOptions: any = {}, sourceRoot?: string ): AsyncEnvHandler { @@ -166,7 +167,7 @@ export abstract class AngularBaseEnv implements AngularEnvInterface { bundlerContext: BundlerContext, ngEnvOptions: AngularEnvOptions, transformers: WebpackConfigTransformer[] = [], - angularOptions: Partial = {}, + angularOptions: Partial<(BrowserOptions | ApplicationOptions) & DevServerOptions> = {}, webpackOptions: any = {}, sourceRoot?: string ): AsyncEnvHandler { @@ -185,7 +186,7 @@ export abstract class AngularBaseEnv implements AngularEnvInterface { const devServerProvider: DevServerProvider = ( devServerContext: DevServerContext, transformers: WebpackConfigTransformer[] = [], - angularOptions: Partial = {}, + angularOptions: Partial<(BrowserOptions | ApplicationOptions) & DevServerOptions> = {}, webpackOptions: any = {}, sourceRoot?: string ) => this.getDevServer(devServerContext, ngEnvOptions, transformers, angularOptions, webpackOptions, sourceRoot); diff --git a/angular/examples/my-angular-env/component.json b/angular/examples/my-angular-env/component.json index 5c315ebf..0d2e7acb 100644 --- a/angular/examples/my-angular-env/component.json +++ b/angular/examples/my-angular-env/component.json @@ -12,7 +12,7 @@ "@bitdev/angular.dev-services.linter.eslint": ">= 1.0.0", "@types/jest": "^29.5.0", "jest": "^29.5.0", - "jest-preset-angular": "~13.1.0" + "jest-preset-angular": "~13.1.3" } } } diff --git a/angular/examples/my-angular-env/env.jsonc b/angular/examples/my-angular-env/env.jsonc index 4777eaa5..d0cf9158 100644 --- a/angular/examples/my-angular-env/env.jsonc +++ b/angular/examples/my-angular-env/env.jsonc @@ -70,6 +70,16 @@ "version": "^17.0.0", "supportedRange": "^17.0.0" }, + { + "name": "@angular/platform-server", + "version": "^17.0.0", + "supportedRange": "^17.0.0" + }, + { + "name": "@angular/router", + "version": "^17.0.0", + "supportedRange": "^17.0.0" + }, { "name": "jest", "version": "^29.5.0", diff --git a/angular/examples/my-angular-v17-env/env.jsonc b/angular/examples/my-angular-v17-env/env.jsonc index b3028bec..d0cf9158 100644 --- a/angular/examples/my-angular-v17-env/env.jsonc +++ b/angular/examples/my-angular-v17-env/env.jsonc @@ -70,6 +70,16 @@ "version": "^17.0.0", "supportedRange": "^17.0.0" }, + { + "name": "@angular/platform-server", + "version": "^17.0.0", + "supportedRange": "^17.0.0" + }, + { + "name": "@angular/router", + "version": "^17.0.0", + "supportedRange": "^17.0.0" + }, { "name": "jest", "version": "^29.5.0", @@ -78,7 +88,7 @@ { "name": "rxjs", "version": "^7.8.1", - "supportedRange": "^7.8.0" + "supportedRange": "^6.5.3 || ^7.4.0" }, { "name": "tslib", @@ -88,7 +98,7 @@ { "name": "typescript", "version": "~5.2.2", - "supportedRange": ">=5.2 <5.3" + "supportedRange": ">=4.9.3 <5.3.0" }, { "name": "zone.js", diff --git a/angular/templates/generators/ng-app/index.ts b/angular/templates/generators/ng-app/index.ts index 6abffa7c..76ab5e1d 100644 --- a/angular/templates/generators/ng-app/index.ts +++ b/angular/templates/generators/ng-app/index.ts @@ -1,24 +1,32 @@ import { AngularComponentTemplateOptions } from '@bitdev/angular.dev-services.common'; +import { confirm, group, select } from '@clack/prompts'; +import { EnvContext, EnvHandler } from '@teambit/envs'; import { ComponentContext, ComponentTemplate } from '@teambit/generator'; +import { Logger } from '@teambit/logger'; +import { isCI } from 'std-env'; import { indexFile } from './template-files'; import { docsFile } from './template-files/docs'; import { ngAppFile } from './template-files/ng-app'; import { appComponentFile } from './template-files/src/app/app.component'; import { appComponentHtmlFile } from './template-files/src/app/app.component-html'; -import { appComponentScssFile } from './template-files/src/app/app.component-scss'; +import { appComponentStyleSheetFile } from './template-files/src/app/app.component-scss'; import { appComponentSpecFile } from './template-files/src/app/app.component-spec'; import { appConfigFile } from './template-files/src/app/app.config'; +import { serverConfigFile } from './template-files/src/app/app.config.server'; import { appModuleFile } from './template-files/src/app/app.module'; import { appRoutesFile } from './template-files/src/app/app.routes'; import { gitKeepFile } from './template-files/src/assets/gitkeep'; import { indexHtmlFile } from './template-files/src/index-html'; import { mainNgAppFile } from './template-files/src/main'; +import { mainServerFile } from './template-files/src/main.server'; import { polyfillsFile } from './template-files/src/polyfills'; +import { helloApiFile } from './template-files/src/server/api/hello'; import { stylesFile } from './template-files/src/styles'; import { tsconfigFile } from './template-files/tsconfig.app'; export class NgAppTemplate implements ComponentTemplate { private constructor( + private logger: Logger, readonly angularVersion: number, readonly name = 'ng-app', readonly description = 'create an Angular application', @@ -26,51 +34,113 @@ export class NgAppTemplate implements ComponentTemplate { ) { } - generateFiles(context: ComponentContext) { + async prompt(context: ComponentContext) { + this.logger.off(); + + const prompts: { [id: string]: () => Promise } = { + styleSheet: () => select({ + message: 'Which stylesheet format would you like to use?', + options: [ + { label: 'CSS', value: 'css' }, + { label: 'SCSS', value: 'scss', hint: 'https://sass-lang.com/documentation/syntax#scss' }, + { + label: 'Sass', + value: 'sass', + hint: 'https://sass-lang.com/documentation/syntax#the-indented-syntax' + }, + { label: 'Less', value: 'less', hint: 'http://lesscss.org' } + ] + }) as Promise + }; + + if (this.angularVersion > 13) { + prompts.standalone = () => confirm({ + message: 'Do you want to use standalone components?' + }) as Promise; + } + + if (this.angularVersion >= 16) { + prompts.ssr = () => confirm({ + message: 'Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)?' + }) as Promise; + } + + const params = await group(prompts); + + this.logger.on(); + + return params; + } + + async generateFiles(context: ComponentContext) { + let params: { [id: string]: any } = { + styleSheet: 'scss', + standalone: this.angularVersion > 13, + ssr: this.angularVersion >= 17 // todo: check if we can use 16 here + }; + + if (!isCI) { + params = await this.prompt(context); + } + const files = [ docsFile(context), indexFile(context), - ngAppFile(context, this.angularVersion), - tsconfigFile(this.angularVersion), + ngAppFile(context, params.styleSheet, params.ssr), + tsconfigFile(this.angularVersion, params.ssr), indexHtmlFile(context), - mainNgAppFile(this.angularVersion), - stylesFile(), + mainNgAppFile(params.standalone), + stylesFile(params.styleSheet), appComponentHtmlFile(), - appComponentScssFile(), - appComponentSpecFile(context, this.angularVersion), - appComponentFile(context, this.angularVersion), + appComponentStyleSheetFile(params.styleSheet), + appComponentSpecFile(context, params.standalone), + appComponentFile(context, params.styleSheet, params.standalone), gitKeepFile(), + appRoutesFile() ]; + if (params.ssr) { + files.push( + mainServerFile(params.standalone), + helloApiFile() + ); + + if (params.standalone) { + files.push(serverConfigFile()); + } + } + if (this.angularVersion < 15) { files.push( // starting from Angular 15, the `polyfills` option accept an array of module specifiers. // https://github.com/angular/angular-cli/commit/597bfea1b29cc7b25d1f466eb313cbeeb6dffc98 - polyfillsFile(), + polyfillsFile() ); } - if (this.angularVersion >= 17) { - files.push( - appConfigFile(), - appRoutesFile(), - ); - } else { - files.push( - appModuleFile(), - ); + if (params.standalone) { + files.push(appConfigFile(this.angularVersion, params.ssr)); + } + if (!params.standalone) { + files.push(appModuleFile(params.ssr)); } return files; } - static from(options: AngularComponentTemplateOptions & { angularVersion: number }) { - return () => - new NgAppTemplate( + static from(options: AngularComponentTemplateOptions & { + angularVersion: number + }): EnvHandler { + return (context: EnvContext) => { + const name = options.name || 'ng-app-template'; + const logger = context.createLogger(name); + return new NgAppTemplate( + logger, options.angularVersion, options.name, options.description, options.hidden ); + }; } } diff --git a/angular/templates/generators/ng-app/template-files/docs.ts b/angular/templates/generators/ng-app/template-files/docs.ts index 0adabadd..2d187f46 100644 --- a/angular/templates/generators/ng-app/template-files/docs.ts +++ b/angular/templates/generators/ng-app/template-files/docs.ts @@ -1,7 +1,7 @@ import { ComponentContext, ComponentFile } from '@teambit/generator'; export const docsFile = (context: ComponentContext): ComponentFile => { - const { name, namePascalCase: Name } = context; + const { name } = context; return { relativePath: `${name}.docs.md`, diff --git a/angular/templates/generators/ng-app/template-files/ng-app.ts b/angular/templates/generators/ng-app/template-files/ng-app.ts index c3b1d6e5..4445b08a 100644 --- a/angular/templates/generators/ng-app/template-files/ng-app.ts +++ b/angular/templates/generators/ng-app/template-files/ng-app.ts @@ -1,26 +1,26 @@ import { ComponentContext, ComponentFile } from '@teambit/generator'; -export const ngAppFile = (context: ComponentContext, angularVersion: number): ComponentFile => { +export const ngAppFile = (context: ComponentContext, styleSheet: string, ssr: boolean): ComponentFile => { const { name, namePascalCase: Name } = context; return { relativePath: `${name}.ng-app.ts`, content: `import { AngularAppOptions } from '@bitdev/angular.app-types.angular-app-type'; -import { BrowserOptions, DevServerOptions } from '@bitdev/angular.dev-services.common'; +import { ${ssr ? `ApplicationOptions`: `BrowserOptions` }, DevServerOptions } from '@bitdev/angular.dev-services.common'; -const angularOptions: BrowserOptions & DevServerOptions = { - main: './src/main.ts', - polyfills: ${angularVersion >= 15 ? `[ - "zone.js", - "zone.js/testing" - ]` : `'./src/polyfills.ts'`}, +const angularOptions: ${ssr ? `ApplicationOptions`: `BrowserOptions` } & DevServerOptions = { + ${ssr ? `browser: './src/main.ts', + server: './src/main.server.ts', + prerender: true, + ssr: true,` : `main: './src/main.ts',`} index: './src/index.html', tsConfig: './tsconfig.app.json', + inlineStyleLanguage: '${styleSheet}', assets: [{ "glob": "**/*", "input": "src/assets/", "output": "/assets/" }], - styles: ['./src/styles.scss'], + styles: ['./src/styles.${styleSheet}'], }; export const ${Name}Options: AngularAppOptions = { diff --git a/angular/templates/generators/ng-app/template-files/src/app/app.component-html.ts b/angular/templates/generators/ng-app/template-files/src/app/app.component-html.ts index f0ecbeb4..1b163777 100644 --- a/angular/templates/generators/ng-app/template-files/src/app/app.component-html.ts +++ b/angular/templates/generators/ng-app/template-files/src/app/app.component-html.ts @@ -20,6 +20,8 @@ export const appComponentHtmlFile = (): ComponentFile => { box-sizing: border-box; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + --electric-violet: #8815f5; + --vivid-pink: #ff006e; } h1, @@ -47,7 +49,7 @@ export const appComponentHtmlFile = (): ComponentFile => { height: 60px; display: flex; align-items: center; - background-color: #1976d2; + background-color: #000; color: white; font-weight: 600; } @@ -140,7 +142,7 @@ export const appComponentHtmlFile = (): ComponentFile => { } .card.highlight-card { - background-color: #1976d2; + background-color: #000; color: white; font-weight: 600; border: none; @@ -312,7 +314,7 @@ export const appComponentHtmlFile = (): ComponentFile => { Angular Logo Welcome
@@ -335,9 +337,13 @@ export const appComponentHtmlFile = (): ComponentFile => {
+ + + + Rocket Ship - + diff --git a/angular/templates/generators/ng-app/template-files/src/app/app.component-scss.ts b/angular/templates/generators/ng-app/template-files/src/app/app.component-scss.ts index 546258a3..565b0956 100644 --- a/angular/templates/generators/ng-app/template-files/src/app/app.component-scss.ts +++ b/angular/templates/generators/ng-app/template-files/src/app/app.component-scss.ts @@ -1,8 +1,8 @@ import { ComponentFile } from '@teambit/generator'; -export const appComponentScssFile = (): ComponentFile => { +export const appComponentStyleSheetFile = (styleSheet: string): ComponentFile => { return { - relativePath: `src/app/app.component.scss`, + relativePath: `src/app/app.component.${styleSheet}`, content: ``, }; }; diff --git a/angular/templates/generators/ng-app/template-files/src/app/app.component-spec.ts b/angular/templates/generators/ng-app/template-files/src/app/app.component-spec.ts index 3e7358e3..6a893b84 100644 --- a/angular/templates/generators/ng-app/template-files/src/app/app.component-spec.ts +++ b/angular/templates/generators/ng-app/template-files/src/app/app.component-spec.ts @@ -1,6 +1,6 @@ import { ComponentContext, ComponentFile } from '@teambit/generator'; -export const appComponentSpecFile = (context: ComponentContext, angularVersion: number): ComponentFile => { +export const appComponentSpecFile = (context: ComponentContext, standalone: boolean): ComponentFile => { const { name } = context; return { relativePath: `src/app/app.component.spec.ts`, @@ -10,7 +10,7 @@ import { AppComponent } from './app.component'; describe('AppComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - ${angularVersion >= 17 ? `imports: [AppComponent]` : `declarations: [AppComponent]`}, + ${standalone ? `imports: [AppComponent]` : `declarations: [AppComponent]`}, }).compileComponents(); }); diff --git a/angular/templates/generators/ng-app/template-files/src/app/app.component.ts b/angular/templates/generators/ng-app/template-files/src/app/app.component.ts index de9ede36..8930f7e2 100644 --- a/angular/templates/generators/ng-app/template-files/src/app/app.component.ts +++ b/angular/templates/generators/ng-app/template-files/src/app/app.component.ts @@ -1,29 +1,19 @@ import { ComponentContext, ComponentFile } from '@teambit/generator'; -export const appComponentFile = (context: ComponentContext, angularVersion: number): ComponentFile => { +export const appComponentFile = (context: ComponentContext, styleSheet: string, standalone: boolean): ComponentFile => { const { name } = context; return { relativePath: `src/app/app.component.ts`, - content: angularVersion >= 17 ? `import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; + content: `import { Component } from '@angular/core';${standalone ? ` +import { CommonModule } from '@angular/common';` : ''} import { RouterOutlet } from '@angular/router'; @Component({ - selector: 'app-root', + selector: 'app-root',${standalone ? ` standalone: true, - imports: [CommonModule, RouterOutlet], + imports: [CommonModule, RouterOutlet],` : ''} templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'] -}) -export class AppComponent { - title = '${name}'; -} -` : `import { Component } from '@angular/core'; - -@Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'] + styleUrls: ['./app.component.${ styleSheet }'] }) export class AppComponent { title = '${name}'; diff --git a/angular/templates/generators/ng-app/template-files/src/app/app.config.server.ts b/angular/templates/generators/ng-app/template-files/src/app/app.config.server.ts new file mode 100644 index 00000000..c86dff49 --- /dev/null +++ b/angular/templates/generators/ng-app/template-files/src/app/app.config.server.ts @@ -0,0 +1,19 @@ +import { ComponentFile } from '@teambit/generator'; + +export const serverConfigFile = (): ComponentFile => { + return { + relativePath: `src/app/app.config.server.ts`, + content: `import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; +import { provideServerRendering } from '@angular/platform-server'; +import { appConfig } from './app.config'; + +const serverConfig: ApplicationConfig = { + providers: [ + provideServerRendering() + ] +}; + +export const config = mergeApplicationConfig(appConfig, serverConfig); +`, + }; +}; diff --git a/angular/templates/generators/ng-app/template-files/src/app/app.config.ts b/angular/templates/generators/ng-app/template-files/src/app/app.config.ts index 59775b28..ba4bfa05 100644 --- a/angular/templates/generators/ng-app/template-files/src/app/app.config.ts +++ b/angular/templates/generators/ng-app/template-files/src/app/app.config.ts @@ -1,15 +1,16 @@ import { ComponentFile } from '@teambit/generator'; -export const appConfigFile = (): ComponentFile => { +export const appConfigFile = (angularVersion: number, ssr: boolean): ComponentFile => { return { relativePath: `src/app/app.config.ts`, - content: `import { ApplicationConfig } from '@angular/core'; + content: `import { ApplicationConfig } from '${angularVersion >= 16 ? '@angular/core' : '@angular/platform-browser'}'; import { provideRouter } from '@angular/router'; -import { routes } from './app.routes'; +import { routes } from './app.routes';${ssr ? ` +import { provideClientHydration } from '@angular/platform-browser';` : ''} export const appConfig: ApplicationConfig = { - providers: [provideRouter(routes)] + providers: [provideRouter(routes)${ssr ? ', provideClientHydration()' : ''}] }; `, }; diff --git a/angular/templates/generators/ng-app/template-files/src/app/app.module.ts b/angular/templates/generators/ng-app/template-files/src/app/app.module.ts index 1f4e9b05..af323209 100644 --- a/angular/templates/generators/ng-app/template-files/src/app/app.module.ts +++ b/angular/templates/generators/ng-app/template-files/src/app/app.module.ts @@ -1,11 +1,13 @@ import { ComponentFile } from '@teambit/generator'; -export const appModuleFile = (): ComponentFile => { +export const appModuleFile = (ssr: boolean): ComponentFile => { return { relativePath: `src/app/app.module.ts`, content: `import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; - +import { RouterModule } from '@angular/router';${ssr ? ` +import { provideClientHydration } from '@angular/platform-browser';` : ''} +import { routes } from './app.routes'; import { AppComponent } from './app.component'; @NgModule({ @@ -13,9 +15,10 @@ import { AppComponent } from './app.component'; AppComponent ], imports: [ - BrowserModule + BrowserModule, + RouterModule.forRoot(routes) ], - providers: [], + providers: [${ssr ? `provideClientHydration()` : ``}], bootstrap: [AppComponent] }) export class AppModule { } diff --git a/angular/templates/generators/ng-app/template-files/src/index-html.ts b/angular/templates/generators/ng-app/template-files/src/index-html.ts index 5522d02f..c151562b 100644 --- a/angular/templates/generators/ng-app/template-files/src/index-html.ts +++ b/angular/templates/generators/ng-app/template-files/src/index-html.ts @@ -11,7 +11,7 @@ export const indexHtmlFile = (context: ComponentContext): ComponentFile => { ${Name} - + diff --git a/angular/templates/generators/ng-app/template-files/src/main.server.ts b/angular/templates/generators/ng-app/template-files/src/main.server.ts new file mode 100644 index 00000000..14889084 --- /dev/null +++ b/angular/templates/generators/ng-app/template-files/src/main.server.ts @@ -0,0 +1,29 @@ +import { ComponentFile } from '@teambit/generator'; + +export const mainServerFile = (standalone: boolean): ComponentFile => { + return { + relativePath: `src/main.server.ts`, + content: `import 'zone.js/node'; +${standalone ? `import { bootstrapApplication } from '@angular/platform-browser'; +import { AppComponent } from './app/app.component'; +import { config } from './app/app.config.server'; + +export default function bootstrap() { + return bootstrapApplication(AppComponent, config); +}` : `import { provideServerRendering } from '@angular/platform-server'; +import { NgModule } from '@angular/core'; +import { ServerModule } from '@angular/platform-server'; +import { AppModule } from './app/app.module'; +import { AppComponent } from './app/app.component'; + +@NgModule({ + imports: [ + AppModule, + ServerModule, + ], + providers: [provideServerRendering()], + bootstrap: [AppComponent], +}) +export default class AppServerModule {}`}`, + }; +}; diff --git a/angular/templates/generators/ng-app/template-files/src/main.ts b/angular/templates/generators/ng-app/template-files/src/main.ts index c54d63f7..b63f6c39 100644 --- a/angular/templates/generators/ng-app/template-files/src/main.ts +++ b/angular/templates/generators/ng-app/template-files/src/main.ts @@ -1,9 +1,10 @@ import { ComponentFile } from '@teambit/generator'; -export const mainNgAppFile = (angularVersion: number): ComponentFile => { +export const mainNgAppFile = (standalone: boolean): ComponentFile => { return { relativePath: `src/main.ts`, - content: angularVersion >= 17 ? `import { bootstrapApplication } from '@angular/platform-browser'; + content: `import 'zone.js'; +${standalone ? `import { bootstrapApplication } from '@angular/platform-browser'; import { appConfig } from './app/app.config'; import { AppComponent } from './app/app.component'; @@ -14,6 +15,6 @@ import { AppModule } from './app/app.module'; platformBrowserDynamic().bootstrapModule(AppModule) .catch(err => console.error(err)); -`, +`}`, }; }; diff --git a/angular/templates/generators/ng-app/template-files/src/server/api/hello.ts b/angular/templates/generators/ng-app/template-files/src/server/api/hello.ts new file mode 100644 index 00000000..efd7282f --- /dev/null +++ b/angular/templates/generators/ng-app/template-files/src/server/api/hello.ts @@ -0,0 +1,10 @@ +import { ComponentFile } from '@teambit/generator'; + +export const helloApiFile = (): ComponentFile => { + return { + relativePath: `src/server/api/hello.ts`, + content: `import { defineEventHandler } from 'h3'; + +export default defineEventHandler(() => ({ message: 'Hello World' }));`, + }; +}; diff --git a/angular/templates/generators/ng-app/template-files/src/styles.ts b/angular/templates/generators/ng-app/template-files/src/styles.ts index 7119a19a..39cf45a3 100644 --- a/angular/templates/generators/ng-app/template-files/src/styles.ts +++ b/angular/templates/generators/ng-app/template-files/src/styles.ts @@ -1,8 +1,8 @@ import { ComponentFile } from '@teambit/generator'; -export const stylesFile = (): ComponentFile => { +export const stylesFile = (styleSheet: string): ComponentFile => { return { - relativePath: `src/styles.scss`, + relativePath: `src/styles.${styleSheet}`, content: `/* You can add global styles to this file, and also import other style files */ `, }; diff --git a/angular/templates/generators/ng-app/template-files/tsconfig.app.ts b/angular/templates/generators/ng-app/template-files/tsconfig.app.ts index 3ca5d415..00e0e8cb 100644 --- a/angular/templates/generators/ng-app/template-files/tsconfig.app.ts +++ b/angular/templates/generators/ng-app/template-files/tsconfig.app.ts @@ -1,6 +1,6 @@ import { ComponentFile } from '@teambit/generator'; -export const tsconfigFile = (angularVersion: number): ComponentFile => { +export const tsconfigFile = (angularVersion: number, ssr: boolean): ComponentFile => { return { relativePath: 'tsconfig.app.json', content: `/* To learn more about this file see: https://angular.io/config/tsconfig. */ @@ -23,7 +23,8 @@ export const tsconfigFile = (angularVersion: number): ComponentFile => { "allowJs": true, ${angularVersion >= 17 ? `"target": "ES2022", "module": "ES2022"` : `"target": "es2017", - "module": "es2020"`}, + "module": "es2020"`},${angularVersion >= 15 ? ` + "useDefineForClassFields": false,` : ``} "preserveSymlinks": false, "lib": [ "${angularVersion >= 17 ? `ES2022` : `ES2018`}", @@ -37,7 +38,8 @@ export const tsconfigFile = (angularVersion: number): ComponentFile => { "strictInputAccessModifiers": true }, "files": [ - "./src/main.ts"${angularVersion >= 15 ? `` : `, + "./src/main.ts",${ssr ? ` + "./src/main.server.ts",` : ``}${angularVersion >= 15 ? `` : ` "./src/polyfills.ts"`} ], "include": [ diff --git a/angular/templates/generators/ng-env/files/env.ts b/angular/templates/generators/ng-env/files/env.ts index 2e4be7c9..88e59f79 100644 --- a/angular/templates/generators/ng-env/files/env.ts +++ b/angular/templates/generators/ng-env/files/env.ts @@ -2,7 +2,7 @@ import { ComponentContext } from '@teambit/generator'; export function envFile({ namePascalCase: Name, name }: ComponentContext, envName: string, angularVersion: number, envPkgName: string) { // language=TypeScript - return `import { BrowserOptions, DevServerOptions } from '@bitdev/angular.dev-services.common'; + return `import { ApplicationOptions, BrowserOptions, DevServerOptions } from '@bitdev/angular.dev-services.common'; import { AngularPreview, BundlerProvider, DevServerProvider } from '@bitdev/angular.dev-services.preview.preview'; import { ${envName} } from '${envPkgName}'; import { NgAppTemplate, NgEnvTemplate, NgModuleTemplate, NgStandaloneTemplate } from '@bitdev/angular.templates.generators'; @@ -88,7 +88,7 @@ export class ${Name} extends ${envName} { const devServerProvider: DevServerProvider = ( devServerContext: DevServerContext, transformers: WebpackConfigTransformer[] = [], - angularOptions: Partial = {}, + angularOptions: Partial<(BrowserOptions | ApplicationOptions) & DevServerOptions> = {}, webpackOptions: any = {}, sourceRoot?: string ) => this.getDevServer(devServerContext, ngEnvOptions, transformers, angularOptions, webpackOptions, sourceRoot); diff --git a/integration/demo-app/demo-app.ng-app.ts b/integration/demo-app/demo-app.ng-app.ts index 43a61f43..5a56a413 100644 --- a/integration/demo-app/demo-app.ng-app.ts +++ b/integration/demo-app/demo-app.ng-app.ts @@ -1,13 +1,16 @@ import type { AngularAppOptions } from '@bitdev/angular.app-types.angular-app-type'; -import type { BrowserOptions, DevServerOptions } from '@bitdev/angular.dev-services.common'; +import type { ApplicationOptions, DevServerOptions } from '@bitdev/angular.dev-services.common'; -const angularOptions: BrowserOptions & DevServerOptions = { - main: './src/main.ts', - polyfills: './src/polyfills.ts', +const angularOptions: ApplicationOptions & DevServerOptions = { + browser: './src/main.ts', + server: './src/main.server.ts', index: './src/index.html', tsConfig: 'tsconfig.app.json', assets: ['./src/favicon.ico', './src/assets'], - styles: ['./src/styles.scss'] + styles: ['./src/styles.scss'], + inlineStyleLanguage: "scss", + prerender: true, + ssr: true }; export const DemoAppOptions: AngularAppOptions = { @@ -29,8 +32,7 @@ export const DemoAppOptions: AngularAppOptions = { /** * Angular options for `bit run` */ - angularServeOptions: angularOptions, - + angularServeOptions: angularOptions }; export default DemoAppOptions; diff --git a/integration/demo-app/src/app/app.component.html b/integration/demo-app/src/app/app.component.html index 2a42646f..e4916cf6 100644 --- a/integration/demo-app/src/app/app.component.html +++ b/integration/demo-app/src/app/app.component.html @@ -15,6 +15,8 @@ box-sizing: border-box; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + --electric-violet: #8815f5; + --vivid-pink: #ff006e; } h1, @@ -42,7 +44,7 @@ height: 60px; display: flex; align-items: center; - background-color: #1976d2; + background-color: #000; color: white; font-weight: 600; } @@ -135,7 +137,7 @@ } .card.highlight-card { - background-color: #1976d2; + background-color: #000; color: white; font-weight: 600; border: none; @@ -307,7 +309,7 @@ Angular Logo Welcome
@@ -331,9 +333,13 @@
+ + + + Rocket Ship - + @@ -479,3 +485,6 @@

Next Steps

+ + + diff --git a/integration/demo-app/src/app/app.component.spec.ts b/integration/demo-app/src/app/app.component.spec.ts index 88bc80b3..051ec289 100644 --- a/integration/demo-app/src/app/app.component.spec.ts +++ b/integration/demo-app/src/app/app.component.spec.ts @@ -4,7 +4,7 @@ import { AppComponent } from './app.component'; describe('AppComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [AppComponent] + imports: [AppComponent], }).compileComponents(); }); diff --git a/integration/demo-app/src/app/app.component.ts b/integration/demo-app/src/app/app.component.ts index 08ea3d9e..e5bb61af 100644 --- a/integration/demo-app/src/app/app.component.ts +++ b/integration/demo-app/src/app/app.component.ts @@ -1,9 +1,13 @@ import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterOutlet } from '@angular/router'; @Component({ selector: 'app-root', + standalone: true, + imports: [CommonModule, RouterOutlet], templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'] + styleUrls: ['./app.component.scss'], }) export class AppComponent { title = 'demo-app'; diff --git a/integration/demo-app/src/app/app.config.server.ts b/integration/demo-app/src/app/app.config.server.ts new file mode 100644 index 00000000..b4d57c94 --- /dev/null +++ b/integration/demo-app/src/app/app.config.server.ts @@ -0,0 +1,11 @@ +import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; +import { provideServerRendering } from '@angular/platform-server'; +import { appConfig } from './app.config'; + +const serverConfig: ApplicationConfig = { + providers: [ + provideServerRendering() + ] +}; + +export const config = mergeApplicationConfig(appConfig, serverConfig); diff --git a/integration/demo-app/src/app/app.config.ts b/integration/demo-app/src/app/app.config.ts new file mode 100644 index 00000000..e5a3cf10 --- /dev/null +++ b/integration/demo-app/src/app/app.config.ts @@ -0,0 +1,9 @@ +import { ApplicationConfig } from '@angular/core'; +import { provideRouter } from '@angular/router'; + +import { routes } from './app.routes'; +import { provideClientHydration } from '@angular/platform-browser'; + +export const appConfig: ApplicationConfig = { + providers: [provideRouter(routes), provideClientHydration()] +}; diff --git a/integration/demo-app/src/app/app.routes.ts b/integration/demo-app/src/app/app.routes.ts new file mode 100644 index 00000000..dc39edb5 --- /dev/null +++ b/integration/demo-app/src/app/app.routes.ts @@ -0,0 +1,3 @@ +import { Routes } from '@angular/router'; + +export const routes: Routes = []; diff --git a/integration/demo-app/src/assets/favicon.ico b/integration/demo-app/src/assets/favicon.ico index b20b53e6..57614f9c 100644 Binary files a/integration/demo-app/src/assets/favicon.ico and b/integration/demo-app/src/assets/favicon.ico differ diff --git a/integration/demo-app/src/index.html b/integration/demo-app/src/index.html index 3dc7b647..058b5ded 100644 --- a/integration/demo-app/src/index.html +++ b/integration/demo-app/src/index.html @@ -5,7 +5,7 @@ DemoApp - + diff --git a/integration/demo-app/src/main.server.ts b/integration/demo-app/src/main.server.ts new file mode 100644 index 00000000..25ad97a0 --- /dev/null +++ b/integration/demo-app/src/main.server.ts @@ -0,0 +1,6 @@ +import 'zone.js/node'; +import { bootstrapApplication } from '@angular/platform-browser'; +import { AppComponent } from './app/app.component'; +import { config } from './app/app.config.server'; + +export default () => bootstrapApplication(AppComponent, config); diff --git a/integration/demo-app/src/main.ts b/integration/demo-app/src/main.ts index c7b673cf..272abdb0 100644 --- a/integration/demo-app/src/main.ts +++ b/integration/demo-app/src/main.ts @@ -1,12 +1,20 @@ -import { enableProdMode } from '@angular/core'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +// import { enableProdMode } from '@angular/core'; +// import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +// +// import { AppModule } from './app/app.module'; +// import { environment } from './environments/environment'; +// +// if (environment.production) { +// enableProdMode(); +// } +// +// platformBrowserDynamic().bootstrapModule(AppModule) +// .catch(err => console.error(err)); -import { AppModule } from './app/app.module'; -import { environment } from './environments/environment'; +import 'zone.js'; +import { bootstrapApplication } from '@angular/platform-browser'; -if (environment.production) { - enableProdMode(); -} +import { AppComponent } from './app/app.component'; +import { appConfig } from './app/app.config'; -platformBrowserDynamic().bootstrapModule(AppModule) - .catch(err => console.error(err)); +bootstrapApplication(AppComponent, appConfig); diff --git a/integration/demo-app/src/polyfills.ts b/integration/demo-app/src/polyfills.ts deleted file mode 100644 index 373f538a..00000000 --- a/integration/demo-app/src/polyfills.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * This file includes polyfills needed by Angular and is loaded before the app. - * You can add your own extra polyfills to this file. - * - * This file is divided into 2 sections: - * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. - * 2. Application imports. Files imported after ZoneJS that should be loaded before your main - * file. - * - * The current setup is for so-called "evergreen" browsers; the last versions of browsers that - * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), - * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. - * - * Learn more in https://angular.io/guide/browser-support - */ - -/*************************************************************************************************** - * BROWSER POLYFILLS - */ - -/** - * IE11 requires the following for NgClass support on SVG elements - */ -// import 'classlist.js'; // Run `npm install --save classlist.js`. - -/** - * Web Animations `@angular/platform-browser/animations` - * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. - * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). - */ -// import 'web-animations-js'; // Run `npm install --save web-animations-js`. - -/** - * By default, zone.js will patch all possible macroTask and DomEvents - * user can disable parts of macroTask/DomEvents patch by setting following flags - * because those flags need to be set before `zone.js` being loaded, and webpack - * will put import in the top of bundle, so user need to create a separate file - * in this directory (for example: zone-flags.ts), and put the following flags - * into that file, and then add the following code before importing zone.js. - * import './zone-flags'; - * - * The flags allowed in zone-flags.ts are listed here. - * - * The following flags will work for all browsers. - * - * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame - * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick - * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames - * - * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js - * with the following flag, it will bypass `zone.js` patch for IE/Edge - * - * (window as any).__Zone_enable_cross_context_check = true; - * - */ - -/*************************************************************************************************** - * Zone JS is required by default for Angular itself. - */ -import 'zone.js'; // Included with Angular CLI. - - -/*************************************************************************************************** - * APPLICATION IMPORTS - */ diff --git a/integration/demo-app/src/server/api/v1/hello.ts b/integration/demo-app/src/server/api/v1/hello.ts new file mode 100644 index 00000000..594c5d71 --- /dev/null +++ b/integration/demo-app/src/server/api/v1/hello.ts @@ -0,0 +1,3 @@ +import { defineEventHandler } from 'h3'; + +export default defineEventHandler(() => ({ message: 'Hello World' })); diff --git a/integration/demo-app/tsconfig.app.json b/integration/demo-app/tsconfig.app.json index 98303991..68d14bea 100644 --- a/integration/demo-app/tsconfig.app.json +++ b/integration/demo-app/tsconfig.app.json @@ -16,8 +16,9 @@ "allowSyntheticDefaultImports": true, "importHelpers": true, "allowJs": true, - "target": "es2017", - "module": "es2020", + "useDefineForClassFields": true, + "target": "ES2022", + "module": "ES2022", "preserveSymlinks": false, "lib": [ "es2018", @@ -34,7 +35,7 @@ }, "files": [ "./src/main.ts", - "./src/polyfills.ts" + "./src/main.server.ts" ], "include": [ "./src/**/*.d.ts" diff --git a/tsconfig.json b/tsconfig.json index ceb7788b..af837ed3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,7 @@ /* Basic Options */ "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ - "lib": ["ESNext", "dom"], /* Specify library files to be included in the compilation. */ + "lib": ["ESNext.AsyncIterable","ESNext", "dom"], /* Specify library files to be included in the compilation. */ "allowJs": true, /* Allow javascript files to be compiled. */ /* Strict Type-Checking Options */ @@ -17,7 +17,7 @@ // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ - + "useDefineForClassFields": false, /* Emit a define for each class field declaration. */ /* Additional Checks */ // "noUnusedLocals": true, /* Report errors on unused locals. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */ diff --git a/workspace.jsonc b/workspace.jsonc index 2f0bfb35..2621aacf 100644 --- a/workspace.jsonc +++ b/workspace.jsonc @@ -35,32 +35,37 @@ "packageManager": "teambit.dependencies/pnpm", "policy": { "dependencies": { - "@teambit/compilation.compiler-task": "1.0.2", - "@teambit/defender.eslint-linter": "1.0.8", - "@teambit/defender.jest-tester": "1.0.5", - "@teambit/defender.prettier-formatter": "1.0.2", - "@teambit/dependencies.modules.packages-excluder": "1.0.4", + "@clack/prompts": "^0.7.0", + "@teambit/compilation.compiler-task": "1.0.6", + "@teambit/defender.eslint-linter": "1.0.15", + "@teambit/defender.jest-tester": "1.0.12", + "@teambit/defender.prettier-formatter": "1.0.7", + "@teambit/dependencies.modules.packages-excluder": "1.0.8", "@teambit/envs.docs.env-overview-template": "0.0.2", "@teambit/graph.cleargraph": "^0.0.7", - "@teambit/react.ui.docs-app": "1.0.6", - "@teambit/toolbox.network.get-port": "1.0.2", - "@teambit/toolbox.performance.v8-cache": "0.0.29", - "@teambit/typescript.typescript-compiler": "2.0.7", + "@teambit/node.deps-detectors.detective-es6": "0.0.5", + "@teambit/react.ui.docs-app": "1.0.11", + "@teambit/toolbox.network.get-port": "1.0.4", + "@teambit/toolbox.performance.v8-cache": "0.0.31", + "@teambit/typescript.typescript-compiler": "2.0.13", "@teambit/ui-foundation.ui.constants.z-indexes": "0.0.504", - "@teambit/webpack.modules.generate-style-loaders": "1.0.2", - "@teambit/webpack.modules.style-regexps": "1.0.2", - "@teambit/webpack.webpack-bundler": "1.0.2", + "@teambit/webpack.modules.generate-style-loaders": "1.0.4", + "@teambit/webpack.modules.style-regexps": "1.0.4", + "@teambit/webpack.webpack-bundler": "1.0.7", "@types/fs-extra": "~9.0.7", "@types/lodash": "4.14.165", "@types/object-hash": "~2.1.0", "fs-extra": "9.1.0", + "json5": "^2.2.3", "lodash": "4.17.21", "object-hash": "~2.1.1", "prettier": "2.3.2", "react-dev-utils": "10.2.1", "remark-frontmatter": "~3.0.0", "remark-html": "13.0.1", - "remark-prism": "~1.3.6" + "remark-prism": "~1.3.6", + "replace-in-file": "^6.3.5", + "std-env": "^3.4.3" }, "peerDependencies": {} }, @@ -82,7 +87,7 @@ "bitdev.angular/envs/angular-v16-env", "bitdev.angular/envs/angular-v15-env", "bitdev.angular/envs/angular-v14-env", - "bitdev.angular/envs/angular-v13-env", + "bitdev.angular/envs/angular-v13-env" ] }, "bitdev.angular/angular-env": {},