Skip to content

Commit

Permalink
Add preboot support to ng-render (you can give it a configuration JSO…
Browse files Browse the repository at this point in the history
…N or a static JSON string in the command-line arguments, and it will inject preboot into the application)
  • Loading branch information
clbond committed Apr 11, 2017
1 parent bb07502 commit 22b4cec
Show file tree
Hide file tree
Showing 10 changed files with 171 additions and 25 deletions.
3 changes: 2 additions & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "angular-ssr",
"version": "0.1.34",
"version": "0.1.38",
"description": "Angular server-side rendering implementation",
"main": "build/index.js",
"typings": "build/index.d.ts",
Expand Down Expand Up @@ -103,6 +103,7 @@
"domino": "https://github.com/clbond/domino/archive/1.0.39.tar.gz",
"es2015-imports-to-commonjs-loose": "^1.0.4",
"jest": "^19.0.2",
"jsonschema": "^1.1.1",
"lru_map": "^0.3.3",
"mkdir-recursive": "^0.3.0",
"mock-local-storage": "^1.0.2",
Expand Down
14 changes: 7 additions & 7 deletions source/application/builder/builder-base.ts
Expand Up @@ -16,13 +16,6 @@ export abstract class ApplicationBuilderBase<V> implements ApplicationBuilder<V>

abstract build(): Application<V>;

preboot(config?: PrebootConfiguration) {
if (config != null) {
this.operation.preboot = config;
}
return this.operation.preboot;
}

