build(docs-infra): add a tool to create new examples (#39283)
This tool can be run from anywhere in the aio folder as: ```sh yarn create-example <example-name> ``` It will create some basic scaffold files to get the example started. After creation the developer should then use `yarn boilerplate:add` or similar to ensure that the example can be run and tested. You can optionally provide an absolute path to a pre-existing CLI project and it will copy over appropriate files (ignoring boilerplate) to the newly created example. ```sh yarn create-example <example-name> /path/to/other/cli/project ``` Fixes #39275 PR Close #39283
This commit is contained in:
committed by
Andrew Kushnir
parent
81aa119739
commit
34b74cecb6
@@ -150,6 +150,14 @@ See [aio/README.md](../../README.md#developer-tasks) for the available command-l
|
||||
|
||||
Running the script will create an `aio/protractor-results.txt` file with the results of the tests.
|
||||
|
||||
### `create-example.js`
|
||||
|
||||
The [create-example.js](./create-example.js) script creates a new example under the `aio/content/examples` directory.
|
||||
|
||||
You must provide a new name for the example.
|
||||
By default the script will place basic scaffold files into the new example (from [shared/example-scaffold](./shared/example-scaffold)).
|
||||
But you can also specify the path to a separate CLI project, from which the script will copy files that would not be considered "boilerplate".
|
||||
See the [Boilerplate overview](#boilerplate-overview) for more information.
|
||||
|
||||
### Updating example dependencies
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ Follow these steps to update the examples to the latest versions of Angular (and
|
||||
> NOTE:
|
||||
> The [angular-cli-diff](https://github.com/cexbrayat/angular-cli-diff) repo can be a useful resource for discovering what dependency versions are used for a basic CLI app at a specific CLI version.
|
||||
|
||||
- In the [shared/](./shared) folder, run `yarn` to update the dependencies in the [shared/node_modules/](./shared/node_modules) folder and the [shared/yarn.lock](./shared/yarn.lock) file.
|
||||
- In the [shared/](./shared) directory, run `yarn` to update the dependencies in the [shared/node_modules/](./shared/node_modules) directory and the [shared/yarn.lock](./shared/yarn.lock) file.
|
||||
|
||||
- In the [shared/](./shared) folder, run `yarn sync-deps` to update the dependency versions of the `package.json` files in each sub-folder of [shared/boilerplate/](./shared/boilerplate) to match the ones in [shared/package.json](./shared/package.json).
|
||||
- In the [shared/](./shared) directory, run `yarn sync-deps` to update the dependency versions of the `package.json` files in each sub-folder of [shared/boilerplate/](./shared/boilerplate) to match the ones in [shared/package.json](./shared/package.json).
|
||||
|
||||
- Follow the steps in the following section to update the rest of the boilerplate files.
|
||||
|
||||
@@ -24,7 +24,7 @@ Any necessary changes to boilerplate files will be done automatically through mi
|
||||
> You have to make these changes (if any) manually.
|
||||
> Again, the [angular-cli-diff](https://github.com/cexbrayat/angular-cli-diff) repo can be a useful resource for discovering changes between versions.
|
||||
|
||||
- In the [shared/boilerplate/cli/](./shared/boilerplate/cli) folder, run the following commands to migrate the the project to the current versions of Angular CLI and the Angular framework (updated in previous steps):
|
||||
- In the [shared/boilerplate/cli/](./shared/boilerplate/cli) directory, run the following commands to migrate the the project to the current versions of Angular CLI and the Angular framework (updated in previous steps):
|
||||
```sh
|
||||
# Ensure dependencies are installed.
|
||||
yarn install
|
||||
@@ -38,8 +38,11 @@ Any necessary changes to boilerplate files will be done automatically through mi
|
||||
> In order for `ng update` to work, there must be a `node_modules/` directory with installed dependencies inside the [shared/boilerplate/cli/](./shared/boilerplate/cli) directory.
|
||||
> This `node_modules/` directory is only needed during the update operation and is otherwise ignored (both by git and by the [example-boilerplate.js](./example-boilerplate.js) script) by means of the [shared/boilerplate/.gitignore](./shared/boilerplate/.gitignore) file.
|
||||
|
||||
- The previous command made any necessary changes to boilerplate files inside the `cli/` folder, but the same changes need to be applied to the other CLI-based boilerplate folders.
|
||||
Inspect the changes in `cli/` and manually apply the necessary ones to other CLI-based boilerplate folders.
|
||||
- The previous command made any necessary changes to boilerplate files inside the `cli/` directory, but the same changes need to be applied to the other CLI-based boilerplate directories.
|
||||
Inspect the changes in `cli/` and manually apply the necessary ones to other CLI-based boilerplate directories.
|
||||
|
||||
- Also ensure that any relevant changes in the [shared/boilerplate/cli/](./shared/boilerplate/cli) directory are copied to the [shared/example-scaffold/](./shared/example-scaffold) directory, which is used when creating new examples (via `yarn create-example ...`).
|
||||
Only files that would not be considered boilerplate should be added to the `example-scaffold/` directory.
|
||||
|
||||
- Ensure any changes to [cli/tslint.json](./shared/boilerplate/cli/tslint.json) are ported over to [systemjs/tslint.json](./shared/boilerplate/systemjs/tslint.json) and also [aio/content/examples/tslint.json](../../content/examples/tslint.json).
|
||||
This last part is important, since this file is used to lint example code on CI.
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
const path = require('canonical-path');
|
||||
|
||||
exports.EXAMPLES_BASE_PATH = path.resolve(__dirname, '../../content/examples');
|
||||
exports.EXAMPLE_CONFIG_FILENAME = 'example-config.json';
|
||||
exports.SHARED_PATH = path.resolve(__dirname, 'shared');
|
||||
exports.STACKBLITZ_CONFIG_FILENAME = 'stackblitz.json';
|
||||
@@ -0,0 +1,140 @@
|
||||
const fs = require('fs-extra');
|
||||
const glob = require('glob');
|
||||
const ignore = require('ignore');
|
||||
const path = require('canonical-path');
|
||||
const shelljs = require('shelljs');
|
||||
const yargs = require('yargs');
|
||||
|
||||
const {EXAMPLES_BASE_PATH, EXAMPLE_CONFIG_FILENAME, SHARED_PATH, STACKBLITZ_CONFIG_FILENAME} =
|
||||
require('./constants');
|
||||
const BASIC_SOURCE_PATH = path.resolve(SHARED_PATH, 'example-scaffold');
|
||||
|
||||
shelljs.set('-e');
|
||||
|
||||
if (require.main === module) {
|
||||
const options =
|
||||
yargs(process.argv.slice(2))
|
||||
.command(
|
||||
'$0 <name> [source]',
|
||||
[
|
||||
'Create a new <name> example.',
|
||||
'',
|
||||
'If [source] is provided then the relevant files from the CLI project at that path are copied into the example.',
|
||||
].join('\n'))
|
||||
.strict()
|
||||
.version(false)
|
||||
.argv;
|
||||
|
||||
const exampleName = options.name;
|
||||
const examplePath = path.resolve(EXAMPLES_BASE_PATH, exampleName);
|
||||
|
||||
console.log('Creating new example at', examplePath);
|
||||
createEmptyExample(exampleName, examplePath);
|
||||
|
||||
const sourcePath =
|
||||
options.source !== undefined ? path.resolve(options.source) : BASIC_SOURCE_PATH;
|
||||
console.log('Copying files from', sourcePath);
|
||||
copyExampleFiles(sourcePath, examplePath, exampleName);
|
||||
|
||||
console.log(`The new "${exampleName}" example has been created.`);
|
||||
console.log('Now run "yarn boilerplate:add" to set it up for development.');
|
||||
console.log(
|
||||
'You can find more info on working with docs examples in aio/tools/examples/README.md.')
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the directory and marker files for the new example.
|
||||
*/
|
||||
function createEmptyExample(exampleName, examplePath) {
|
||||
ensureExamplePath(examplePath);
|
||||
writeExampleConfigFile(examplePath);
|
||||
writeStackBlitzFile(exampleName, examplePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the new example directory exists.
|
||||
*/
|
||||
function ensureExamplePath(examplePath) {
|
||||
if (fs.existsSync(examplePath)) {
|
||||
throw new Error(
|
||||
`Unable to create example. The path to the new example already exists: ${examplePath}`);
|
||||
}
|
||||
fs.ensureDirSync(examplePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the `example-config.json` file to the new example.
|
||||
*/
|
||||
function writeExampleConfigFile(examplePath) {
|
||||
fs.writeFileSync(path.resolve(examplePath, EXAMPLE_CONFIG_FILENAME), '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the `stackblitz.json` file into the new example.
|
||||
*/
|
||||
function writeStackBlitzFile(exampleName, examplePath) {
|
||||
const config = {
|
||||
description: titleize(exampleName),
|
||||
files: ['!**/*.d.ts', '!**/*.js', '!**/*.[1,2].*'],
|
||||
tags: [exampleName.split('-')]
|
||||
};
|
||||
fs.writeFileSync(
|
||||
path.resolve(examplePath, STACKBLITZ_CONFIG_FILENAME),
|
||||
JSON.stringify(config, null, 2) + '\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy all the files from the `sourcePath`, which are not ignored by the `.gitignore` file in the
|
||||
* `EXAMPLES_BASE_PATH`, to the `examplePath`.
|
||||
*/
|
||||
function copyExampleFiles(sourcePath, examplePath, exampleName) {
|
||||
const gitIgnoreSource = getGitIgnore(sourcePath);
|
||||
const gitIgnoreExamples = getGitIgnore(EXAMPLES_BASE_PATH);
|
||||
|
||||
// Grab the files in the source folder and filter them based on the gitignore rules.
|
||||
const sourceFiles =
|
||||
glob.sync('**/*', {
|
||||
cwd: sourcePath,
|
||||
dot: true,
|
||||
ignore: ['**/node_modules/**', '.git/**', '.gitignore'],
|
||||
mark: true
|
||||
})
|
||||
// Filter out the directories, leaving only files
|
||||
.filter(filePath => !/\/$/.test(filePath))
|
||||
// Filter out files that match the source directory .gitignore rules
|
||||
.filter(filePath => !gitIgnoreSource.ignores(filePath))
|
||||
// Filter out files that match the examples directory .gitignore rules
|
||||
.filter(filePath => !gitIgnoreExamples.ignores(path.join(exampleName, filePath)));
|
||||
|
||||
for (const sourceFile of sourceFiles) {
|
||||
console.log(' - ', sourceFile);
|
||||
const destPath = path.resolve(examplePath, sourceFile)
|
||||
fs.ensureDirSync(path.dirname(destPath));
|
||||
fs.copySync(path.resolve(sourcePath, sourceFile), destPath);
|
||||
}
|
||||
}
|
||||
|
||||
function getGitIgnore(directory) {
|
||||
const gitIgnoreMatcher = ignore();
|
||||
const gitignoreFilePath = path.resolve(directory, '.gitignore');
|
||||
if (fs.existsSync(gitignoreFilePath)) {
|
||||
const gitignoreFile = fs.readFileSync(gitignoreFilePath, 'utf8');
|
||||
gitIgnoreMatcher.add(gitignoreFile);
|
||||
}
|
||||
return gitIgnoreMatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a kebab-case string to space separated Title Case string.
|
||||
*/
|
||||
function titleize(input) {
|
||||
return input.replace(
|
||||
/(-|^)(.)/g, (_, pre, char) => `${pre === '-' ? ' ' : ''}${char.toUpperCase()}`);
|
||||
}
|
||||
|
||||
exports.createEmptyExample = createEmptyExample;
|
||||
exports.ensureExamplePath = ensureExamplePath;
|
||||
exports.writeExampleConfigFile = writeExampleConfigFile;
|
||||
exports.writeStackBlitzFile = writeStackBlitzFile;
|
||||
exports.copyExampleFiles = copyExampleFiles;
|
||||
exports.titleize = titleize;
|
||||
@@ -0,0 +1,130 @@
|
||||
const path = require('canonical-path');
|
||||
const fs = require('fs-extra');
|
||||
const {glob} = require('glob');
|
||||
|
||||
const {EXAMPLES_BASE_PATH, EXAMPLE_CONFIG_FILENAME, SHARED_PATH, STACKBLITZ_CONFIG_FILENAME} =
|
||||
require('./constants');
|
||||
|
||||
const {
|
||||
copyExampleFiles,
|
||||
createEmptyExample,
|
||||
ensureExamplePath,
|
||||
titleize,
|
||||
writeExampleConfigFile,
|
||||
writeStackBlitzFile
|
||||
} = require('./create-example');
|
||||
|
||||
describe('create-example tool', () => {
|
||||
describe('createEmptyExample', () => {
|
||||
it('should create an empty example with marker files', () => {
|
||||
spyOn(fs, 'existsSync').and.returnValue(false);
|
||||
spyOn(fs, 'ensureDirSync');
|
||||
const writeFileSpy = spyOn(fs, 'writeFileSync');
|
||||
|
||||
createEmptyExample('foo-bar', '/path/to/foo-bar');
|
||||
expect(writeFileSpy).toHaveBeenCalledTimes(2);
|
||||
expect(writeFileSpy)
|
||||
.toHaveBeenCalledWith(`/path/to/foo-bar/${EXAMPLE_CONFIG_FILENAME}`, jasmine.any(String));
|
||||
expect(writeFileSpy)
|
||||
.toHaveBeenCalledWith(
|
||||
`/path/to/foo-bar/${STACKBLITZ_CONFIG_FILENAME}`, jasmine.any(String));
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensureExamplePath', () => {
|
||||
it('should error if the path already exists', () => {
|
||||
spyOn(fs, 'existsSync').and.returnValue(true);
|
||||
expect(() => ensureExamplePath('foo/bar'))
|
||||
.toThrowError(
|
||||
`Unable to create example. The path to the new example already exists: foo/bar`);
|
||||
});
|
||||
|
||||
it('should create the directory on disk', () => {
|
||||
spyOn(fs, 'existsSync').and.returnValue(false);
|
||||
const spy = spyOn(fs, 'ensureDirSync');
|
||||
ensureExamplePath('foo/bar');
|
||||
expect(spy).toHaveBeenCalledWith('foo/bar');
|
||||
});
|
||||
});
|
||||
|
||||
describe('writeExampleConfigFile', () => {
|
||||
it('should write a JSON file to disk', () => {
|
||||
const spy = spyOn(fs, 'writeFileSync');
|
||||
writeExampleConfigFile('/foo/bar');
|
||||
expect(spy).toHaveBeenCalledWith(`/foo/bar/${EXAMPLE_CONFIG_FILENAME}`, '');
|
||||
});
|
||||
});
|
||||
|
||||
describe('writeStackBlitzFile', () => {
|
||||
it('should write a JSON file to disk', () => {
|
||||
const spy = spyOn(fs, 'writeFileSync');
|
||||
writeStackBlitzFile('bar-bar', '/foo/bar-bar');
|
||||
expect(spy).toHaveBeenCalledWith(`/foo/bar-bar/${STACKBLITZ_CONFIG_FILENAME}`, [
|
||||
'{',
|
||||
' "description": "Bar Bar",',
|
||||
' "files": [',
|
||||
' "!**/*.d.ts",',
|
||||
' "!**/*.js",',
|
||||
' "!**/*.[1,2].*"',
|
||||
' ],',
|
||||
' "tags": [',
|
||||
' [',
|
||||
' "bar",',
|
||||
' "bar"',
|
||||
' ]',
|
||||
' ]',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('copyExampleFiles', () => {
|
||||
it('should copy over files that are not ignored by git', () => {
|
||||
const examplesGitIgnorePath = path.resolve(EXAMPLES_BASE_PATH, '.gitignore');
|
||||
const sourceGitIgnorePath = path.resolve('/source/path', '.gitignore');
|
||||
|
||||
spyOn(console, 'log');
|
||||
spyOn(fs, 'existsSync').and.returnValue(true);
|
||||
const readFileSyncSpy = spyOn(fs, 'readFileSync').and.callFake(p => {
|
||||
switch (p) {
|
||||
case examplesGitIgnorePath:
|
||||
return '**/a/b/**';
|
||||
case sourceGitIgnorePath:
|
||||
return '**/*.bad';
|
||||
default:
|
||||
throw new Error('Unexpected path');
|
||||
}
|
||||
});
|
||||
spyOn(glob, 'sync').and.returnValue([
|
||||
'a/', 'a/b/', 'a/c', 'x.ts', 'x.bad', 'a/b/y.ts', 'a/b/y.bad'
|
||||
]);
|
||||
const ensureDirSyncSpy = spyOn(fs, 'ensureDirSync');
|
||||
const copySyncSpy = spyOn(fs, 'copySync');
|
||||
|
||||
copyExampleFiles('/source/path', '/path/to/test-example', 'test-example');
|
||||
|
||||
expect(readFileSyncSpy).toHaveBeenCalledWith(examplesGitIgnorePath, 'utf8');
|
||||
expect(readFileSyncSpy).toHaveBeenCalledWith(sourceGitIgnorePath, 'utf8');
|
||||
|
||||
expect(ensureDirSyncSpy.calls.allArgs()).toEqual([
|
||||
['/path/to/test-example/a'],
|
||||
['/path/to/test-example'],
|
||||
]);
|
||||
|
||||
expect(copySyncSpy.calls.allArgs()).toEqual([
|
||||
['/source/path/a/c', '/path/to/test-example/a/c'],
|
||||
['/source/path/x.ts', '/path/to/test-example/x.ts'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('titleize', () => {
|
||||
it('should convert a kebab-case string to title-case', () => {
|
||||
expect(titleize('abc')).toEqual('Abc');
|
||||
expect(titleize('abc-def')).toEqual('Abc Def');
|
||||
expect(titleize('123')).toEqual('123');
|
||||
expect(titleize('abc---def')).toEqual('Abc - Def');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,8 +4,8 @@ const ignore = require('ignore');
|
||||
const path = require('canonical-path');
|
||||
const shelljs = require('shelljs');
|
||||
const yargs = require('yargs');
|
||||
const {EXAMPLES_BASE_PATH, EXAMPLE_CONFIG_FILENAME, SHARED_PATH} = require('./constants');
|
||||
|
||||
const SHARED_PATH = path.resolve(__dirname, 'shared');
|
||||
const SHARED_NODE_MODULES_PATH = path.resolve(SHARED_PATH, 'node_modules');
|
||||
|
||||
const BOILERPLATE_BASE_PATH = path.resolve(SHARED_PATH, 'boilerplate');
|
||||
@@ -13,9 +13,6 @@ const BOILERPLATE_CLI_PATH = path.resolve(BOILERPLATE_BASE_PATH, 'cli');
|
||||
const BOILERPLATE_COMMON_PATH = path.resolve(BOILERPLATE_BASE_PATH, 'common');
|
||||
const BOILERPLATE_VIEWENGINE_PATH = path.resolve(BOILERPLATE_BASE_PATH, 'viewengine');
|
||||
|
||||
const EXAMPLES_BASE_PATH = path.resolve(__dirname, '../../content/examples');
|
||||
const EXAMPLE_CONFIG_FILENAME = 'example-config.json';
|
||||
|
||||
class ExampleBoilerPlate {
|
||||
/**
|
||||
* Add boilerplate files to all the examples
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { AppPage } from './app.po';
|
||||
import { browser, logging } from 'protractor';
|
||||
|
||||
describe('workspace-project App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
// Add your e2e tests here
|
||||
|
||||
afterEach(async () => {
|
||||
// Assert that there are no errors emitted from the browser
|
||||
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||
expect(logs).not.toContain(jasmine.objectContaining({
|
||||
level: logging.Level.SEVERE,
|
||||
} as logging.Entry));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
<h1>Replace the src folder in this {{title}} with yours.</h1>
|
||||
@@ -0,0 +1,20 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
// Add your unit tests here
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css'],
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'example';
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Ponyracer</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,12 @@
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||
.catch(err => console.error(err));
|
||||
Reference in New Issue
Block a user