Skip to content

Commit b000445

Browse files
filipesilvaZhicheng Wang
authored and
Zhicheng Wang
committed
feat(@angular/cli): allow assets from outside of app root.
Fix angular#3555 Close angular#4691 BREAKING CHANGE: 'assets' as a string in angular-cli.json is no longer allowed, use an array instead.
1 parent 8e8af4c commit b000445

File tree

8 files changed

+276
-120
lines changed

8 files changed

+276
-120
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,39 @@
11
# Project assets
22

3-
You use the `assets` array in `angular-cli.json` to list files or folders you want to copy as-is when building your project:
3+
You use the `assets` array in `angular-cli.json` to list files or folders you want to copy as-is
4+
when building your project.
5+
6+
By default, the `src/assets/` folder and `src/favicon.ico` are copied over.
7+
48
```json
59
"assets": [
610
"assets",
711
"favicon.ico"
812
]
9-
```
13+
```
14+
15+
You can also further configure assets to be copied by using objects as configuration.
16+
17+
The array below does the same as the default one:
18+
19+
```json
20+
"assets": [
21+
{ "glob": "**/*", "input": "./assets/", "output": "./assets/" },
22+
{ "glob": "favicon.ico", "input": "./", "output": "./" },
23+
]
24+
```
25+
26+
`glob` is the a [node-glob](https://github.com/isaacs/node-glob) using `input` as base directory.
27+
`input` is relative to the project root (`src/` default), while `output` is
28+
relative to `outDir` (`dist` default).
29+
30+
You can use this extended configuration to copy assets from outside your project.
31+
For instance, you can copy assets from a node package:
32+
33+
```json
34+
"assets": [
35+
{ "glob": "**/*", "input": "../node_modules/some-package/images", "output": "./some-package/" },
36+
]
37+
```
38+
39+
The contents of `node_modules/some-package/images/` will be available in `dist/some-package/`.

packages/@angular/cli/lib/config/schema.json

+24-9
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,32 @@
3131
"default": "dist/"
3232
},
3333
"assets": {
34-
"oneOf": [
35-
{
36-
"type": "string"
37-
},
38-
{
39-
"type": "array",
40-
"items": {
34+
"type": "array",
35+
"items": {
36+
"oneOf": [
37+
{
4138
"type": "string"
39+
},
40+
{
41+
"type": "object",
42+
"properties": {
43+
"glob": {
44+
"type": "string",
45+
"default": ""
46+
},
47+
"input": {
48+
"type": "string",
49+
"default": ""
50+
},
51+
"output": {
52+
"type": "string",
53+
"default": ""
54+
}
55+
},
56+
"additionalProperties": false
4257
}
43-
}
44-
],
58+
]
59+
},
4560
"default": []
4661
},
4762
"deployUrl": {

packages/@angular/cli/plugins/glob-copy-webpack-plugin.ts

+70-27
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as path from 'path';
33
import * as glob from 'glob';
44
import * as denodeify from 'denodeify';
55

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

@@ -14,48 +15,90 @@ function isDirectory(path: string) {
1415
}
1516
}
1617

18+
interface Asset {
19+
originPath: string;
20+
destinationPath: string;
21+
relativePath: string;
22+
}
23+
24+
export interface Pattern {
25+
glob: string;
26+
input?: string;
27+
output?: string;
28+
}
29+
1730
export interface GlobCopyWebpackPluginOptions {
18-
patterns: string[];
31+
patterns: (string | Pattern)[];
1932
globOptions: any;
2033
}
2134

35+
// Adds an asset to the compilation assets;
36+
function addAsset(compilation: any, asset: Asset) {
37+
const realPath = path.resolve(asset.originPath, asset.relativePath);
38+
// Make sure that asset keys use forward slashes, otherwise webpack dev server
39+
const servedPath = path.join(asset.destinationPath, asset.relativePath).replace(/\\/g, '/');
40+
41+
// Don't re-add existing assets.
42+
if (compilation.assets[servedPath]) {
43+
return Promise.resolve();
44+
}
45+
46+
// Read file and add it to assets;
47+
return statPromise(realPath)
48+
.then((stat: any) => compilation.assets[servedPath] = {
49+
size: () => stat.size,
50+
source: () => fs.readFileSync(realPath)
51+
});
52+
}
53+
2254
export class GlobCopyWebpackPlugin {
2355
constructor(private options: GlobCopyWebpackPluginOptions) { }
2456

2557
apply(compiler: any): void {
2658
let { patterns, globOptions } = this.options;
27-
let context = globOptions.cwd || compiler.options.context;
28-
let optional = !!globOptions.optional;
59+
const defaultCwd = globOptions.cwd || compiler.options.context;
2960

30-
// convert dir patterns to globs
31-
patterns = patterns.map(pattern => isDirectory(path.resolve(context, pattern))
32-
? pattern += '/**/*'
33-
: pattern
34-
);
35-
36-
// force nodir option, since we can't add dirs to assets
61+
// Force nodir option, since we can't add dirs to assets.
3762
globOptions.nodir = true;
3863

64+
// Process patterns.
65+
patterns = patterns.map(pattern => {
66+
// Convert all string patterns to Pattern type.
67+
pattern = typeof pattern === 'string' ? { glob: pattern } : pattern;
68+
// Add defaults
69+
// Input is always resolved relative to the defaultCwd (appRoot)
70+
pattern.input = path.resolve(defaultCwd, pattern.input || '');
71+
pattern.output = pattern.output || '';
72+
pattern.glob = pattern.glob || '';
73+
// Convert dir patterns to globs.
74+
if (isDirectory(path.resolve(pattern.input, pattern.glob))) {
75+
pattern.glob = pattern.glob + '/**/*';
76+
}
77+
return pattern;
78+
});
79+
3980
compiler.plugin('emit', (compilation: any, cb: any) => {
40-
let globs = patterns.map(pattern => globPromise(pattern, globOptions));
41-
42-
let addAsset = (relPath: string) => compilation.assets[relPath]
43-
// don't re-add to assets
44-
? Promise.resolve()
45-
: statPromise(path.resolve(context, relPath))
46-
.then((stat: any) => compilation.assets[relPath] = {
47-
size: () => stat.size,
48-
source: () => fs.readFileSync(path.resolve(context, relPath))
49-
})
50-
.catch((err: any) => optional ? Promise.resolve() : Promise.reject(err));
81+
// Create an array of promises for each pattern glob
82+
const globs = patterns.map((pattern: Pattern) => new Promise((resolve, reject) =>
83+
// Individual patterns can override cwd
84+
globPromise(pattern.glob, Object.assign({}, globOptions, { cwd: pattern.input }))
85+
// Map the results onto an Asset
86+
.then((globResults: string[]) => globResults.map(res => ({
87+
originPath: pattern.input,
88+
destinationPath: pattern.output,
89+
relativePath: res
90+
})))
91+
.then((asset: Asset) => resolve(asset))
92+
.catch(reject)
93+
));
5194

95+
// Wait for all globs.
5296
Promise.all(globs)
53-
// flatten results
54-
.then(globResults => [].concat.apply([], globResults))
55-
// add each file to compilation assets
56-
.then((relPaths: string[]) =>
57-
Promise.all(relPaths.map((relPath: string) => addAsset(relPath))))
58-
.catch((err) => compilation.errors.push(err))
97+
// Flatten results.
98+
.then(assets => flattenDeep(assets))
99+
// Add each asset to the compilation.
100+
.then(assets =>
101+
Promise.all(assets.map((asset: Asset) => addAsset(compilation, asset))))
59102
.then(() => cb());
60103
});
61104
}

packages/@angular/cli/plugins/karma.js

+37-10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ const fs = require('fs');
44
const getTestConfig = require('../models/webpack-configs/test').getTestConfig;
55
const CliConfig = require('../models/config').CliConfig;
66

7+
function isDirectory(path) {
8+
try {
9+
return fs.statSync(path).isDirectory();
10+
} catch (_) {
11+
return false;
12+
}
13+
}
14+
715
const init = (config) => {
816
// load Angular CLI config
917
if (!config.angularCli || !config.angularCli.config) {
@@ -19,24 +27,43 @@ const init = (config) => {
1927
progress: config.angularCli.progress
2028
}
2129

22-
// add assets
30+
// Add assets. This logic is mimics the one present in GlobCopyWebpackPlugin.
2331
if (appConfig.assets) {
24-
const assets = typeof appConfig.assets === 'string' ? [appConfig.assets] : appConfig.assets;
2532
config.proxies = config.proxies || {};
26-
assets.forEach(asset => {
27-
const fullAssetPath = path.join(config.basePath, appConfig.root, asset);
28-
const isDirectory = fs.lstatSync(fullAssetPath).isDirectory();
29-
const filePattern = isDirectory ? fullAssetPath + '/**' : fullAssetPath;
30-
const proxyPath = isDirectory ? asset + '/' : asset;
33+
appConfig.assets.forEach(pattern => {
34+
// Convert all string patterns to Pattern type.
35+
pattern = typeof pattern === 'string' ? { glob: pattern } : pattern;
36+
// Add defaults.
37+
// Input is always resolved relative to the appRoot.
38+
pattern.input = path.resolve(appRoot, pattern.input || '');
39+
pattern.output = pattern.output || '';
40+
pattern.glob = pattern.glob || '';
41+
42+
// Build karma file pattern.
43+
const assetPath = path.join(pattern.input, pattern.glob);
44+
const filePattern = isDirectory(assetPath) ? assetPath + '/**' : assetPath;
3145
config.files.push({
3246
pattern: filePattern,
3347
included: false,
3448
served: true,
3549
watched: true
3650
});
37-
// The `files` entry serves the file from `/base/{appConfig.root}/{asset}`
38-
// so, we need to add a URL rewrite that exposes the asset as `/{asset}` only
39-
config.proxies['/' + proxyPath] = '/base/' + appConfig.root + '/' + proxyPath;
51+
52+
// The `files` entry serves the file from `/base/{asset.input}/{asset.glob}`.
53+
// We need to add a URL rewrite that exposes the asset as `/{asset.output}/{asset.glob}`.
54+
let relativePath, proxyPath;
55+
if (fs.existsSync(assetPath)) {
56+
relativePath = path.relative(config.basePath, assetPath);
57+
proxyPath = path.join(pattern.output, pattern.glob);
58+
} else {
59+
// For globs (paths that don't exist), proxy pattern.output to pattern.input.
60+
relativePath = path.relative(config.basePath, pattern.input);
61+
proxyPath = path.join(pattern.output);
62+
63+
}
64+
// Proxy paths must have only forward slashes.
65+
proxyPath = proxyPath.replace(/\\/g, '/');
66+
config.proxies['/' + proxyPath] = '/base/' + relativePath;
4067
});
4168
}
4269

packages/@angular/cli/tasks/serve.ts

-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ export default Task.extend({
9898
}
9999

100100
const webpackDevServerConfiguration: IWebpackDevServerConfigurationOptions = {
101-
contentBase: path.join(this.project.root, `./${appConfig.root}`),
102101
headers: { 'Access-Control-Allow-Origin': '*' },
103102
historyApiFallback: {
104103
index: `/${appConfig.index}`,

0 commit comments

Comments
 (0)