Skip to content

Commit

Permalink
feat: add CLI to transpile dependencies to ES5
Browse files Browse the repository at this point in the history
  • Loading branch information
asyncLiz committed Aug 3, 2018
1 parent f777468 commit 9b5d70b
Show file tree
Hide file tree
Showing 11 changed files with 909 additions and 190 deletions.
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ To setup Origami, follow these steps:
2. Install [webcomponent polyfills](#polyfills)
1. Add links to them in [`index.html`](#indexhtml)
2. Add assets to include them in [`angular.json`](#angularjson-angular-6) or [`.angular-cli.json`](#angular-clijson-angular-5)
3. Read the [Usage Summary](#usage-summary)
3. [Prepare dependencies](#prepare-dependencies-es5-only) if targeting ES5
4. Read the [Usage Summary](#usage-summary)

## Install

Expand Down Expand Up @@ -220,6 +221,39 @@ Add an asset glob to the app's `"assets"` array. The glob will vary depending on
}
```

## Prepare Dependencies (ES5 only)

Angular will not transpile `node_modules/`, and a common pattern among webcomponents is to be distributed as ES2015 classes. Origami provides a simple CLI to effeciently transpile dependencies to ES5 or back to ES2015 before building.

For example, this command will prepare all `@polymer/` and `@vaadin/` dependencies.

```sh
origami prepare es5 node_modules/{@polymer,@vaadin}/*

# to restore to ES2015
origami prepare es2015 node_modules/{@polymer,@vaadin}/*

# for more info
origami --help
```

The CLI can also restore the previous ES2015 files for projects that compile to both targets.

It is recommended to add a script before `ng build` and `ng serve` tasks in `package.json`.

```json
{
"scripts": {
"prepare:es5": "origami prepare es5 node_modules/{@polymer,@vaadin}/*",
"prepare:es2015": "origami prepare es2015 node_modules/{@polymer,@vaadin}/*",
"start": "npm run prepare:es5 && ng serve es5App",
"start:es2015": "npm run prepare:es2015 && ng serve es2015App",
"build": "npm run prepare:es5 && ng build es5App --prod",
"build:es2015": "npm run prepare:es2015 && ng build es2015App --prod"
}
}
```

## Usage Summary

### [Angular Form Support](forms/README.md)
Expand Down
24 changes: 23 additions & 1 deletion UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,33 @@ rm -rf src/bower_components/

If you have any `.html` Polymer elements in the `src/elements/` folder, convert them to `.js` or `.ts` files at this time. [polymer-modulizer](/~https://github.com/Polymer/polymer-modulizer) may assist in the conversion.

## Add prepare dependencies script (ES5 only)

If an app is targeting ES5, add a script to `package.json` before `ng serve` and `ng build` tasks to prepare dependencies for ES5 or ES2015 targets. This section may be skipped if an app does not target ES5.

`origami prepare es5 <globs...>` must be added before serve and build commands targeting ES5.

`origami prepare es2015 <globs...>` must be added before serve and build commands targeting ES6 (ES2015).

Change the glob, or add additional globs, to target all webcomponent npm folders.

```diff
{
"scripts": {
+ "prepare:es5": "origami prepare es5 node_modules/@polymer/*",
- "start": "ng serve",
- "build": "ng build --prod"
+ "start": "npm run prepare:es5 && ng serve",
+ "build": "npm run prepare:es5 && ng build --prod"
}
}
```

## Update `angular.json` or `.angular-cli.json` Polyfill Assets

Remember that the `"input"` is relative to the `"root"` key of the project. If your root is not the same directory that `node_modules/` are installed to, you may need to go up a directory.

### ES6 (es2015) Target Apps
### ES6 (ES2015) Target Apps

`angular.json` (Angular 6+)

Expand Down
102 changes: 102 additions & 0 deletions bin/lib/compile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import { copyFolder, getFilesWithExt } from './file-util';
import { warn } from './log';
import { getPackageJson } from './package-util';

export const ES5_DIR_NAME = '_origami-es5';
export const ES2015_DIR_NAME = '_origami-es2015';

export function getEs5Dir(packagePath: string): string {
return path.join(packagePath, ES5_DIR_NAME);
}

export function getEs2015Dir(packagePath: string): string {
return path.join(packagePath, ES2015_DIR_NAME);
}

export interface CompileOptions {
force?: boolean;
}

export async function compile(
packagePath: string,
opts: CompileOptions = {}
): Promise<boolean> {
try {
if (
!needsCompile(packagePath) ||
(isCompiled(packagePath) && !opts.force)
) {
return false;
}

const jsFiles = await getFilesWithExt('.js', packagePath, {
excludeDir: [ES5_DIR_NAME, ES2015_DIR_NAME]
});
if (!jsFiles.length) {
return false;
}

await copyFolder(packagePath, getEs2015Dir(packagePath), {
include: jsFiles,
excludeDir: [ES5_DIR_NAME, ES2015_DIR_NAME]
});
const program = ts.createProgram(jsFiles, {
allowJs: true,
importHelpers: true,
module: ts.ModuleKind.ES2015,
moduleResolution: ts.ModuleResolutionKind.NodeJs,
noEmitOnError: true,
outDir: getEs5Dir(packagePath),
skipLibCheck: true,
target: ts.ScriptTarget.ES5
});

const emitResult = program.emit();
if (emitResult.emitSkipped) {
const allDiagnostics = ts
.getPreEmitDiagnostics(program)
.concat(emitResult.diagnostics);
let errorMessage = '';
allDiagnostics.forEach(diag => {
const message = ts.flattenDiagnosticMessageText(
diag.messageText,
ts.sys.newLine
);
if (diag.file) {
const pos = ts.getLineAndCharacterOfPosition(diag.file, diag.start!);
errorMessage += `${diag.file.fileName}:${pos.line +
1}:${pos.character + 1} ${message}`;
} else {
errorMessage += message;
}
});

throw new Error(errorMessage);
}

return true;
} catch (error) {
warn('Failed to compile()');
throw error;
}
}

function needsCompile(packagePath: string): boolean {
const packageJson = getPackageJson(packagePath);
return ![
'es2015',
'esm2015',
'esm5',
'fesm2015',
'fesm5',
'esm2015',
'module'
].some(key => key in packageJson);
}

function isCompiled(packagePath: string): boolean {
return fs.existsSync(getEs5Dir(packagePath));
}
83 changes: 83 additions & 0 deletions bin/lib/file-util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import * as fs from 'fs';
import * as path from 'path';
import * as util from 'util';
import { warn } from './log';

const existsAsync = util.promisify(fs.exists);
const mkdirAsync = util.promisify(fs.mkdir);
const readdirAsync = util.promisify(fs.readdir);
const readFileAsync = util.promisify(fs.readFile);
const statAsync = util.promisify(fs.stat);
const writeFileAsync = util.promisify(fs.writeFile);

export interface CopyFolderSyncOptions {
excludeDir?: string[];
include?: string[];
}

export async function copyFolder(
fromDir: string,
toDir: string,
opts: CopyFolderSyncOptions = {}
): Promise<void> {
try {
if (!(await existsAsync(toDir))) {
await mkdirAsync(toDir);
}

const names = await readdirAsync(fromDir);
for (let name of names) {
if (opts.excludeDir && opts.excludeDir.indexOf(name) > -1) {
continue;
}

const fileOrFolder = path.join(fromDir, name);
const target = path.join(toDir, name);
if ((await statAsync(fileOrFolder)).isDirectory()) {
if (!(await existsAsync(target))) {
await mkdirAsync(target);
}

await copyFolder(fileOrFolder, target, opts);
} else if (!opts.include || opts.include.indexOf(fileOrFolder) > -1) {
await writeFileAsync(target, await readFileAsync(fileOrFolder));
}
}
} catch (error) {
warn('Failed to copyFolder()');
throw error;
}
}

export interface GetFilesWithExtOptions {
excludeDir?: string[];
}

export async function getFilesWithExt(
ext: string,
directory: string,
opts: GetFilesWithExtOptions = {},
allFiles: string[] = []
): Promise<string[]> {
try {
const directoryName = path.basename(directory);
if (opts.excludeDir && opts.excludeDir.indexOf(directoryName) > -1) {
return [];
}

const files = await readdirAsync(directory);
for (let file of files) {
const absolutePath = path.resolve(directory, file);
if ((await statAsync(absolutePath)).isDirectory()) {
await getFilesWithExt(ext, absolutePath, opts, allFiles);
} else if (path.extname(absolutePath) === ext) {
allFiles.push(absolutePath);
}
}

return allFiles;
} catch (error) {
warn('Failed to getFilesWithExt()');
throw error;
}
}
24 changes: 24 additions & 0 deletions bin/lib/log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import chalk, { Chalk } from 'chalk';

export function info(message: string) {
log(message, chalk.white);
}

export function warn(message: string) {
log(message, chalk.yellow);
}

export function error(message: string, fatal?: boolean) {
log(message, chalk.red);
if (fatal) {
process.exit(1);
}
}

export function getPrefix(): string {
return chalk.cyan('Origami: ');
}

function log(message: string, color: Chalk) {
console.log(getPrefix() + color(message));
}
28 changes: 28 additions & 0 deletions bin/lib/package-util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as fs from 'fs';
import * as glob from 'glob';
import * as path from 'path';

export function getPackagePaths(patterns: string[]): string[] {
const paths: string[] = [];
patterns.forEach(pattern => {
paths.push(...glob.sync(pattern));
});

return paths.filter(p => isPackagePath(p)).map(p => path.resolve(p));
}

export function isPackagePath(packagePath: string): boolean {
return (
fs.existsSync(packagePath) &&
fs.statSync(packagePath).isDirectory() &&
fs.existsSync(getPackageJsonPath(packagePath))
);
}

export function getPackageJsonPath(packagePath: string): string {
return path.join(packagePath, 'package.json');
}

export function getPackageJson(packagePath: string): any {
return JSON.parse(fs.readFileSync(getPackageJsonPath(packagePath), 'utf8'));
}
19 changes: 19 additions & 0 deletions bin/lib/prepare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { compile, getEs2015Dir, getEs5Dir } from './compile';
import { copyFolder } from './file-util';

export interface PrepareOptions {
es5: boolean;
force?: boolean;
}

export async function prepare(
packagePath: string,
opts: PrepareOptions
): Promise<void> {
await compile(packagePath, opts);
if (opts.es5) {
await copyFolder(getEs5Dir(packagePath), packagePath);
} else {
await copyFolder(getEs2015Dir(packagePath), packagePath);
}
}
Loading

0 comments on commit 9b5d70b

Please sign in to comment.