react-native-debug-toolkit 3.3.2 → 3.3.4
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 +40 -1
- package/README.zh-CN.md +40 -1
- package/app.plugin.js +51 -0
- package/bin/debug-toolkit.js +18 -2
- package/dev-client.js +3 -0
- package/ios/DebugToolkitDevConnect.h +17 -0
- package/ios/DebugToolkitDevConnect.mm +260 -212
- package/lib/commonjs/features/devConnect/DevConnectTab.js +127 -126
- package/lib/commonjs/features/devConnect/DevConnectTab.js.map +1 -1
- package/lib/commonjs/features/devConnect/nativeDevConnect.js +0 -12
- package/lib/commonjs/features/devConnect/nativeDevConnect.js.map +1 -1
- package/lib/module/features/devConnect/DevConnectTab.js +129 -128
- package/lib/module/features/devConnect/DevConnectTab.js.map +1 -1
- package/lib/module/features/devConnect/nativeDevConnect.js +0 -11
- package/lib/module/features/devConnect/nativeDevConnect.js.map +1 -1
- package/lib/typescript/src/features/devConnect/DevConnectTab.d.ts.map +1 -1
- package/lib/typescript/src/features/devConnect/nativeDevConnect.d.ts +6 -4
- package/lib/typescript/src/features/devConnect/nativeDevConnect.d.ts.map +1 -1
- package/package.json +6 -2
- package/scripts/bundle/android.js +101 -0
- package/scripts/bundle/cli.js +57 -0
- package/scripts/bundle/doctor.js +38 -0
- package/scripts/bundle/ios.js +179 -0
- package/scripts/bundle/setup.js +39 -0
- package/scripts/debug-bundle.gradle +147 -0
- package/src/features/devConnect/DevConnectTab.tsx +92 -92
- package/src/features/devConnect/nativeDevConnect.ts +8 -16
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execFileSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const BEGIN = '// react-native-debug-toolkit: begin debug bundle';
|
|
8
|
+
const END = '// react-native-debug-toolkit: end debug bundle';
|
|
9
|
+
const REL_SCRIPT = '../../node_modules/react-native-debug-toolkit/scripts/debug-bundle.gradle';
|
|
10
|
+
const EXPECTED_TASK = 'createDebugToolkitDebugJsAndAssets';
|
|
11
|
+
const GRADLE_TASKS_ARGS = [':app:tasks', '--all', '--no-daemon', '--console=plain'];
|
|
12
|
+
|
|
13
|
+
function findGradleFile(cwd) {
|
|
14
|
+
const groovy = path.join(cwd, 'android/app/build.gradle');
|
|
15
|
+
const kotlin = path.join(cwd, 'android/app/build.gradle.kts');
|
|
16
|
+
|
|
17
|
+
if (fs.existsSync(kotlin)) {
|
|
18
|
+
return { file: kotlin, kind: 'kotlin' };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (fs.existsSync(groovy)) {
|
|
22
|
+
return { file: groovy, kind: 'groovy' };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
throw new Error('android/app/build.gradle(.kts) not found.');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function block(kind) {
|
|
29
|
+
const applyLine = kind === 'kotlin'
|
|
30
|
+
? `apply(from = "${REL_SCRIPT}")`
|
|
31
|
+
: `apply from: "${REL_SCRIPT}"`;
|
|
32
|
+
|
|
33
|
+
return `${BEGIN}\n${applyLine}\n${END}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function removeBlock(content) {
|
|
37
|
+
return content.replace(
|
|
38
|
+
/\/\/ react-native-debug-toolkit: begin debug bundle\n[\s\S]*?\/\/ react-native-debug-toolkit: end debug bundle\n?/g,
|
|
39
|
+
'',
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function setupAndroidBundle(options) {
|
|
44
|
+
const gradle = findGradleFile(options.cwd);
|
|
45
|
+
const content = fs.readFileSync(gradle.file, 'utf8');
|
|
46
|
+
|
|
47
|
+
if (content.includes(BEGIN)) {
|
|
48
|
+
return { ok: true, changed: false, file: gradle.file };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fs.writeFileSync(gradle.file, `${content.trimEnd()}\n\n${block(gradle.kind)}\n`);
|
|
52
|
+
return { ok: true, changed: true, file: gradle.file };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function undoAndroidBundle(options) {
|
|
56
|
+
const gradle = findGradleFile(options.cwd);
|
|
57
|
+
const content = fs.readFileSync(gradle.file, 'utf8');
|
|
58
|
+
const next = removeBlock(content).replace(/\n{3,}/g, '\n\n');
|
|
59
|
+
|
|
60
|
+
if (next === content) {
|
|
61
|
+
return { ok: true, changed: false, file: gradle.file };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fs.writeFileSync(gradle.file, next);
|
|
65
|
+
return { ok: true, changed: true, file: gradle.file };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function checkAndroidBundle(options) {
|
|
69
|
+
const gradle = findGradleFile(options.cwd);
|
|
70
|
+
const content = fs.readFileSync(gradle.file, 'utf8');
|
|
71
|
+
const hasMarker = content.includes(BEGIN) && content.includes('scripts/debug-bundle.gradle');
|
|
72
|
+
|
|
73
|
+
if (!hasMarker) {
|
|
74
|
+
return { ok: false, reason: 'marker_missing', file: gradle.file };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const androidDir = path.join(options.cwd, 'android');
|
|
78
|
+
const gradlew = path.join(androidDir, 'gradlew');
|
|
79
|
+
const command = fs.existsSync(gradlew) ? gradlew : 'gradle';
|
|
80
|
+
const runGradle = options.runGradle || ((cmd, args, opts) => execFileSync(cmd, args, {
|
|
81
|
+
cwd: opts.cwd,
|
|
82
|
+
encoding: 'utf8',
|
|
83
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
84
|
+
}));
|
|
85
|
+
const output = runGradle(command, GRADLE_TASKS_ARGS, { cwd: androidDir });
|
|
86
|
+
|
|
87
|
+
if (!String(output).includes(EXPECTED_TASK)) {
|
|
88
|
+
return { ok: false, reason: 'gradle_task_missing', file: gradle.file };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { ok: true, file: gradle.file };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = {
|
|
95
|
+
setupAndroidBundle,
|
|
96
|
+
undoAndroidBundle,
|
|
97
|
+
checkAndroidBundle,
|
|
98
|
+
BEGIN,
|
|
99
|
+
END,
|
|
100
|
+
EXPECTED_TASK,
|
|
101
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function readOption(args, name) {
|
|
4
|
+
const index = args.indexOf(name);
|
|
5
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function hasFlag(args, name) {
|
|
9
|
+
return args.includes(name);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseCommon(args, cwd) {
|
|
13
|
+
return {
|
|
14
|
+
cwd,
|
|
15
|
+
platform: readOption(args, '--platform') || 'all',
|
|
16
|
+
undo: hasFlag(args, '--undo'),
|
|
17
|
+
check: hasFlag(args, '--check'),
|
|
18
|
+
iosTarget: readOption(args, '--ios-target'),
|
|
19
|
+
yes: hasFlag(args, '--yes'),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function runBundleCli(args, deps = {}) {
|
|
24
|
+
const cwd = deps.cwd || process.cwd();
|
|
25
|
+
const io = deps.io || {
|
|
26
|
+
writeOut: (value) => process.stdout.write(value),
|
|
27
|
+
writeErr: (value) => process.stderr.write(value),
|
|
28
|
+
};
|
|
29
|
+
const command = args[0];
|
|
30
|
+
|
|
31
|
+
if (command === 'setup-bundle') {
|
|
32
|
+
const setupBundle = deps.setupBundle || require('./setup').setupBundle;
|
|
33
|
+
await setupBundle(parseCommon(args.slice(1), cwd));
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (command === 'doctor-bundle') {
|
|
38
|
+
const doctorBundle = deps.doctorBundle || require('./doctor').doctorBundle;
|
|
39
|
+
await doctorBundle({
|
|
40
|
+
cwd,
|
|
41
|
+
platform: readOption(args.slice(1), '--platform') || 'all',
|
|
42
|
+
app: readOption(args.slice(1), '--app'),
|
|
43
|
+
apk: readOption(args.slice(1), '--apk'),
|
|
44
|
+
});
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (command === 'embed') {
|
|
49
|
+
io.writeErr('Unknown command: embed\nUse: debug-toolkit setup-bundle\n');
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
io.writeErr(`Unknown command: ${command || '(none)'}\n`);
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { runBundleCli };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execFileSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
function readZipEntriesWithUnzip(file) {
|
|
8
|
+
const output = execFileSync('unzip', ['-Z1', file], { encoding: 'utf8' });
|
|
9
|
+
return output.split(/\r?\n/).filter(Boolean);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function doctorIos(options) {
|
|
13
|
+
if (!options.app) throw new Error('--app is required for iOS doctor.');
|
|
14
|
+
const bundle = path.join(options.app, 'main.jsbundle');
|
|
15
|
+
if (!fs.existsSync(bundle)) {
|
|
16
|
+
throw new Error(`main.jsbundle not found in ${options.app}`);
|
|
17
|
+
}
|
|
18
|
+
return { ok: true, platform: 'ios', bundle };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function doctorAndroid(options) {
|
|
22
|
+
if (!options.apk) throw new Error('--apk is required for Android doctor.');
|
|
23
|
+
const readZipEntries = options.readZipEntries || readZipEntriesWithUnzip;
|
|
24
|
+
const entries = await readZipEntries(options.apk);
|
|
25
|
+
const bundle = entries.find((entry) => /^assets\/.+\.bundle$/.test(entry));
|
|
26
|
+
if (!bundle) {
|
|
27
|
+
throw new Error(`Android JS bundle not found in ${options.apk}`);
|
|
28
|
+
}
|
|
29
|
+
return { ok: true, platform: 'android', bundle };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function doctorBundle(options) {
|
|
33
|
+
if (options.platform === 'ios') return doctorIos(options);
|
|
34
|
+
if (options.platform === 'android') return doctorAndroid(options);
|
|
35
|
+
throw new Error(`Unsupported platform for doctor-bundle: ${options.platform}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { doctorBundle, readZipEntriesWithUnzip };
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const xcode = require('xcode');
|
|
6
|
+
|
|
7
|
+
const PHASE_NAME = 'Bundle React Native code and images';
|
|
8
|
+
const BEGIN = '# react-native-debug-toolkit: begin debug bundle';
|
|
9
|
+
const END = '# react-native-debug-toolkit: end debug bundle';
|
|
10
|
+
const BLOCK = `${BEGIN}\nexport FORCE_BUNDLING=1\n${END}`;
|
|
11
|
+
|
|
12
|
+
function stripQuotes(value) {
|
|
13
|
+
return String(value || '').replace(/^"|"$/g, '');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function findProjects(cwd) {
|
|
17
|
+
const iosDir = path.join(cwd, 'ios');
|
|
18
|
+
if (!fs.existsSync(iosDir)) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return fs.readdirSync(iosDir)
|
|
23
|
+
.filter((entry) => entry.endsWith('.xcodeproj') && entry !== 'Pods.xcodeproj')
|
|
24
|
+
.map((entry) => path.join(iosDir, entry, 'project.pbxproj'))
|
|
25
|
+
.filter((file) => fs.existsSync(file));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function decodeScript(value) {
|
|
29
|
+
if (!value) {
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(value);
|
|
35
|
+
} catch {
|
|
36
|
+
return stripQuotes(value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function encodeScript(value) {
|
|
41
|
+
return JSON.stringify(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function phaseName(phase) {
|
|
45
|
+
return stripQuotes(phase.name || phase.comment);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isAppTarget(target) {
|
|
49
|
+
return stripQuotes(target.productType) === 'com.apple.product-type.application';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function targetName(target) {
|
|
53
|
+
return stripQuotes(target.name || target.productName || target.comment);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function targetBuildPhaseIds(target) {
|
|
57
|
+
return (target.buildPhases || []).map((phaseRef) => {
|
|
58
|
+
if (typeof phaseRef === 'string') {
|
|
59
|
+
return phaseRef;
|
|
60
|
+
}
|
|
61
|
+
return phaseRef.value;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function sectionEntries(objects, sectionName, isaName) {
|
|
66
|
+
const section = objects[sectionName];
|
|
67
|
+
if (section) {
|
|
68
|
+
return Object.entries(section)
|
|
69
|
+
.filter(([key]) => !key.endsWith('_comment'));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return Object.entries(objects)
|
|
73
|
+
.filter(([key, value]) => !key.endsWith('_comment') && value && value.isa === isaName)
|
|
74
|
+
.map(([key, value]) => [key, { ...value, comment: objects[`${key}_comment`] }]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function loadTarget(cwd, iosTarget) {
|
|
78
|
+
const projects = findProjects(cwd);
|
|
79
|
+
if (projects.length !== 1) {
|
|
80
|
+
throw new Error(`Expected one Xcode project, found ${projects.length}. Pass --ios-target after selecting the app project.`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const pbxFile = projects[0];
|
|
84
|
+
const proj = xcode.project(pbxFile).parseSync();
|
|
85
|
+
const objects = proj.hash.project.objects;
|
|
86
|
+
const nativeTargets = sectionEntries(objects, 'PBXNativeTarget', 'PBXNativeTarget');
|
|
87
|
+
const shellPhases = new Map(sectionEntries(objects, 'PBXShellScriptBuildPhase', 'PBXShellScriptBuildPhase'));
|
|
88
|
+
|
|
89
|
+
const appTargets = nativeTargets
|
|
90
|
+
.filter(([, target]) => isAppTarget(target));
|
|
91
|
+
|
|
92
|
+
const matches = iosTarget
|
|
93
|
+
? appTargets.filter(([, target]) => targetName(target) === iosTarget)
|
|
94
|
+
: appTargets;
|
|
95
|
+
|
|
96
|
+
if (matches.length !== 1) {
|
|
97
|
+
throw new Error(`Expected one iOS app target, found ${matches.length}. Pass --ios-target <name>.`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const [, target] = matches[0];
|
|
101
|
+
for (const phaseId of targetBuildPhaseIds(target)) {
|
|
102
|
+
const phase = shellPhases.get(phaseId);
|
|
103
|
+
if (!phase || phaseName(phase) !== PHASE_NAME) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const script = decodeScript(phase.shellScript);
|
|
108
|
+
if (!script.includes('react-native-xcode.sh') && !script.includes('with-environment.sh')) {
|
|
109
|
+
throw new Error(`${PHASE_NAME} does not call React Native bundling script.`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { pbxFile, proj, phase };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
throw new Error(`${PHASE_NAME} phase not found on iOS app target.`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function insertBlock(script) {
|
|
119
|
+
if (script.includes(BEGIN)) {
|
|
120
|
+
return script;
|
|
121
|
+
}
|
|
122
|
+
return `${BLOCK}\n${script}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function removeBlock(script) {
|
|
126
|
+
return script.replace(
|
|
127
|
+
/# react-native-debug-toolkit: begin debug bundle\r?\nexport FORCE_BUNDLING=1\r?\n# react-native-debug-toolkit: end debug bundle\r?\n?/g,
|
|
128
|
+
'',
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function writeProject(ctx, script) {
|
|
133
|
+
ctx.phase.shellScript = encodeScript(script);
|
|
134
|
+
fs.writeFileSync(ctx.pbxFile, ctx.proj.writeSync());
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function setupIosBundle(options) {
|
|
138
|
+
const ctx = loadTarget(options.cwd, options.iosTarget);
|
|
139
|
+
const script = decodeScript(ctx.phase.shellScript);
|
|
140
|
+
const next = insertBlock(script);
|
|
141
|
+
|
|
142
|
+
if (next === script) {
|
|
143
|
+
return { ok: true, changed: false };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
writeProject(ctx, next);
|
|
147
|
+
return { ok: true, changed: true };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function undoIosBundle(options) {
|
|
151
|
+
const ctx = loadTarget(options.cwd, options.iosTarget);
|
|
152
|
+
const script = decodeScript(ctx.phase.shellScript);
|
|
153
|
+
const next = removeBlock(script);
|
|
154
|
+
|
|
155
|
+
if (next === script) {
|
|
156
|
+
return { ok: true, changed: false };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
writeProject(ctx, next);
|
|
160
|
+
return { ok: true, changed: true };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function checkIosBundle(options) {
|
|
164
|
+
const ctx = loadTarget(options.cwd, options.iosTarget);
|
|
165
|
+
const script = decodeScript(ctx.phase.shellScript);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
ok: script.includes(BLOCK),
|
|
169
|
+
changed: false,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = {
|
|
174
|
+
setupIosBundle,
|
|
175
|
+
undoIosBundle,
|
|
176
|
+
checkIosBundle,
|
|
177
|
+
BEGIN,
|
|
178
|
+
END,
|
|
179
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { setupIosBundle, undoIosBundle, checkIosBundle } = require('./ios');
|
|
4
|
+
const { setupAndroidBundle, undoAndroidBundle, checkAndroidBundle } = require('./android');
|
|
5
|
+
|
|
6
|
+
async function setupBundle(options) {
|
|
7
|
+
const platforms = options.platform === 'all' ? ['ios', 'android'] : [options.platform];
|
|
8
|
+
const results = [];
|
|
9
|
+
|
|
10
|
+
for (const platform of platforms) {
|
|
11
|
+
if (platform === 'ios') {
|
|
12
|
+
if (options.undo) {
|
|
13
|
+
results.push(undoIosBundle(options));
|
|
14
|
+
} else if (options.check) {
|
|
15
|
+
results.push(checkIosBundle(options));
|
|
16
|
+
} else {
|
|
17
|
+
results.push(setupIosBundle(options));
|
|
18
|
+
}
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (platform === 'android') {
|
|
23
|
+
if (options.undo) {
|
|
24
|
+
results.push(undoAndroidBundle(options));
|
|
25
|
+
} else if (options.check) {
|
|
26
|
+
results.push(checkAndroidBundle(options));
|
|
27
|
+
} else {
|
|
28
|
+
results.push(setupAndroidBundle(options));
|
|
29
|
+
}
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return results;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { setupBundle };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// react-native-debug-toolkit: persistent debug/development bundle generation
|
|
2
|
+
|
|
3
|
+
import java.util.Locale
|
|
4
|
+
import javax.inject.Inject
|
|
5
|
+
import org.gradle.api.DefaultTask
|
|
6
|
+
import org.gradle.api.GradleException
|
|
7
|
+
import org.gradle.api.file.DirectoryProperty
|
|
8
|
+
import org.gradle.api.file.RegularFileProperty
|
|
9
|
+
import org.gradle.api.provider.ListProperty
|
|
10
|
+
import org.gradle.api.provider.Property
|
|
11
|
+
import org.gradle.api.tasks.Input
|
|
12
|
+
import org.gradle.api.tasks.InputDirectory
|
|
13
|
+
import org.gradle.api.tasks.InputFile
|
|
14
|
+
import org.gradle.api.tasks.Optional
|
|
15
|
+
import org.gradle.api.tasks.OutputDirectory
|
|
16
|
+
import org.gradle.api.tasks.TaskAction
|
|
17
|
+
import org.gradle.process.ExecOperations
|
|
18
|
+
|
|
19
|
+
abstract class DebugToolkitBundleTask extends DefaultTask {
|
|
20
|
+
@Inject
|
|
21
|
+
abstract ExecOperations getExecOperations()
|
|
22
|
+
|
|
23
|
+
@InputDirectory
|
|
24
|
+
abstract DirectoryProperty getRoot()
|
|
25
|
+
|
|
26
|
+
@InputFile
|
|
27
|
+
abstract RegularFileProperty getCliFile()
|
|
28
|
+
|
|
29
|
+
@InputFile
|
|
30
|
+
@Optional
|
|
31
|
+
abstract RegularFileProperty getEntryFile()
|
|
32
|
+
|
|
33
|
+
@InputFile
|
|
34
|
+
@Optional
|
|
35
|
+
abstract RegularFileProperty getBundleConfig()
|
|
36
|
+
|
|
37
|
+
@Input
|
|
38
|
+
abstract ListProperty<String> getNodeExecutableAndArgs()
|
|
39
|
+
|
|
40
|
+
@Input
|
|
41
|
+
abstract Property<String> getBundleCommand()
|
|
42
|
+
|
|
43
|
+
@Input
|
|
44
|
+
abstract Property<String> getBundleAssetName()
|
|
45
|
+
|
|
46
|
+
@Input
|
|
47
|
+
abstract ListProperty<String> getExtraPackagerArgs()
|
|
48
|
+
|
|
49
|
+
@OutputDirectory
|
|
50
|
+
abstract DirectoryProperty getJsBundleDir()
|
|
51
|
+
|
|
52
|
+
@OutputDirectory
|
|
53
|
+
abstract DirectoryProperty getResourcesDir()
|
|
54
|
+
|
|
55
|
+
@TaskAction
|
|
56
|
+
void run() {
|
|
57
|
+
def rootFile = root.get().asFile
|
|
58
|
+
def bundleOutput = new File(jsBundleDir.get().asFile, bundleAssetName.get())
|
|
59
|
+
def entry = resolveEntryFile(rootFile)
|
|
60
|
+
jsBundleDir.get().asFile.mkdirs()
|
|
61
|
+
resourcesDir.get().asFile.mkdirs()
|
|
62
|
+
|
|
63
|
+
def command = []
|
|
64
|
+
command.addAll(nodeExecutableAndArgs.get())
|
|
65
|
+
command.add(cliFile.get().asFile.absolutePath)
|
|
66
|
+
command.add(bundleCommand.get())
|
|
67
|
+
command.addAll([
|
|
68
|
+
"--platform", "android",
|
|
69
|
+
"--dev", "true",
|
|
70
|
+
"--entry-file", entry.absolutePath,
|
|
71
|
+
"--bundle-output", bundleOutput.absolutePath,
|
|
72
|
+
"--assets-dest", resourcesDir.get().asFile.absolutePath,
|
|
73
|
+
])
|
|
74
|
+
|
|
75
|
+
if (bundleConfig.isPresent()) {
|
|
76
|
+
command.add("--config")
|
|
77
|
+
command.add(bundleConfig.get().asFile.absolutePath)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
command.addAll(extraPackagerArgs.get())
|
|
81
|
+
|
|
82
|
+
execOperations.exec {
|
|
83
|
+
workingDir(rootFile)
|
|
84
|
+
commandLine(command)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private File resolveEntryFile(File rootFile) {
|
|
89
|
+
if (entryFile.isPresent()) {
|
|
90
|
+
return entryFile.get().asFile
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
def androidEntry = new File(rootFile, "index.android.js")
|
|
94
|
+
return androidEntry.exists() ? androidEntry : new File(rootFile, "index.js")
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
def toolkitCapitalize = { String value ->
|
|
99
|
+
value.length() == 0 ? value : value.substring(0, 1).toUpperCase(Locale.ROOT) + value.substring(1)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
def toolkitReactExt = project.extensions.findByName("react")
|
|
103
|
+
def toolkitAndroidComponents = project.extensions.findByName("androidComponents")
|
|
104
|
+
|
|
105
|
+
if (toolkitAndroidComponents == null) {
|
|
106
|
+
throw new GradleException("react-native-debug-toolkit debug-bundle.gradle requires the Android Gradle Plugin.")
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
def toolkitDefaultRoot = project.rootProject.layout.projectDirectory.dir("../")
|
|
110
|
+
def toolkitDebuggableVariants = toolkitReactExt?.debuggableVariants?.getOrElse(["debug"]) ?: ["debug"]
|
|
111
|
+
|
|
112
|
+
toolkitAndroidComponents.onVariants(toolkitAndroidComponents.selector().all()) { variant ->
|
|
113
|
+
def variantName = variant.name
|
|
114
|
+
def isDebuggable = toolkitDebuggableVariants.any { it.equalsIgnoreCase(variantName) }
|
|
115
|
+
|
|
116
|
+
if (!isDebuggable) {
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
def capName = toolkitCapitalize(variantName)
|
|
121
|
+
def bundleTask = tasks.register("createDebugToolkit${capName}JsAndAssets", DebugToolkitBundleTask) {
|
|
122
|
+
group = "react"
|
|
123
|
+
description = "Generate embedded React Native JS bundle for ${variantName} debug build."
|
|
124
|
+
|
|
125
|
+
root.set(toolkitReactExt?.root ?: toolkitDefaultRoot)
|
|
126
|
+
cliFile.set(toolkitReactExt?.cliFile ?: root.file("node_modules/react-native/cli.js"))
|
|
127
|
+
if (toolkitReactExt?.entryFile?.isPresent()) {
|
|
128
|
+
entryFile.set(toolkitReactExt.entryFile)
|
|
129
|
+
}
|
|
130
|
+
if (toolkitReactExt?.bundleConfig?.isPresent()) {
|
|
131
|
+
bundleConfig.set(toolkitReactExt.bundleConfig)
|
|
132
|
+
}
|
|
133
|
+
nodeExecutableAndArgs.set(toolkitReactExt?.nodeExecutableAndArgs ?: ["node"])
|
|
134
|
+
bundleCommand.set(toolkitReactExt?.bundleCommand ?: "bundle")
|
|
135
|
+
bundleAssetName.set(toolkitReactExt?.bundleAssetName ?: "index.android.bundle")
|
|
136
|
+
extraPackagerArgs.set(toolkitReactExt?.extraPackagerArgs ?: [])
|
|
137
|
+
jsBundleDir.set(project.layout.buildDirectory.dir("generated/assets/react-native-debug-toolkit/${variantName}"))
|
|
138
|
+
resourcesDir.set(project.layout.buildDirectory.dir("generated/res/react-native-debug-toolkit/${variantName}"))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
variant.sources.assets?.addGeneratedSourceDirectory(bundleTask) { task ->
|
|
142
|
+
task.jsBundleDir
|
|
143
|
+
}
|
|
144
|
+
variant.sources.res?.addGeneratedSourceDirectory(bundleTask) { task ->
|
|
145
|
+
task.resourcesDir
|
|
146
|
+
}
|
|
147
|
+
}
|