1 | const Config = require('webpack-chain');
|
2 | const merge = require('deepmerge');
|
3 | const Future = require('fluture');
|
4 | const mitt = require('mitt');
|
5 | const { join } = require('path');
|
6 | const {
|
7 | defaultTo, is, map, omit, replace
|
8 | } = require('ramda');
|
9 | const { normalizePath, toArray, req, pathOptions } = require('./utils');
|
10 |
|
11 | class Api {
|
12 | constructor(options) {
|
13 | this.options = this.getOptions(options);
|
14 | this.listeners = {};
|
15 | this.emitter = mitt(this.listeners);
|
16 | this.commands = {};
|
17 | this.commandDescriptions = {};
|
18 | this.config = new Config();
|
19 | }
|
20 |
|
21 |
|
22 | getOptions(opts = {}) {
|
23 | let moduleExtensions = new Set(['js', 'jsx', 'vue', 'ts', 'tsx', 'mjs']);
|
24 | const options = merge.all([
|
25 | {
|
26 | env: {
|
27 | NODE_ENV: 'development'
|
28 | },
|
29 | debug: false,
|
30 | quiet: false
|
31 | },
|
32 | opts.mains ? { mains: opts.mains } : { mains: { index: 'index' } },
|
33 | omit(['mains'], opts)
|
34 | ]);
|
35 |
|
36 | Object
|
37 | .keys(options.env)
|
38 | .forEach(env => process.env[env] = options.env[env]);
|
39 |
|
40 | pathOptions.forEach(([path, defaultValue, getNormalizeBase]) => {
|
41 | let value = defaultTo(defaultValue, options[path]);
|
42 |
|
43 | Object.defineProperty(options, path, {
|
44 | enumerable: true,
|
45 | get() {
|
46 | return normalizePath(getNormalizeBase(options), value);
|
47 | },
|
48 | set(newValue) {
|
49 | value = defaultTo(defaultValue, newValue);
|
50 | }
|
51 | });
|
52 | });
|
53 |
|
54 | try {
|
55 | options.packageJson = require(join(options.root, 'package.json'));
|
56 | } catch (err) {
|
57 | options.packageJson = null;
|
58 | }
|
59 |
|
60 | Object.defineProperty(options, 'extensions', {
|
61 | enumerable: true,
|
62 | get() {
|
63 | return [...moduleExtensions];
|
64 | },
|
65 | set(extensions) {
|
66 | moduleExtensions = new Set(extensions.map(replace('.', '')));
|
67 | }
|
68 | });
|
69 |
|
70 | this.bindMainsOnOptions(options);
|
71 |
|
72 | return options;
|
73 | }
|
74 |
|
75 |
|
76 | mergeOptions(options, newOptions) {
|
77 |
|
78 | const paths = pathOptions.map(([path]) => path);
|
79 |
|
80 | Object
|
81 | .keys(newOptions)
|
82 | .forEach((key) => {
|
83 | if (key === 'mains') {
|
84 | this.bindMainsOnOptions(newOptions, options);
|
85 | options.mains = newOptions.mains;
|
86 | return;
|
87 | }
|
88 |
|
89 | const value = newOptions[key];
|
90 |
|
91 | if (paths.includes(key)) {
|
92 | options[key] = value;
|
93 | return;
|
94 | }
|
95 |
|
96 |
|
97 |
|
98 |
|
99 |
|
100 | if (
|
101 | options[key] &&
|
102 | (
|
103 | is(Object, options[key]) && is(Object, value) ||
|
104 | is(Array, options[key]) && is(Array, value)
|
105 | )
|
106 | ) {
|
107 | options[key] = merge(options[key], newOptions[key]);
|
108 | } else {
|
109 | options[key] = newOptions[key];
|
110 | }
|
111 | });
|
112 |
|
113 |
|
114 | return options;
|
115 | }
|
116 |
|
117 |
|
118 | bindMainsOnOptions(options, optionsSource) {
|
119 | Object
|
120 | .keys(options.mains)
|
121 | .forEach(key => {
|
122 | let value = options.mains[key];
|
123 |
|
124 | Object.defineProperty(options.mains, key, {
|
125 | enumerable: true,
|
126 | get() {
|
127 | const source = optionsSource && optionsSource.source || options.source;
|
128 |
|
129 | return normalizePath(source, value);
|
130 | },
|
131 | set(newValue) {
|
132 | value = newValue;
|
133 | }
|
134 | });
|
135 | });
|
136 |
|
137 | this.mainsProxy = new Proxy(options.mains, {
|
138 | defineProperty: (target, prop, { value }) => {
|
139 | let currentValue = value;
|
140 |
|
141 | return Reflect.defineProperty(target, prop, {
|
142 | enumerable: true,
|
143 | get() {
|
144 | const source = optionsSource && optionsSource.source || options.source;
|
145 |
|
146 | return normalizePath(source, currentValue);
|
147 | },
|
148 | set(newValue) {
|
149 | currentValue = newValue;
|
150 | }
|
151 | });
|
152 | }
|
153 | });
|
154 | }
|
155 |
|
156 |
|
157 | regexFromExtensions(extensions = this.options.extensions) {
|
158 | return new RegExp(String.raw`\.(${extensions.join('|')})$`);
|
159 | }
|
160 |
|
161 |
|
162 | emit(...args) {
|
163 | return this.emitter.emit(...args);
|
164 | }
|
165 |
|
166 |
|
167 | on(...args) {
|
168 | return this.emitter.on(...args);
|
169 | }
|
170 |
|
171 |
|
172 | off(...args) {
|
173 | return this.emitter.off(...args);
|
174 | }
|
175 |
|
176 |
|
177 | emitForAll(eventName, payload) {
|
178 | return Promise.all([
|
179 | ...(this.listeners[eventName] || []).map(f => f(payload)),
|
180 | ...(this.listeners['*'] || []).map(f => f(eventName, payload))
|
181 | ]);
|
182 | }
|
183 |
|
184 |
|
185 | register(commandName, handler, description = '') {
|
186 | this.commands[commandName] = handler;
|
187 | this.commandDescriptions[commandName] = description;
|
188 | return this;
|
189 | }
|
190 |
|
191 |
|
192 | require(moduleId, root = this.options.root) {
|
193 | return req(moduleId, root);
|
194 | }
|
195 |
|
196 |
|
197 | use(middleware, options) {
|
198 | if (is(Function, middleware)) {
|
199 |
|
200 | middleware(this, options);
|
201 | } else if (is(String, middleware)) {
|
202 |
|
203 |
|
204 | this.use(this.require(middleware, this.options.root), options);
|
205 | } else if (is(Array, middleware)) {
|
206 |
|
207 | this.use(...middleware);
|
208 | } else if (is(Object, middleware)) {
|
209 |
|
210 |
|
211 |
|
212 |
|
213 | if (middleware.options) {
|
214 | this.options = this.mergeOptions(this.options, middleware.options);
|
215 | }
|
216 |
|
217 | if (middleware.env) {
|
218 | const envMiddleware = Object
|
219 | .keys(middleware.env)
|
220 | .map((key) => {
|
221 | const envValue = this.options.env[key] || process.env[key];
|
222 | const env = middleware.env[key][envValue];
|
223 |
|
224 | if (!env) {
|
225 | return null;
|
226 | }
|
227 |
|
228 | if (!is(Object, env) || !env.options) {
|
229 | return env;
|
230 | }
|
231 |
|
232 | this.options = this.mergeOptions(this.options, env.options);
|
233 |
|
234 | return omit(['options'], env);
|
235 | });
|
236 |
|
237 | if (Array.isArray(middleware.use)) {
|
238 | map(use => this.use(use), middleware.use);
|
239 | } else {
|
240 | this.use(middleware.use);
|
241 | }
|
242 |
|
243 | map(env => this.use(env), envMiddleware.filter(Boolean));
|
244 | } else if (middleware.use) {
|
245 | if (Array.isArray(middleware.use)) {
|
246 | map(use => this.use(use), middleware.use);
|
247 | } else {
|
248 | this.use(middleware.use);
|
249 | }
|
250 | }
|
251 | }
|
252 |
|
253 | return this;
|
254 | }
|
255 |
|
256 |
|
257 | call(commandName) {
|
258 | const command = this.commands[commandName];
|
259 |
|
260 | if (!command) {
|
261 | throw new Error(`A command with the name "${commandName}" was not registered`);
|
262 | }
|
263 |
|
264 | return command(this.config.toConfig(), this);
|
265 | }
|
266 |
|
267 |
|
268 | run(commandName) {
|
269 | const emitForAll = this.emitForAll.bind(this);
|
270 |
|
271 | return Future((reject, resolve) => (this.commands[commandName] ?
|
272 | resolve() :
|
273 | reject(new Error(`A command with the name "${commandName}" was not registered`))
|
274 | ))
|
275 |
|
276 | .chain(() => Future.encaseP2(emitForAll, `pre${commandName}`, this.options.args))
|
277 |
|
278 | .chain(() => Future.encaseP2(emitForAll, 'prerun', this.options.args))
|
279 |
|
280 | .chain(() => {
|
281 | const result = this.commands[commandName](this.config.toConfig(), this);
|
282 |
|
283 | return Future.isFuture(result) ?
|
284 | result :
|
285 | Future.tryP(() => Promise.resolve().then(() => result));
|
286 | })
|
287 |
|
288 | .chain(value => Future
|
289 | .encaseP2(emitForAll, commandName, this.options.args)
|
290 | .chain(() => Future.of(value)))
|
291 |
|
292 | .chain(value => Future
|
293 | .encaseP2(emitForAll, 'run', this.options.args)
|
294 | .chain(() => Future.of(value)))
|
295 | .mapRej(toArray);
|
296 | }
|
297 | }
|
298 |
|
299 | module.exports = options => new Api(options);
|