UNPKG

8.08 kBJavaScriptView Raw
1#!/usr/bin/env node
2// Copyright IBM Corp. and LoopBack contributors 2017,2020. All Rights Reserved.
3// Node module: @loopback/build
4// This file is licensed under the MIT License.
5// License text available at https://opensource.org/licenses/MIT
6
7/*
8========
9
10Usage:
11 node ./bin/compile-package <target>
12
13Where <target> is one of es2015, es2017 or es2018.
14
15========
16*/
17
18'use strict';
19
20const debug = require('debug')('loopback:build');
21const utils = require('./utils');
22const path = require('path');
23const fs = require('fs');
24const {globSync} = require('glob');
25const fse = require('fs-extra');
26const {buildOpts: buildOptions} = require('typescript');
27
28function run(argv, options) {
29 if (options === true) {
30 options = {dryRun: true};
31 } else {
32 options = options || {};
33 }
34
35 const packageDir = utils.getPackageDir();
36
37 const runnerName = argv[1];
38
39 const compilerOpts = argv.slice(2);
40 const runnerIsLbttsc = runnerName.includes('lb-ttsc');
41 const isUseTtscSet = utils.isOptionSet(compilerOpts, '--use-ttypescript');
42 const useTtsc = runnerIsLbttsc || isUseTtscSet;
43 const isTargetSet = utils.isOptionSet(compilerOpts, '--target');
44 const isOutDirSet = utils.isOptionSet(compilerOpts, '--outDir');
45 const isProjectSet = utils.isOptionSet(compilerOpts, '-p', '--project');
46 const isCopyResourcesSet = utils.isOptionSet(
47 compilerOpts,
48 '--copy-resources',
49 );
50
51 let TSC_CLI = 'typescript/lib/tsc';
52 if (useTtsc) {
53 try {
54 require.resolve('ttypescript');
55 TSC_CLI = 'ttypescript/lib/tsc';
56 } catch (e) {
57 if (isUseTtscSet) {
58 console.error(
59 'Error using the --use-ttypescript option - ttypescript is not installed',
60 );
61 } else {
62 console.error('Error using lb-ttsc - ttypescript is not installed');
63 }
64 process.exit(1);
65 }
66 }
67 debug(`Using ${TSC_CLI} to compile package`);
68
69 // --copy-resources and --use-ttypescript are not a TS Compiler options,
70 // so we remove them from the list of compiler options to avoid compiler
71 // errors.
72 if (isCopyResourcesSet) {
73 compilerOpts.splice(compilerOpts.indexOf('--copy-resources'), 1);
74 }
75
76 if (isUseTtscSet) {
77 compilerOpts.splice(compilerOpts.indexOf('--use-ttypescript'), 1);
78 }
79
80 let target;
81 if (isTargetSet) {
82 const targetIx = compilerOpts.indexOf('--target');
83 target = compilerOpts[targetIx + 1];
84 compilerOpts.splice(targetIx, 2);
85 }
86
87 let outDir;
88 if (isOutDirSet) {
89 const outDirIx = compilerOpts.indexOf('--outDir');
90 outDir = path.resolve(process.cwd(), compilerOpts[outDirIx + 1]);
91 compilerOpts.splice(outDirIx, 2);
92 }
93
94 let tsConfigFile;
95
96 let rootDir;
97 if (!isProjectSet) {
98 rootDir = utils.getRootDir();
99 tsConfigFile = utils.getConfigFile('tsconfig.build.json', 'tsconfig.json');
100 if (tsConfigFile === path.join(rootDir, 'config/tsconfig.build.json')) {
101 // No local tsconfig.build.json or tsconfig.json found
102 let baseConfigFile = path.join(rootDir, 'config/tsconfig.common.json');
103 baseConfigFile = path.relative(packageDir, baseConfigFile);
104 if (baseConfigFile.indexOf('..' + path.sep) !== 0) {
105 // tsconfig only supports relative or rooted path
106 baseConfigFile = '.' + path.sep + baseConfigFile;
107 }
108 baseConfigFile = baseConfigFile.replace(/\\/g, '/');
109 // Create tsconfig.json under the package as it's required to parse
110 // include/exclude correctly
111 tsConfigFile = path.join(packageDir, 'tsconfig.json');
112 fs.writeFileSync(
113 tsConfigFile,
114 JSON.stringify(
115 {
116 extends: baseConfigFile,
117 compilerOptions: {
118 outDir: 'dist',
119 rootDir: 'src',
120 },
121 include: ['src'],
122 },
123 null,
124 ' ',
125 ),
126 );
127 }
128 }
129
130 const args = [];
131
132 let cwd = process.env.LERNA_ROOT_PATH || process.cwd();
133 if (tsConfigFile && fs.existsSync(tsConfigFile)) {
134 const tsconfig = require(tsConfigFile);
135 if (tsconfig.references) {
136 args.unshift('-b');
137 // Reset the cwd for a composite project
138 cwd = process.cwd();
139 } else {
140 // Make the config file relative the current directory
141 args.push('-p', path.relative(cwd, tsConfigFile));
142 }
143 }
144
145 if (outDir) {
146 args.push('--outDir', path.relative(cwd, outDir));
147 }
148
149 if (target) {
150 args.push('--target', target);
151 }
152
153 if (isCopyResourcesSet) {
154 // Since outDir is set, ts files are compiled into that directory.
155 // If copy-resources flag is passed, copy resources (non-ts files)
156 // to the same outDir as well.
157 copyResources(rootDir, packageDir, tsConfigFile, outDir, options);
158 }
159
160 if (target) {
161 args.push('--target', target);
162 }
163
164 args.push(...compilerOpts);
165
166 const validArgs = validArgsForBuild(args);
167
168 return utils.runCLI(TSC_CLI, validArgs, {cwd, ...options});
169}
170
171/**
172 * `tsc -b` only accepts valid arguments. `npm run build -- --<other-arg>` may
173 * pass in extra arguments. We need to remove such arguments.
174 * @param {string[]} args An array of arguments
175 */
176function validArgsForBuild(args) {
177 const validBooleanOptions = [];
178 const validValueOptions = [];
179
180 // See https://github.com/microsoft/TypeScript/blob/v3.8.3/src/compiler/commandLineParser.ts#L122
181 buildOptions.forEach(opt => {
182 /**
183 * name: "help",
184 * shortName: "h",
185 * type: "boolean",
186 */
187 const options =
188 opt.type === 'boolean' ? validBooleanOptions : validValueOptions;
189 options.push(`--${opt.name}`);
190 if (opt.shortName) {
191 validBooleanOptions.push(`-${opt.shortName}`);
192 }
193 });
194 let validArgs = args;
195 if (args.includes('-b') || args.includes('--build')) {
196 validArgs = filterArgs(args, (arg, next) => {
197 if (validBooleanOptions.includes(arg)) {
198 return next === 'false' || next === 'true' ? 2 : 1;
199 }
200 if (validValueOptions.includes(arg)) return 2;
201 return 0;
202 });
203 // `-b` has to be the first argument
204 validArgs.unshift('-b');
205 }
206 debug('Valid args for tsc -b', validArgs);
207 return validArgs;
208}
209
210/**
211 * Filter arguments by name from the args
212 * @param {string[]} args - Array of args
213 * @param {function} filter - (arg: string) => 0, 1, 2
214 */
215function filterArgs(args, filter) {
216 const validArgs = [];
217 let i = 0;
218 while (i < args.length) {
219 const length = filter(args[i], args[i + 1]);
220 if (length === 0) {
221 i++;
222 } else if (length === 1) {
223 validArgs.push(args[i]);
224 i++;
225 } else if (length === 2) {
226 validArgs.push(args[i], args[i + 1]);
227 i += 2;
228 }
229 }
230 return validArgs;
231}
232
233module.exports = run;
234if (require.main === module) run(process.argv);
235
236function copyResources(rootDir, packageDir, tsConfigFile, outDir, options) {
237 if (!rootDir) {
238 console.warn('Ignoring --copy-resources option - rootDir was not set.');
239 return;
240 }
241 if (!tsConfigFile) {
242 console.warn(
243 'Ignoring --copy-resources option - no tsconfig file was found.',
244 );
245 return;
246 }
247
248 const tsConfig = require(tsConfigFile);
249
250 if (!outDir) {
251 outDir = tsConfig.compilerOptions && tsConfig.compilerOptions.outDir;
252 if (!outDir) {
253 console.warn(
254 'Ignoring --copy-resources option - outDir was not configured.',
255 );
256 return;
257 }
258 }
259
260 const dirs = tsConfig.include
261 ? tsConfig.include.join('|')
262 : ['src', 'test'].join('|');
263
264 const compilerRootDir =
265 (tsConfig.compilerOptions && tsConfig.compilerOptions.rootDir) || '';
266
267 const pattern = `@(${dirs})/**/!(*.ts)`;
268 const files = globSync(pattern, {root: packageDir, nodir: true});
269 for (const file of files) {
270 /**
271 * Trim path that matches tsConfig.compilerOptions.rootDir
272 */
273 let targetFile = file;
274 if (compilerRootDir && file.startsWith(compilerRootDir + '/')) {
275 targetFile = file.substring(compilerRootDir.length + 1);
276 }
277
278 const copyFrom = path.join(packageDir, file);
279 const copyTo = path.join(outDir, targetFile);
280 debug(' copy %j to %j', copyFrom, copyTo);
281 if (!options.dryRun) {
282 fse.copySync(copyFrom, copyTo);
283 }
284 }
285}