build(aio): migrate plunker to stackblitz (#20165)

PR Close #20165
This commit is contained in:
Jesus Rodriguez
2017-11-03 18:08:28 +01:00
committed by Miško Hevery
parent ed670a36fb
commit 0cbccc06dd
123 changed files with 735 additions and 1317 deletions

View File

@ -44,14 +44,13 @@ runnable project packaged as a zip file. These zip files are generated by the ut
See the [README.md](example-zipper/README.md) for more details.
## plunker-builder
## stackblitz-builder
In the AIO application, we can embed a running version of the example as a [Plunker](http://plnkr.co/).
We can also provide a link to create a runnable version of the example in the [Plunker](http://plnkr.co/edit)
In the AIO application, we can embed a running version of the example as a [Stackblitz](https://stackblitz.com/) session.
We can also provide a link to create a runnable version of the example in the [Stackblitz](https://stackblitz.com/)
editor.
This folder contains three utilities: `regularPlunker.js`, `embeddedPlunker.js`, `generatePlunkers.js`.
See the [README.md](plunker-builder/README.md) for more details.
See the [README.md](stackblitz-builder/README.md) for more details.
## transforms

View File

@ -14,6 +14,11 @@ There, select all the packages that are updated on the new Angular release.
**2)** Changes to the tsconfig.json? There is one to update at `/aio/tools/examples/shared/boilerplate/src/tsconfig.json`
**3)** The file `/aio/tools/examples/shared/boilerplate/src/systemjs.config.web.js` contains the configuration for plunkers. It has some hardcoded versions that could be updated.
**3)** The file `/aio/tools/examples/shared/boilerplate/src/systemjs.config.web.js` contains the configuration for plunkers. It has some hardcoded versions that could be updated.
>N.B.: Plunkers have been replaced by Stackblitz and (almost) all examples have be replaced by CLI/WebPack-based examples that do not use SystemJS.
The upgrade examples may still rely on SystemJS.
**4)** As in step 3, more hardcoded versions at `/aio/tools/plunker-builder/translator/rules/indexHtml.js`
>Note the same caveat about migration from plunker to Stackblitz.

View File

@ -5,14 +5,13 @@ runnable project packaged as a zip file. These zip files are generated by the ut
## Example zipper
The `exampleZipper.js` tool is very similar to the Plunker's `builder.js`. The latter creates an HTML file
with all the examples' files and the former creates a zip file instead. They both use the `plnkr.json` file
to flag an example as something to plunker or zip. For example:
The `exampleZipper.js` tool is very similar to the Stackblitz's `builder.js`. The latter creates an HTML file
with all the examples' files and the former creates a zip file instead. They both use the `stackblitz.json` file
to flag an example as something to stackblitz or zip. For example:
```json
{
"description": "Tour of Heroes: Part 6",
"basePath": "src/",
"files":[
"!**/*.d.ts",
"!**/*.js",
@ -55,15 +54,15 @@ The tool will also give some standard names to the scripts.
## The zipper.json
As mentioned, the tool uses the `plnkr.json` as a flag and also a configuration file for the zipper.
The problem is that not all examples have a plunker but they could offer a zip instead.
As mentioned, the tool uses the `stackblitz.json` as a flag and also a configuration file for the zipper.
The problem is that not all examples have a stackblitz but they could offer a zip instead.
In those cases, you can create a `zipper.json` file with the same syntax. It will be ignored by the
plunker tool.
stackblitz tool.
## Choosing the zip "type"
In both `plnkr.json` and `zipper.json` you can use two extra properties for the zipper configuration:
In both `stackblitz.json` and `zipper.json` you can use two extra properties for the zipper configuration:
```
{
@ -78,7 +77,7 @@ SystemJS.
## Executing the zip generation
`generateZips.js` will create a zip for each `plnkr.json` or `zipper.json` it finds.
`generateZips.js` will create a zip for each `stackblitz.json` or `zipper.json` it finds.
Where? At `src/generated/zips/`.

View File

@ -7,9 +7,7 @@
{ "name": "lint", "command": "ng lint" },
{ "name": "e2e", "command": "ng e2e" }
],
"dependencies": [
"web-animations-js"
],
"dependencies": [],
"devDependencies": [
"@angular/cli",
"@types/jasminewd2",

View File

@ -9,9 +9,7 @@
{ "name": "e2e", "command": "ng e2e --aot --i18nFile=src/locale/messages.fr.xlf --i18nFormat=xlf --locale=fr" },
{ "name": "extract", "command": "ng xi18n --outputPath=src/locale" }
],
"dependencies": [
"web-animations-js"
],
"dependencies": [],
"devDependencies": [
"@angular/cli",
"@types/jasminewd2",

View File

@ -0,0 +1,19 @@
{
"scripts": [
{ "name": "ng", "command": "ng" },
{ "name": "build", "command": "ng build" },
{ "name": "start", "command": "ng serve" },
{ "name": "test", "command": "ng test" },
{ "name": "lint", "command": "ng lint" },
{ "name": "e2e", "command": "ng e2e" }
],
"dependencies": [
"web-animations-js"
],
"devDependencies": [
"@angular/cli",
"@types/jasminewd2",
"karma-coverage-istanbul-reporter",
"ts-node"
]
}

View File

@ -11,7 +11,6 @@
{ "name": "webpack:server", "command": "webpack --config webpack.server.config.js --progress --colors" }
],
"dependencies": [
"web-animations-js",
"@nguniversal/express-engine",
"@nguniversal/module-map-ngfactory-loader",
"ts-loader"

View File

@ -19,9 +19,9 @@ class ExampleZipper {
this.exampleTsconfig = path.join(__dirname, '../examples/shared/boilerplate/systemjs/src/tsconfig.json');
this.customizer = new PackageJsonCustomizer();
let gpathPlnkr = path.join(sourceDirName, '**/*plnkr.json');
let gpathStackblitz = path.join(sourceDirName, '**/*stackblitz.json');
let gpathZipper = path.join(sourceDirName, '**/zipper.json');
let configFileNames = globby.sync([gpathPlnkr, gpathZipper], { ignore: ['**/node_modules/**'] });
let configFileNames = globby.sync([gpathStackblitz, gpathZipper], { ignore: ['**/node_modules/**'] });
configFileNames.forEach((configFileName) => {
this._zipExample(configFileName, sourceDirName, outputDirName);
});
@ -77,7 +77,7 @@ class ExampleZipper {
if (relativeDirName.indexOf('/') !== -1) { // Special example
exampleZipName = relativeDirName.split('/').join('-');
} else {
exampleZipName = jsonFileName.replace(/(plnkr|zipper).json/, relativeDirName);
exampleZipName = jsonFileName.replace(/(stackblitz|zipper).json/, relativeDirName);
}
const exampleDirName = path.dirname(configFileName);
@ -103,7 +103,7 @@ class ExampleZipper {
];
var alwaysExcludes = [
'!**/bs-config.e2e.json',
'!**/*plnkr.*',
'!**/*stackblitz.*',
'!**/*zipper.*',
'!**/systemjs.config.js',
'!**/npm-debug.log',

View File

@ -28,15 +28,15 @@ in any Angular application using System.js. This is the boilerplate that will be
Among these files, there are a few special ones:
* **src/systemjs.config.js** - This is the configuration of System.js used to run the example locally.
* **src/systemjs.config.web.js** - This configuration replaces the previous one on Plunkers.
* **src/systemjs.config.web.js** - This configuration replaces the previous one on Stackblitz.
* **src/systemjs.config.web.build.js** - Same as the previous one but for using angular's `-builds`
versions.
* **src/systemjs-angular-loader.js** - It is a System.js plugin that removes the need of `moduleId`.
* **package.json** - This package.json only contains scripts, no dependencies. It contains the
different tasks needed to run any example. Doesn't matter if CLI, System.js or Webpack.
* **plnkr.json** - This file is used by the Plunker tool to generate a plunker for an example. This
* **stackblitz.json** - This file is used by the Stackblitz tool to generate a stackblitz for an example. This
concrete file is just a placeholder. Authors needs to tweak it for each guide. More at the
[plunker docs](../plunker-builder/README.md).
[stackblitz docs](../stackblitz-builder/README.md).
* **example-config.json** - This file serves as a flag to indicate that the current folder is an
example. This concrete file is just a placeholder. More on this later in this readme.
@ -88,7 +88,7 @@ as shown earlier.
This script installs all the dependencies that are shared among all the examples, creates the
`node_modules` symlinks and copy all the boilerplate files where needed. It won't do anything
about plunkers nor e2e tests.
about stackblitz nor e2e tests.
It also contains a function to remove all the boilerplate. It uses a `git clean -xdf` to do
the job. It will remove all files that don't exist in the git repository, **including any

View File

@ -60,6 +60,11 @@ BOILERPLATE_PATHS.universal = [
'package.json'
];
BOILERPLATE_PATHS.testing = [
...cliRelativePath,
'.angular-cli.json'
];
const EXAMPLE_CONFIG_FILENAME = 'example-config.json';
class ExampleBoilerPlate {

View File

@ -21,6 +21,7 @@
"@angular/platform-browser": "^5.0.0",
"@angular/platform-browser-dynamic": "^5.0.0",
"@angular/router": "^5.0.0",
"angular-in-memory-web-api": "~0.5.0",
"core-js": "^2.4.1",
"rxjs": "^5.5.2",
"zone.js": "^0.8.14"

View File

@ -50,7 +50,7 @@ import 'core-js/es7/reflect';
* Required to support Web Animations `@angular/platform-browser/animations`.
* Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
**/
import 'web-animations-js';
// import 'web-animations-js';

View File

@ -0,0 +1,61 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"project": {
"name": "angular.io-example"
},
"apps": [
{
"root": "src",
"outDir": "dist",
"assets": [
"assets",
"favicon.ico"
],
"index": "index.html",
"main": "main.ts",
"polyfills": "polyfills.ts",
"test": "test.ts",
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"styles": [
"test.css",
"styles.css"
],
"scripts": [],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
}
}
],
"e2e": {
"protractor": {
"config": "./protractor.conf.js"
}
},
"lint": [
{
"project": "src/tsconfig.app.json",
"exclude": "**/node_modules/**"
},
{
"project": "src/tsconfig.spec.json",
"exclude": "**/node_modules/**"
},
{
"project": "e2e/tsconfig.e2e.json",
"exclude": "**/node_modules/**"
}
],
"test": {
"karma": {
"config": "./karma.conf.js"
}
},
"defaults": {
"styleExt": "css",
"component": {}
}
}

View File

@ -1,78 +0,0 @@
# Overview
[Plunker](http://plnkr.co) is an online tool for creating, collaborating and sharing ideas. In AIO
we use it to share one or more runnable versions of our examples.
Plunker comes in two flavours. The [classic UI](http://plnkr.co/edit) and an [embedded UI](http://embed.plnkr.co).
The latter can be used both in a new tab or embedded within a guide. The AIO project uses the
embedded version in both ways.
* `regularPlunker.js` - generates an HTML file for each example that will post to Plunker to create
a new editable project, when rendered.
* `embeddedPlunker.js` - generates an HTML file for each example that can be used in an iframe to
render an embedded Plunker project.
* `generatePlunkers.js` - executes each of the `regularPlunker.js` and `embeddedPlunker.js` utilities
to generate all the example plunker files.
## Plunker generation
Both flavours are created within `builder.js`. How is a plunker created? What is the process from a
directory with files to a link with a plunker.
An "executable" plunker is an HTML file with a `<form>` that makes a post to plunker on submit. It
contains an `<input>` element for each file we need in the plunker.
The form will be submitted on load, so you can either double click the HTML file or open it with an
anchor tag to open the plunker.
So the `builder.js` job is to get all the needed files from an example and build this HTML file for you.
For plunkers, we use a special `systemjs.config` that exists in
`/aio/tools/examples/shared/boilerplate/src/systemjs.config.web.js` and we also add the Google's copyright
to each file.
## Customizing the generation per example basis
How does this tool know what is an example and what is not? It will look for all folders containing a
`plnkr.json` file. If found, all files within the folder and subfolders will be used in the plunker, with
a few generic exceptions that you can find at `builder.js`.
You can use the `plnkr.json` to customize the plunker generation. For example:
```json
{
"description": "Tour of Heroes: Part 6",
"basePath": "src/",
"files":[
"!**/*.d.ts",
"!**/*.js",
"!**/*.[1,2].*"
],
"tags": ["tutorial", "tour", "heroes", "http"]
}
```
Here you can specify a description for the plunker, some tags, a basePath and also a files array where you
can specify extra files to add or to ignore.
## Classic plunkers and embedded ones
Luckily, both kind of plunkers are very similar, they are created in the same way with minor exceptions.
To handle those exceptions, we have the `embeddedPlunker.js` and the `regularPlunker.js`. Thanks to them,
the `builder.js` is as generic as possible.
## Executing the plunker generation
`generatePlunkers.js` will create a classic plunker and an embedded plunker for each `plnkr.json` it finds.
Where? At `src/generated/live-examples/`.
Then the `<live-example>` embedded component will look at this folder to get the plunker it needs for the
example.
## Appendix: Why not generating plunkers at runtime?
At AngularJS, all the plunkers were generated a runtime. The downside is that all the example codes would
need to be deployed as well and they won't be no longer useful after the plunker is generated. This tool
takes a few seconds to run, and the end result is only 3mb~.

View File

@ -1,34 +0,0 @@
var PlunkerBuilder = require('./builder');
function buildPlunkers(basePath, destPath, options = {}) {
configureBuilder(options);
var builder = new PlunkerBuilder(basePath, destPath, options);
builder.buildPlunkers();
}
function configureBuilder(options) {
options.addField = addField;
options.plunkerFileName = 'eplnkr';
options.url = 'https://embed.plnkr.co?show=preview';
options.writeNoLink = false;
options.embedded = true;
options.extraData = extraData;
}
function extraData(postData, config) {
postData['source[type]'] = config.description || 'Angular example';
postData['source[url]'] = 'https://angular.io'
}
function addField(postData, name, content) {
var encoding = 'utf8';
if (name.split('.').pop() === 'png') {
encoding = 'base64';
}
postData[`entries[${name}][content]`] = content;
postData[`entries[${name}][encoding]`] = encoding;
}
module.exports = {
buildPlunkers: buildPlunkers
};

View File

@ -1,9 +0,0 @@
const path = require('path');
const regularPlunker = require('./regularPlunker');
const embeddedPlunker = require('./embeddedPlunker');
const EXAMPLES_PATH = path.join(__dirname, '../../content/examples');
const LIVE_EXAMPLES_PATH = path.join(__dirname, '../../src/generated/live-examples');
regularPlunker.buildPlunkers(EXAMPLES_PATH, LIVE_EXAMPLES_PATH);
embeddedPlunker.buildPlunkers(EXAMPLES_PATH, LIVE_EXAMPLES_PATH);

View File

@ -1,23 +0,0 @@
var PlunkerBuilder = require('./builder');
function buildPlunkers(basePath, destPath, options = {}) {
configureBuilder(options);
var builder = new PlunkerBuilder(basePath, destPath, options);
builder.buildPlunkers();
}
function configureBuilder(options) {
options.addField = addField;
options.plunkerFileName = 'plnkr';
options.url = 'http://plnkr.co/edit/?p=preview';
options.writeNoLink = true;
options.embedded = false;
}
function addField(postData, name, content) {
postData[`files[${name}]`] = content;
}
module.exports = {
buildPlunkers: buildPlunkers
};

View File

@ -1,51 +0,0 @@
// var first_time = true; // DIAGNOSTIC
function translate(html, rulesFile) {
rulesFile.rulesToApply.forEach(function(rxDatum) {
var rxRule = rulesFile.rules[rxDatum.pattern];
// rxFrom is a rexexp
var rxFrom = rxRule.from;
// check if there is an exception
if (rxDatum.exceptIf) {
if (html.indexOf(rxDatum.exceptIf) !== -1) return;
}
if (rxDatum.from) {
var from = rxDatum.from.replace('/', '\/');
var rxTemp = rxFrom.toString();
rxTemp = rxTemp.replace('%tag%', from);
rxFrom = rxFromString(rxTemp);
}
// rxTo is a string
var rxTo = rxRule.to;
if (rxDatum.to) {
var to = rxDatum.to;
to = Array.isArray(to) ? to : [to];
to = to.map(function (toItem) {
return rxTo.replace("%tag%", toItem);
});
rxTo = to.join("\n ");
}
/* DIAGNOSTIC
if (first_time && rxDatum.pattern === 'zone_pkg') {
first_time = false;
console.log('zone_pkg');
console.log(' rxFrom: '+rxFrom);
console.log(' rxTo: '+rxTo);
console.log(' replace: ' + html.replace(rxFrom, rxTo ));
}
*/
html = html.replace(rxFrom, rxTo);
});
return html;
}
function rxFromString(rxString) {
var rx = /^\/(.*)\/(.*)/;
var pieces = rx.exec(rxString);
return RegExp(pieces[1], pieces[2]);
}
module.exports = {translate: translate};

View File

@ -1,133 +0,0 @@
var rules = {
basehref: {
from: /<base href=".*"[/]?>/,
to: '<script>document.write(\'<base href="\' + document.location + \'" />\');</script>'
},
angular_pkg: {
from: /src=".?node_modules\/@angular/g,
to: 'src="https://unpkg.com/@angular'
},
script: {
from: /<script.*".*%tag%".*>.*<\/script>/,
to: '<script src="%tag%"></script>'
},
link: {
from: '/<link rel="stylesheet" href=".*%tag%".*>/',
to: '<link rel="stylesheet" href="%tag%">'
},
// Clear script like this:
// <script>
// System.import('app').catch(function(err){ console.error(err); });
// </script>
system_strip_import_app: {
from: /<script>[^]?\s*System.import\('app'\)[^]*\/script>/,
to: ''
},
system_extra_main: {
from: /main:\s*[\'|\"]index.js[\'|\"]/,
to: 'main: "index.ts"'
},
system_extra_defaultExtension: {
from: /defaultExtension:\s*[\'|\"]js[\'|\"]/,
to: 'defaultExtension: "ts"'
},
zone_pkg: {
from: /src=".?node_modules\/zone.js\/dist\/(.*)"/g,
to: 'src="https://unpkg.com/zone.js/dist/$1?main=browser"'
},
system_js_header: {
from: '</head>',
to: `
<link rel="stylesheet" href="styles.css">
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('main.js').catch(function(err){ console.error(err); });
</script>
</head>
`
}
};
var rulesToApply = [
{
pattern: 'system_js_header',
exceptIf: 'system.src.js'
},
{
pattern: 'basehref',
},
{
pattern: 'script',
from: 'node_modules/core-js/client/shim.min.js',
to: 'https://unpkg.com/core-js/client/shim.min.js'
},
{
pattern: 'script',
from: 'node_modules/zone.js/dist/zone.js',
to: 'https://unpkg.com/zone.js@0.7.4?main=browser'
},
{
pattern: 'script',
from: 'node_modules/rxjs/bundles/Rx.js',
to: 'https://unpkg.com/rxjs@5.5.2/bundles/Rx.js'
},
{
pattern: 'script',
from: 'node_modules/systemjs/dist/system.src.js',
to: 'https://unpkg.com/systemjs@0.19.39/dist/system.src.js'
},
{
pattern: 'script',
from: 'node_modules/angular/in-memory-web-api/web-api.js',
to: 'https://unpkg.com/angular/in-memory-web-api/web-api.js'
},
// Test libraries
// Plunker recommends getting jasmine from cloudfare
// Don't upgrade to 2.5.x until following issue resolved
// https://github.com/jasmine/jasmine/issues/1231
{
pattern: 'script',
from: 'node_modules/jasmine-core/lib/jasmine-core/jasmine.js',
to: 'https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine.js'
},
{
pattern: 'script',
from: 'node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js',
to: 'https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine-html.js'
},
{
pattern: 'script',
from: 'node_modules/jasmine-core/lib/jasmine-core/boot.js',
to: 'https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/boot.js'
},
{
pattern: 'link',
from: 'node_modules/jasmine-core/lib/jasmine-core/jasmine.css',
to: 'https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine.css'
},
{
pattern: 'angular_pkg',
},
{
pattern: 'zone_pkg',
},
// {
// pattern: 'system_strip_import_app',
// },
{
pattern: 'system_extra_main'
},
{
pattern: 'system_extra_defaultExtension'
}
];
module.exports = {
rules: rules,
rulesToApply: rulesToApply
};

View File

@ -1,24 +0,0 @@
var rules = {
environment_import: {
from: `import { environment } from './environments/environment';`,
to: ''
},
environment_check: {
from: /if \(environment\.production\)\s*{\n*\s*enableProdMode\(\);\s*}/g,
to: ''
}
};
var rulesToApply = [
{
pattern: 'environment_import'
},
{
pattern: 'environment_check'
}
];
module.exports = {
rules: rules,
rulesToApply: rulesToApply
};

View File

@ -1,24 +0,0 @@
var rules = {
system_extra_main: {
from: /main:\s*[\'|\"]index.js[\'|\"]/g,
to: 'main: "index.ts"'
},
system_extra_defaultExtension: {
from: /defaultExtension:\s*[\'|\"]js[\'|\"]/g,
to: 'defaultExtension: "ts"'
}
};
var rulesToApply = [
{
pattern: 'system_extra_main'
},
{
pattern: 'system_extra_defaultExtension'
}
];
module.exports = {
rules: rules,
rulesToApply: rulesToApply
};

View File

@ -0,0 +1,59 @@
# Overview
[Stackblitz](https://stackblitz.com/) is an online tool for creating, collaborating and sharing ideas.
In AIO we use it to share one or more runnable versions of our examples.
Stackblitz can be used both in a separate page and in an embedded form.
* `generateStackblitz.js` - executes each of the the StackblitzBuilder to generate a stackblitz file for each example.
## Stackblitz generation
Both forms are created within `builder.js`. How is a stackblitz created? What is the process from a
directory with files to a link with a stackblitz.
An "executable" stackblitz is an HTML file with a `<form>` that makes a post to stackblitz on submit. It
contains an `<input>` element for each file we need in the stackblitz.
The form will be submitted on load, so you can either double click the HTML file or open it with an
anchor tag to open the stackblitz.
So the `builder.js` job is to get all the needed files from an example and build this HTML file for you.
## Customizing the generation per example basis
How does this tool know what is an example and what is not? It will look for all folders containing a
`stackblitz.json` file. If found, all files within the folder and subfolders will be used in the stackblitz, with
a few generic exceptions that you can find at `builder.js`.
You can use the `stackblitz.json` to customize the stackblitz generation. For example:
```json
{
"description": "Tour of Heroes: Part 6",
"files":[
"!**/*.d.ts",
"!**/*.js",
"!**/*.[1,2].*"
],
"tags": ["tutorial", "tour", "heroes", "http"]
}
```
Here you can specify a description for the stackblitz, some tags and also a files array where you
can specify extra files to add or to ignore.
## Executing the stackblitz generation
`generateStackblitz.js` will create a stackblitz for each `stackblitz.json` it finds.
Where? At `src/generated/live-examples/`.
Then the `<live-example>` embedded component will look at this folder to get the stackblitz it needs for the
example.
## Appendix: Why not generating stackblitz at runtime?
At AngularJS, all the plunker examples were generated a runtime. The downside was that all the example code had to be
deployed as well and would no longer be useful after the plunker was generated.
This `StackblitzBuilder` tool takes a few seconds to run, and the end result is only 3mb~.

View File

@ -8,40 +8,43 @@ var jsdom = require("jsdom");
var fs = require("fs-extra");
var globby = require('globby');
var fileTranslator = require('./translator/fileTranslator');
var indexHtmlRules = require('./translator/rules/indexHtml');
var systemjsConfigExtrasRules = require('./translator/rules/systemjsConfigExtras');
var mainTsRules = require('./translator/rules/mainTs');
var regionExtractor = require('../transforms/examples-package/services/region-parser');
class PlunkerBuilder {
constructor(basePath, destPath, options) {
class StackblitzBuilder {
constructor(basePath, destPath) {
this.basePath = basePath;
this.destPath = destPath;
this.options = options;
this.boilerplate = path.join(__dirname, '../examples/shared/boilerplate/systemjs');
// Extract npm package dependencies
var packageJson = require(path.join(__dirname, '../examples/shared/boilerplate/cli/package.json'));
this.examplePackageDependencies = packageJson.dependencies;
// Add jasmine-core (which is a devDependency) for unit test examples.
var devDependencies = packageJson.devDependencies;
this.examplePackageDependencies['jasmine-core'] = devDependencies['jasmine-core'];
this.copyrights = {};
this._buildCopyrightStrings();
}
buildPlunkers() {
this._getPlunkerFiles();
var errFn = this.options.errFn || function(e) { console.log(e); };
var plunkerPaths = path.join(this.basePath, '**/*plnkr.json');
var fileNames = globby.sync(plunkerPaths, { ignore: ['**/node_modules/**'] });
build() {
// When testing it sometimes helps to look a just one example directory like so:
// var stackblitzPaths = path.join(this.basePath, '**/testing/*stackblitz.json');
var stackblitzPaths = path.join(this.basePath, '**/*stackblitz.json');
var fileNames = globby.sync(stackblitzPaths, { ignore: ['**/node_modules/**'] });
fileNames.forEach((configFileName) => {
try {
this._buildPlunkerFrom(configFileName);
// console.log('***'+configFileName)
this._buildStackblitzFrom(configFileName);
} catch (e) {
errFn(e);
console.log(e);
}
});
}
_addPlunkerFiles(postData) {
this.options.addField(postData, 'systemjs.config.js', this.systemjsConfig);
this.options.addField(postData, 'systemjs-angular-loader.js', this.systemjsModulePlugin);
_addDependencies(postData) {
postData['dependencies'] = JSON.stringify(this.examplePackageDependencies);
}
_buildCopyrightStrings() {
@ -53,17 +56,17 @@ class PlunkerBuilder {
this.copyrights.html = `${pad}<!-- \n${copyright}\n-->`;
}
// Build plunker from JSON configuration file (e.g., plnkr.json):
// Build stackblitz from JSON configuration file (e.g., stackblitz.json):
// all properties are optional
// files: string[] - array of globs - defaults to all js, ts, html, json, css and md files (with certain files removed)
// description: string - description of this plunker - defaults to the title in the index.html page.
// tags: string[] - optional array of plunker tags (for searchability)
// main: string - name of file that will become index.html in the plunker - defaults to index.html
// open: string - name of file to display within the plunker as in "open": "app/app.module.ts"
_buildPlunkerFrom(configFileName) {
// replace ending 'plnkr.json' with 'plnkr.no-link.html' to create output file name;
var outputFileName = `${this.options.plunkerFileName}.no-link.html`;
outputFileName = configFileName.replace(/plnkr\.json$/, outputFileName);
// description: string - description of this stackblitz - defaults to the title in the index.html page.
// tags: string[] - optional array of stackblitz tags (for searchability)
// main: string - name of file that will become index.html in the stackblitz - defaults to index.html
// file: string - name of file to display within the stackblitz as in "open": "app/app.module.ts"
_buildStackblitzFrom(configFileName) {
// replace ending 'stackblitz.json' with 'stackblitz.no-link.html' to create output file name;
var outputFileName = `stackblitz.no-link.html`;
outputFileName = configFileName.replace(/stackblitz\.json$/, outputFileName);
var altFileName;
if (this.destPath && this.destPath.length > 0) {
var partPath = path.dirname(path.relative(this.basePath, outputFileName));
@ -72,11 +75,9 @@ class PlunkerBuilder {
try {
var config = this._initConfigAndCollectFileNames(configFileName);
var postData = this._createPostData(config);
this._addPlunkerFiles(postData);
var html = this._createPlunkerHtml(config, postData);
if (this.options.writeNoLink) {
fs.writeFileSync(outputFileName, html, 'utf-8');
}
this._addDependencies(postData);
var html = this._createStackblitzHtml(config, postData);
fs.writeFileSync(outputFileName, html, 'utf-8');
if (altFileName) {
var altDirName = path.dirname(altFileName);
fs.ensureDirSync(altDirName);
@ -94,20 +95,30 @@ class PlunkerBuilder {
}
}
_createBasePlunkerHtml(config, embedded) {
var open = '';
_createBaseStackblitzHtml(config) {
var file = '';
if (config.open) {
open = embedded ? `&show=${config.open}` : `&open=${config.open}`;
// TODO: Doesn't work properly yet
if (config.file) {
file = `?file=${config.file}`;
}
var action = `${this.options.url}${open}`;
var html = '<!DOCTYPE html><html lang="en"><body>';
html += `<form id="mainForm" method="post" action="${action}" target="_self">`;
var action = `https://run.stackblitz.com/api/angular/v1${file}`;
var html = `<!DOCTYPE html><html lang="en"><body>
<form id="mainForm" method="post" action="${action}" target="_self"></form>
<script>
var embedded = 'ctl=1';
var isEmbedded = window.location.search.indexOf(embedded) > -1;
// html += '<div class="button"><button id="formButton" type="submit">Create Plunker</button></div>'
// html += '</form><script>document.getElementById("formButton").click();</script>'
html += '</form><script>document.getElementById("mainForm").submit();</script>';
html += '</body></html>';
if (isEmbedded) {
var form = document.getElementById('mainForm');
var action = form.action;
var actionHasParams = action.indexOf('?') > -1;
var symbol = actionHasParams ? '&' : '?'
form.action = form.action + symbol + embedded;
}
document.getElementById("mainForm").submit();
</script>
</body></html>`;
return html;
}
@ -119,8 +130,6 @@ class PlunkerBuilder {
if (extn == '.png') {
content = this._encodeBase64(fileName);
fileName = fileName.substr(0, fileName.length - 4) + '.base64.png'
} else if (-1 < fileName.indexOf('systemjs.config.extras')) {
content = this._getSystemjsConfigExtras(config);
} else {
content = fs.readFileSync(fileName, 'utf-8');
}
@ -134,12 +143,17 @@ class PlunkerBuilder {
var relativeFileName = path.relative(config.basePath, fileName);
// Is the main a custom index-xxx.html file? Rename it
if (relativeFileName == config.main) {
relativeFileName = 'index.html';
relativeFileName = 'src/index.html';
}
// A custom main.ts file? Rename it
if (/src\/main[-.]\w+\.ts$/.test(relativeFileName)) {
relativeFileName = 'src/main.ts'
}
if (relativeFileName == 'index.html') {
content = fileTranslator.translate(content, indexHtmlRules);
if (config.description == null) {
// set config.description to title from index.html
var matches = /<title>(.*)<\/title>/.exec(content);
@ -149,19 +163,9 @@ class PlunkerBuilder {
}
}
// Matches main.ts or main.1.ts
if (/^main(?:[.-]\w+)?\.ts$/.test(relativeFileName)) {
content = fileTranslator.translate(content, mainTsRules);
relativeFileName = 'main.ts';
}
if (relativeFileName == 'systemjs.config.extras.js') {
content = fileTranslator.translate(content, systemjsConfigExtrasRules);
}
content = regionExtractor()(content, extn.substr(1)).contents;
this.options.addField(postData, relativeFileName, content);
postData[`files[${relativeFileName}]`] = content;
});
var tags = ['angular', 'example'].concat(config.tags || []);
@ -169,23 +173,13 @@ class PlunkerBuilder {
postData['tags[' + ix + ']'] = tag;
});
if (!this.options.embedded) {
postData.private = true;
postData.description = "Angular Example - " + config.description;
postData.description = "Angular Example - " + config.description;
} else {
postData.title = "Angular Example - " + config.description;
}
// Embedded needs to add more content, so if the callback is available, we call it
if (this.options.extraData) {
this.options.extraData(postData, config);
}
return postData;
}
_createPlunkerHtml(config, postData) {
var baseHtml = this._createBasePlunkerHtml(config, this.options.embedded);
_createStackblitzHtml(config, postData) {
var baseHtml = this._createBaseStackblitzHtml(config);
var doc = jsdom.jsdom(baseHtml);
var form = doc.querySelector('form');
_.forEach(postData, (value, key) => {
@ -214,39 +208,6 @@ class PlunkerBuilder {
}
}
_getPlunkerFiles() {
var systemJsModulePlugin = '/src/systemjs-angular-loader.js';
var systemJsConfigPath = '/src/systemjs.config.web.js';
if (this.options.build) {
systemJsConfigPath = '/src/systemjs.config.web.build.js';
}
this.systemjsConfig = fs.readFileSync(this.boilerplate + systemJsConfigPath, 'utf-8');
this.systemjsModulePlugin = fs.readFileSync(this.boilerplate + systemJsModulePlugin, 'utf-8');
// Copyright already added to web versions of systemjs.config
// this.systemjsConfig += this.copyrights.jsCss;
}
// Try to replace `systemjs.config.extras.js` with the
// `systemjs.config.extras.web.js` web version that
// should default SystemJS barrels to `.ts` files rather than `.js` files
// Example: see docs `testing`.
// HACK-O-MATIC!
_getSystemjsConfigExtras(config) {
var extras = config.basePath + '/systemjs.config.extras.js';
var webExtras = config.basePath + '/systemjs.config.extras.web.js';
if (this._existsSync(webExtras)) {
// console.log('** Substituted "' + webExtras + '" for "' + extras + '".');
return fs.readFileSync(webExtras, 'utf-8');
} else if (this._existsSync(extras)){
console.log('** WARNING: no "' + webExtras + '" replacement for "' + extras + '".');
return fs.readFileSync(extras, 'utf-8');
} else {
console.log('** WARNING: no "' + extras + '" file; returning empty content.');
return '';
}
}
_htmlToElement(document, html) {
var div = document.createElement('div');
div.innerHTML = html;
@ -258,12 +219,13 @@ class PlunkerBuilder {
var configSrc = fs.readFileSync(configFileName, 'utf-8');
try {
var config = (configSrc && configSrc.trim().length) ? JSON.parse(configSrc) : {};
config.basePath = config.basePath ? path.resolve(configDir, config.basePath) : configDir;
config.basePath = configDir; // assumes 'stackblitz.json' is at `/src` level.
} catch (e) {
throw new Error(`Plunker config - unable to parse json file: ${configFileName}\n${e}`);
throw new Error(`Stackblitz config - unable to parse json file: ${configFileName}\n${e}`);
}
var defaultIncludes = ['**/*.ts', '**/*.js', '**/*.css', '**/*.html', '**/*.md', '**/*.json', '**/*.png'];
var boilerplateIncludes = ['src/environments/*.*', '.angular-cli.json', 'src/polyfills.ts'];
if (config.files) {
if (config.files.length > 0) {
if (config.files[0].substr(0, 1) == '!') {
@ -273,11 +235,13 @@ class PlunkerBuilder {
} else {
config.files = defaultIncludes;
}
config.files = config.files.concat(boilerplateIncludes);
var includeSpec = false;
var gpaths = config.files.map(function(fileName) {
fileName = fileName.trim();
if (fileName.substr(0,1) == '!') {
return "!" + path.join(config.basePath, fileName.substr(1));
return '!' + path.join(config.basePath, fileName.substr(1));
} else {
includeSpec = includeSpec || /\.spec\.(ts|js)$/.test(fileName);
return path.join(config.basePath, fileName);
@ -285,23 +249,18 @@ class PlunkerBuilder {
});
var defaultExcludes = [
'!**/e2e/**/*.*',
'!**/tsconfig.json',
'!**/*plnkr.*',
'!**/package.json',
'!**/example-config.json',
'!**/tslint.json',
'!**/.editorconfig',
'!**/systemjs.config.js',
'!**/wallaby.js',
'!**/karma-test-shim.js',
'!**/karma.conf.js',
'!**/test.ts',
'!**/polyfills.ts',
'!**/tsconfig.app.json',
'!**/environments/**',
// AoT related files
'!**/aot/**/*.*',
'!**/*-aot.*'
'!**/*stackblitz.*'
];
// exclude all specs if no spec is mentioned in `files[]`
@ -317,4 +276,4 @@ class PlunkerBuilder {
}
}
module.exports = PlunkerBuilder;
module.exports = StackblitzBuilder;

View File

@ -0,0 +1,8 @@
const path = require('path');
const StackblitzBuilder = require('./builder');
const EXAMPLES_PATH = path.join(__dirname, '../../content/examples');
const LIVE_EXAMPLES_PATH = path.join(__dirname, '../../src/generated/live-examples');
new StackblitzBuilder(EXAMPLES_PATH, LIVE_EXAMPLES_PATH).build();

View File

@ -2,7 +2,7 @@
* The point of this reader is to tag all the files that are going to be used as examples in the
* documentation.
* Later on we can extract the regions, via "shredding"; and we can also construct runnable examples
* for passing to plunker and the like.
* for passing to Stackblitz and the like.
*/
module.exports = function exampleFileReader() {
return {