Skip to content

Commit 108c19d

Browse files
committed
feat: share executor setups between build/test to ensure consistent behavior
1 parent 25b54c6 commit 108c19d

File tree

8 files changed

+428
-550
lines changed

8 files changed

+428
-550
lines changed

packages/nx/src/executors/build/executor.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { ExecutorContext } from '@nrwl/devkit';
2-
import { BuildBuilderSchema } from './schema';
3-
import runBuilder from './executor';
2+
import { BuildExecutorSchema } from '../../utils';
3+
import runExecutor from './executor';
44

5-
const options: BuildBuilderSchema = {
5+
const options: BuildExecutorSchema = {
66
noHmr: true,
77
prepare: true,
88
platform: 'ios',
Lines changed: 4 additions & 367 deletions
Original file line numberDiff line numberDiff line change
@@ -1,369 +1,6 @@
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';
83

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);
3696
}

0 commit comments

Comments
 (0)