Skip to content

Commit

Permalink
feat: add esbuild, ssr and interactive ng-app template
Browse files Browse the repository at this point in the history
  • Loading branch information
ocombe committed Dec 1, 2023
1 parent c90c789 commit d5c0894
Showing 104 changed files with 2,113 additions and 983 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -61,6 +61,7 @@ dist-legacy/
flow-typed/
eslint-report.html
types/*
.nitro

#distrubution
distribution/
15 changes: 9 additions & 6 deletions angular/app-types/angular-app-type/angular-app-options.ts
Original file line number Diff line number Diff line change
@@ -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);
};
10 changes: 7 additions & 3 deletions angular/app-types/angular-app-type/angular.app-type.ts
Original file line number Diff line number Diff line change
@@ -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,15 +13,17 @@ interface AngularAppTypeOptions {
}

export class AngularAppType implements ApplicationType<AngularAppOptions> {
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(
this.angularEnv,
this.context,
options,
this.depsResolver,
this.workspace,
this.logger,
this.workspace
);
}

@@ -29,7 +32,8 @@ export class AngularAppType implements ApplicationType<AngularAppOptions> {
const name = options.name || NG_APP_NAME;
const depsResolver = context.getAspect<DependencyResolverMain>(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);
};
}
}
149 changes: 97 additions & 52 deletions angular/app-types/angular-app-type/angular.application.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>();

@@ -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,19 +68,20 @@ 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
});
}

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,84 +111,115 @@ 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<DevServer> {
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<DevServer> {
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<any> {
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<number> {
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<Bundler> {
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);

return preview.getBundler(bundlerContext)(this.envContext);
}

async build(context: AppBuildContext): Promise<AngularAppBuildResult> {
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
};
}
}
Loading

0 comments on commit d5c0894

Please sign in to comment.