UNPKG

5.56 kBPlain TextView Raw
1import resolve from 'resolve';
2import { join } from 'path';
3import { readFileSync } from 'fs';
4import { Configuration } from 'webpack';
5
6const cache: WeakMap<any, Package> = new WeakMap();
7let pkgGeneration = 0;
8
9export function reloadDevPackages() {
10 pkgGeneration++;
11}
12
13export interface Options {
14 exclude?: string[];
15 alias?: { [fromName: string]: string };
16 webpack?: Configuration;
17 publicAssetURL?: string;
18 forbidEval?: boolean;
19}
20
21export default class Package {
22 public name: string;
23 public root: string;
24 public isAddon: boolean;
25 public babelOptions: any;
26 public babelMajorVersion: number;
27 private autoImportOptions: Options | undefined;
28 private emberCLIBabelExtensions: string[];
29 private isAddonCache = new Map<string, boolean>();
30 private isDeveloping: boolean;
31 private pkgGeneration: number;
32 private pkgCache: any;
33
34 static lookup(appOrAddon: any): Package {
35 if (!cache.has(appOrAddon)) {
36 cache.set(appOrAddon, new this(appOrAddon));
37 }
38 return cache.get(appOrAddon)!;
39 }
40
41 constructor(appOrAddon: any) {
42 this.name = appOrAddon.parent.pkg.name;
43 this.root = appOrAddon.parent.root;
44 this.isAddon = appOrAddon.parent !== appOrAddon.project;
45 this.isDeveloping = !this.isAddon || this.root === appOrAddon.project.root;
46
47 // This is the per-package options from ember-cli
48 let options = this.isAddon
49 ? appOrAddon.parent.options
50 : appOrAddon.app.options;
51
52 // Stash our own config options
53 this.autoImportOptions = options.autoImport;
54
55 this.emberCLIBabelExtensions = options['ember-cli-babel']
56 && options['ember-cli-babel'].extensions || ['js'];
57
58 let { babelOptions, version } = this.buildBabelOptions(appOrAddon.parent, options);
59
60 this.babelOptions = babelOptions;
61 this.babelMajorVersion = version;
62
63 this.pkgCache = appOrAddon.parent.pkg;
64 this.pkgGeneration = pkgGeneration;
65 }
66
67 private buildBabelOptions(instance: any, options: any) {
68 // Generate the same babel options that the package (meaning app or addon)
69 // is using. We will use these so we can configure our parser to
70 // match.
71 let babelAddon = instance.addons.find(
72 (addon: any) => addon.name === 'ember-cli-babel'
73 );
74 let babelOptions = babelAddon.buildBabelOptions(options);
75 // https://github.com/babel/ember-cli-babel/issues/227
76 delete babelOptions.annotation;
77 delete babelOptions.throwUnlessParallelizable;
78 delete babelOptions.filterExtensions;
79 if (babelOptions.plugins) {
80 babelOptions.plugins = babelOptions.plugins.filter(
81 (p: any) => !p._parallelBabel
82 );
83 }
84 let version = parseInt(babelAddon.pkg.version.split('.')[0], 10);
85 return { babelOptions, version };
86 }
87
88 private get pkg() {
89 if (
90 !this.pkgCache ||
91 (this.isDeveloping && pkgGeneration !== this.pkgGeneration)
92 ) {
93 // avoiding `require` here because we don't want to go through the
94 // require cache.
95 this.pkgCache = JSON.parse(
96 readFileSync(join(this.root, 'package.json'), 'utf-8')
97 );
98 this.pkgGeneration = pkgGeneration;
99 }
100 return this.pkgCache;
101 }
102
103 get namespace(): string {
104 // This namespacing ensures we can be used by multiple packages as
105 // well as by an addon and its dummy app simultaneously
106 return `${this.name}/${this.isAddon ? 'addon' : 'app'}`;
107 }
108
109 hasDependency(name: string): boolean {
110 let pkg = this.pkg;
111 return (
112 (pkg.dependencies && Boolean(pkg.dependencies[name])) ||
113 (pkg.devDependencies && Boolean(pkg.devDependencies[name])) ||
114 (pkg.peerDependencies && Boolean(pkg.peerDependencies[name]))
115 );
116 }
117
118 private hasNonDevDependency(name: string): boolean {
119 let pkg = this.pkg;
120 return (
121 (pkg.dependencies && Boolean(pkg.dependencies[name])) ||
122 (pkg.peerDependencies && Boolean(pkg.peerDependencies[name]))
123 );
124 }
125
126 isEmberAddonDependency(name: string): boolean {
127 if (!this.isAddonCache.has(name)) {
128 let packageJSON = require(resolve.sync(`${name}/package.json`, {
129 basedir: this.root
130 }));
131 let keywords = packageJSON.keywords;
132 this.isAddonCache.set(name, keywords && keywords.includes('ember-addon'));
133 }
134 return this.isAddonCache.get(name) || false;
135 }
136
137 assertAllowedDependency(name: string) {
138 if (this.isAddon && !this.hasNonDevDependency(name)) {
139 throw new Error(
140 `${
141 this.name
142 } tried to import "${name}" from addon code, but "${name}" is a devDependency. You may need to move it into dependencies.`
143 );
144 }
145 }
146
147 excludesDependency(name: string): boolean {
148 return Boolean(
149 this.autoImportOptions &&
150 this.autoImportOptions.exclude &&
151 this.autoImportOptions.exclude.includes(name)
152 );
153 }
154
155 get webpackConfig(): any {
156 return this.autoImportOptions && this.autoImportOptions.webpack;
157 }
158
159 aliasFor(name: string): string {
160 return (
161 (this.autoImportOptions &&
162 this.autoImportOptions.alias &&
163 this.autoImportOptions.alias[name]) ||
164 name
165 );
166 }
167
168 get fileExtensions(): string[] {
169 return this.emberCLIBabelExtensions;
170 }
171
172 get publicAssetURL(): string | undefined {
173 return this.autoImportOptions && this.autoImportOptions.publicAssetURL;
174 }
175
176 get forbidsEval(): boolean {
177 // only apps (not addons) are allowed to set this, because it's motivated by
178 // the apps own Content Security Policy.
179 return Boolean(
180 !this.isAddon && this.autoImportOptions && this.autoImportOptions.forbidEval
181 );
182 }
183}