diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..5c2bb3a
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,14 @@
+# http://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+
+[*.md]
+max_line_length = 0
+trim_trailing_whitespace = false
\ No newline at end of file
diff --git a/bs-config.json b/bs-config.json
new file mode 100644
index 0000000..f74f19d
--- /dev/null
+++ b/bs-config.json
@@ -0,0 +1,9 @@
+{
+ "server": {
+ "baseDir": "src/demo",
+ "routes": {
+ "/node_modules": "node_modules",
+ "/spring-flo": "src/lib"
+ }
+ }
+}
diff --git a/build.js b/build.js
new file mode 100644
index 0000000..59bb5fe
--- /dev/null
+++ b/build.js
@@ -0,0 +1,157 @@
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const glob = require('glob');
+const camelCase = require('camelcase');
+const ngc = require('@angular/compiler-cli/src/main').main;
+const rollup = require('rollup');
+const uglify = require('rollup-plugin-uglify');
+const sourcemaps = require('rollup-plugin-sourcemaps');
+
+const inlineResources = require('./inline-resources');
+
+
+const libName = require('./package.json').name;
+const rootFolder = path.join(__dirname);
+const compilationFolder = path.join(rootFolder, 'out-tsc');
+const srcFolder = path.join(rootFolder, 'src/lib');
+const distFolder = path.join(rootFolder, 'dist');
+const tempLibFolder = path.join(compilationFolder, 'lib');
+const es5OutputFolder = path.join(compilationFolder, 'lib-es5');
+const es2015OutputFolder = path.join(compilationFolder, 'lib-es2015');
+
+return Promise.resolve()
+ // Copy library to temporary folder and inline html/css.
+ .then(() => _relativeCopy(`**/*`, srcFolder, tempLibFolder)
+ .then(() => inlineResources(tempLibFolder))
+ .then(() => console.log('Inlining succeeded.'))
+ )
+ // Compile to ES2015.
+ .then(() => ngc({ project: `${tempLibFolder}/tsconfig.lib.json` })
+ .then(exitCode => exitCode === 0 ? Promise.resolve() : Promise.reject())
+ .then(() => console.log('ES2015 compilation succeeded.'))
+ )
+ // Compile to ES5.
+ .then(() => ngc({ project: `${tempLibFolder}/tsconfig.es5.json` })
+ .then(exitCode => exitCode === 0 ? Promise.resolve() : Promise.reject())
+ .then(() => console.log('ES5 compilation succeeded.'))
+ )
+ // Copy typings and metadata to `dist/` folder.
+ .then(() => Promise.resolve()
+ .then(() => _relativeCopy('**/*.d.ts', es2015OutputFolder, distFolder))
+ .then(() => _relativeCopy('**/*.metadata.json', es2015OutputFolder, distFolder))
+ .then(() => console.log('Typings and metadata copy succeeded.'))
+ )
+ // Bundle lib.
+ .then(() => {
+ // Base configuration.
+ const es5Entry = path.join(es5OutputFolder, `${libName}.js`);
+ const es2015Entry = path.join(es2015OutputFolder, `${libName}.js`);
+ const rollupBaseConfig = {
+ name: camelCase(libName),
+ sourcemap: true,
+ // ATTENTION:
+ // Add any dependency or peer dependency your library to `globals` and `external`.
+ // This is required for UMD bundle users.
+ globals: {
+ // The key here is library name, and the value is the the name of the global variable name
+ // the window object.
+ // See https://github.com/rollup/rollup/wiki/JavaScript-API#globals for more.
+ '@angular/core': 'ng.core',
+ 'jquery': '$',
+ 'lodash': '_'
+ },
+ external: [
+ // List of dependencies
+ // See https://github.com/rollup/rollup/wiki/JavaScript-API#external for more.
+ '@angular/core',
+ '@angular/forms',
+ '@angular/platform-browser',
+ 'codemirror',
+ 'jointjs',
+ 'lodash',
+ 'ts-disposables'
+ ],
+ plugins: [
+ sourcemaps()
+ ]
+ };
+
+ // UMD bundle.
+ const umdConfig = Object.assign({}, rollupBaseConfig, {
+ input: es5Entry,
+ file: path.join(distFolder, `bundles`, `${libName}.umd.js`),
+ format: 'umd',
+ });
+
+ // Minified UMD bundle.
+ const minifiedUmdConfig = Object.assign({}, rollupBaseConfig, {
+ input: es5Entry,
+ file: path.join(distFolder, `bundles`, `${libName}.umd.min.js`),
+ format: 'umd',
+ plugins: rollupBaseConfig.plugins.concat([uglify({})])
+ });
+
+ // ESM+ES5 flat module bundle.
+ const fesm5config = Object.assign({}, rollupBaseConfig, {
+ input: es5Entry,
+ file: path.join(distFolder, `${libName}.es5.js`),
+ format: 'es'
+ });
+
+ // ESM+ES2015 flat module bundle.
+ const fesm2015config = Object.assign({}, rollupBaseConfig, {
+ input: es2015Entry,
+ file: path.join(distFolder, `${libName}.js`),
+ format: 'es'
+ });
+
+ const allBundles = [
+ umdConfig,
+ minifiedUmdConfig,
+ fesm5config,
+ fesm2015config
+ ].map(cfg => rollup.rollup(cfg).then(bundle => bundle.write(cfg)));
+
+ return Promise.all(allBundles)
+ .then(() => console.log('All bundles generated successfully.'))
+ })
+ // Copy package files
+ .then(() => Promise.resolve()
+ .then(() => _relativeCopy('LICENSE', rootFolder, distFolder))
+ .then(() => _relativeCopy('package.json', rootFolder, distFolder))
+ .then(() => _relativeCopy('README.md', rootFolder, distFolder))
+ .then(() => console.log('Package files copy succeeded.'))
+ )
+ .catch(e => {
+ console.error('\Build failed. See below for errors.\n');
+ console.error(e);
+ process.exit(1);
+ });
+
+
+// Copy files maintaining relative paths.
+function _relativeCopy(fileGlob, from, to) {
+ return new Promise((resolve, reject) => {
+ glob(fileGlob, { cwd: from, nodir: true }, (err, files) => {
+ if (err) reject(err);
+ files.forEach(file => {
+ const origin = path.join(from, file);
+ const dest = path.join(to, file);
+ const data = fs.readFileSync(origin, 'utf-8');
+ _recursiveMkDir(path.dirname(dest));
+ fs.writeFileSync(dest, data);
+ resolve();
+ })
+ })
+ });
+}
+
+// Recursively create a dir.
+function _recursiveMkDir(dir) {
+ if (!fs.existsSync(dir)) {
+ _recursiveMkDir(path.dirname(dir));
+ fs.mkdirSync(dir);
+ }
+}
diff --git a/inline-resources.js b/inline-resources.js
new file mode 100644
index 0000000..d44f46b
--- /dev/null
+++ b/inline-resources.js
@@ -0,0 +1,119 @@
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const glob = require('glob');
+
+
+/**
+ * Simple Promiseify function that takes a Node API and return a version that supports promises.
+ * We use promises instead of synchronized functions to make the process less I/O bound and
+ * faster. It also simplifies the code.
+ */
+function promiseify(fn) {
+ return function () {
+ const args = [].slice.call(arguments, 0);
+ return new Promise((resolve, reject) => {
+ fn.apply(this, args.concat([function (err, value) {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(value);
+ }
+ }]));
+ });
+ };
+}
+
+const readFile = promiseify(fs.readFile);
+const writeFile = promiseify(fs.writeFile);
+
+/**
+ * Inline resources in a tsc/ngc compilation.
+ * @param projectPath {string} Path to the project.
+ */
+function inlineResources(projectPath) {
+
+ // Match only TypeScript files in projectPath.
+ const files = glob.sync('**/*.ts', {cwd: projectPath});
+
+ // For each file, inline the templates and styles under it and write the new file.
+ return Promise.all(files.map(filePath => {
+ const fullFilePath = path.join(projectPath, filePath);
+ return readFile(fullFilePath, 'utf-8')
+ .then(content => inlineResourcesFromString(content, url => {
+ // Resolve the template url.
+ return path.join(path.dirname(fullFilePath), url);
+ }))
+ .then(content => writeFile(fullFilePath, content))
+ .catch(err => {
+ console.error('An error occured: ', err);
+ });
+ }));
+}
+
+/**
+ * Inline resources from a string content.
+ * @param content {string} The source file's content.
+ * @param urlResolver {Function} A resolver that takes a URL and return a path.
+ * @returns {string} The content with resources inlined.
+ */
+function inlineResourcesFromString(content, urlResolver) {
+ // Curry through the inlining functions.
+ return [
+ inlineTemplate,
+ inlineStyle
+ ].reduce((content, fn) => fn(content, urlResolver), content);
+}
+
+/**
+ * Inline the templates for a source file. Simply search for instances of `templateUrl: ...` and
+ * replace with `template: ...` (with the content of the file included).
+ * @param content {string} The source file's content.
+ * @param urlResolver {Function} A resolver that takes a URL and return a path.
+ * @return {string} The content with all templates inlined.
+ */
+function inlineTemplate(content, urlResolver) {
+ return content.replace(/templateUrl:\s*'([^']+?\.html)'/g, function (m, templateUrl) {
+ const templateFile = urlResolver(templateUrl);
+ const templateContent = fs.readFileSync(templateFile, 'utf-8');
+ const shortenedTemplate = templateContent
+ .replace(/([\n\r]\s*)+/gm, ' ')
+ .replace(/"/g, '\\"');
+ return `template: "${shortenedTemplate}"`;
+ });
+}
+
+
+/**
+ * Inline the styles for a source file. Simply search for instances of `styleUrls: [...]` and
+ * replace with `styles: [...]` (with the content of the file included).
+ * @param urlResolver {Function} A resolver that takes a URL and return a path.
+ * @param content {string} The source file's content.
+ * @return {string} The content with all styles inlined.
+ */
+function inlineStyle(content, urlResolver) {
+ return content.replace(/styleUrls:\s*(\[[\s\S]*?\])/gm, function (m, styleUrls) {
+ const urls = eval(styleUrls);
+ return 'styles: ['
+ + urls.map(styleUrl => {
+ const styleFile = urlResolver(styleUrl);
+ const styleContent = fs.readFileSync(styleFile, 'utf-8');
+ const shortenedStyle = styleContent
+ .replace(/([\n\r]\s*)+/gm, ' ')
+ .replace(/"/g, '\\"');
+ return `"${shortenedStyle}"`;
+ })
+ .join(',\n')
+ + ']';
+ });
+}
+
+module.exports = inlineResources;
+module.exports.inlineResourcesFromString = inlineResourcesFromString;
+
+// Run inlineResources if module is being called directly from the CLI with arguments.
+if (require.main === module && process.argv.length > 2) {
+ console.log('Inlining resources from project:', process.argv[2]);
+ return inlineResources(process.argv[2]);
+}
diff --git a/integration/.gitignore b/integration/.gitignore
new file mode 100644
index 0000000..1f47b98
--- /dev/null
+++ b/integration/.gitignore
@@ -0,0 +1,10 @@
+node_modules
+npm-debug.log
+src/**/*.js
+!src/systemjs.config.js
+!src/systemjs-angular-loader.js
+*.js.map
+e2e/**/*.js
+e2e/**/*.js.map
+out-tsc/*
+dist/*
diff --git a/integration/README.md b/integration/README.md
new file mode 100644
index 0000000..58c845d
--- /dev/null
+++ b/integration/README.md
@@ -0,0 +1,26 @@
+# Integration App
+
+This is a simplified version of https://github.com/angular/quickstart used to test the built lib.
+
+## npm scripts
+
+We've captured many of the most useful commands in npm scripts defined in the `package.json`:
+
+* `npm start` - runs the compiler and a server at the same time, both in "watch mode".
+* `npm run e2e` - compiles the app and run e2e tests.
+* `npm run e2e:aot` - compiles and the app with AOT and run e2e tests.
+
+
+If you need to manually test a library build, follow these steps:
+```
+# starting at the project root, build the library
+npm run build
+# clean the integration app
+npm run preintegration
+cd integration
+npm install
+```
+
+Now the library is installed in your integration app.
+
+You can use `npm start` to start a live reload server running the app in JIT mode, or `npm run build && npm run serve:aot` to run a static server in AOT mode.
diff --git a/integration/bs-config.aot.json b/integration/bs-config.aot.json
new file mode 100644
index 0000000..01f2e4a
--- /dev/null
+++ b/integration/bs-config.aot.json
@@ -0,0 +1,5 @@
+{
+ "server": {
+ "baseDir": "dist"
+ }
+}
diff --git a/integration/bs-config.e2e-aot.json b/integration/bs-config.e2e-aot.json
new file mode 100644
index 0000000..ac61d35
--- /dev/null
+++ b/integration/bs-config.e2e-aot.json
@@ -0,0 +1,11 @@
+{
+ "open": false,
+ "logLevel": "silent",
+ "port": 8080,
+ "server": {
+ "baseDir": "dist",
+ "middleware": {
+ "0": null
+ }
+ }
+}
diff --git a/integration/bs-config.e2e.json b/integration/bs-config.e2e.json
new file mode 100644
index 0000000..24570db
--- /dev/null
+++ b/integration/bs-config.e2e.json
@@ -0,0 +1,14 @@
+{
+ "open": false,
+ "logLevel": "silent",
+ "port": 8080,
+ "server": {
+ "baseDir": "src",
+ "routes": {
+ "/node_modules": "node_modules"
+ },
+ "middleware": {
+ "0": null
+ }
+ }
+}
diff --git a/integration/bs-config.json b/integration/bs-config.json
new file mode 100644
index 0000000..4e58595
--- /dev/null
+++ b/integration/bs-config.json
@@ -0,0 +1,8 @@
+{
+ "server": {
+ "baseDir": "src",
+ "routes": {
+ "/node_modules": "node_modules"
+ }
+ }
+}
diff --git a/integration/build.js b/integration/build.js
new file mode 100644
index 0000000..61dafe5
--- /dev/null
+++ b/integration/build.js
@@ -0,0 +1,93 @@
+const fs = require('fs');
+const path = require('path');
+const glob = require('glob');
+const rollup = require('rollup');
+const uglify = require('rollup-plugin-uglify');
+const commonjs = require('rollup-plugin-commonjs');
+const nodeResolve = require('rollup-plugin-node-resolve');
+const ngc = require('@angular/compiler-cli/src/main').main;
+
+
+const srcDir = path.join(__dirname, 'src/');
+const distDir = path.join(__dirname, 'dist/');
+const aotDir = path.join(__dirname, 'aot/');
+const rollupConfig = {
+ entry: `${srcDir}/main-aot.js`,
+ sourceMap: false,
+ format: 'iife',
+ onwarn: function (warning) {
+ // Skip certain warnings
+ if (warning.code === 'THIS_IS_UNDEFINED') { return; }
+ // console.warn everything else
+ console.warn(warning.message);
+ },
+ plugins: [
+ nodeResolve({ jsnext: true, module: true }),
+ commonjs({
+ include: ['node_modules/rxjs/**']
+ }),
+ uglify()
+ ]
+};
+
+return Promise.resolve()
+ // Compile using ngc.
+ .then(() => ngc({ project: `./tsconfig.aot.json` }))
+ // Create dist dir.
+ .then(() => _recursiveMkDir(distDir))
+ // Copy files.
+ .then(() => {
+ // Copy and rename index-aot.html.
+ fs.createReadStream(path.join(srcDir, 'index-aot.html'))
+ .pipe(fs.createWriteStream(path.join(distDir, 'index.html')));
+
+ // Copy global stylesheets, images, etc.
+ const assets = [
+ 'favicon.ico',
+ 'styles.css'
+ ];
+
+ return Promise.all(assets.map(asset => _relativeCopy(asset, srcDir, distDir)));
+ })
+ // Bundle app.
+ .then(() => rollup.rollup(rollupConfig))
+ // Concatenate app and scripts.
+ .then(bundle => {
+ const appBundle = bundle.generate(rollupConfig);
+
+ const scripts = [
+ 'node_modules/core-js/client/shim.min.js',
+ 'node_modules/zone.js/dist/zone.min.js'
+ ];
+
+ let concatenatedScripts = scripts.map((script) => {
+ return fs.readFileSync(path.join(__dirname, script)).toString();
+ }).join('\n;');
+
+ concatenatedScripts = concatenatedScripts.concat('\n;', appBundle.code);
+
+ fs.writeFileSync(path.join(distDir, 'bundle.js'), concatenatedScripts);
+ });
+
+
+
+// Copy files maintaining relative paths.
+function _relativeCopy(fileGlob, from, to) {
+ return glob(fileGlob, { cwd: from, nodir: true }, (err, files) => {
+ if (err) throw err;
+ files.forEach(file => {
+ const origin = path.join(from, file);
+ const dest = path.join(to, file);
+ _recursiveMkDir(path.dirname(dest));
+ fs.createReadStream(origin).pipe(fs.createWriteStream(dest));
+ })
+ })
+}
+
+// Recursively create a dir.
+function _recursiveMkDir(dir) {
+ if (!fs.existsSync(dir)) {
+ _recursiveMkDir(path.dirname(dir));
+ fs.mkdirSync(dir);
+ }
+}
diff --git a/integration/e2e/app.e2e-spec.d.ts b/integration/e2e/app.e2e-spec.d.ts
new file mode 100644
index 0000000..e69de29
diff --git a/integration/e2e/app.e2e-spec.ts b/integration/e2e/app.e2e-spec.ts
new file mode 100644
index 0000000..f2340b1
--- /dev/null
+++ b/integration/e2e/app.e2e-spec.ts
@@ -0,0 +1,21 @@
+import { browser, element, by } from 'protractor';
+
+describe('QuickStart Lib E2E Tests', function () {
+
+ beforeEach(() => browser.get(''));
+
+ afterEach(() => {
+ browser.manage().logs().get('browser').then((browserLog: any[]) => {
+ expect(browserLog).toEqual([]);
+ });
+ });
+
+ it('should display lib', () => {
+ expect(element(by.css('h2')).getText()).toEqual('Hello Angular Library');
+ });
+
+ it('should display meaning', () => {
+ expect(element(by.css('h3')).getText()).toEqual('Meaning is: 42');
+ });
+
+});
diff --git a/integration/e2e/tsconfig.json b/integration/e2e/tsconfig.json
new file mode 100644
index 0000000..2c7260d
--- /dev/null
+++ b/integration/e2e/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "sourceMap": true,
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "lib": [ "es2015", "dom" ],
+ "noImplicitAny": true,
+ "suppressImplicitAnyIndexErrors": true
+ }
+}
diff --git a/integration/package.json b/integration/package.json
new file mode 100644
index 0000000..2dc0be7
--- /dev/null
+++ b/integration/package.json
@@ -0,0 +1,55 @@
+{
+ "name": "integration-test",
+ "version": "1.0.0",
+ "description": "App for integration tests",
+ "scripts": {
+ "clean": "rimraf aot/ dist/ node_modules/spring-flo/",
+ "build": "tsc -p src/",
+ "build:watch": "tsc -p src/ -w",
+ "build:e2e": "tsc -p e2e/",
+ "build:aot": "node build.js",
+ "serve": "lite-server -c=bs-config.json",
+ "serve:aot": "lite-server -c bs-config.aot.json",
+ "serve:e2e": "lite-server -c=bs-config.e2e.json",
+ "serve:e2e-aot": "lite-server -c bs-config.e2e-aot.json",
+ "prestart": "npm run build",
+ "start": "concurrently \"npm run build:watch\" \"npm run serve\"",
+ "pree2e": "npm run build:e2e && npm run build",
+ "e2e": "concurrently \"npm run serve:e2e\" \"npm run protractor\" --kill-others --success first",
+ "pree2e:aot": "npm run build:e2e && npm run build:aot",
+ "e2e:aot": "concurrently \"npm run serve:e2e-aot\" \"npm run protractor\" --kill-others --success first",
+ "preprotractor": "webdriver-manager update",
+ "protractor": "protractor protractor.config.js"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "MIT",
+ "dependencies": {
+ "@angular/common": "^4.1.3",
+ "@angular/compiler": "^4.1.3",
+ "@angular/compiler-cli": "^4.1.3",
+ "@angular/core": "^4.1.3",
+ "@angular/platform-browser": "^4.1.3",
+ "@angular/platform-browser-dynamic": "^4.1.3",
+ "spring-flo": "../dist/",
+ "core-js": "^2.4.1",
+ "rxjs": "5.0.1",
+ "systemjs": "0.19.40",
+ "zone.js": "^0.8.4"
+ },
+ "devDependencies": {
+ "@types/jasmine": "2.5.36",
+ "concurrently": "^3.4.0",
+ "jasmine-core": "~2.4.1",
+ "glob": "^7.1.1",
+ "lite-server": "^2.2.2",
+ "protractor": "~5.1.0",
+ "rimraf": "^2.5.4",
+ "rollup": "^0.42.0",
+ "rollup-plugin-commonjs": "^8.0.2",
+ "rollup-plugin-node-resolve": "3.0.0",
+ "rollup-plugin-uglify": "^2.0.1",
+ "typescript": "~2.3.0"
+ },
+ "repository": {}
+}
diff --git a/integration/protractor.config.js b/integration/protractor.config.js
new file mode 100644
index 0000000..856d4a9
--- /dev/null
+++ b/integration/protractor.config.js
@@ -0,0 +1,12 @@
+exports.config = {
+ allScriptsTimeout: 11000,
+ specs: [
+ './e2e/**/*.e2e-spec.js'
+ ],
+ capabilities: {
+ 'browserName': 'chrome'
+ },
+ directConnect: true,
+ baseUrl: 'http://localhost:8080/',
+ framework: 'jasmine'
+};
diff --git a/integration/src/app/app.component.d.ts b/integration/src/app/app.component.d.ts
new file mode 100644
index 0000000..90c492b
--- /dev/null
+++ b/integration/src/app/app.component.d.ts
@@ -0,0 +1,5 @@
+import { LibService } from 'spring-flo';
+export declare class AppComponent {
+ meaning: number;
+ constructor(libService: LibService);
+}
diff --git a/integration/src/app/app.component.html b/integration/src/app/app.component.html
new file mode 100644
index 0000000..a92482c
--- /dev/null
+++ b/integration/src/app/app.component.html
@@ -0,0 +1,2 @@
+
+
Meaning is: {{meaning}}
diff --git a/integration/src/app/app.component.ts b/integration/src/app/app.component.ts
new file mode 100644
index 0000000..eff1fd4
--- /dev/null
+++ b/integration/src/app/app.component.ts
@@ -0,0 +1,13 @@
+import { Component } from '@angular/core';
+import { LibService } from 'spring-flo';
+
+@Component({
+ selector: 'integration-app',
+ templateUrl: './app.component.html',
+})
+export class AppComponent {
+ meaning: number;
+ constructor(libService: LibService) {
+ this.meaning = libService.getMeaning();
+ }
+}
diff --git a/integration/src/app/app.module.d.ts b/integration/src/app/app.module.d.ts
new file mode 100644
index 0000000..09cdb35
--- /dev/null
+++ b/integration/src/app/app.module.d.ts
@@ -0,0 +1,2 @@
+export declare class AppModule {
+}
diff --git a/integration/src/app/app.module.ts b/integration/src/app/app.module.ts
new file mode 100644
index 0000000..f9077c1
--- /dev/null
+++ b/integration/src/app/app.module.ts
@@ -0,0 +1,12 @@
+import { NgModule } from '@angular/core';
+import { BrowserModule } from '@angular/platform-browser';
+import { LibModule } from 'spring-flo';
+
+import { AppComponent } from './app.component';
+
+@NgModule({
+ imports: [ BrowserModule, LibModule],
+ declarations: [ AppComponent ],
+ bootstrap: [ AppComponent ]
+})
+export class AppModule { }
diff --git a/integration/src/favicon.ico b/integration/src/favicon.ico
new file mode 100644
index 0000000..8081c7c
Binary files /dev/null and b/integration/src/favicon.ico differ
diff --git a/integration/src/index-aot.html b/integration/src/index-aot.html
new file mode 100644
index 0000000..3e9117b
--- /dev/null
+++ b/integration/src/index-aot.html
@@ -0,0 +1,18 @@
+
+
+
+ Angular QuickStart
+
+
+
+
+
+
+
+
+
+
+ Loading...
+
+
+
diff --git a/integration/src/index.html b/integration/src/index.html
new file mode 100644
index 0000000..65e0079
--- /dev/null
+++ b/integration/src/index.html
@@ -0,0 +1,25 @@
+
+
+
+ Angular QuickStart
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Loading AppComponent content here ...
+
+
diff --git a/integration/src/main-aot.d.ts b/integration/src/main-aot.d.ts
new file mode 100644
index 0000000..e69de29
diff --git a/integration/src/main-aot.ts b/integration/src/main-aot.ts
new file mode 100644
index 0000000..09b2210
--- /dev/null
+++ b/integration/src/main-aot.ts
@@ -0,0 +1,5 @@
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { AppModuleNgFactory } from '../out-tsc/src/app/app.module.ngfactory';
+
+platformBrowserDynamic().bootstrapModuleFactory(AppModuleNgFactory);
diff --git a/integration/src/main.d.ts b/integration/src/main.d.ts
new file mode 100644
index 0000000..e69de29
diff --git a/integration/src/main.ts b/integration/src/main.ts
new file mode 100644
index 0000000..311c44b
--- /dev/null
+++ b/integration/src/main.ts
@@ -0,0 +1,5 @@
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { AppModule } from './app/app.module';
+
+platformBrowserDynamic().bootstrapModule(AppModule);
diff --git a/integration/src/styles.css b/integration/src/styles.css
new file mode 100644
index 0000000..58e1a7d
--- /dev/null
+++ b/integration/src/styles.css
@@ -0,0 +1,5 @@
+h1 {
+ color: #369;
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 250%;
+}
diff --git a/integration/src/systemjs-angular-loader.js b/integration/src/systemjs-angular-loader.js
new file mode 100644
index 0000000..38e5cc5
--- /dev/null
+++ b/integration/src/systemjs-angular-loader.js
@@ -0,0 +1,49 @@
+var templateUrlRegex = /templateUrl\s*:(\s*['"`](.*?)['"`]\s*)/gm;
+var stylesRegex = /styleUrls *:(\s*\[[^\]]*?\])/g;
+var stringRegex = /(['`"])((?:[^\\]\\\1|.)*?)\1/g;
+
+module.exports.translate = function (load) {
+ if (load.source.indexOf('moduleId') != -1) return load;
+
+ var url = document.createElement('a');
+ url.href = load.address;
+
+ var basePathParts = url.pathname.split('/');
+
+ basePathParts.pop();
+ var basePath = basePathParts.join('/');
+
+ var baseHref = document.createElement('a');
+ baseHref.href = this.baseURL;
+ baseHref = baseHref.pathname;
+
+ if (!baseHref.startsWith('/base/')) { // it is not karma
+ basePath = basePath.replace(baseHref, '');
+ }
+
+ load.source = load.source
+ .replace(templateUrlRegex, function (match, quote, url) {
+ let resolvedUrl = url;
+
+ if (url.startsWith('.')) {
+ resolvedUrl = basePath + url.substr(1);
+ }
+
+ return 'templateUrl: "' + resolvedUrl + '"';
+ })
+ .replace(stylesRegex, function (match, relativeUrls) {
+ var urls = [];
+
+ while ((match = stringRegex.exec(relativeUrls)) !== null) {
+ if (match[2].startsWith('.')) {
+ urls.push('"' + basePath + match[2].substr(1) + '"');
+ } else {
+ urls.push('"' + match[2] + '"');
+ }
+ }
+
+ return "styleUrls: [" + urls.join(', ') + "]";
+ });
+
+ return load;
+};
diff --git a/integration/src/systemjs.config.js b/integration/src/systemjs.config.js
new file mode 100644
index 0000000..83aa958
--- /dev/null
+++ b/integration/src/systemjs.config.js
@@ -0,0 +1,46 @@
+/**
+ * System configuration for Angular samples
+ * Adjust as necessary for your application needs.
+ */
+(function (global) {
+ System.config({
+ paths: {
+ // paths serve as alias
+ 'npm:': 'node_modules/'
+ },
+ // map tells the System loader where to look for things
+ map: {
+ // our app is within the app folder
+ app: 'app',
+
+ // angular bundles
+ '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
+ '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
+ '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
+ '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
+ '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
+ '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
+ '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
+ '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
+
+ // other libraries
+ 'rxjs': 'npm:rxjs',
+ 'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js',
+ 'spring-flo': 'npm:spring-flo/bundles/spring-flo.umd.js'
+ },
+ // packages tells the System loader how to load when no filename and/or no extension
+ packages: {
+ app: {
+ defaultExtension: 'js',
+ meta: {
+ './*.js': {
+ loader: 'systemjs-angular-loader.js'
+ }
+ }
+ },
+ rxjs: {
+ defaultExtension: 'js'
+ }
+ }
+ });
+})(this);
diff --git a/integration/src/tsconfig.json b/integration/src/tsconfig.json
new file mode 100644
index 0000000..ca12633
--- /dev/null
+++ b/integration/src/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "sourceMap": true,
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "lib": [ "es2015", "dom" ],
+ "noImplicitAny": true,
+ "suppressImplicitAnyIndexErrors": true
+ },
+ "exclude": [
+ "main-aot.ts"
+ ]
+}
diff --git a/integration/tsconfig.aot.json b/integration/tsconfig.aot.json
new file mode 100644
index 0000000..6f608e9
--- /dev/null
+++ b/integration/tsconfig.aot.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "module": "es2015",
+ "moduleResolution": "node",
+ "sourceMap": true,
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "lib": [
+ "es2015",
+ "dom"
+ ],
+ "noImplicitAny": true,
+ "suppressImplicitAnyIndexErrors": true
+ },
+ "files": [
+ "src/app/app.module.ts",
+ "src/main-aot.ts"
+ ],
+ "angularCompilerOptions": {
+ "genDir": "out-tsc",
+ "skipMetadataEmit": true
+ }
+}
diff --git a/karma-test-shim.js b/karma-test-shim.js
new file mode 100644
index 0000000..ac3a3fd
--- /dev/null
+++ b/karma-test-shim.js
@@ -0,0 +1,107 @@
+// /*global jasmine, __karma__, window*/
+Error.stackTraceLimit = 0; // "No stacktrace"" is usually best for testing.
+
+// Uncomment to get full stacktrace output. Sometimes helpful, usually not.
+// Error.stackTraceLimit = Infinity; //
+
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;
+
+// builtPaths: root paths for output ("built") files
+// get from karma.config.js, then prefix with '/base/' (default is 'src/')
+var builtPaths = (__karma__.config.builtPaths || ['src/'])
+ .map(function(p) { return '/base/'+p;});
+
+__karma__.loaded = function () { };
+
+function isJsFile(path) {
+ return path.slice(-3) == '.js';
+}
+
+function isSpecFile(path) {
+ return /\.spec\.(.*\.)?js$/.test(path);
+}
+
+// Is a "built" file if is JavaScript file in one of the "built" folders
+function isBuiltFile(path) {
+ return isJsFile(path) &&
+ builtPaths.reduce(function(keep, bp) {
+ return keep || (path.substr(0, bp.length) === bp);
+ }, false);
+}
+
+var allSpecFiles = Object.keys(window.__karma__.files)
+ .filter(isSpecFile)
+ .filter(isBuiltFile);
+
+System.config({
+ paths: {
+ // paths serve as alias
+ 'npm:': 'node_modules/'
+ },
+ // Base URL for System.js calls. 'base/' is where Karma serves files from.
+ baseURL: 'base/src/lib',
+ // Extend usual application package list with test folder
+ packages: {
+ rxjs: { defaultExtension: 'js' },
+ '': { defaultExtension: 'js' },
+ src: {
+ defaultExtension: 'js',
+ meta: {
+ './*.js': {
+ loader: 'system-loader'
+ }
+ }
+ }
+ },
+ // Map the angular umd bundles
+ map: {
+ 'system-loader': 'demo/systemjs-angular-loader.js',
+ '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
+ '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
+ '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
+ '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
+ '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
+ '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
+ '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
+ '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
+ // Testing bundles
+ '@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js',
+ '@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js',
+ '@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js',
+ '@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js',
+ '@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js',
+ '@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js',
+ '@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js',
+ '@angular/forms/testing': 'npm:@angular/forms/bundles/forms-testing.umd.js',
+ 'rxjs': 'npm:rxjs',
+ 'src': 'src'
+ }
+});
+
+initTestBed().then(initTesting);
+
+function initTestBed(){
+ return Promise.all([
+ System.import('@angular/core/testing'),
+ System.import('@angular/platform-browser-dynamic/testing')
+ ])
+
+ .then(function (providers) {
+ var coreTesting = providers[0];
+ var browserTesting = providers[1];
+
+ coreTesting.TestBed.initTestEnvironment(
+ browserTesting.BrowserDynamicTestingModule,
+ browserTesting.platformBrowserDynamicTesting());
+ })
+}
+
+// Import all spec files and start karma
+function initTesting () {
+ return Promise.all(
+ allSpecFiles.map(function (moduleName) {
+ return System.import(moduleName);
+ })
+ )
+ .then(__karma__.start, __karma__.error);
+}
diff --git a/src/demo/app/app.component.css b/src/demo/app/app.component.css
new file mode 100644
index 0000000..7bde748
--- /dev/null
+++ b/src/demo/app/app.component.css
@@ -0,0 +1,191 @@
+.flow-view {
+ height: 100%;
+}
+
+.header {
+ font-weight: 400;
+ font-family: "Varela Round",sans-serif;
+ font-size: 36px;
+ color: #eeeeee;
+ padding: 2px;
+ background-color: #34302d;
+ border: none;
+ border-top: 4px solid #6db33f;
+ z-index: 1;
+}
+
+body {
+ background-color: #eeeeee;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -o-user-select: none;
+ user-select: none;
+}
+
+.control-button {
+ width: 16px;
+ height: 16px;
+}
+
+.header-small {
+ font: 300 24px "Helvetica Neue";
+}
+
+pre {
+ font-size: 18px;
+}
+
+.border-selected {
+ stroke: #34302d;
+ stroke-width: 3;
+}
+
+.controls {
+ border-radius: 2px;
+ border: solid;
+ border-color: #6db33f;
+ padding: 5px;
+ margin-top: 3px;
+ background-color: #eeeeee;
+ border-width: 1px;
+}
+
+.button {
+ background-color: #34302d;
+ background-image: none;
+ border-radius: 2px;
+ color: #f1f1f1;
+ font-size: 14px;
+ line-height: 14px;
+ font-family: Montserrat,sans-serif;
+ border: 2px solid #6db33f;
+ padding: 5px 20px;
+ text-shadow: none;
+}
+
+.button span {
+ background-color: #34302d;
+ background-image: none;
+ border-radius: 2px;
+ color: #f1f1f1;
+ font-size: 14px;
+ line-height: 14px;
+ font-family: Montserrat,sans-serif;
+ border: 2px solid #6db33f;
+ padding: 5px 20px;
+ text-shadow: none;
+}
+
+.button input {
+ background-color: #34302d;
+ background-image: none;
+ color: #f1f1f1;
+ font-size: 14px;
+ font-family: Montserrat,sans-serif;
+ text-shadow: none;
+ border: 0px;
+ text-align:right;
+}
+
+.button input[type=range] {
+ display: inline;
+ width: 100px;
+}
+
+button.on {
+ background-color: #5fa134;
+}
+
+.flow-definition-container {
+ border: 1px solid;
+ border-color: #6db33f;
+ border-radius: 2px;
+ margin-top: 3px;
+ background-color: #ffffff;
+ font-family: monospace;
+ z-index: 2;
+ width:100%;
+ height:100px;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+textarea:input {
+ outline: none;
+ border: 1px solid #6db33f;
+}
+
+textarea:input:focus {
+ outline: none;
+ border: 1px solid #000000;
+}
+
+.flow-definition {
+ border: 5px;
+ height:100%;
+ width:100%;
+ font-size: 16px;
+ resize: none;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+/* The text label on the nodes */
+.label {
+ font-family: 'Lucida Console';
+ font-size: 12px;
+}
+
+/* The class for the 'icon/unicode_char' on the nodes */
+.label2 {
+ font-size: 18px;
+}
+
+[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
+ display: none !important;
+}
+
+#flo-container {
+ height: 800px;
+}
+
+
+/* Modules DnD shape animation */
+
+@keyframes flash-stroke-width {
+ 0% { stroke-width: 1; }
+ 50% { stroke-width: 4; }
+ 100% { stroke-width: 1; }
+}
+
+/*.dnd-source-feedback.joint-element .box {*/
+ /*animation: flash-stroke-width 2s linear infinite;*/
+/*}*/
+.dnd-target-feedback.joint-element .box {
+ animation: flash-stroke-width 2s linear infinite;
+}
+
+/* Ports DnD animation */
+.dnd-source-feedback.input-port {
+ animation: flash-stroke-width 2s linear infinite;
+}
+.dnd-source-feedback.output-port {
+ animation: flash-stroke-width 2s linear infinite;
+}
+.dnd-target-feedback.input-port {
+ animation: flash-stroke-width 2s linear infinite;
+}
+.dnd-target-feedback.output-port {
+ animation: flash-stroke-width 2s linear infinite;
+}
+
+/* Links DnD feedback animation */
+.dnd-source-feedback .connection {
+ animation: flash-stroke-width 2s linear infinite;
+}
+.dnd-target-feedback .connection {
+ animation: flash-stroke-width 2s linear infinite;
+}
diff --git a/src/demo/app/app.component.d.ts b/src/demo/app/app.component.d.ts
new file mode 100644
index 0000000..310c027
--- /dev/null
+++ b/src/demo/app/app.component.d.ts
@@ -0,0 +1,14 @@
+import { Flo } from 'spring-flo';
+import { BsModalService } from 'ngx-bootstrap';
+export declare class AppComponent {
+ private modelService;
+ metamodel: Flo.Metamodel;
+ renderer: Flo.Renderer;
+ editor: Flo.Editor;
+ dsl: string;
+ dslEditor: boolean;
+ private editorContext;
+ paletteSize: number;
+ constructor(modelService: BsModalService);
+ arrangeAll(): void;
+}
diff --git a/src/demo/app/app.component.html b/src/demo/app/app.component.html
new file mode 100644
index 0000000..7fe3ec6
--- /dev/null
+++ b/src/demo/app/app.component.html
@@ -0,0 +1,41 @@
+
+
+
+
diff --git a/src/demo/app/app.component.ts b/src/demo/app/app.component.ts
new file mode 100644
index 0000000..b673ebc
--- /dev/null
+++ b/src/demo/app/app.component.ts
@@ -0,0 +1,37 @@
+import { Component, ViewEncapsulation } from '@angular/core';
+import { NgModel } from '@angular/forms';
+import { Flo } from 'spring-flo';
+import { BsModalService } from 'ngx-bootstrap';
+const { Metamodel } = require('./metamodel');
+const { Renderer } = require('./renderer');
+const { Editor } = require('./editor');
+
+@Component({
+ selector: 'demo-app',
+ templateUrl: './app.component.html',
+ styleUrls: [ './app.component.css' ],
+ encapsulation: ViewEncapsulation.None
+})
+export class AppComponent {
+
+ metamodel : Flo.Metamodel;
+ renderer : Flo.Renderer;
+ editor : Flo.Editor;
+ dsl : string;
+ dslEditor = false;
+
+ private editorContext : Flo.EditorContext;
+
+ paletteSize = 170;
+
+ constructor(private modelService : BsModalService) {
+ this.metamodel = new Metamodel();
+ this.renderer = new Renderer();
+ this.editor = new Editor(modelService);
+ this.dsl = '';
+ }
+
+ arrangeAll() {
+ this.editorContext.performLayout().then(() => this.editorContext.fitToPage());
+ }
+}
diff --git a/src/demo/app/app.module.d.ts b/src/demo/app/app.module.d.ts
new file mode 100644
index 0000000..09cdb35
--- /dev/null
+++ b/src/demo/app/app.module.d.ts
@@ -0,0 +1,2 @@
+export declare class AppModule {
+}
diff --git a/src/demo/app/app.module.ts b/src/demo/app/app.module.ts
new file mode 100644
index 0000000..4e59f01
--- /dev/null
+++ b/src/demo/app/app.module.ts
@@ -0,0 +1,16 @@
+import { NgModule } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { BrowserModule } from '@angular/platform-browser';
+import { FloModule } from 'spring-flo';
+import { ModalModule } from 'ngx-bootstrap';
+
+import { PropertiesDialogComponent } from './properties.dialog.component';
+import { AppComponent } from './app.component';
+
+@NgModule({
+ imports: [ BrowserModule, FormsModule, FloModule, ModalModule.forRoot() ],
+ declarations: [ AppComponent, PropertiesDialogComponent ],
+ entryComponents: [ PropertiesDialogComponent ],
+ bootstrap: [ AppComponent ]
+})
+export class AppModule { }
diff --git a/src/demo/app/editor.d.ts b/src/demo/app/editor.d.ts
new file mode 100644
index 0000000..4ff958a
--- /dev/null
+++ b/src/demo/app/editor.d.ts
@@ -0,0 +1,36 @@
+import { Flo } from 'spring-flo';
+import { dia } from 'jointjs';
+import { BsModalService } from 'ngx-bootstrap';
+/**
+ * @author Alex Boyko
+ * @author Andy Clement
+ */
+export declare class Editor implements Flo.Editor {
+ private modelService;
+ constructor(modelService: BsModalService);
+ createHandles(context: Flo.EditorContext, createHandle: (owner: dia.CellView, kind: string, action: () => void, location: dia.Point) => void, owner: dia.CellView): void;
+ openPropertiesDialog(cell: dia.Cell): void;
+ validatePort(context: Flo.EditorContext, view: dia.ElementView, magnet: SVGElement): boolean;
+ validateLink(context: Flo.EditorContext, cellViewS: dia.ElementView, magnetS: SVGElement, cellViewT: dia.ElementView, magnetT: SVGElement, isSource: boolean, linkView: dia.LinkView): boolean;
+ preDelete(context: Flo.EditorContext, deletedElement: dia.Cell): void;
+ handleNodeDropping(context: Flo.EditorContext, dragDescriptor: Flo.DnDDescriptor): void;
+ calculateDragDescriptor(context: Flo.EditorContext, draggedView: dia.CellView, targetUnderMouse: dia.CellView, point: dia.Point, sourceComponent: string): Flo.DnDDescriptor;
+ validate(graph: dia.Graph): Promise