templateDocument(template?: string) {
if (template != null) {
this.operation.templateDocument = templateFileToTemplateString(template);
Expand Down Expand Up @@ -64,6 +57,13 @@ export abstract class ApplicationBuilderBase<V> implements ApplicationBuilder<V>
return this.operation.routes;
}

preboot(config?: PrebootConfiguration) {
if (config != null) {
this.operation.preboot = config;
}
return this.operation.preboot;
}

stateReader<R>(stateReader?: ApplicationStateReader<R>) {
if (stateReader) {
this.operation.stateReader = stateReader;
Expand Down
1 change: 1 addition & 0 deletions source/application/contracts.ts
Expand Up @@ -72,6 +72,7 @@ export interface EventSelector {
noReplay?: boolean;
}

// If you specify a null application root, it will be automatically detected for you
export type PrebootApplicationRoot = {appRoot: string | Array<string>};

export type PrebootSeparateRoots = {serverClientRoot: Array<{clientSelector: string, serverSelector: string}>};
Expand Down
51 changes: 48 additions & 3 deletions source/bin/options.ts
Expand Up @@ -4,23 +4,30 @@ import chalk = require('chalk');

import {dirname, join, resolve} from 'path';

import {cwd} from 'process';

import {
ConfigurationException,
FileReference,
FileType,
Files,
PathReference,
PrebootConfiguration,
Project,
absoluteFile,
fileFromString,
fromJson,
pathFromRandomId,
pathFromString,
} from '../index';

import {validatePrebootOptionsAgainstSchema} from './preboot';

const {version} = require('../../package.json');

export interface CommandLineOptions {
debug: boolean;
preboot: PrebootConfiguration;
project: Project;
output: PathReference;
templateDocument: string;
Expand All @@ -38,7 +45,7 @@ export const commandLineToOptions = (): CommandLineOptions => {
throw new ConfigurationException(`Project path does not exist: ${path}`);
}

const source = options['module'];
const source = options['module'] ? options['module'].replace(/\.(js|ts)$/, String()) : null;
const symbol = options['symbol'];

const environment = options['environment'];
Expand All @@ -60,7 +67,7 @@ export const commandLineToOptions = (): CommandLineOptions => {
let outputString = options['output'];

if (/^(\\|\/)/.test(outputString) === false) {
outputString = join(process.cwd(), outputString);
outputString = join(cwd(), outputString);
}

if (options['ipc']) {
Expand All @@ -74,9 +81,12 @@ export const commandLineToOptions = (): CommandLineOptions => {

const webpack = options['webpack'];

const preboot = getPrebootConfiguration(options['preboot'] as string);

return {
debug,
output,
preboot,
project,
templateDocument: template.content(),
webpack
Expand All @@ -88,13 +98,14 @@ const parseCommandLine = () => {
.version(version)
.description(chalk.green('Prerender Angular applications'))
.option('-d, --debug Enable debugging (stack traces and so forth)', false)
.option('-p, --project <path>', 'Path to tsconfig.json file or project root (if tsconfig.json lives in the root)', process.cwd())
.option('-p, --project <path>', 'Path to tsconfig.json file or project root (if tsconfig.json lives in the root)', cwd())
.option('-w, --webpack <config>', 'Optional path to webpack configuration file')
.option('-t, --template <path>', 'HTML template document', 'dist/index.html')
.option('-m, --module <path>', 'Path to root application module TypeScript file')
.option('-s, --symbol <identifier>', 'Class name of application root module')
.option('-o, --output <path>', 'Output path to write rendered HTML documents to', 'dist')
.option('-a, --application <application ID>', 'Optional application ID if your CLI configuration contains multiple apps')
.option('-b, --preboot [file or json]', 'Enable preboot and optionally read the preboot configuration from the specified JSON file (otherwise will use sensible defaults and automatically find the root element name)', null)
.option('-i, --ipc', 'Send rendered documents to parent process through IPC instead of writing them to disk', false)
.parse(process.argv);
};
Expand Down Expand Up @@ -133,4 +144,38 @@ const tsconfigFromRoot = (fromRoot: PathReference): FileReference => {
}

return null;
};

const getPrebootConfiguration = (filenameOrJson: string): PrebootConfiguration => {
if (filenameOrJson == null || filenameOrJson.length === 0) {
return null;
}

let options: PrebootConfiguration;

if (filenameOrJson.startsWith('{')) {
try {
options = fromJson<PrebootConfiguration>(filenameOrJson);
}
catch (exception) {
throw new ConfigurationException('Preboot configuration: invalid JSON document', exception);
}
}
else {
const file = absoluteFile(cwd(), filenameOrJson);

if (file.exists() === false || file.type() !== FileType.File) {
throw new ConfigurationException(`Preboot configuration file does not exist or is not a file: ${file.toString()}`);
}

options = fromJson<PrebootConfiguration>(file.content());
}

const validation = validatePrebootOptionsAgainstSchema(options);

if (validation.errors.length > 0) {
throw new ConfigurationException(`Preboot configuration ${filenameOrJson} is invalid: ${validation.toString()}`)
}

return options;
};
56 changes: 56 additions & 0 deletions source/bin/preboot.ts
@@ -0,0 +1,56 @@
import {PrebootConfiguration} from '../application/contracts';

import {JsonSchema, ValidatorResult} from '../transformation';

const prebootSchema = new JsonSchema({
$schema: 'http://json-schema.org/draft-04/schema#',
type: 'object',
oneOf: [
{required: ['appRoot']},
{required: ['serverClientRoot']}
],
properties: {
appRoot: {
anyOf: [
{type: 'string'},
{type: 'array', items: {type: 'string'}}
]
},
serverClientRoot: {
type: 'array',
items: {
type: 'object',
properties: {
clientSelector: {type: 'string'},
serverSelector: {type: 'string'}
},
},
},
buffer: {type: 'boolean'},
uglify: {type: 'boolean'},
noInlineCache: {type: 'boolean'},
eventSelectors: {
type: 'array',
items: {
type: 'object',
properties: {
selector: {type: 'string'},
events: {
type: 'array',
items: {type: 'string'}
},
keyCodes: {
type: 'array',
items: {type: 'number'},
},
preventDefault: {type: 'boolean'},
freeze: {type: 'boolean'},
noReplay: {type: 'boolean'}
}
},
},
}
});

export const validatePrebootOptionsAgainstSchema =
(configuration: PrebootConfiguration): ValidatorResult => prebootSchema.validate(configuration);
33 changes: 22 additions & 11 deletions source/bin/render.ts
@@ -1,26 +1,29 @@
import './vendor';

import chalk = require('chalk');

import {
ApplicationRenderer,
ApplicationBuilderFromSource,
Files,
HtmlOutput,
PathReference,
log,
pathFromString,
} from '../index';

import {commandLineToOptions} from './options';

const Module = require('module');

const options = commandLineToOptions();

patchModuleSearch(options.project.basePath);
patchModuleSearch(options.project.basePath, pathFromString(__dirname));

log.info(`Rendering application from source (working path: ${options.project.workingPath})`);

const builder = new ApplicationBuilderFromSource(options.project, options.templateDocument);

builder.preboot(options.preboot);

const application = builder.build();

const applicationRenderer = new ApplicationRenderer(application);
Expand All @@ -33,8 +36,8 @@ const execute = async () => {
}
catch (exception) {
const message = options.debug
? exception.stack
: exception.message + ' (use --debug to see a full stack trace)';
? chalk.red(exception.stack)
: chalk.red(exception.message) + ' (use --debug to see a full stack trace)';

log.error(`Failed to render application: ${message}`);

Expand All @@ -47,17 +50,25 @@ const execute = async () => {

execute();

function patchModuleSearch(root: PathReference) {
// Because we compile our outputs to a temporary path outside the filesystem structure of
// the project, we must tweak the module search paths to look inside the project node
// modules folder as well as our own modules folder. Otherwise we are going to encounter
// require exceptions when the application attempts to load libraries.
function patchModuleSearch(...roots: Array<PathReference>) {
const Module = require('module');

const paths = Module._nodeModulePaths;

const search = new Array<string>();

for (let iterator = root; iterator; iterator = iterator.parent()) {
const modules = iterator.findInAncestor(Files.modules);
if (modules == null) {
break;
for (const root of roots) {
for (let iterator = root; iterator; iterator = iterator.parent()) {
const modules = iterator.findInAncestor(Files.modules);
if (modules == null) {
break;
}
search.push(modules.toString());
}
search.push(modules.toString());
}

Module._nodeModulePaths = function (from) {
Expand Down
2 changes: 1 addition & 1 deletion source/snapshot/creator/preboot.ts
Expand Up @@ -14,4 +14,4 @@ export const injectPreboot = <M>(moduleRef: NgModuleRef<M>, vop: RenderVariantOp

injectIntoDocument(container.document, prebootImpl.getInlineCode(preboot));
}
};
};
20 changes: 18 additions & 2 deletions source/snapshot/orchestrate.ts
Expand Up @@ -3,11 +3,27 @@ import {ApplicationRef, NgModuleRef} from '@angular/core';
import {Subscription} from 'rxjs';

import {ConsoleLog} from './console';
import {ConsoleCollector, DocumentContainer, ExceptionCollector, waitForApplicationToBecomeStable, waitForRouterNavigation} from '../platform';

import {RenderVariantOperation} from '../application/operation';

import {Snapshot} from './snapshot';

import {timeouts} from '../static';
import {executeBootstrap, injectPreboot, injectState, transformDocument} from './creator';

import {
executeBootstrap,
injectPreboot,
injectState,
transformDocument
} from './creator';

import {
ConsoleCollector,
DocumentContainer,
ExceptionCollector,
waitForApplicationToBecomeStable,
waitForRouterNavigation
} from '../platform';

export const snapshot = async <M, V>(moduleRef: NgModuleRef<M>, vop: RenderVariantOperation<V>): Promise<Snapshot<V>> => {
const {variant, uri, transition, scope: {stateReader, bootstrappers, postprocessors}} = vop;
Expand Down
1 change: 1 addition & 0 deletions source/transformation/index.ts
@@ -1,4 +1,5 @@
export * from './array';
export * from './flatten';
export * from './json';
export * from './schema';
export * from './type-to-function';
15 changes: 15 additions & 0 deletions source/transformation/schema.ts
@@ -0,0 +1,15 @@
import {Schema, Validator, ValidatorResult} from 'jsonschema';

export {ValidatorResult};

export class JsonSchema {
constructor(private schema: Schema) {}

add(schema: Schema) {}

validate<T>(document: T): ValidatorResult {
const validator = new Validator();

return validator.validate(document, this.schema, {allowUnknownAttributes: false});
}
}

0 comments on commit 22b4cec

Please sign in to comment.