|
1 | | -import { ExecutorContext, convertNxExecutor } from '@nrwl/devkit'; |
2 | | -import * as childProcess from 'child_process'; |
3 | | -import { resolve as nodeResolve } from 'path'; |
4 | | -import { parse, build } from 'plist'; |
5 | | -import { parseString, Builder } from 'xml2js'; |
6 | | -import { readFileSync, writeFileSync } from 'fs-extra'; |
7 | | -import { BuildBuilderSchema } from './schema'; |
| 1 | +import { ExecutorContext } from '@nrwl/devkit'; |
| 2 | +import { BuildExecutorSchema, commonExecutor } from '../../utils'; |
8 | 3 |
|
9 | | -export default async function runExecutor(options: BuildBuilderSchema, context: ExecutorContext): Promise<{ success: boolean }> { |
10 | | - return new Promise((resolve, reject) => { |
11 | | - try { |
12 | | - const projectConfig = context.workspace.projects[context.projectName]; |
13 | | - // determine if running or building only |
14 | | - const isBuild = context.targetName === 'build' || process.argv.find((a) => a === 'build' || a.endsWith(':build')); |
15 | | - if (isBuild) { |
16 | | - // allow build options to override run target options |
17 | | - const buildTarget = projectConfig.targets['build']; |
18 | | - if (buildTarget && buildTarget.options) { |
19 | | - options = { |
20 | | - ...options, |
21 | | - ...buildTarget.options, |
22 | | - }; |
23 | | - } |
24 | | - } |
25 | | - // console.log('context.projectName:', context.projectName); |
26 | | - const projectCwd = projectConfig.root; |
27 | | - // console.log('projectCwd:', projectCwd); |
28 | | - // console.log('context.targetName:', context.targetName); |
29 | | - // console.log('context.configurationName:', context.configurationName); |
30 | | - // console.log('context.target.options:', context.target.options); |
31 | | - |
32 | | - let targetConfigName = ''; |
33 | | - if (context.configurationName && context.configurationName !== 'build') { |
34 | | - targetConfigName = context.configurationName; |
35 | | - } |
36 | | - |
37 | | - // determine if any trailing args that need to be added to run/build command |
38 | | - const configTarget = targetConfigName ? `:${targetConfigName}` : ''; |
39 | | - const projectTargetCmd = `${context.projectName}:${context.targetName}${configTarget}`; |
40 | | - const projectTargetCmdIndex = process.argv.findIndex((c) => c === projectTargetCmd); |
41 | | - |
42 | | - const nsCliFileReplacements: Array<string> = []; |
43 | | - let configOptions; |
44 | | - if (context.target.configurations) { |
45 | | - configOptions = context.target.configurations[targetConfigName]; |
46 | | - // console.log('configOptions:', configOptions) |
47 | | - |
48 | | - if (isBuild) { |
49 | | - // merge any custom build options for the target |
50 | | - const targetBuildConfig = context.target.configurations['build']; |
51 | | - if (targetBuildConfig) { |
52 | | - options = { |
53 | | - ...options, |
54 | | - ...targetBuildConfig, |
55 | | - }; |
56 | | - } |
57 | | - } |
58 | | - |
59 | | - if (configOptions) { |
60 | | - if (configOptions.fileReplacements) { |
61 | | - for (const r of configOptions.fileReplacements) { |
62 | | - nsCliFileReplacements.push(`${r.replace.replace(projectCwd, './')}:${r.with.replace(projectCwd, './')}`); |
63 | | - } |
64 | | - } |
65 | | - if (configOptions.combineWithConfig) { |
66 | | - const configParts = configOptions.combineWithConfig.split(':'); |
67 | | - const combineWithTargetName = configParts[0]; |
68 | | - const combineWithTarget = projectConfig.targets[combineWithTargetName]; |
69 | | - if (combineWithTarget && combineWithTarget.configurations) { |
70 | | - if (configParts.length > 1) { |
71 | | - const configName = configParts[1]; |
72 | | - const combineWithTargetConfig = combineWithTarget.configurations[configName]; |
73 | | - // TODO: combine configOptions with combineWithConfigOptions |
74 | | - if (combineWithTargetConfig) { |
75 | | - if (combineWithTargetConfig.fileReplacements) { |
76 | | - for (const r of combineWithTargetConfig.fileReplacements) { |
77 | | - nsCliFileReplacements.push(`${r.replace.replace(projectCwd, './')}:${r.with.replace(projectCwd, './')}`); |
78 | | - } |
79 | | - } |
80 | | - } |
81 | | - } |
82 | | - } else { |
83 | | - console.warn(`Warning: No configurations will be combined. "${combineWithTargetName}" was not found for project name: "${context.projectName}"`); |
84 | | - } |
85 | | - } |
86 | | - } |
87 | | - } |
88 | | - |
89 | | - const nsOptions = []; |
90 | | - if (options.clean) { |
91 | | - nsOptions.push('clean'); |
92 | | - } else { |
93 | | - if (isBuild) { |
94 | | - nsOptions.push('build'); |
95 | | - } else if (options.prepare) { |
96 | | - nsOptions.push('prepare'); |
97 | | - } else { |
98 | | - if (options.debug === false) { |
99 | | - nsOptions.push('run'); |
100 | | - } else { |
101 | | - // default to debug mode |
102 | | - nsOptions.push('debug'); |
103 | | - } |
104 | | - } |
105 | | - |
106 | | - if (!options.platform) { |
107 | | - // a platform must be set |
108 | | - // absent of it being explicitly set, we default to 'ios' |
109 | | - // this helps cases where nx run-many is used for general targets used in, for example, pull request auto prepare/build checks (just need to know if there are any .ts errors in the build where platform often doesn't matter - much) |
110 | | - options.platform = 'ios'; |
111 | | - } |
112 | | - |
113 | | - if (options.platform) { |
114 | | - nsOptions.push(options.platform); |
115 | | - } |
116 | | - if (options.device && !options.emulator) { |
117 | | - nsOptions.push(`--device=${options.device}`); |
118 | | - } |
119 | | - if (options.emulator) { |
120 | | - nsOptions.push('--emulator'); |
121 | | - } |
122 | | - if (options.noHmr) { |
123 | | - nsOptions.push('--no-hmr'); |
124 | | - } |
125 | | - if (options.uglify) { |
126 | | - nsOptions.push('--env.uglify'); |
127 | | - } |
128 | | - if (options.verbose) { |
129 | | - nsOptions.push('--env.verbose'); |
130 | | - } |
131 | | - if (options.production) { |
132 | | - nsOptions.push('--env.production'); |
133 | | - } |
134 | | - if (options.forDevice) { |
135 | | - nsOptions.push('--for-device'); |
136 | | - } |
137 | | - if (options.release) { |
138 | | - nsOptions.push('--release'); |
139 | | - } |
140 | | - if (options.aab) { |
141 | | - nsOptions.push('--aab'); |
142 | | - } |
143 | | - if (options.keyStorePath) { |
144 | | - nsOptions.push(`--key-store-path=${options.keyStorePath}`); |
145 | | - } |
146 | | - if (options.keyStorePassword) { |
147 | | - nsOptions.push(`--key-store-password=${options.keyStorePassword}`); |
148 | | - } |
149 | | - if (options.keyStoreAlias) { |
150 | | - nsOptions.push(`--key-store-alias=${options.keyStoreAlias}`); |
151 | | - } |
152 | | - if (options.keyStoreAliasPassword) { |
153 | | - nsOptions.push(`--key-store-alias-password=${options.keyStoreAliasPassword}`); |
154 | | - } |
155 | | - if (options.provision) { |
156 | | - nsOptions.push(`--provision=${options.provision}`); |
157 | | - } |
158 | | - if (options.copyTo) { |
159 | | - nsOptions.push(`--copy-to=${options.copyTo}`); |
160 | | - } |
161 | | - |
162 | | - if (nsCliFileReplacements.length) { |
163 | | - // console.log('nsCliFileReplacements:', nsCliFileReplacements); |
164 | | - nsOptions.push(`--env.replace=${nsCliFileReplacements.join(',')}`); |
165 | | - } |
166 | | - // always add --force (unless explicity set to false) for now since within Nx we use @nativescript/webpack at root only and the {N} cli shows a blocking error if not within the app |
167 | | - if (options?.force !== false) { |
168 | | - nsOptions.push('--force'); |
169 | | - } |
170 | | - } |
171 | | - |
172 | | - // some options should never be duplicated |
173 | | - const enforceSingularOptions = ['provision', 'device', 'copy-to']; |
174 | | - const parseOptionName = (flag: string) => { |
175 | | - // strip just the option name from extra arguments |
176 | | - // --provision='match AppStore my.bundle.com' > provision |
177 | | - return flag.split('=')[0].replace('--', ''); |
178 | | - }; |
179 | | - // additional cli flags |
180 | | - // console.log('projectTargetCmdIndex:', projectTargetCmdIndex) |
181 | | - const additionalArgs = []; |
182 | | - if (options.flags) { |
183 | | - // persisted flags in configurations |
184 | | - additionalArgs.push(...options.flags.split(' ')); |
185 | | - } |
186 | | - if (!options.clean && process.argv.length > projectTargetCmdIndex + 1) { |
187 | | - // manually added flags to the execution command |
188 | | - const extraFlags = process.argv.slice(projectTargetCmdIndex + 1, process.argv.length); |
189 | | - for (const flag of extraFlags) { |
190 | | - const optionName = parseOptionName(flag); |
191 | | - if (optionName?.indexOf('/') === -1 && optionName?.indexOf('{') === -1) { |
192 | | - // no valid options should start with '/' or '{' - those are often extra process.argv context args that should be ignored |
193 | | - if (!nsOptions.includes(flag) && !additionalArgs.includes(flag) && !enforceSingularOptions.includes(optionName)) { |
194 | | - additionalArgs.push(flag); |
195 | | - } |
196 | | - } |
197 | | - } |
198 | | - // console.log('additionalArgs:', additionalArgs); |
199 | | - } |
200 | | - |
201 | | - const runCommand = function () { |
202 | | - console.log(`――――――――――――――――――――――――${options.clean ? '' : options.platform === 'ios' ? ' ' : ' 🤖'}`); |
203 | | - console.log(`Running NativeScript CLI within ${projectCwd}`); |
204 | | - console.log(' '); |
205 | | - console.log([`ns`, ...nsOptions, ...additionalArgs].join(' ')); |
206 | | - console.log(' '); |
207 | | - if (additionalArgs.length) { |
208 | | - console.log('Note: When using extra cli flags, ensure all key/value pairs are separated with =, for example: --provision="Name"'); |
209 | | - console.log(' '); |
210 | | - } |
211 | | - console.log(`---`); |
212 | | - const child = childProcess.spawn(/^win/.test(process.platform) ? 'ns.cmd' : 'ns', [...nsOptions, ...additionalArgs], { |
213 | | - cwd: projectCwd, |
214 | | - stdio: 'inherit', |
215 | | - }); |
216 | | - child.on('close', (code) => { |
217 | | - console.log(`Done.`); |
218 | | - child.kill('SIGKILL'); |
219 | | - resolve({ success: code === 0 }); |
220 | | - }); |
221 | | - }; |
222 | | - |
223 | | - const checkAppId = function () { |
224 | | - return new Promise((resolve) => { |
225 | | - const child = childProcess.spawn(/^win/.test(process.platform) ? 'ns.cmd' : 'ns', ['config', 'get', `id`], { |
226 | | - cwd: projectCwd, |
227 | | - }); |
228 | | - child.stdout.setEncoding('utf8'); |
229 | | - child.stdout.on('data', function (data) { |
230 | | - // ensure no newline chars at the end |
231 | | - const appId = (data || '').toString().replace('\n', '').replace('\r', ''); |
232 | | - // console.log('existing app id:', appId); |
233 | | - resolve(appId); |
234 | | - }); |
235 | | - child.on('close', (code) => { |
236 | | - child.kill('SIGKILL'); |
237 | | - }); |
238 | | - }); |
239 | | - }; |
240 | | - |
241 | | - const checkOptions = function () { |
242 | | - if (options.id) { |
243 | | - // only modify app id if doesn't match (modifying nativescript.config will cause full native build) |
244 | | - checkAppId().then(id => { |
245 | | - if (options.id !== id) { |
246 | | - // set custom app bundle id before running the app |
247 | | - const child = childProcess.spawn(/^win/.test(process.platform) ? 'ns.cmd' : 'ns', ['config', 'set', `${options.platform}.id`, options.id], { |
248 | | - cwd: projectCwd, |
249 | | - stdio: 'inherit', |
250 | | - }); |
251 | | - child.on('close', (code) => { |
252 | | - child.kill('SIGKILL'); |
253 | | - runCommand(); |
254 | | - }); |
255 | | - } else { |
256 | | - runCommand(); |
257 | | - } |
258 | | - }) |
259 | | - } else { |
260 | | - runCommand(); |
261 | | - } |
262 | | - }; |
263 | | - |
264 | | - if (options.clean) { |
265 | | - runCommand(); |
266 | | - } else { |
267 | | - const plistKeys = Object.keys(options.plistUpdates || {}); |
268 | | - if (plistKeys.length) { |
269 | | - for (const filepath of plistKeys) { |
270 | | - let plistPath: string; |
271 | | - if (filepath.indexOf('.') === 0) { |
272 | | - // resolve relative to project directory |
273 | | - plistPath = nodeResolve(projectCwd, filepath); |
274 | | - } else { |
275 | | - // default to locating in App_Resources |
276 | | - plistPath = nodeResolve(projectCwd, 'App_Resources', 'iOS', filepath); |
277 | | - } |
278 | | - const plistFile = parse(readFileSync(plistPath, 'utf8')); |
279 | | - const plistUpdates = options.plistUpdates[filepath]; |
280 | | - // check if updates are needed to avoid native build if not needed |
281 | | - let needsUpdate = false; |
282 | | - for (const key in plistUpdates) { |
283 | | - if (Array.isArray(plistUpdates[key])) { |
284 | | - try { |
285 | | - // compare stringified |
286 | | - const plistString = JSON.stringify(plistFile[key] || {}); |
287 | | - const plistUpdateString = JSON.stringify(plistUpdates[key]); |
288 | | - if (plistString !== plistUpdateString) { |
289 | | - plistFile[key] = plistUpdates[key]; |
290 | | - console.log(`Updating ${filepath}: ${key}=`, plistFile[key]); |
291 | | - needsUpdate = true; |
292 | | - } |
293 | | - } catch (err) { |
294 | | - console.log(`plist file parse error:`, err); |
295 | | - } |
296 | | - } else if (plistFile[key] !== plistUpdates[key]) { |
297 | | - plistFile[key] = plistUpdates[key]; |
298 | | - console.log(`Updating ${filepath}: ${key}=${plistFile[key]}`); |
299 | | - needsUpdate = true; |
300 | | - } |
301 | | - } |
302 | | - if (needsUpdate) { |
303 | | - writeFileSync(plistPath, build(plistFile)); |
304 | | - console.log(`Updated: ${plistPath}`); |
305 | | - } |
306 | | - } |
307 | | - } |
308 | | - |
309 | | - const xmlKeys = Object.keys(options.xmlUpdates || {}); |
310 | | - if (xmlKeys.length) { |
311 | | - for (const filepath of xmlKeys) { |
312 | | - let xmlPath: string; |
313 | | - if (filepath.indexOf('.') === 0) { |
314 | | - // resolve relative to project directory |
315 | | - xmlPath = nodeResolve(projectCwd, filepath); |
316 | | - } else { |
317 | | - // default to locating in App_Resources |
318 | | - xmlPath = nodeResolve(projectCwd, 'App_Resources', 'Android', filepath); |
319 | | - } |
320 | | - parseString(readFileSync(xmlPath, 'utf8'), (err, result) => { |
321 | | - if (err) { |
322 | | - throw err; |
323 | | - } |
324 | | - if (!result) { |
325 | | - result = {}; |
326 | | - } |
327 | | - // console.log('BEFORE---'); |
328 | | - // console.log(JSON.stringify(result, null, 2)); |
329 | | - |
330 | | - const xmlUpdates = options.xmlUpdates[filepath]; |
331 | | - for (const key in xmlUpdates) { |
332 | | - result[key] = {}; |
333 | | - for (const subKey in xmlUpdates[key]) { |
334 | | - result[key][subKey] = []; |
335 | | - for (let i = 0; i < xmlUpdates[key][subKey].length; i++) { |
336 | | - const node = xmlUpdates[key][subKey][i]; |
337 | | - const attrName = Object.keys(node)[0]; |
338 | | - |
339 | | - result[key][subKey].push({ |
340 | | - _: node[attrName], |
341 | | - $: { |
342 | | - name: attrName, |
343 | | - }, |
344 | | - }); |
345 | | - } |
346 | | - } |
347 | | - } |
348 | | - |
349 | | - // console.log('AFTER---'); |
350 | | - // console.log(JSON.stringify(result, null, 2)); |
351 | | - |
352 | | - const builder = new Builder(); |
353 | | - const xml = builder.buildObject(result); |
354 | | - writeFileSync(xmlPath, xml); |
355 | | - console.log(`Updated: ${xmlPath}`); |
356 | | - |
357 | | - checkOptions(); |
358 | | - }); |
359 | | - } |
360 | | - } else { |
361 | | - checkOptions(); |
362 | | - } |
363 | | - } |
364 | | - } catch (err) { |
365 | | - console.error(err); |
366 | | - reject(err); |
367 | | - } |
368 | | - }); |
| 4 | +export default async function runExecutor(options: BuildExecutorSchema, context: ExecutorContext) { |
| 5 | + return commonExecutor(options, context); |
369 | 6 | } |
0 commit comments