@wavemaker-ai/wm-reactnative-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +236 -0
- package/assets/CLI-EnvironmentVariable.png +0 -0
- package/assets/EnvironmentVariable.png +0 -0
- package/assets/EnvironmentVariable1.png +0 -0
- package/files/ui-build.js +331 -0
- package/index.js +381 -0
- package/package.json +39 -0
- package/src/android.js +479 -0
- package/src/command.js +552 -0
- package/src/config.js +11 -0
- package/src/custom-logger/progress-bar.js +97 -0
- package/src/custom-logger/steps.js +117 -0
- package/src/custom-logger/task-logger.js +147 -0
- package/src/exec.js +73 -0
- package/src/expo-launcher.js +596 -0
- package/src/ios.js +517 -0
- package/src/logger.js +104 -0
- package/src/mobileprovision-parse/index.js +72 -0
- package/src/project-sync.service.js +390 -0
- package/src/requirements.js +250 -0
- package/src/utils.js +100 -0
- package/src/web-preview-launcher.js +548 -0
- package/src/zip.js +19 -0
- package/templates/embed/android/ReactNativeAppFragment.java +78 -0
- package/templates/embed/android/SplashScreenReactActivityLifecycleListener.kt +41 -0
- package/templates/embed/android/fragment_react_native_app.xml +14 -0
- package/templates/embed/ios/ReactNativeView.h +12 -0
- package/templates/embed/ios/ReactNativeView.m +59 -0
- package/templates/embed/ios/ReactNativeView.swift +53 -0
- package/templates/expo-camera-patch/useWebQRScanner.js +100 -0
- package/templates/ios-build-patch/podFIlePostInstall.js +72 -0
- package/templates/package/packageLock.json +14334 -0
- package/templates/wm-rn-runtime/App.js +479 -0
- package/templates/wm-rn-runtime/App.navigator.js +109 -0
- package/test.js +0 -0
- package/tools-site/index.html.template +17 -0
- package/tools-site/page_background.svg +99 -0
- package/tools-site/qrcode.js +614 -0
- package/tools-site/styles.css +39 -0
package/src/ios.js
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const logger = require('./logger');
|
|
3
|
+
const config = require('./config');
|
|
4
|
+
const plist = require('plist');
|
|
5
|
+
const xcode = require('xcode');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const pparse = require('./mobileprovision-parse');
|
|
8
|
+
const {
|
|
9
|
+
exec
|
|
10
|
+
} = require('./exec');
|
|
11
|
+
const {
|
|
12
|
+
validateForIos
|
|
13
|
+
} = require('./requirements');
|
|
14
|
+
const { readAndReplaceFileContent, iterateFiles } = require('./utils');
|
|
15
|
+
const { newPostInstallBlock } = require('../templates/ios-build-patch/podFIlePostInstall');
|
|
16
|
+
const taskLogger = require('./custom-logger/task-logger').spinnerBar;
|
|
17
|
+
const {androidBuildSteps} = require('./custom-logger/steps');
|
|
18
|
+
|
|
19
|
+
const loggerLabel = 'Generating ipa file';
|
|
20
|
+
|
|
21
|
+
async function importCertToKeyChain(keychainName, certificate, certificatePassword) {
|
|
22
|
+
await exec('security', ['create-keychain', '-p', keychainName, keychainName], {log: false});
|
|
23
|
+
await exec('security', ['unlock-keychain', '-p', keychainName, keychainName], {log: false});
|
|
24
|
+
await exec('security', ['set-keychain-settings', '-t', '3600', keychainName], {log: false});
|
|
25
|
+
let keychains = await exec('security', ['list-keychains', '-d', 'user'], {log: false});
|
|
26
|
+
keychains = keychains.map(k => k.replace(/[\"\s]+/g, '')).filter(k => k !== '');
|
|
27
|
+
await exec('security', ['list-keychains', '-d', 'user', '-s', keychainName, ...keychains], {log: false});
|
|
28
|
+
await exec('security',
|
|
29
|
+
['import',
|
|
30
|
+
certificate,
|
|
31
|
+
'-k', keychainName,
|
|
32
|
+
'-P', certificatePassword,
|
|
33
|
+
'-T', '/usr/bin/codesign',
|
|
34
|
+
'-T', '/usr/bin/productsign',
|
|
35
|
+
'-T', '/usr/bin/productbuild',
|
|
36
|
+
'-T', '/Applications/Xcode.app'], {log: false});
|
|
37
|
+
await exec('security', ['set-key-partition-list', '-S', 'apple-tool:,apple:,codesign', '-s', '-k', keychainName, keychainName], {log: false});
|
|
38
|
+
logger.info({
|
|
39
|
+
label: loggerLabel,
|
|
40
|
+
message: `Cerificate at (${certificate}) imported in (${keychainName})`
|
|
41
|
+
});
|
|
42
|
+
let signingDetails = await exec('security', ['find-identity', '-v', '-p', 'codesigning'], {log: false});
|
|
43
|
+
console.log(signingDetails);
|
|
44
|
+
return async () => {
|
|
45
|
+
keychains = keychains.map(k => k.replace(/[\"\s]+/g, ''));
|
|
46
|
+
await exec('security', ['list-keychains', '-d', 'user', '-s', ...keychains], {log: false});
|
|
47
|
+
await deleteKeyChain(keychainName);
|
|
48
|
+
logger.info({
|
|
49
|
+
label: loggerLabel,
|
|
50
|
+
message: `removed keychain (${keychainName}).`
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function deleteKeyChain(keychainName) {
|
|
56
|
+
await exec('security', ['delete-keychain', keychainName]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async function extractUUID(provisionalFile) {
|
|
61
|
+
const content = await exec('grep', ['UUID', '-A1', '-a', provisionalFile], {log: false});
|
|
62
|
+
return content.join('\n').match(/[-A-F0-9]{36}/i)[0];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function getLoginKeyChainName() {
|
|
66
|
+
const content = await exec('security list-keychains | grep login.keychain', null, {
|
|
67
|
+
shell: true
|
|
68
|
+
});
|
|
69
|
+
return content[0].substring(content[0].lastIndexOf('/') + 1, content[0].indexOf('-'));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function extractTeamId(provisionalFile) {
|
|
73
|
+
const content = await exec('grep', ['TeamIdentifier', '-A2', '-a', provisionalFile], {log: false});
|
|
74
|
+
return content[2].match(/>[A-Z0-9]+/i)[0].substr(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function getUsername() {
|
|
78
|
+
const content = await exec('id', ['-un'], false);
|
|
79
|
+
return content[0];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function updateJSEnginePreference() {
|
|
83
|
+
const jsEngine = require(config.src + 'app.json').expo.jsEngine;
|
|
84
|
+
const podJSON = config.src + 'ios/Podfile.properties.json';
|
|
85
|
+
if (fs.existsSync(podJSON)) {
|
|
86
|
+
let data = require(podJSON, 'utf8');
|
|
87
|
+
data['expo.jsEngine'] = jsEngine;
|
|
88
|
+
fs.writeFileSync(podJSON, JSON.stringify(data, null, 4));
|
|
89
|
+
logger.info({
|
|
90
|
+
label: loggerLabel,
|
|
91
|
+
message: `js engine is set as ${jsEngine}`
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function addResourceFileToProject(iosProject, path, opt, group) {
|
|
97
|
+
const file = iosProject.addFile(path, group);
|
|
98
|
+
file.uuid = iosProject.generateUuid();
|
|
99
|
+
iosProject.addToPbxBuildFileSection(file); // PBXBuildFile
|
|
100
|
+
iosProject.addToPbxResourcesBuildPhase(file); // PBXResourcesBuildPhase
|
|
101
|
+
iosProject.addToPbxFileReferenceSection(file); // PBXFileReference
|
|
102
|
+
if (group) {
|
|
103
|
+
if (iosProject.getPBXGroupByKey(group)) {
|
|
104
|
+
iosProject.addToPbxGroup(file, group); //Group other than Resources (i.e. 'splash')
|
|
105
|
+
}
|
|
106
|
+
else if (iosProject.getPBXVariantGroupByKey(group)) {
|
|
107
|
+
iosProject.addToPbxVariantGroup(file, group); // PBXVariantGroup
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return file;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async function embed(args) {
|
|
115
|
+
const rnIosProject = config.src;
|
|
116
|
+
const embedProject = `${rnIosProject}ios-embed`;
|
|
117
|
+
fs.copySync(args.mp, embedProject);
|
|
118
|
+
const rnModulePath = `${embedProject}/rnApp`;
|
|
119
|
+
fs.removeSync(rnModulePath);
|
|
120
|
+
fs.mkdirpSync(rnModulePath);
|
|
121
|
+
fs.copyFileSync(`${__dirname}/../templates/embed/ios/ReactNativeView.swift`, `${rnModulePath}/ReactNativeView.swift`);
|
|
122
|
+
fs.copyFileSync(`${__dirname}/../templates/embed/ios/ReactNativeView.h`, `${rnModulePath}/ReactNativeView.h`);
|
|
123
|
+
fs.copyFileSync(`${__dirname}/../templates/embed/ios/ReactNativeView.m`, `${rnModulePath}/ReactNativeView.m`);
|
|
124
|
+
const projectName = fs.readdirSync(`${config.src}ios-embed`)
|
|
125
|
+
.find(f => f.endsWith('xcodeproj'))
|
|
126
|
+
.split('.')[0];
|
|
127
|
+
|
|
128
|
+
// xcode 16 issue https://github.com/CocoaPods/CocoaPods/issues/12456 - not required can be removed
|
|
129
|
+
await readAndReplaceFileContent(`${embedProject}/${projectName}.xcodeproj/project.pbxproj`, (content) => {
|
|
130
|
+
content = content.replaceAll("PBXFileSystemSynchronizedRootGroup", "PBXGroup")
|
|
131
|
+
return content.replaceAll(`objectVersion = 77`, `objectVersion = 56`)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
fs.copyFileSync(`${rnIosProject}/ios/Podfile`, `${rnIosProject}/ios-embed/Podfile`);
|
|
135
|
+
await readAndReplaceFileContent(`${embedProject}/Podfile`, (content) => {
|
|
136
|
+
return content.replace(/target .* do/g, `target '${projectName}' do`);
|
|
137
|
+
})
|
|
138
|
+
await readAndReplaceFileContent(
|
|
139
|
+
`${rnIosProject}/app.js`,
|
|
140
|
+
(content) => content.replace('props = props || {};', 'props = props || {};\n\tprops.landingPage = props.landingPage || props.pageName;'));
|
|
141
|
+
await exec('npx', ['react-native', 'bundle', '--platform', 'ios',
|
|
142
|
+
'--dev', 'false', '--entry-file', 'index.js',
|
|
143
|
+
'--bundle-output', 'ios-embed/rnApp/main.jsbundle',
|
|
144
|
+
'--assets-dest', 'ios-embed/rnApp'], {
|
|
145
|
+
cwd: config.src
|
|
146
|
+
});
|
|
147
|
+
await exec('pod', ['install'], {
|
|
148
|
+
cwd: embedProject
|
|
149
|
+
});
|
|
150
|
+
logger.info({
|
|
151
|
+
label: loggerLabel,
|
|
152
|
+
message: 'Changed Native Ios project.'
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function invokeiosBuild(args) {
|
|
157
|
+
taskLogger.info("Invoke IOS build")
|
|
158
|
+
const certificate = args.iCertificate;
|
|
159
|
+
const certificatePassword = args.iCertificatePassword;
|
|
160
|
+
const provisionalFile = args.iProvisioningFile;
|
|
161
|
+
const buildType = args.buildType;
|
|
162
|
+
const errors = validateForIos(certificate, certificatePassword, provisionalFile, buildType);
|
|
163
|
+
if (errors.length > 0) {
|
|
164
|
+
return {
|
|
165
|
+
success: false,
|
|
166
|
+
errors: errors
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
updateJSEnginePreference();
|
|
170
|
+
const random = Date.now();
|
|
171
|
+
const username = await getUsername();
|
|
172
|
+
const keychainName = `wm-reactnative-${random}.keychain`;
|
|
173
|
+
const provisionuuid = await extractUUID(provisionalFile);
|
|
174
|
+
|
|
175
|
+
logger.info({
|
|
176
|
+
label: loggerLabel,
|
|
177
|
+
message: `provisional UUID : ${provisionuuid}`
|
|
178
|
+
});
|
|
179
|
+
taskLogger.info(`provisional UUID : ${provisionuuid}`);
|
|
180
|
+
const developmentTeamId = await extractTeamId(provisionalFile);
|
|
181
|
+
logger.info({
|
|
182
|
+
label: loggerLabel,
|
|
183
|
+
message: `developmentTeamId : ${developmentTeamId}`
|
|
184
|
+
});
|
|
185
|
+
taskLogger.info(`developmentTeamId : ${developmentTeamId}`);
|
|
186
|
+
const ppFolder = `/Users/${username}/Library/MobileDevice/Provisioning\ Profiles`;
|
|
187
|
+
fs.mkdirSync(ppFolder, {
|
|
188
|
+
recursive: true
|
|
189
|
+
})
|
|
190
|
+
const targetProvisionsalPath = `${ppFolder}/${provisionuuid}.mobileprovision`;
|
|
191
|
+
fs.copyFileSync(provisionalFile, targetProvisionsalPath);
|
|
192
|
+
logger.info({
|
|
193
|
+
label: loggerLabel,
|
|
194
|
+
message: `copied provisionalFile (${provisionalFile}).`
|
|
195
|
+
});
|
|
196
|
+
taskLogger.info(`copied provisionalFile (${provisionalFile}).`);
|
|
197
|
+
const removeKeyChain = await importCertToKeyChain(keychainName, certificate, certificatePassword);
|
|
198
|
+
|
|
199
|
+
// Extract code signing identity from the keychain after importing
|
|
200
|
+
let codeSignIdentity;
|
|
201
|
+
try {
|
|
202
|
+
const identities = await exec('security', ['find-identity', '-v', '-p', 'codesigning', keychainName], {log: false});
|
|
203
|
+
// Find the first valid identity line (format: "1) HASH "Certificate Name"")
|
|
204
|
+
const identityLine = identities.find(line => line.match(/\d+\)\s+[A-F0-9]+\s+".*"/));
|
|
205
|
+
if (identityLine) {
|
|
206
|
+
// Extract the certificate name from quotes
|
|
207
|
+
const match = identityLine.match(/"([^"]+)"/);
|
|
208
|
+
if (match && match[1]) {
|
|
209
|
+
codeSignIdentity = match[1];
|
|
210
|
+
logger.info({
|
|
211
|
+
label: loggerLabel,
|
|
212
|
+
message: `Code Sign Identity: ${codeSignIdentity}`
|
|
213
|
+
});
|
|
214
|
+
taskLogger.info(`Code Sign Identity: ${codeSignIdentity}`);
|
|
215
|
+
} else {
|
|
216
|
+
throw new Error('Could not parse certificate name from identity');
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
throw new Error('No valid code signing identity found in keychain');
|
|
220
|
+
}
|
|
221
|
+
} catch (e) {
|
|
222
|
+
logger.error({
|
|
223
|
+
label: loggerLabel,
|
|
224
|
+
message: `Failed to extract code signing identity: ${e.message}`
|
|
225
|
+
});
|
|
226
|
+
// Fallback to openssl method
|
|
227
|
+
logger.info({
|
|
228
|
+
label: loggerLabel,
|
|
229
|
+
message: 'Attempting fallback method using openssl...'
|
|
230
|
+
});
|
|
231
|
+
try {
|
|
232
|
+
let opensslOutput = await exec(`openssl pkcs12 -in ${certificate} -passin pass:${certificatePassword} -nodes | openssl x509 -noout -subject -nameopt multiline | grep commonName | sed -n 's/ *commonName *= //p'`, null, {
|
|
233
|
+
shell: true
|
|
234
|
+
});
|
|
235
|
+
// Filter out any error/usage lines and get the actual certificate name
|
|
236
|
+
codeSignIdentity = opensslOutput.find(line => line && !line.includes('usage:') && !line.includes('pkcs12'));
|
|
237
|
+
if (!codeSignIdentity) {
|
|
238
|
+
throw new Error('Openssl fallback failed - no valid certificate name found');
|
|
239
|
+
}
|
|
240
|
+
logger.info({
|
|
241
|
+
label: loggerLabel,
|
|
242
|
+
message: `Code Sign Identity (via openssl): ${codeSignIdentity}`
|
|
243
|
+
});
|
|
244
|
+
} catch (opensslError) {
|
|
245
|
+
throw new Error(`Failed to extract code signing identity: ${opensslError.message}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
let useModernBuildSystem = 'YES';
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
// XCode14 issue https://github.com/expo/expo/issues/19759
|
|
252
|
+
// This is not required when expo 47 is used.
|
|
253
|
+
await readAndReplaceFileContent(`${config.src}ios/Podfile`, (content) => {
|
|
254
|
+
return content.replace('__apply_Xcode_12_5_M1_post_install_workaround(installer)',
|
|
255
|
+
'__apply_Xcode_12_5_M1_post_install_workaround(installer)' + '\n' +
|
|
256
|
+
' # Add these lines for Xcode 14 builds' + '\n' +
|
|
257
|
+
' installer.pods_project.targets.each do |target| ' + '\n' +
|
|
258
|
+
' if target.respond_to?(:product_type) and target.product_type == "com.apple.product-type.bundle"' + '\n' +
|
|
259
|
+
' target.build_configurations.each do |config|'+ '\n' +
|
|
260
|
+
' config.build_settings[\'CODE_SIGNING_ALLOWED\'] = \'NO\'' + '\n' +
|
|
261
|
+
' end' + '\n' +
|
|
262
|
+
' end' + '\n' +
|
|
263
|
+
' end')
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const appJsonPath = path.join(config.src, 'app.json');
|
|
267
|
+
const appJson = JSON.parse(fs.readFileSync(appJsonPath, 'utf-8'));
|
|
268
|
+
const buildPropertiesPlugin = appJson.expo.plugins && appJson.expo.plugins.find(plugin => plugin[0] === 'expo-build-properties');
|
|
269
|
+
|
|
270
|
+
if (buildPropertiesPlugin){
|
|
271
|
+
const iosConfig = buildPropertiesPlugin[1].ios;
|
|
272
|
+
if (iosConfig && iosConfig.useFrameworks === 'static'){
|
|
273
|
+
await readAndReplaceFileContent(`${config.src}ios/Podfile`, (podfileContent) => {
|
|
274
|
+
const postInstallRegex = /^(\s*)post_install\s+do\s+\|installer\|[\s\S]*?^\1end$/m;
|
|
275
|
+
const modifiedPodContent = podfileContent.replace(postInstallRegex, newPostInstallBlock);
|
|
276
|
+
return modifiedPodContent;
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
await exec('pod', ['install'], {cwd: config.src + 'ios'});
|
|
282
|
+
return await xcodebuild(args, codeSignIdentity, provisionuuid, developmentTeamId);
|
|
283
|
+
} catch (e) {
|
|
284
|
+
console.error(e);
|
|
285
|
+
return {
|
|
286
|
+
errors: e,
|
|
287
|
+
success: false
|
|
288
|
+
}
|
|
289
|
+
} finally {
|
|
290
|
+
await removeKeyChain();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function getPackageType(provisionalFile) {
|
|
295
|
+
const data = await pparse(provisionalFile);
|
|
296
|
+
//data.
|
|
297
|
+
if (data.type === 'appstore') {
|
|
298
|
+
return 'app-store';
|
|
299
|
+
}
|
|
300
|
+
if (data.type === 'inhouse') {
|
|
301
|
+
return 'enterprise';
|
|
302
|
+
}
|
|
303
|
+
if (data.type === 'adhoc') {
|
|
304
|
+
return 'ad-hoc';
|
|
305
|
+
}
|
|
306
|
+
throw new Error('Not able find the type of provisioning file.');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function createExportPList(projectPath, {
|
|
310
|
+
appId,
|
|
311
|
+
provisioningProfile,
|
|
312
|
+
teamId,
|
|
313
|
+
packageType,
|
|
314
|
+
codeSignIdentity,
|
|
315
|
+
buildType
|
|
316
|
+
}) {
|
|
317
|
+
const exportOptions = {
|
|
318
|
+
compileBitcode: true,
|
|
319
|
+
provisioningProfiles : { [appId]: String(provisioningProfile) },
|
|
320
|
+
signingCertificate: codeSignIdentity,
|
|
321
|
+
signingStyle: 'manual',
|
|
322
|
+
teamId: teamId,
|
|
323
|
+
method: packageType,
|
|
324
|
+
testFlightInternalTestingOnly: false
|
|
325
|
+
};
|
|
326
|
+
if (buildType === 'development') {
|
|
327
|
+
exportOptions.stripSwiftSymbols = false;
|
|
328
|
+
} else {
|
|
329
|
+
exportOptions.stripSwiftSymbols = true;
|
|
330
|
+
}
|
|
331
|
+
const exportOptionsPlist = plist.build(exportOptions);
|
|
332
|
+
const exportOptionsPath = path.join(projectPath, 'exportOptions.plist');
|
|
333
|
+
fs.writeFileSync(exportOptionsPath, exportOptionsPlist, 'utf-8');
|
|
334
|
+
return 'success'
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const removePushNotifications = (projectDir, projectName) => {
|
|
338
|
+
const dir = `${projectDir}ios/${projectName}/`;
|
|
339
|
+
const entitlements = dir + fs.readdirSync(dir).find(f => f.endsWith('entitlements'));
|
|
340
|
+
const o = plist.parse(fs.readFileSync(entitlements, 'utf8'));
|
|
341
|
+
delete o['aps-environment'];
|
|
342
|
+
fs.writeFileSync(entitlements, plist.build(o), 'utf8');
|
|
343
|
+
logger.info({
|
|
344
|
+
label: loggerLabel,
|
|
345
|
+
message: `removed aps-environment from entitlements`
|
|
346
|
+
});
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const endWith = (str, suffix) => {
|
|
350
|
+
if (!str.endsWith(suffix)) {
|
|
351
|
+
return str += suffix;
|
|
352
|
+
}
|
|
353
|
+
return str;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
function findFile(path, nameregex) {
|
|
357
|
+
const files = fs.readdirSync(path);
|
|
358
|
+
const f = files.find(f => f.match(nameregex));
|
|
359
|
+
return f ? endWith(path, '/') + f : '';
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function xcodebuild(args, CODE_SIGN_IDENTITY_VAL, PROVISIONING_UUID, DEVELOPMENT_TEAM) {
|
|
363
|
+
try {
|
|
364
|
+
taskLogger.start(androidBuildSteps[4].start);
|
|
365
|
+
taskLogger.setTotal(androidBuildSteps[4].total);
|
|
366
|
+
let xcworkspacePath = findFile(config.src + 'ios', /\.xcworkspace?/) || findFile(config.src + 'ios', /\.xcodeproj?/);
|
|
367
|
+
if (!xcworkspacePath) {
|
|
368
|
+
return {
|
|
369
|
+
errors: '.xcworkspace or .xcodeproj files are not found in ios directory',
|
|
370
|
+
success: false
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const projectName = fs.readdirSync(`${config.src}ios`)
|
|
374
|
+
.find(f => f.endsWith('xcodeproj'))
|
|
375
|
+
.split('.')[0];
|
|
376
|
+
const pathArr = xcworkspacePath.split('/');
|
|
377
|
+
const xcworkspaceFileName = pathArr[pathArr.length - 1];
|
|
378
|
+
const fileName = xcworkspaceFileName.split('.')[0];
|
|
379
|
+
taskLogger.incrementProgress(0.4);
|
|
380
|
+
|
|
381
|
+
let _buildType;
|
|
382
|
+
if (args.buildType === 'development' || args.buildType === 'debug') {
|
|
383
|
+
_buildType = 'Debug';
|
|
384
|
+
|
|
385
|
+
// Get Expo SDK version from package.json
|
|
386
|
+
const packageJson = JSON.parse(fs.readFileSync(`${config.src}package.json`, 'utf8'));
|
|
387
|
+
const expoVersion = packageJson.dependencies?.expo ? parseInt(packageJson.dependencies.expo.replace(/[^0-9]/g, '').substring(0, 2)) : 0;
|
|
388
|
+
|
|
389
|
+
if (expoVersion >= 54) {
|
|
390
|
+
// Expo 54+: Modify bundling script to force bundling with --dev false
|
|
391
|
+
logger.info({
|
|
392
|
+
label: loggerLabel,
|
|
393
|
+
message: 'Expo 54+: Configuring production mode bundle for Debug build...'
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
await readAndReplaceFileContent(`${config.src}ios/${projectName}.xcodeproj/project.pbxproj`, (content) => {
|
|
397
|
+
// Replace SKIP_BUNDLING with FORCE_BUNDLING
|
|
398
|
+
content = content.replace('SKIP_BUNDLING=1', 'FORCE_BUNDLING=1');
|
|
399
|
+
|
|
400
|
+
// Add --dev false flag to the bundling command by setting EXTRA_PACKAGER_ARGS
|
|
401
|
+
// This is the standard way React Native handles additional bundler arguments
|
|
402
|
+
content = content.replace(
|
|
403
|
+
'if [[ \\"$CONFIGURATION\\" = *Debug* ]]; then\\n export FORCE_BUNDLING=1\\nfi',
|
|
404
|
+
'if [[ \\"$CONFIGURATION\\" = *Debug* ]]; then\\n export FORCE_BUNDLING=1\\n export EXTRA_PACKAGER_ARGS=\\"--dev false\\"\\nfi'
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
return content;
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Modify AppDelegate.swift to load from embedded bundle
|
|
411
|
+
const appDelegateSwiftPath = `${config.src}ios/${projectName}/AppDelegate.swift`;
|
|
412
|
+
if (fs.existsSync(appDelegateSwiftPath)) {
|
|
413
|
+
await readAndReplaceFileContent(appDelegateSwiftPath, (content) => {
|
|
414
|
+
return content.replace(
|
|
415
|
+
`#if DEBUG
|
|
416
|
+
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry")
|
|
417
|
+
#else
|
|
418
|
+
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
|
|
419
|
+
#endif`,
|
|
420
|
+
`return Bundle.main.url(forResource: "main", withExtension: "jsbundle")`
|
|
421
|
+
);
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
logger.info({
|
|
426
|
+
label: loggerLabel,
|
|
427
|
+
message: 'Expo 54+: Configured to bundle with production mode (no Metro/devtools)'
|
|
428
|
+
});
|
|
429
|
+
} else {
|
|
430
|
+
// Expo 52 and below: Use legacy Objective-C AppDelegate.mm modification
|
|
431
|
+
await readAndReplaceFileContent(`${config.src}ios/${projectName}.xcodeproj/project.pbxproj`, (content) => {
|
|
432
|
+
return content.replace('SKIP_BUNDLING=1', 'FORCE_BUNDLING=1');
|
|
433
|
+
});
|
|
434
|
+
if (fs.existsSync(`${config.src}ios/${projectName}/AppDelegate.mm`)) {
|
|
435
|
+
await readAndReplaceFileContent(`${config.src}ios/${projectName}/AppDelegate.mm`, (content) => {
|
|
436
|
+
return content.replace(
|
|
437
|
+
'return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];',
|
|
438
|
+
'return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];')
|
|
439
|
+
.replace(
|
|
440
|
+
'return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"];',
|
|
441
|
+
'return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];');
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
_buildType = 'Release';
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const env = {
|
|
450
|
+
RCT_NO_LAUNCH_PACKAGER: 1
|
|
451
|
+
};
|
|
452
|
+
taskLogger.incrementProgress(0.4);
|
|
453
|
+
await exec('xcodebuild', [
|
|
454
|
+
'-workspace', fileName + '.xcworkspace',
|
|
455
|
+
'-scheme', fileName,
|
|
456
|
+
'-configuration', _buildType,
|
|
457
|
+
'-destination', 'generic/platform=iOS',
|
|
458
|
+
'-archivePath', 'build/' + fileName + '.xcarchive',
|
|
459
|
+
'CODE_SIGN_IDENTITY=' + CODE_SIGN_IDENTITY_VAL,
|
|
460
|
+
'PROVISIONING_PROFILE=' + PROVISIONING_UUID,
|
|
461
|
+
'CODE_SIGN_STYLE=Manual',
|
|
462
|
+
'archive'], {
|
|
463
|
+
cwd: config.src + 'ios',
|
|
464
|
+
env: env
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
let packageType = 'development';
|
|
468
|
+
if (args.buildType === 'release') {
|
|
469
|
+
packageType = await getPackageType(args.iProvisioningFile);
|
|
470
|
+
}
|
|
471
|
+
const status = await createExportPList(config.src + 'ios', {
|
|
472
|
+
appId: config.metaData.id,
|
|
473
|
+
provisioningProfile: PROVISIONING_UUID,
|
|
474
|
+
teamId: DEVELOPMENT_TEAM,
|
|
475
|
+
packageType: packageType,
|
|
476
|
+
codeSignIdentity: CODE_SIGN_IDENTITY_VAL,
|
|
477
|
+
buildType: args.buildType
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
if (status === 'success') {
|
|
481
|
+
await exec('xcodebuild', [
|
|
482
|
+
'-exportArchive',
|
|
483
|
+
'-archivePath', 'build/' + fileName + '.xcarchive',
|
|
484
|
+
'-exportOptionsPlist', './exportOptions.plist',
|
|
485
|
+
'-exportPath',
|
|
486
|
+
'build'], {
|
|
487
|
+
cwd: config.src + 'ios',
|
|
488
|
+
env: env
|
|
489
|
+
});
|
|
490
|
+
const output = args.dest + 'output/ios/';
|
|
491
|
+
const outputFilePath = `${output}${fileName}(${config.metaData.version}).${args.buildType}.ipa`;
|
|
492
|
+
fs.mkdirSync(output, {recursive: true});
|
|
493
|
+
fs.copyFileSync(findFile(`${args.dest}ios/build/`, /\.ipa?/), outputFilePath);
|
|
494
|
+
return {
|
|
495
|
+
success: true,
|
|
496
|
+
output: outputFilePath
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
taskLogger.succeed(androidBuildSteps[4].succeed);
|
|
500
|
+
} catch (e) {
|
|
501
|
+
logger.error({
|
|
502
|
+
label: loggerLabel,
|
|
503
|
+
message: e
|
|
504
|
+
});
|
|
505
|
+
taskLogger.fail(androidBuildSteps[4].fail);
|
|
506
|
+
console.error(e);
|
|
507
|
+
return {
|
|
508
|
+
errors: e,
|
|
509
|
+
success: false
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
module.exports = {
|
|
515
|
+
invokeiosBuild: invokeiosBuild,
|
|
516
|
+
embed: embed
|
|
517
|
+
}
|
package/src/logger.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const {
|
|
2
|
+
createLogger,
|
|
3
|
+
format,
|
|
4
|
+
transports
|
|
5
|
+
} = require('winston');
|
|
6
|
+
const {
|
|
7
|
+
colorize,
|
|
8
|
+
combine,
|
|
9
|
+
timestamp,
|
|
10
|
+
printf
|
|
11
|
+
} = format;
|
|
12
|
+
|
|
13
|
+
const taskLogger = require('./custom-logger/task-logger').spinnerBar;
|
|
14
|
+
|
|
15
|
+
const consoleFormat = printf(({
|
|
16
|
+
level,
|
|
17
|
+
message,
|
|
18
|
+
label,
|
|
19
|
+
timestamp
|
|
20
|
+
}) => {
|
|
21
|
+
if(level === 'error'){
|
|
22
|
+
taskLogger.warn(message);
|
|
23
|
+
}
|
|
24
|
+
return `${timestamp} [${label}] [${level}]: ${message}`;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const jsonFormat = printf(({
|
|
28
|
+
level,
|
|
29
|
+
message,
|
|
30
|
+
label,
|
|
31
|
+
timestamp
|
|
32
|
+
}) => {
|
|
33
|
+
return JSON.stringify({
|
|
34
|
+
timestamp,
|
|
35
|
+
label,
|
|
36
|
+
level,
|
|
37
|
+
message
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
var logger = createLogger({
|
|
41
|
+
level: 'debug',
|
|
42
|
+
silent: !global.verbose,
|
|
43
|
+
transports: [
|
|
44
|
+
new(transports.Console)({
|
|
45
|
+
timestamp: function () {
|
|
46
|
+
return Date.now();
|
|
47
|
+
},
|
|
48
|
+
format: combine(
|
|
49
|
+
colorize(),
|
|
50
|
+
timestamp(),
|
|
51
|
+
consoleFormat
|
|
52
|
+
)
|
|
53
|
+
}),
|
|
54
|
+
]
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
logger.setLogDirectory = (path) => {
|
|
58
|
+
logger.configure({
|
|
59
|
+
level: 'debug',
|
|
60
|
+
transports: [
|
|
61
|
+
new(transports.Console)({
|
|
62
|
+
silent: !global.verbose,
|
|
63
|
+
timestamp: function () {
|
|
64
|
+
return Date.now();
|
|
65
|
+
},
|
|
66
|
+
format: combine(
|
|
67
|
+
colorize(),
|
|
68
|
+
timestamp(),
|
|
69
|
+
consoleFormat
|
|
70
|
+
)
|
|
71
|
+
}),
|
|
72
|
+
new(transports.File)({
|
|
73
|
+
filename: path + 'build.log',
|
|
74
|
+
timestamp: function () {
|
|
75
|
+
return Date.now();
|
|
76
|
+
},
|
|
77
|
+
format: combine(
|
|
78
|
+
timestamp(),
|
|
79
|
+
consoleFormat
|
|
80
|
+
)
|
|
81
|
+
}),
|
|
82
|
+
new(transports.File)({
|
|
83
|
+
filename: path + '/build.json.log',
|
|
84
|
+
timestamp: function () {
|
|
85
|
+
return Date.now();
|
|
86
|
+
},
|
|
87
|
+
format: combine(
|
|
88
|
+
timestamp(),
|
|
89
|
+
jsonFormat
|
|
90
|
+
)
|
|
91
|
+
}),
|
|
92
|
+
new transports.File({
|
|
93
|
+
filename: path + '/error.log',
|
|
94
|
+
level: 'error',
|
|
95
|
+
format: combine(
|
|
96
|
+
timestamp(),
|
|
97
|
+
consoleFormat
|
|
98
|
+
),
|
|
99
|
+
})
|
|
100
|
+
]
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
module.exports = logger;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
var htmlDecode = require('htmlencode').htmlDecode
|
|
2
|
+
var child_process = require('child_process')
|
|
3
|
+
var escape = require('shell-argument-escape').escape
|
|
4
|
+
var Path = require('path');
|
|
5
|
+
|
|
6
|
+
function exec(cmd, opt) {
|
|
7
|
+
opt = Object.assign({
|
|
8
|
+
cwd: __dirname
|
|
9
|
+
}, opt)
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
child_process.exec(cmd, opt, (err, stdout, stderr) => {
|
|
12
|
+
if(err) {
|
|
13
|
+
reject(stderr)
|
|
14
|
+
} else {
|
|
15
|
+
resolve(stdout)
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getVal(xml, name) {
|
|
22
|
+
var m = new RegExp(`<key>${name}<\\/key>\\n\\s*<string>(.+)<\\/string>`)
|
|
23
|
+
return htmlDecode(xml.match(m)[1])
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getType(xml) {
|
|
27
|
+
var types = {
|
|
28
|
+
appstore: 'appstore',
|
|
29
|
+
inhouse: 'inhouse',
|
|
30
|
+
adhoc: 'adhoc',
|
|
31
|
+
dev: 'dev',
|
|
32
|
+
}
|
|
33
|
+
if(xml.indexOf('<key>ProvisionsAllDevices</key>') >= 0) {
|
|
34
|
+
return types.inhouse
|
|
35
|
+
}
|
|
36
|
+
if(xml.indexOf('<key>ProvisionedDevices</key>') < 0) {
|
|
37
|
+
return types.appstore
|
|
38
|
+
}
|
|
39
|
+
if(xml.match(/<key>get-task-allow<\/key>\n\s*<true\/>/)) {
|
|
40
|
+
return types.dev
|
|
41
|
+
}
|
|
42
|
+
return types.adhoc
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getInfo(xml) {
|
|
46
|
+
var info = {}
|
|
47
|
+
info.uuid = getVal(xml, 'UUID')
|
|
48
|
+
info.team = {
|
|
49
|
+
name: getVal(xml, 'TeamName'),
|
|
50
|
+
id: getVal(xml, 'com.apple.developer.team-identifier'),
|
|
51
|
+
}
|
|
52
|
+
info.appid = getVal(xml, 'application-identifier')
|
|
53
|
+
info.name = getVal(xml, 'Name')
|
|
54
|
+
info.type = getType(xml)
|
|
55
|
+
var cers = xml.match(/<key>DeveloperCertificates<\/key>\n\s*<array>\n\s*((?:<data>\S+?<\/data>\n\s*)+)<\/array>/)[1]
|
|
56
|
+
info.cers = cers.match(/[^<>]{10,}/g)
|
|
57
|
+
return info
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function main(profilePath, cb) {
|
|
61
|
+
var cmd = `security cms -D -i ${escape(Path.resolve(profilePath))}`
|
|
62
|
+
return exec(cmd)
|
|
63
|
+
.then(stdout => {
|
|
64
|
+
var info = getInfo(stdout)
|
|
65
|
+
if(typeof cb === 'function') {
|
|
66
|
+
cb(info)
|
|
67
|
+
}
|
|
68
|
+
return Promise.resolve(info)
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = main
|