Skip to content

Commit

Permalink
feat(core): support define module.ts in .docgeni/app folder (#401)
Browse files Browse the repository at this point in the history
* feat(core): support .docgeni/app folder define module.ts

* test: add test for site-plugin
  • Loading branch information
why520crazy committed Nov 2, 2022
1 parent 55f2371 commit 03e8c52
Show file tree
Hide file tree
Showing 19 changed files with 500 additions and 239 deletions.
7 changes: 7 additions & 0 deletions .docgeni/app/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { FormsModule } from '@angular/forms';
import { myProviders } from './providers';

export default {
imports: [FormsModule],
providers: [...myProviders]
};
1 change: 1 addition & 0 deletions .docgeni/app/providers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const myProviders = [];
84 changes: 82 additions & 2 deletions packages/core/src/angular/site-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { toolkit } from '@docgeni/toolkit';
import { AngularCommandOptions, SiteProject } from './types';
import Handlebars from 'handlebars';
import { getSystemPath, HostWatchEventType, normalize, relative, resolve } from '../fs';
import { of, from } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { createNgSourceFile } from '@docgeni/ngdoc';
import { ValidationError } from '../errors';
import semver from 'semver';
import { spawn } from 'child_process';
import { SITE_ASSETS_RELATIVE_PATH } from '../constants';
import { NgModuleMetadata } from '../types/module';
import { combineNgModuleMetadata } from '../ast-utils';
import { NgSourceUpdater } from '../ng-source-updater';

interface CopyFile {
from: string;
Expand Down Expand Up @@ -43,6 +45,7 @@ export class SiteBuilder {
public ngVersion: string;
public enableIvy: boolean;
private publicDirPath: string;
private srcAppDirPath: string;
private siteProject: SiteProject;

spawn = spawn;
Expand All @@ -55,6 +58,7 @@ export class SiteBuilder {
if (this.docgeni.config.publicDir) {
this.publicDirPath = this.docgeni.paths.getAbsPath(this.docgeni.config.publicDir);
}
this.srcAppDirPath = this.docgeni.paths.getAbsPath('.docgeni/app');
}

public async build() {
Expand All @@ -66,7 +70,9 @@ export class SiteBuilder {
} else {
await this.createSiteProject();
await this.syncPublic();
await this.syncSrcApp();
this.watchPublic();
this.watchSrcApp();
}
}

Expand Down Expand Up @@ -163,6 +169,14 @@ export class SiteBuilder {
return false;
}

private async srcAppDirExists() {
if (this.srcAppDirPath) {
const result = await this.docgeni.host.exists(this.srcAppDirPath);
return result;
}
return false;
}

private async syncPublic() {
if (await this.publicDirExists()) {
const assetsPath = resolve(this.publicDirPath, `assets`);
Expand All @@ -179,6 +193,72 @@ export class SiteBuilder {
}
}

private async syncSrcApp() {
if (await this.srcAppDirExists()) {
await this.docgeni.host.copy(this.srcAppDirPath, resolve(this.siteProject.sourceRoot, 'app'));
await this.buildAppModule();
}
}

private async watchSrcApp() {
if (this.docgeni.watch && (await this.srcAppDirExists())) {
this.docgeni.host.watchAggregated(this.srcAppDirPath).subscribe(async events => {
for (const event of events) {
const distPath = event.path.replace(this.srcAppDirPath, resolve(this.siteProject.sourceRoot, 'app'));
if (event.type === HostWatchEventType.Deleted) {
await this.docgeni.host.delete(distPath);
} else {
await this.docgeni.host.copy(event.path, distPath);
}
if (event.path.includes(resolve(this.srcAppDirPath, 'module.ts'))) {
this.buildAppModule();
}
}
});
}
}

private async buildAppModule() {
const modulePath = resolve(this.srcAppDirPath, './module.ts');
if (await this.docgeni.host.pathExists(modulePath)) {
const moduleText = await this.docgeni.host.readFile(modulePath);
const ngSourceFile = createNgSourceFile(modulePath, moduleText);
const defaultExports = ngSourceFile.getDefaultExports() as NgModuleMetadata;
const defaultExportNode = ngSourceFile.getDefaultExportNode();
if (defaultExportNode) {
const metadata = combineNgModuleMetadata(defaultExports, {
declarations: [],
imports: [
'BrowserModule',
'BrowserAnimationsModule',
'DocgeniTemplateModule',
'RouterModule.forRoot([])',
' ...IMPORT_MODULES'
],
providers: ['...DOCGENI_SITE_PROVIDERS'],
bootstrap: ['RootComponent']
});

const updater = new NgSourceUpdater(ngSourceFile);
updater.insertImports([
{ name: 'NgModule', moduleSpecifier: '@angular/core' },
{ name: 'RouterModule', moduleSpecifier: '@angular/router' },
{ name: 'BrowserModule', moduleSpecifier: '@angular/platform-browser' },
{ name: 'BrowserAnimationsModule', moduleSpecifier: '@angular/platform-browser/animations' },
{ name: 'DocgeniTemplateModule', moduleSpecifier: '@docgeni/template' },
{ name: 'DOCGENI_SITE_PROVIDERS', moduleSpecifier: './content/index' },
{ name: 'IMPORT_MODULES', moduleSpecifier: './content/index' },
{ name: 'RootComponent', moduleSpecifier: './content/index' }
]);
updater.insertNgModule('AppModule', metadata);
updater.removeDefaultExport();

updater.update();
await this.docgeni.host.writeFile(resolve(this.siteProject.sourceRoot, './app/app.module.ts'), updater.update());
}
}
}

private async watchPublic() {
if (this.docgeni.watch && (await this.publicDirExists())) {
const assetsPath = resolve(this.publicDirPath, 'assets');
Expand Down
110 changes: 108 additions & 2 deletions packages/core/src/angular/site-plugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const SITE_TEMPLATE_PATH = resolve(__dirname, '../site-template');

const PUBLIC_PATH = `${DEFAULT_TEST_ROOT_PATH}/.docgeni/public`;
const DEFAULT_SITE_PATH = `${DEFAULT_TEST_ROOT_PATH}/.docgeni/site`;
const SRC_APP_PATH = `${DEFAULT_TEST_ROOT_PATH}/.docgeni/app`;

describe('#site-plugin', () => {
let ngSitePlugin: AngularSitePlugin;
Expand All @@ -39,7 +40,8 @@ describe('#site-plugin', () => {
initialFiles: {
[`${DEFAULT_TEST_ROOT_PATH}/node_modules/@angular/core/package.json`]: fixture.src['package.json'],
[`${DEFAULT_TEST_ROOT_PATH}/angular.json`]: fixture.src['angular.json'],
[`${SITE_TEMPLATE_PATH}/src/main.ts`]: 'main.ts'
[`${SITE_TEMPLATE_PATH}/src/main.ts`]: 'main.ts',
[`${SITE_TEMPLATE_PATH}/src/app/app.module.ts`]: fixture.src['app/app.module.ts']
}
});
ngSitePlugin = new AngularSitePlugin();
Expand All @@ -53,7 +55,8 @@ describe('#site-plugin', () => {
{
[`${DEFAULT_SITE_PATH}/angular.json`]: fixture.getOutputContent('angular.json'),
[`${DEFAULT_SITE_PATH}/tsconfig.app.json`]: fixture.getOutputContent('tsconfig.app.json'),
[`${DEFAULT_SITE_PATH}/src/main.ts`]: 'main.ts'
[`${DEFAULT_SITE_PATH}/src/main.ts`]: 'main.ts',
[`${DEFAULT_SITE_PATH}/src/app/app.module.ts`]: fixture.getOutputContent('app/app.module.ts')
},
true
);
Expand Down Expand Up @@ -258,4 +261,107 @@ describe('#site-plugin', () => {
await context.hooks.done.promise();
expect(calledSpawn).toEqual(true);
});

describe('src/app', () => {
it('should generate new ng module and copy other source files in ".docgeni/app" dir', async () => {
const moduleText = `export default { providers: [ AClass ] }`;
await writeFilesToHost(context.host, {
[`${SRC_APP_PATH}/module.ts`]: moduleText,
[`${SRC_APP_PATH}/a.ts`]: 'const export a = "aaa"',
[`${SRC_APP_PATH}/sub/b.ts`]: 'const export b = "bbb"'
});
await context.hooks.beforeRun.promise();
await assertExpectedFiles(
context.host,
{
[`${DEFAULT_SITE_PATH}/src/app/a.ts`]: 'const export a = "aaa"',
[`${DEFAULT_SITE_PATH}/src/app/sub/b.ts`]: 'const export b = "bbb"'
},
true
);
const appModule = await context.host.readFile(`${DEFAULT_SITE_PATH}/src/app/app.module.ts`);
expect(appModule).toContain(`providers: [ AClass, ...DOCGENI_SITE_PROVIDERS ]`);
});

it('should copy new files when ".docgeni/app" dir files changed', async () => {
await writeFilesToHost(context.host, {
[`${SRC_APP_PATH}/a.ts`]: 'const export a = "aaa"',
[`${SRC_APP_PATH}/sub/b.ts`]: 'const export b = "bbb"'
});
updateContext(context, { watch: true });
const watchAggregatedSpy = spyOn(context.host, 'watchAggregated');
const watchAggregated$ = new Subject<HostWatchEvent[]>();
watchAggregatedSpy.and.callFake(files => {
return watchAggregated$.asObservable();
});
await context.hooks.beforeRun.promise();

await writeFilesToHost(context.host, {
[`${SRC_APP_PATH}/a.ts`]: 'const export a = "new"',
[`${SRC_APP_PATH}/c.ts`]: 'const export c = "ccc"'
});

watchAggregated$.next([
{
type: HostWatchEventType.Created,
path: normalize(`${SRC_APP_PATH}/c.ts`),
time: new Date()
},
{
type: HostWatchEventType.Changed,
path: normalize(`${SRC_APP_PATH}/a.ts`),
time: new Date()
},
{
type: HostWatchEventType.Deleted,
path: normalize(`${SRC_APP_PATH}/sub/b.ts`),
time: new Date()
}
]);

await toolkit.utils.wait(2000);
expect(await context.host.exists(`${DEFAULT_SITE_PATH}/src/sub/b.ts`)).toBeFalsy();
await assertExpectedFiles(
context.host,
{
[`${DEFAULT_SITE_PATH}/src/app/a.ts`]: 'const export a = "new"',
[`${DEFAULT_SITE_PATH}/src/app/c.ts`]: 'const export c = "ccc"'
},
true
);
});

it('should rebuild app module when module.ts changed', async () => {
const moduleText = `export default { providers: [ AClass ] }`;
await writeFilesToHost(context.host, {
[`${SRC_APP_PATH}/module.ts`]: moduleText
});
updateContext(context, { watch: true });
const watchAggregatedSpy = spyOn(context.host, 'watchAggregated');
const watchAggregated$ = new Subject<HostWatchEvent[]>();
watchAggregatedSpy.and.callFake(files => {
return watchAggregated$.asObservable();
});
await context.hooks.beforeRun.promise();
const appModule = await context.host.readFile(resolve(DEFAULT_SITE_PATH, './src/app/app.module.ts'));
expect(appModule).toContain(`providers: [ AClass, ...DOCGENI_SITE_PROVIDERS ],`);

const newModuleText = `export default { providers: [ NewClass ] }`;
await writeFilesToHost(context.host, {
[`${SRC_APP_PATH}/module.ts`]: newModuleText
});

watchAggregated$.next([
{
type: HostWatchEventType.Changed,
path: normalize(`${SRC_APP_PATH}/module.ts`),
time: new Date()
}
]);

await toolkit.utils.wait(2000);
const newAppModule = await context.host.readFile(resolve(DEFAULT_SITE_PATH, './src/app/app.module.ts'));
expect(newAppModule).toContain(`providers: [ NewClass, ...DOCGENI_SITE_PROVIDERS ],`);
});
});
});

0 comments on commit 03e8c52

Please sign in to comment.