Skip to content

Commit

Permalink
feat(@angular/cli): allow assets from outside of app root.
Browse files Browse the repository at this point in the history
Fix #3555
Close #4691

BREAKING CHANGE: 'assets' as a string in angular-cli.json is no longer allowed, use an array instead.
  • Loading branch information
filipesilva committed Feb 15, 2017
1 parent ca29eab commit 9e91d86
Show file tree
Hide file tree
Showing 8 changed files with 276 additions and 120 deletions.
34 changes: 32 additions & 2 deletions docs/documentation/stories/asset-configuration.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,39 @@
# Project assets

You use the `assets` array in `angular-cli.json` to list files or folders you want to copy as-is when building your project:
You use the `assets` array in `angular-cli.json` to list files or folders you want to copy as-is
when building your project.

By default, the `src/assets/` folder and `src/favicon.ico` are copied over.

```json
"assets": [
"assets",
"favicon.ico"
]
```
```

You can also further configure assets to be copied by using objects as configuration.

The array below does the same as the default one:

```json
"assets": [
{ "glob": "**/*", "input": "./assets/", "output": "./assets/" },
{ "glob": "favicon.ico", "input": "./", "output": "./" },
]
```

`glob` is the a [node-glob](https://github.com/isaacs/node-glob) using `input` as base directory.
`input` is relative to the project root (`src/` default), while `output` is
relative to `outDir` (`dist` default).

You can use this extended configuration to copy assets from outside your project.
For instance, you can copy assets from a node package:

```json
"assets": [
{ "glob": "**/*", "input": "../node_modules/some-package/images", "output": "./some-package/" },
]
```

The contents of `node_modules/some-package/images/` will be available in `dist/some-package/`.
33 changes: 24 additions & 9 deletions packages/@angular/cli/lib/config/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,32 @@
"default": "dist/"
},
"assets": {
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "array",
"items": {
"oneOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"glob": {
"type": "string",
"default": ""
},
"input": {
"type": "string",
"default": ""
},
"output": {
"type": "string",
"default": ""
}
},
"additionalProperties": false
}
}
],
]
},
"default": []
},
"deployUrl": {
Expand Down
97 changes: 70 additions & 27 deletions packages/@angular/cli/plugins/glob-copy-webpack-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as path from 'path';
import * as glob from 'glob';
import * as denodeify from 'denodeify';

const flattenDeep = require('lodash/flattenDeep');
const globPromise = <any>denodeify(glob);
const statPromise = <any>denodeify(fs.stat);

Expand All @@ -14,48 +15,90 @@ function isDirectory(path: string) {
}
}

interface Asset {
originPath: string;
destinationPath: string;
relativePath: string;
}

export interface Pattern {
glob: string;
input?: string;
output?: string;
}

export interface GlobCopyWebpackPluginOptions {
patterns: string[];
patterns: (string | Pattern)[];
globOptions: any;
}

// Adds an asset to the compilation assets;
function addAsset(compilation: any, asset: Asset) {
const realPath = path.resolve(asset.originPath, asset.relativePath);
// Make sure that asset keys use forward slashes, otherwise webpack dev server
const servedPath = path.join(asset.destinationPath, asset.relativePath).replace(/\\/g, '/');

// Don't re-add existing assets.
if (compilation.assets[servedPath]) {
return Promise.resolve();
}

// Read file and add it to assets;
return statPromise(realPath)
.then((stat: any) => compilation.assets[servedPath] = {
size: () => stat.size,
source: () => fs.readFileSync(realPath)
});
}

export class GlobCopyWebpackPlugin {
constructor(private options: GlobCopyWebpackPluginOptions) { }

apply(compiler: any): void {
let { patterns, globOptions } = this.options;
let context = globOptions.cwd || compiler.options.context;
let optional = !!globOptions.optional;
const defaultCwd = globOptions.cwd || compiler.options.context;

// convert dir patterns to globs
patterns = patterns.map(pattern => isDirectory(path.resolve(context, pattern))
? pattern += '/**/*'
: pattern
);

// force nodir option, since we can't add dirs to assets
// Force nodir option, since we can't add dirs to assets.
globOptions.nodir = true;

// Process patterns.
patterns = patterns.map(pattern => {
// Convert all string patterns to Pattern type.
pattern = typeof pattern === 'string' ? { glob: pattern } : pattern;
// Add defaults
// Input is always resolved relative to the defaultCwd (appRoot)
pattern.input = path.resolve(defaultCwd, pattern.input || '');
pattern.output = pattern.output || '';
pattern.glob = pattern.glob || '';
// Convert dir patterns to globs.
if (isDirectory(path.resolve(pattern.input, pattern.glob))) {
pattern.glob = pattern.glob + '/**/*';
}
return pattern;
});

compiler.plugin('emit', (compilation: any, cb: any) => {
let globs = patterns.map(pattern => globPromise(pattern, globOptions));

let addAsset = (relPath: string) => compilation.assets[relPath]
// don't re-add to assets
? Promise.resolve()
: statPromise(path.resolve(context, relPath))
.then((stat: any) => compilation.assets[relPath] = {
size: () => stat.size,
source: () => fs.readFileSync(path.resolve(context, relPath))
})
.catch((err: any) => optional ? Promise.resolve() : Promise.reject(err));
// Create an array of promises for each pattern glob
const globs = patterns.map((pattern: Pattern) => new Promise((resolve, reject) =>
// Individual patterns can override cwd
globPromise(pattern.glob, Object.assign({}, globOptions, { cwd: pattern.input }))
// Map the results onto an Asset
.then((globResults: string[]) => globResults.map(res => ({
originPath: pattern.input,
destinationPath: pattern.output,
relativePath: res
})))
.then((asset: Asset) => resolve(asset))
.catch(reject)
));

// Wait for all globs.
Promise.all(globs)
// flatten results
.then(globResults => [].concat.apply([], globResults))
// add each file to compilation assets
.then((relPaths: string[]) =>
Promise.all(relPaths.map((relPath: string) => addAsset(relPath))))
.catch((err) => compilation.errors.push(err))
// Flatten results.
.then(assets => flattenDeep(assets))
// Add each asset to the compilation.
.then(assets =>
Promise.all(assets.map((asset: Asset) => addAsset(compilation, asset))))
.then(() => cb());
});
}
Expand Down
47 changes: 37 additions & 10 deletions packages/@angular/cli/plugins/karma.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ const fs = require('fs');
const getTestConfig = require('../models/webpack-configs/test').getTestConfig;
const CliConfig = require('../models/config').CliConfig;

function isDirectory(path) {
try {
return fs.statSync(path).isDirectory();
} catch (_) {
return false;
}
}

const init = (config) => {
// load Angular CLI config
if (!config.angularCli || !config.angularCli.config) {
Expand All @@ -19,24 +27,43 @@ const init = (config) => {
progress: config.angularCli.progress
}

// add assets
// Add assets. This logic is mimics the one present in GlobCopyWebpackPlugin.
if (appConfig.assets) {
const assets = typeof appConfig.assets === 'string' ? [appConfig.assets] : appConfig.assets;
config.proxies = config.proxies || {};
assets.forEach(asset => {
const fullAssetPath = path.join(config.basePath, appConfig.root, asset);
const isDirectory = fs.lstatSync(fullAssetPath).isDirectory();
const filePattern = isDirectory ? fullAssetPath + '/**' : fullAssetPath;
const proxyPath = isDirectory ? asset + '/' : asset;
appConfig.assets.forEach(pattern => {
// Convert all string patterns to Pattern type.
pattern = typeof pattern === 'string' ? { glob: pattern } : pattern;
// Add defaults.
// Input is always resolved relative to the appRoot.
pattern.input = path.resolve(appRoot, pattern.input || '');
pattern.output = pattern.output || '';
pattern.glob = pattern.glob || '';

// Build karma file pattern.
const assetPath = path.join(pattern.input, pattern.glob);
const filePattern = isDirectory(assetPath) ? assetPath + '/**' : assetPath;
config.files.push({
pattern: filePattern,
included: false,
served: true,
watched: true
});
// The `files` entry serves the file from `/base/{appConfig.root}/{asset}`
// so, we need to add a URL rewrite that exposes the asset as `/{asset}` only
config.proxies['/' + proxyPath] = '/base/' + appConfig.root + '/' + proxyPath;

// The `files` entry serves the file from `/base/{asset.input}/{asset.glob}`.
// We need to add a URL rewrite that exposes the asset as `/{asset.output}/{asset.glob}`.
let relativePath, proxyPath;
if (fs.existsSync(assetPath)) {
relativePath = path.relative(config.basePath, assetPath);
proxyPath = path.join(pattern.output, pattern.glob);
} else {
// For globs (paths that don't exist), proxy pattern.output to pattern.input.
relativePath = path.relative(config.basePath, pattern.input);
proxyPath = path.join(pattern.output);

}
// Proxy paths must have only forward slashes.
proxyPath = proxyPath.replace(/\\/g, '/');
config.proxies['/' + proxyPath] = '/base/' + relativePath;
});
}

Expand Down
1 change: 0 additions & 1 deletion packages/@angular/cli/tasks/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ export default Task.extend({
}

const webpackDevServerConfiguration: IWebpackDevServerConfigurationOptions = {
contentBase: path.join(this.project.root, `./${appConfig.root}`),
headers: { 'Access-Control-Allow-Origin': '*' },
historyApiFallback: {
index: `/${appConfig.index}`,
Expand Down
Loading

0 comments on commit 9e91d86

Please sign in to comment.