@cloudcare/hvigor-ohos-plugin 0.1.1 → 0.1.2-alpha.1
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/CHANGELOG.md +10 -1
- package/README.md +31 -6
- package/hvigorCompile/ft-cold-launch-transform.ts +599 -121
- package/hvigorCompile/ft-console-log-transform.ts +73 -95
- package/hvigorCompile/ft-generated-names.ts +19 -0
- package/hvigorCompile/ft-lexical-scope.ts +65 -0
- package/hvigorCompile/ft-session-replay-transform.ts +363 -0
- package/hvigorCompile/ft-source-transform-host.ts +45 -22
- package/hvigorCompile/ft-transform-context.ts +36 -0
- package/hvigorCompile/ft-web-replay-transform.ts +51 -0
- package/index.js +5 -3
- package/package.json +12 -3
|
@@ -5,21 +5,45 @@
|
|
|
5
5
|
* the compiler's in-memory source. Application files are never modified.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
import { hasRuntimeDependency } from './ft-transform-context';
|
|
9
|
+
import { createLexicalResolver } from './ft-lexical-scope';
|
|
10
|
+
import { allocateName as selectIdentifierName, collectIdentifierNames } from './ft-generated-names';
|
|
11
|
+
|
|
12
|
+
declare const require: any;
|
|
13
13
|
declare const __dirname: string;
|
|
14
14
|
|
|
15
15
|
type TypeScriptApi = any;
|
|
16
|
-
type AbilityClassKind = 'stage' | 'ui' | 'other';
|
|
16
|
+
type AbilityClassKind = 'stage' | 'ui' | 'other' | 'intent' | 'legacyIntent';
|
|
17
|
+
type SourceReader = (fileName: string) => string | undefined;
|
|
18
|
+
|
|
19
|
+
interface AbilityClassInfo {
|
|
20
|
+
kind: AbilityClassKind | null;
|
|
21
|
+
applicationBase: boolean;
|
|
22
|
+
base?: any;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface AbilityBase {
|
|
26
|
+
kind: AbilityClassKind | null;
|
|
27
|
+
declaration?: any;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface LaunchParameterTypes {
|
|
31
|
+
want: string;
|
|
32
|
+
abilityConstant: string;
|
|
33
|
+
needed: boolean;
|
|
34
|
+
}
|
|
17
35
|
|
|
18
36
|
interface ColdLaunchInjection {
|
|
19
37
|
position: number;
|
|
38
|
+
end?: number;
|
|
20
39
|
content: string;
|
|
21
40
|
}
|
|
22
41
|
|
|
42
|
+
interface ColdLaunchInjections {
|
|
43
|
+
injections: ColdLaunchInjection[];
|
|
44
|
+
hasUIAbility: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
23
47
|
const COLD_LAUNCH_BRIDGE_IMPORT = '@guancecloud/ft_sdk/Index';
|
|
24
48
|
const COLD_LAUNCH_TRANSFORM_MARKER = '/* guance-cold-launch-transform:instrumented */';
|
|
25
49
|
|
|
@@ -35,27 +59,6 @@ function visit(ts: TypeScriptApi, node: any, visitor: (current: any) => void): v
|
|
|
35
59
|
ts.forEachChild(node, (child: any) => visit(ts, child, visitor));
|
|
36
60
|
}
|
|
37
61
|
|
|
38
|
-
function collectIdentifierNames(ts: TypeScriptApi, sourceFile: any): Set<string> {
|
|
39
|
-
const identifiers = new Set<string>();
|
|
40
|
-
visit(ts, sourceFile, (node: any) => {
|
|
41
|
-
if (ts.isIdentifier(node)) {
|
|
42
|
-
identifiers.add(node.text);
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
return identifiers;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function selectBridgeName(identifiers: Set<string>): string {
|
|
49
|
-
const baseName = '__ftColdLaunchBridge';
|
|
50
|
-
let bridgeName = baseName;
|
|
51
|
-
let suffix = 1;
|
|
52
|
-
while (identifiers.has(bridgeName)) {
|
|
53
|
-
bridgeName = `${baseName}${suffix}`;
|
|
54
|
-
suffix++;
|
|
55
|
-
}
|
|
56
|
-
return bridgeName;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
62
|
function getPluginVersion(): string {
|
|
60
63
|
try {
|
|
61
64
|
const fileSystem = require.main.require('fs');
|
|
@@ -82,7 +85,57 @@ function findImportInsertionOffset(ts: TypeScriptApi, sourceFile: any): number {
|
|
|
82
85
|
return offset;
|
|
83
86
|
}
|
|
84
87
|
|
|
88
|
+
/** Only opt in when a readable HAP manifest explicitly has no module entry. */
|
|
89
|
+
function needsUIAbilityModuleFallback(ts: TypeScriptApi, id: string, readSource?: SourceReader): boolean {
|
|
90
|
+
const normalized = id.replace(/\\/g, '/');
|
|
91
|
+
const sourceIndex = normalized.lastIndexOf('/src/main/ets/');
|
|
92
|
+
if (sourceIndex < 0) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
const manifestPath = normalized.slice(0, sourceIndex) + '/src/main/module.json5';
|
|
96
|
+
try {
|
|
97
|
+
const nodeRequire = require.main && typeof require.main.require === 'function'
|
|
98
|
+
? require.main.require.bind(require.main) : require;
|
|
99
|
+
const code = readSource ? readSource(manifestPath) : nodeRequire('fs').readFileSync(manifestPath, 'utf8');
|
|
100
|
+
if (typeof code !== 'string') {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
// Parsing an object initializer accepts JSON5 comments, single quotes and
|
|
104
|
+
// unquoted keys without mistaking nested ability srcEntry fields for Stage.
|
|
105
|
+
const parsed = ts.createSourceFile(manifestPath, `const __manifest = ${code}`,
|
|
106
|
+
ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
107
|
+
if (parsed.parseDiagnostics.length > 0 || parsed.statements.length !== 1 ||
|
|
108
|
+
!ts.isVariableStatement(parsed.statements[0])) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
const root = parsed.statements[0].declarationList.declarations[0].initializer;
|
|
112
|
+
const property = (object: any, name: string): any | undefined => {
|
|
113
|
+
if (!object || !ts.isObjectLiteralExpression(object)) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
const matches = object.properties.filter((item: any) => ts.isPropertyAssignment(item) &&
|
|
117
|
+
(ts.isIdentifier(item.name) || ts.isStringLiteral(item.name)) && item.name.text === name);
|
|
118
|
+
return matches.length > 0 ? matches[matches.length - 1].initializer : undefined;
|
|
119
|
+
};
|
|
120
|
+
const module = property(root, 'module');
|
|
121
|
+
const type = property(module, 'type');
|
|
122
|
+
if (!type || !ts.isStringLiteral(type) || (type.text !== 'entry' && type.text !== 'feature')) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
const stageEntry = property(module, 'srcEntry');
|
|
126
|
+
return stageEntry === undefined || (ts.isStringLiteral(stageEntry) && stageEntry.text.trim().length === 0);
|
|
127
|
+
} catch (_) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
85
132
|
function classifyAbilityBaseName(name: string): AbilityClassKind | null {
|
|
133
|
+
if (name === 'InsightIntentEntryExecutor') {
|
|
134
|
+
return 'intent';
|
|
135
|
+
}
|
|
136
|
+
if (name === 'InsightIntentExecutor') {
|
|
137
|
+
return 'legacyIntent';
|
|
138
|
+
}
|
|
86
139
|
if (name === 'AbilityStage') {
|
|
87
140
|
return 'stage';
|
|
88
141
|
}
|
|
@@ -95,54 +148,218 @@ function classifyAbilityBaseName(name: string): AbilityClassKind | null {
|
|
|
95
148
|
return null;
|
|
96
149
|
}
|
|
97
150
|
|
|
98
|
-
function
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
151
|
+
function bindingContains(ts: TypeScriptApi, binding: any, name: string): boolean {
|
|
152
|
+
if (ts.isIdentifier(binding)) {
|
|
153
|
+
return binding.text === name;
|
|
154
|
+
}
|
|
155
|
+
return !!binding.elements && binding.elements.some((element: any) =>
|
|
156
|
+
ts.isBindingElement(element) && bindingContains(ts, element.name, name));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function findValueDeclaration(ts: TypeScriptApi, location: any, name: string): any | null {
|
|
160
|
+
for (let scope = location.parent; scope; scope = scope.parent) {
|
|
161
|
+
if (ts.isClassDeclaration(scope) && scope.name && scope.name.text === name) {
|
|
162
|
+
return scope;
|
|
108
163
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
bindings.set(importClause.name.text, defaultKind);
|
|
164
|
+
if (ts.isFunctionLike(scope)) {
|
|
165
|
+
const parameter = scope.parameters.find((item: any) => bindingContains(ts, item.name, name));
|
|
166
|
+
if (parameter) {
|
|
167
|
+
return parameter;
|
|
114
168
|
}
|
|
115
169
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
170
|
+
for (const statement of scope.statements || []) {
|
|
171
|
+
if (ts.isImportDeclaration(statement) && statement.importClause) {
|
|
172
|
+
const clause = statement.importClause;
|
|
173
|
+
if (clause.name && clause.name.text === name) {
|
|
174
|
+
return clause;
|
|
175
|
+
}
|
|
176
|
+
if (clause.namedBindings) {
|
|
177
|
+
if (ts.isNamespaceImport(clause.namedBindings) && clause.namedBindings.name.text === name) {
|
|
178
|
+
return clause.namedBindings;
|
|
179
|
+
}
|
|
180
|
+
if (ts.isNamedImports(clause.namedBindings)) {
|
|
181
|
+
const imported = clause.namedBindings.elements.find((item: any) => item.name.text === name);
|
|
182
|
+
if (imported) {
|
|
183
|
+
return imported;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
124
187
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
188
|
+
if ((ts.isClassDeclaration(statement) || ts.isFunctionDeclaration(statement) ||
|
|
189
|
+
ts.isEnumDeclaration(statement) || ts.isModuleDeclaration(statement)) &&
|
|
190
|
+
statement.name && statement.name.text === name) {
|
|
191
|
+
return statement;
|
|
192
|
+
}
|
|
193
|
+
if (ts.isVariableStatement(statement)) {
|
|
194
|
+
const variable = statement.declarationList.declarations.find((item: any) =>
|
|
195
|
+
bindingContains(ts, item.name, name));
|
|
196
|
+
if (variable) {
|
|
197
|
+
return variable;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
128
203
|
}
|
|
129
204
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
205
|
+
/** Resolve source-visible inheritance without reading through our own transform hook. */
|
|
206
|
+
function createAbilityClassResolver(ts: TypeScriptApi, sourceFile: any, readSource?: SourceReader):
|
|
207
|
+
(declaration: any) => AbilityClassInfo {
|
|
208
|
+
const nodeRequire = require.main && typeof require.main.require === 'function'
|
|
209
|
+
? require.main.require.bind(require.main) : require;
|
|
210
|
+
const path = nodeRequire('path');
|
|
211
|
+
const sources = new Map<string, any | null>();
|
|
212
|
+
sources.set(path.resolve(sourceFile.fileName), sourceFile);
|
|
213
|
+
const classCache = new Map<any, AbilityClassInfo>();
|
|
214
|
+
const resolvingClasses = new Set<any>();
|
|
215
|
+
const resolvingExports = new Set<string>();
|
|
216
|
+
const unknown: AbilityBase = { kind: null };
|
|
217
|
+
|
|
218
|
+
const loadRelativeSource = (from: any, moduleName: string): any | null => {
|
|
219
|
+
if (!moduleName.startsWith('.')) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
const base = path.resolve(path.dirname(from.fileName), moduleName);
|
|
223
|
+
const candidates = /\.(ets|ts)$/.test(base) ? [base] :
|
|
224
|
+
[base + '.ets', base + '.ts', path.join(base, 'Index.ets'), path.join(base, 'index.ets'),
|
|
225
|
+
path.join(base, 'Index.ts'), path.join(base, 'index.ts')];
|
|
226
|
+
for (const fileName of candidates) {
|
|
227
|
+
if (!isApplicationArkTsSource(fileName)) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (!sources.has(fileName)) {
|
|
231
|
+
let parsed: any | null = null;
|
|
232
|
+
try {
|
|
233
|
+
const code = readSource ? readSource(fileName) : nodeRequire('fs').readFileSync(fileName, 'utf8');
|
|
234
|
+
if (typeof code === 'string') {
|
|
235
|
+
parsed = ts.createSourceFile(fileName, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
236
|
+
}
|
|
237
|
+
} catch (_) {
|
|
238
|
+
// A missing or unresolved source is not evidence of an Ability base.
|
|
239
|
+
}
|
|
240
|
+
sources.set(fileName, parsed);
|
|
241
|
+
}
|
|
242
|
+
if (sources.get(fileName)) {
|
|
243
|
+
return sources.get(fileName);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
133
246
|
return null;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const resolveModule = (from: any, moduleName: string, exportedName: string): AbilityBase => {
|
|
250
|
+
const name = exportedName === 'default' ? moduleName.slice(moduleName.lastIndexOf('.') + 1) : exportedName;
|
|
251
|
+
const kind = classifyAbilityBaseName(name);
|
|
252
|
+
if (moduleName === '@kit.AbilityKit' || moduleName.startsWith('@ohos.app.ability.') ||
|
|
253
|
+
(kind === 'other' && (moduleName.startsWith('@kit.') || moduleName.startsWith('@ohos.')))) {
|
|
254
|
+
return { kind };
|
|
138
255
|
}
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
256
|
+
const imported = loadRelativeSource(from, moduleName);
|
|
257
|
+
return imported ? resolveExport(imported, exportedName) : unknown;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const resolveBinding = (declaration: any, member?: string): AbilityBase => {
|
|
261
|
+
if (!declaration) {
|
|
262
|
+
return unknown;
|
|
142
263
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
264
|
+
if (ts.isClassDeclaration(declaration)) {
|
|
265
|
+
return { kind: resolveClass(declaration).kind, declaration };
|
|
266
|
+
}
|
|
267
|
+
let importDeclaration = declaration;
|
|
268
|
+
while (importDeclaration && !ts.isImportDeclaration(importDeclaration)) {
|
|
269
|
+
importDeclaration = importDeclaration.parent;
|
|
270
|
+
}
|
|
271
|
+
if (!importDeclaration || importDeclaration.importClause.isTypeOnly || declaration.isTypeOnly) {
|
|
272
|
+
return unknown;
|
|
273
|
+
}
|
|
274
|
+
let exportedName: string;
|
|
275
|
+
if (ts.isImportClause(declaration)) {
|
|
276
|
+
exportedName = 'default';
|
|
277
|
+
} else if (ts.isImportSpecifier(declaration)) {
|
|
278
|
+
exportedName = (declaration.propertyName || declaration.name).text;
|
|
279
|
+
} else if (ts.isNamespaceImport(declaration) && member) {
|
|
280
|
+
exportedName = member;
|
|
281
|
+
} else {
|
|
282
|
+
return unknown;
|
|
283
|
+
}
|
|
284
|
+
return resolveModule(importDeclaration.getSourceFile(), importDeclaration.moduleSpecifier.text, exportedName);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const resolveExpression = (expression: any): AbilityBase => {
|
|
288
|
+
if (ts.isIdentifier(expression)) {
|
|
289
|
+
return resolveBinding(findValueDeclaration(ts, expression, expression.text));
|
|
290
|
+
}
|
|
291
|
+
if (ts.isPropertyAccessExpression(expression) && ts.isIdentifier(expression.expression)) {
|
|
292
|
+
return resolveBinding(findValueDeclaration(ts, expression, expression.expression.text), expression.name.text);
|
|
293
|
+
}
|
|
294
|
+
return unknown;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const resolveExport = (source: any, name: string): AbilityBase => {
|
|
298
|
+
const key = `${source.fileName}:${name}`;
|
|
299
|
+
if (resolvingExports.has(key)) {
|
|
300
|
+
return unknown;
|
|
301
|
+
}
|
|
302
|
+
resolvingExports.add(key);
|
|
303
|
+
try {
|
|
304
|
+
for (const statement of source.statements) {
|
|
305
|
+
if (ts.isClassDeclaration(statement)) {
|
|
306
|
+
const modifiers = statement.modifiers || [];
|
|
307
|
+
const isDefault = modifiers.some((item: any) => item.kind === ts.SyntaxKind.DefaultKeyword);
|
|
308
|
+
const isExported = modifiers.some((item: any) => item.kind === ts.SyntaxKind.ExportKeyword);
|
|
309
|
+
if ((name === 'default' && isDefault) ||
|
|
310
|
+
(name !== 'default' && isExported && statement.name && statement.name.text === name)) {
|
|
311
|
+
return resolveBinding(statement);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (name === 'default' && ts.isExportAssignment(statement) && !statement.isExportEquals) {
|
|
315
|
+
return resolveExpression(statement.expression);
|
|
316
|
+
}
|
|
317
|
+
if (!ts.isExportDeclaration(statement) || statement.isTypeOnly) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (statement.exportClause && ts.isNamedExports(statement.exportClause)) {
|
|
321
|
+
const exported = statement.exportClause.elements.find((item: any) => item.name.text === name);
|
|
322
|
+
if (exported && !exported.isTypeOnly) {
|
|
323
|
+
const localName = (exported.propertyName || exported.name).text;
|
|
324
|
+
return statement.moduleSpecifier
|
|
325
|
+
? resolveModule(source, statement.moduleSpecifier.text, localName)
|
|
326
|
+
: resolveBinding(findValueDeclaration(ts, statement, localName));
|
|
327
|
+
}
|
|
328
|
+
} else if (!statement.exportClause && statement.moduleSpecifier && name !== 'default') {
|
|
329
|
+
const resolved = resolveModule(source, statement.moduleSpecifier.text, name);
|
|
330
|
+
if (resolved.kind) {
|
|
331
|
+
return resolved;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return unknown;
|
|
336
|
+
} finally {
|
|
337
|
+
resolvingExports.delete(key);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const resolveClass = (declaration: any): AbilityClassInfo => {
|
|
342
|
+
const cached = classCache.get(declaration);
|
|
343
|
+
if (cached) {
|
|
344
|
+
return cached;
|
|
345
|
+
}
|
|
346
|
+
if (resolvingClasses.has(declaration)) {
|
|
347
|
+
return { kind: null, applicationBase: false };
|
|
348
|
+
}
|
|
349
|
+
resolvingClasses.add(declaration);
|
|
350
|
+
let base: AbilityBase = unknown;
|
|
351
|
+
for (const clause of declaration.heritageClauses || []) {
|
|
352
|
+
if (clause.token === ts.SyntaxKind.ExtendsKeyword && clause.types.length > 0) {
|
|
353
|
+
base = resolveExpression(clause.types[0].expression);
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const info: AbilityClassInfo = { kind: base.kind, applicationBase: !!base.declaration, base: base.declaration };
|
|
358
|
+
resolvingClasses.delete(declaration);
|
|
359
|
+
classCache.set(declaration, info);
|
|
360
|
+
return info;
|
|
361
|
+
};
|
|
362
|
+
return resolveClass;
|
|
146
363
|
}
|
|
147
364
|
|
|
148
365
|
function methodIndentation(code: string, method: any): string {
|
|
@@ -166,6 +383,50 @@ function blockIndentation(code: string, block: any): string {
|
|
|
166
383
|
return (match ? match[0] : '') + ' ';
|
|
167
384
|
}
|
|
168
385
|
|
|
386
|
+
function intentDecoratorString(ts: TypeScriptApi, declaration: any, field: string): string {
|
|
387
|
+
const decorators = ts.canHaveDecorators && ts.canHaveDecorators(declaration)
|
|
388
|
+
? (ts.getDecorators(declaration) || []) : (declaration.decorators || []);
|
|
389
|
+
for (const decorator of decorators) {
|
|
390
|
+
const call = decorator.expression;
|
|
391
|
+
if (!ts.isCallExpression(call) || call.arguments.length === 0 ||
|
|
392
|
+
!ts.isObjectLiteralExpression(call.arguments[0])) {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
for (const property of call.arguments[0].properties) {
|
|
396
|
+
if (ts.isPropertyAssignment(property) && property.name && property.name.text === field &&
|
|
397
|
+
ts.isStringLiteral(property.initializer)) {
|
|
398
|
+
return JSON.stringify(property.initializer.text);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return "''";
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function callableMember(ts: TypeScriptApi, member: any): any | undefined {
|
|
406
|
+
if (ts.isMethodDeclaration(member) && member.body) return member;
|
|
407
|
+
if (ts.isPropertyDeclaration(member) && member.initializer &&
|
|
408
|
+
(ts.isArrowFunction(member.initializer) || ts.isFunctionExpression(member.initializer))) return member.initializer;
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function memberName(ts: TypeScriptApi, member: any): string | undefined {
|
|
413
|
+
return member.name && (ts.isIdentifier(member.name) || ts.isStringLiteral(member.name)) ? member.name.text : undefined;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Insert into the actual parameter list, ignoring parentheses in return types/comments. */
|
|
417
|
+
function addLaunchParameters(ts: TypeScriptApi, sourceFile: any, callable: any,
|
|
418
|
+
added: string[], injections: ColdLaunchInjection[]): void {
|
|
419
|
+
const close = callable.getChildren(sourceFile).find((child: any) => child.kind === ts.SyntaxKind.CloseParenToken);
|
|
420
|
+
if (close) {
|
|
421
|
+
const separator = callable.parameters.length > 0 && !callable.parameters.hasTrailingComma ? ', ' : '';
|
|
422
|
+
injections.push({ position: close.getStart(sourceFile), content: separator + added.join(', ') });
|
|
423
|
+
} else {
|
|
424
|
+
// A single unparenthesized arrow parameter must become a parameter list.
|
|
425
|
+
injections.push({ position: callable.parameters[0].getStart(sourceFile), content: '(' });
|
|
426
|
+
injections.push({ position: callable.parameters.end, content: ', ' + added.join(', ') + ')' });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
169
430
|
function collectWindowStageParameterNames(ts: TypeScriptApi, sourceFile: any, method: any): Set<string> {
|
|
170
431
|
const names = new Set<string>();
|
|
171
432
|
method.parameters.forEach((parameter: any) => {
|
|
@@ -181,65 +442,221 @@ function collectWindowStageParameterNames(ts: TypeScriptApi, sourceFile: any, me
|
|
|
181
442
|
}
|
|
182
443
|
|
|
183
444
|
function createFirstFrameRetryInjections(ts: TypeScriptApi, sourceFile: any, code: string,
|
|
184
|
-
method: any, bridgeName: string, windowStageNames: Set<string
|
|
185
|
-
|
|
445
|
+
method: any, bridgeName: string, windowStageNames: Set<string>, identifiers: Set<string>,
|
|
446
|
+
resolve: (location: any, name: string) => any | undefined): ColdLaunchInjection[] {
|
|
447
|
+
const candidates: any[] = [];
|
|
186
448
|
visit(ts, method.body, (node: any) => {
|
|
187
|
-
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
449
|
+
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression) ||
|
|
450
|
+
(ts.isOptionalChain && ts.isOptionalChain(node))) return;
|
|
190
451
|
const delegate = node.expression.expression;
|
|
191
452
|
if (!ts.isIdentifier(delegate) || !windowStageNames.has(delegate.text) ||
|
|
192
|
-
node.expression.name.text !== 'loadContent' || node.arguments.length
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const windowStageName = delegate.text;
|
|
453
|
+
node.expression.name.text !== 'loadContent' || node.arguments.length < 2 || node.arguments.length > 3) return;
|
|
454
|
+
const parameter = resolve(delegate, delegate.text);
|
|
455
|
+
if (!parameter || !method.parameters.includes(parameter)) return;
|
|
196
456
|
const callback = node.arguments[node.arguments.length - 1];
|
|
197
|
-
if ((!ts.isArrowFunction(callback) && !ts.isFunctionExpression(callback)) || !ts.isBlock(callback.body))
|
|
198
|
-
|
|
457
|
+
if ((!ts.isArrowFunction(callback) && !ts.isFunctionExpression(callback)) || !ts.isBlock(callback.body)) return;
|
|
458
|
+
candidates.push({ node, callback, parameter,
|
|
459
|
+
stage: selectIdentifierName(identifiers, '__ftWindowStage'),
|
|
460
|
+
arguments: node.arguments.slice(0, -1).map((_arg: any, index: number) =>
|
|
461
|
+
selectIdentifierName(identifiers, index === 0 ? '__ftContentUrl' : '__ftContentStorage')) });
|
|
462
|
+
});
|
|
463
|
+
const start = (candidate: any): number => candidate.node.getStart(sourceFile);
|
|
464
|
+
const contains = (outer: any, inner: any): boolean => outer !== inner &&
|
|
465
|
+
start(outer) <= start(inner) && outer.node.end >= inner.node.end;
|
|
466
|
+
const render = (from: number, to: number): string => {
|
|
467
|
+
const included = candidates.filter(item => start(item) >= from && item.node.end <= to);
|
|
468
|
+
const direct = included.filter(item => !included.some(outer => contains(outer, item)))
|
|
469
|
+
.sort((left, right) => start(right) - start(left));
|
|
470
|
+
let output = code.slice(from, to);
|
|
471
|
+
for (const candidate of direct) {
|
|
472
|
+
const rendered = renderCandidate(candidate);
|
|
473
|
+
output = output.slice(0, start(candidate) - from) + rendered + output.slice(candidate.node.end - from);
|
|
199
474
|
}
|
|
475
|
+
return output;
|
|
476
|
+
};
|
|
477
|
+
const renderCandidate = (candidate: any): string => {
|
|
478
|
+
const { node, callback, parameter, stage } = candidate;
|
|
479
|
+
const bodyStart = callback.body.getStart(sourceFile) + 1;
|
|
200
480
|
const indentation = blockIndentation(code, callback.body);
|
|
201
|
-
const
|
|
481
|
+
const error = callback.parameters.length > 0 && ts.isIdentifier(callback.parameters[0].name)
|
|
202
482
|
? callback.parameters[0].name.text : null;
|
|
203
|
-
const retry = `${bridgeName}.observeFirstFrame(${
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
483
|
+
const retry = `${bridgeName}.observeFirstFrame(${stage});`;
|
|
484
|
+
const hook = error === null ? `\n${indentation}${retry}` :
|
|
485
|
+
`\n${indentation}if (!${error} || !${error}.code) {\n${indentation} ${retry}\n${indentation}}`;
|
|
486
|
+
const callbackCode = code.slice(callback.getStart(sourceFile), bodyStart) + hook + render(bodyStart, callback.end);
|
|
487
|
+
const argumentNames: string[] = candidate.arguments;
|
|
488
|
+
const parameterType = parameter.type ? parameter.type.getText(sourceFile) : 'Object';
|
|
489
|
+
const declarations = [`${stage}: ${parameterType}`, ...argumentNames.map((name: string, index: number) =>
|
|
490
|
+
`${name}: ${index === 0 ? 'string' : 'LocalStorage'}`)];
|
|
491
|
+
// Evaluate the receiver and non-callback arguments at the original call site.
|
|
492
|
+
// Each execution captures a separate stage; callbacks cannot shadow or overwrite it.
|
|
493
|
+
const values = [node.expression.expression.getText(sourceFile), ...node.arguments.slice(0, -1)
|
|
494
|
+
.map((argument: any) => render(argument.getStart(sourceFile), argument.end))];
|
|
495
|
+
const statement = node.parent;
|
|
496
|
+
const separator = ts.isExpressionStatement(statement) && (ts.isBlock(statement.parent) ||
|
|
497
|
+
ts.isSourceFile(statement.parent) || ts.isCaseClause(statement.parent) || ts.isDefaultClause(statement.parent)) ? ';' : '';
|
|
498
|
+
return `${separator}((${declarations.join(', ')}) => ${stage}.loadContent(${argumentNames.join(', ')}, ` +
|
|
499
|
+
`${callbackCode}))(${values.join(', ')})`;
|
|
500
|
+
};
|
|
501
|
+
return candidates.filter(item => !candidates.some(outer => contains(outer, item)))
|
|
502
|
+
.map(item => ({ position: start(item), end: item.node.end, content: renderCandidate(item) }));
|
|
211
503
|
}
|
|
212
504
|
|
|
213
505
|
function createColdLaunchInjections(ts: TypeScriptApi, sourceFile: any, code: string,
|
|
214
|
-
bridgeName: string, pluginVersion: string
|
|
506
|
+
bridgeName: string, pluginVersion: string, identifiers: Set<string>,
|
|
507
|
+
launchTypes: LaunchParameterTypes, readSource?: SourceReader): ColdLaunchInjections {
|
|
215
508
|
const injections: ColdLaunchInjection[] = [];
|
|
216
|
-
|
|
509
|
+
let hasUIAbility = false;
|
|
510
|
+
const resolve = createLexicalResolver(ts, sourceFile);
|
|
511
|
+
const resolveClass = createAbilityClassResolver(ts, sourceFile, readSource);
|
|
217
512
|
const pluginVersionArgument = pluginVersion.length > 0 ? `, ${JSON.stringify(pluginVersion)}` : '';
|
|
513
|
+
const availability = new Map<string, boolean>();
|
|
514
|
+
const inheritedState = (declaration: any, method: string): { covered: boolean; member?: any } => {
|
|
515
|
+
const seen = new Set<any>();
|
|
516
|
+
for (let base = resolveClass(declaration).base; base && !seen.has(base); base = resolveClass(base).base) {
|
|
517
|
+
seen.add(base);
|
|
518
|
+
const id = base.getSourceFile().fileName;
|
|
519
|
+
if (!availability.has(id)) availability.set(id, hasRuntimeDependency(ts, id, '@guancecloud/ft_sdk', readSource));
|
|
520
|
+
// An instrumented intermediate class either owns the hook or supplies a forwarding hook.
|
|
521
|
+
if (availability.get(id)) return { covered: true };
|
|
522
|
+
const member = base.members.find((item: any) => memberName(ts, item) === method &&
|
|
523
|
+
!(item.modifiers || []).some((modifier: any) => modifier.kind === ts.SyntaxKind.StaticKeyword));
|
|
524
|
+
if (member) return { covered: false, member };
|
|
525
|
+
}
|
|
526
|
+
return { covered: false };
|
|
527
|
+
};
|
|
528
|
+
const wrapInherited = (declaration: any, name: string, hook: string, launch: boolean, inherited: any): void => {
|
|
529
|
+
const original = selectIdentifierName(identifiers, '__ftInheritedCall');
|
|
530
|
+
const want = selectIdentifierName(identifiers, '__ftInheritedWant');
|
|
531
|
+
const param = selectIdentifierName(identifiers, '__ftInheritedLaunch');
|
|
532
|
+
if (launch) launchTypes.needed = true;
|
|
533
|
+
const baseType = declaration.heritageClauses.find((item: any) => item.token === ts.SyntaxKind.ExtendsKeyword)
|
|
534
|
+
.types[0].getText(sourceFile);
|
|
535
|
+
const callable = callableMember(ts, inherited);
|
|
536
|
+
const signature = callable || (inherited.type && ts.isFunctionTypeNode(inherited.type) ? inherited.type : undefined);
|
|
537
|
+
const count = signature && !signature.parameters.some((parameter: any) => parameter.dotDotDotToken)
|
|
538
|
+
? signature.parameters.length : 2;
|
|
539
|
+
const parameters = launch ? `${want}?: ${launchTypes.want}, ${param}?: ${launchTypes.abilityConstant}.LaunchParam` : '';
|
|
540
|
+
const passed = launch ? [`${want} as ${launchTypes.want}`, `${param} as ${launchTypes.abilityConstant}.LaunchParam`]
|
|
541
|
+
.slice(0, count).join(', ') : '';
|
|
542
|
+
const hookText = hook.replace(/\$want/g, want).replace(/\$launch/g, param);
|
|
543
|
+
// Store the original callable on the same instance. Calling this slot keeps
|
|
544
|
+
// its receiver and concrete return type, including inherited function fields,
|
|
545
|
+
// without bind's dynamic return type or unsupported generic inference.
|
|
546
|
+
injections.push({ position: declaration.end - 1,
|
|
547
|
+
content: `\n private ${original} = (this as Object as ${baseType}).${name};\n` +
|
|
548
|
+
` ${name} = (${parameters}) => { ${hookText} return this.${original}(${passed}); };\n` });
|
|
549
|
+
};
|
|
550
|
+
|
|
218
551
|
visit(ts, sourceFile, (node: any) => {
|
|
219
552
|
if (!ts.isClassDeclaration(node)) {
|
|
220
553
|
return;
|
|
221
554
|
}
|
|
222
|
-
const
|
|
555
|
+
const classification = resolveClass(node);
|
|
556
|
+
const kind = classification.kind;
|
|
223
557
|
if (!kind) {
|
|
224
558
|
return;
|
|
225
559
|
}
|
|
560
|
+
if (kind === 'ui') {
|
|
561
|
+
hasUIAbility = true;
|
|
562
|
+
}
|
|
226
563
|
let hasOnCreate = false;
|
|
227
|
-
|
|
228
|
-
|
|
564
|
+
let hasOnForeground = false;
|
|
565
|
+
let hasOnBackground = false;
|
|
566
|
+
let hasOnNewWant = false;
|
|
567
|
+
node.members.forEach((declaration: any) => {
|
|
568
|
+
if ((declaration.modifiers || []).some((modifier: any) => modifier.kind === ts.SyntaxKind.StaticKeyword)) return;
|
|
569
|
+
const name = memberName(ts, declaration);
|
|
570
|
+
if (name === 'onCreate') hasOnCreate = true;
|
|
571
|
+
if (name === 'onForeground') hasOnForeground = true;
|
|
572
|
+
if (name === 'onBackground') hasOnBackground = true;
|
|
573
|
+
if (name === 'onNewWant') hasOnNewWant = true;
|
|
574
|
+
const member = callableMember(ts, declaration);
|
|
575
|
+
if (!member || !name) return;
|
|
576
|
+
const indentation = methodIndentation(code, declaration);
|
|
577
|
+
const block = ts.isBlock(member.body);
|
|
578
|
+
const position = member.body.getStart(sourceFile) + (block ? 1 : 0);
|
|
579
|
+
// Expression-bodied lifecycle properties retain their expression result.
|
|
580
|
+
if (!block) {
|
|
581
|
+
const recognized = kind === 'intent' ? name === 'onExecute' : kind === 'legacyIntent'
|
|
582
|
+
? ['onExecuteInUIAbilityForegroundMode', 'onExecuteInUIAbilityBackgroundMode'].includes(name)
|
|
583
|
+
: name === 'onCreate' || (kind === 'ui' &&
|
|
584
|
+
(['onForeground', 'onBackground', 'onNewWant'].includes(name) ||
|
|
585
|
+
(name === 'onWindowStageCreate' && member.parameters.length > 0 && ts.isIdentifier(member.parameters[0].name))));
|
|
586
|
+
if (recognized) {
|
|
587
|
+
injections.push({ position, content: '{' });
|
|
588
|
+
injections.push({ position: member.body.end, content: '; }' });
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const finish = (): void => {
|
|
592
|
+
if (!block) injections.push({ position, content: '\n' + indentation + 'return ' });
|
|
593
|
+
};
|
|
594
|
+
if (kind === 'intent' || kind === 'legacyIntent') {
|
|
595
|
+
let mode: string | null = null;
|
|
596
|
+
if (kind === 'intent' && name === 'onExecute') {
|
|
597
|
+
mode = 'this.executeMode';
|
|
598
|
+
} else if (kind === 'legacyIntent' && name === 'onExecuteInUIAbilityForegroundMode') {
|
|
599
|
+
mode = '0';
|
|
600
|
+
} else if (kind === 'legacyIntent' && name === 'onExecuteInUIAbilityBackgroundMode') {
|
|
601
|
+
mode = '1';
|
|
602
|
+
}
|
|
603
|
+
if (mode !== null) {
|
|
604
|
+
const intentName = kind === 'intent' ? intentDecoratorString(ts, node, 'intentName') :
|
|
605
|
+
(member.parameters.length > 0 && ts.isIdentifier(member.parameters[0].name)
|
|
606
|
+
? member.parameters[0].name.text : "''");
|
|
607
|
+
const abilityName = kind === 'intent' ? intentDecoratorString(ts, node, 'abilityName') : "''";
|
|
608
|
+
injections.push({ position, content: `\n${indentation}${bridgeName}.recordInsightIntentExecute(` +
|
|
609
|
+
`${mode}, ${intentName}, this.context, ${abilityName});` });
|
|
610
|
+
}
|
|
611
|
+
if (mode !== null) finish();
|
|
229
612
|
return;
|
|
230
613
|
}
|
|
231
|
-
|
|
232
|
-
const position = member.body.getStart(sourceFile) + 1;
|
|
233
|
-
if (member.name.text === 'onCreate') {
|
|
614
|
+
if (name === 'onCreate') {
|
|
234
615
|
hasOnCreate = true;
|
|
235
|
-
let content =
|
|
616
|
+
let content = kind === 'ui' ? '' :
|
|
617
|
+
`\n${indentation}${bridgeName}.recordApplicationInit(this.context${pluginVersionArgument});`;
|
|
236
618
|
if (kind === 'ui') {
|
|
237
|
-
|
|
619
|
+
const parameters = member.parameters;
|
|
620
|
+
let wantName = parameters.length > 0 && ts.isIdentifier(parameters[0].name)
|
|
621
|
+
? parameters[0].name.text : 'undefined';
|
|
622
|
+
let launchParamName = parameters.length > 1 && ts.isIdentifier(parameters[1].name)
|
|
623
|
+
? parameters[1].name.text : 'undefined';
|
|
624
|
+
// Even an onCreate() that ignores both parameters must pass the
|
|
625
|
+
// actual system reason to the SDK. Add typed optional parameters
|
|
626
|
+
// to the in-memory source without changing the application's file.
|
|
627
|
+
if (parameters.length < 2) {
|
|
628
|
+
launchTypes.needed = true;
|
|
629
|
+
const added: string[] = [];
|
|
630
|
+
if (parameters.length === 0) {
|
|
631
|
+
wantName = selectIdentifierName(identifiers, '__ftLaunchWant');
|
|
632
|
+
added.push(`${wantName}?: ${launchTypes.want}`);
|
|
633
|
+
}
|
|
634
|
+
launchParamName = selectIdentifierName(identifiers, '__ftLaunchParam');
|
|
635
|
+
added.push(`${launchParamName}?: ${launchTypes.abilityConstant}.LaunchParam`);
|
|
636
|
+
addLaunchParameters(ts, sourceFile, member, added, injections);
|
|
637
|
+
}
|
|
638
|
+
content += `\n${indentation}${bridgeName}.recordFirstUIAbilityCreate(${launchParamName}, ${wantName}, this);`;
|
|
238
639
|
}
|
|
239
640
|
injections.push({ position, content });
|
|
641
|
+
finish();
|
|
240
642
|
return;
|
|
241
643
|
}
|
|
242
|
-
if (kind
|
|
644
|
+
if (kind === 'ui' && (name === 'onForeground' || name === 'onBackground' ||
|
|
645
|
+
name === 'onNewWant')) {
|
|
646
|
+
if (name === 'onForeground') {
|
|
647
|
+
hasOnForeground = true;
|
|
648
|
+
} else if (name === 'onBackground') {
|
|
649
|
+
hasOnBackground = true;
|
|
650
|
+
} else {
|
|
651
|
+
hasOnNewWant = true;
|
|
652
|
+
}
|
|
653
|
+
const hook = name === 'onForeground' ? 'recordUIAbilityForeground' :
|
|
654
|
+
(name === 'onBackground' ? 'recordUIAbilityBackground' : 'recordUIAbilityNewWant');
|
|
655
|
+
injections.push({ position, content: `\n${indentation}${bridgeName}.${hook}(this);` });
|
|
656
|
+
finish();
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (kind !== 'ui' || name !== 'onWindowStageCreate' || member.parameters.length === 0) {
|
|
243
660
|
return;
|
|
244
661
|
}
|
|
245
662
|
const windowStageParameter = member.parameters[0];
|
|
@@ -249,64 +666,125 @@ function createColdLaunchInjections(ts: TypeScriptApi, sourceFile: any, code: st
|
|
|
249
666
|
const windowStageName = windowStageParameter.name.text;
|
|
250
667
|
injections.push({
|
|
251
668
|
position,
|
|
252
|
-
content: `\n${indentation}${bridgeName}.observeFirstFrame(${windowStageName})
|
|
669
|
+
content: `\n${indentation}if (${windowStageName}) { ${bridgeName}.observeFirstFrame(${windowStageName}); }`
|
|
253
670
|
});
|
|
671
|
+
finish();
|
|
254
672
|
});
|
|
255
673
|
if (kind === 'ui') {
|
|
256
|
-
node.members.forEach((
|
|
257
|
-
if (
|
|
258
|
-
|
|
259
|
-
|
|
674
|
+
node.members.forEach((declaration: any) => {
|
|
675
|
+
if ((declaration.modifiers || []).some((modifier: any) => modifier.kind === ts.SyntaxKind.StaticKeyword)) return;
|
|
676
|
+
const member = callableMember(ts, declaration);
|
|
677
|
+
if (!member) return;
|
|
260
678
|
const windowStageNames = collectWindowStageParameterNames(ts, sourceFile, member);
|
|
261
679
|
if (windowStageNames.size > 0) {
|
|
262
680
|
injections.push(...createFirstFrameRetryInjections(
|
|
263
|
-
ts, sourceFile, code, member, bridgeName, windowStageNames));
|
|
681
|
+
ts, sourceFile, code, member, bridgeName, windowStageNames, identifiers, resolve));
|
|
264
682
|
}
|
|
265
683
|
});
|
|
266
684
|
}
|
|
267
685
|
if (kind === 'stage' && !hasOnCreate) {
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
686
|
+
const inherited = inheritedState(node, 'onCreate');
|
|
687
|
+
if (!inherited.covered) {
|
|
688
|
+
const hook = `${bridgeName}.recordApplicationInit(this.context${pluginVersionArgument});`;
|
|
689
|
+
if (inherited.member) wrapInherited(node, 'onCreate', hook, false, inherited.member);
|
|
690
|
+
else injections.push({ position: node.end - 1,
|
|
691
|
+
content: `\n onCreate(): void { ${hook} }\n` });
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (kind === 'ui' && !hasOnCreate) {
|
|
695
|
+
const inherited = inheritedState(node, 'onCreate');
|
|
696
|
+
if (!inherited.covered) {
|
|
697
|
+
launchTypes.needed = true;
|
|
698
|
+
if (inherited.member) {
|
|
699
|
+
wrapInherited(node, 'onCreate', `${bridgeName}.recordFirstUIAbilityCreate($launch, $want, this);`, true, inherited.member);
|
|
700
|
+
} else {
|
|
701
|
+
const wantName = selectIdentifierName(identifiers, '__ftLaunchWant');
|
|
702
|
+
const paramName = selectIdentifierName(identifiers, '__ftLaunchParam');
|
|
703
|
+
injections.push({ position: node.end - 1, content: `\n onCreate(${wantName}: ${launchTypes.want}, ` +
|
|
704
|
+
`${paramName}: ${launchTypes.abilityConstant}.LaunchParam): void {\n` +
|
|
705
|
+
` ${bridgeName}.recordFirstUIAbilityCreate(${paramName}, ${wantName}, this);\n }\n` });
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
if (kind === 'ui') {
|
|
710
|
+
for (const [method, hook, exists] of [
|
|
711
|
+
['onForeground', 'recordUIAbilityForeground', hasOnForeground],
|
|
712
|
+
['onBackground', 'recordUIAbilityBackground', hasOnBackground],
|
|
713
|
+
['onNewWant', 'recordUIAbilityNewWant', hasOnNewWant]
|
|
714
|
+
]) {
|
|
715
|
+
if (exists) continue;
|
|
716
|
+
const inherited = inheritedState(node, method as string);
|
|
717
|
+
if (inherited.covered) continue;
|
|
718
|
+
const call = `${bridgeName}.${hook}(this);`;
|
|
719
|
+
if (inherited.member) wrapInherited(node, method as string, call, method === 'onNewWant', inherited.member);
|
|
720
|
+
else injections.push({ position: node.end - 1, content: `\n ${method}(): void { ${call} }\n` });
|
|
721
|
+
}
|
|
275
722
|
}
|
|
276
723
|
});
|
|
277
|
-
return injections;
|
|
724
|
+
return { injections, hasUIAbility };
|
|
278
725
|
}
|
|
279
726
|
|
|
280
727
|
function applyColdLaunchInjections(code: string, injections: ColdLaunchInjection[]): string {
|
|
281
728
|
const contentsByPosition = new Map<number, string[]>();
|
|
729
|
+
const ends = new Map<number, number>();
|
|
282
730
|
injections.forEach((injection: ColdLaunchInjection) => {
|
|
283
731
|
const contents = contentsByPosition.get(injection.position) || [];
|
|
284
732
|
contents.push(injection.content);
|
|
285
733
|
contentsByPosition.set(injection.position, contents);
|
|
734
|
+
ends.set(injection.position, Math.max(ends.get(injection.position) || injection.position, injection.end || injection.position));
|
|
286
735
|
});
|
|
287
736
|
const positions = Array.from(contentsByPosition.keys()).sort((left: number, right: number) => right - left);
|
|
288
737
|
let transformedCode = code;
|
|
289
738
|
positions.forEach((position: number) => {
|
|
290
739
|
const content = (contentsByPosition.get(position) || []).join('');
|
|
291
|
-
transformedCode = transformedCode.slice(0, position) + content + transformedCode.slice(position);
|
|
740
|
+
transformedCode = transformedCode.slice(0, position) + content + transformedCode.slice(ends.get(position));
|
|
292
741
|
});
|
|
293
742
|
return transformedCode;
|
|
294
743
|
}
|
|
295
744
|
|
|
296
|
-
export function transformColdLaunchSource(ts: TypeScriptApi, id: string, code: string
|
|
297
|
-
|
|
745
|
+
export function transformColdLaunchSource(ts: TypeScriptApi, id: string, code: string,
|
|
746
|
+
readSource?: SourceReader): string {
|
|
747
|
+
if (!isApplicationArkTsSource(id) || code.includes(COLD_LAUNCH_TRANSFORM_MARKER) ||
|
|
748
|
+
!hasRuntimeDependency(ts, id, '@guancecloud/ft_sdk', readSource)) {
|
|
298
749
|
return code;
|
|
299
750
|
}
|
|
300
751
|
const sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
301
|
-
const
|
|
752
|
+
const identifiers = collectIdentifierNames(ts, sourceFile);
|
|
753
|
+
const bridgeName = selectIdentifierName(identifiers, '__ftColdLaunchBridge');
|
|
754
|
+
const launchTypes: LaunchParameterTypes = {
|
|
755
|
+
want: selectIdentifierName(identifiers, '__ftLaunchWantType'),
|
|
756
|
+
abilityConstant: selectIdentifierName(identifiers, '__ftLaunchAbilityConstant'),
|
|
757
|
+
needed: false
|
|
758
|
+
};
|
|
302
759
|
const version = getPluginVersion();
|
|
303
|
-
const
|
|
304
|
-
|
|
760
|
+
const result = createColdLaunchInjections(ts, sourceFile, code, bridgeName, version, identifiers,
|
|
761
|
+
launchTypes, readSource);
|
|
762
|
+
const injections = result.injections;
|
|
763
|
+
const moduleFallback = result.hasUIAbility && needsUIAbilityModuleFallback(ts, id, readSource);
|
|
764
|
+
if (injections.length === 0 && !moduleFallback) {
|
|
305
765
|
return code;
|
|
306
766
|
}
|
|
307
|
-
|
|
767
|
+
// Keep generated imports separate from initialization calls: ArkTS requires
|
|
768
|
+
// imports before other statements, while the no-Stage fallback must still
|
|
769
|
+
// run before executable application code.
|
|
770
|
+
const insertionOffset = moduleFallback
|
|
771
|
+
? (code.startsWith('#!') ? code.indexOf('\n') + 1 : 0)
|
|
772
|
+
: findImportInsertionOffset(ts, sourceFile);
|
|
773
|
+
const firstApplicationStatement = sourceFile.statements.find((statement: any) => !ts.isImportDeclaration(statement));
|
|
774
|
+
const initializationOffset = moduleFallback && firstApplicationStatement
|
|
775
|
+
? firstApplicationStatement.getStart(sourceFile) : insertionOffset;
|
|
776
|
+
const installBridgeName = selectIdentifierName(identifiers, '__ftColdLaunchInstallBridge');
|
|
777
|
+
const sdkName = selectIdentifierName(identifiers, '__ftColdLaunchSDK');
|
|
308
778
|
const bridgeImport = `\n${COLD_LAUNCH_TRANSFORM_MARKER}\n` +
|
|
309
|
-
`import { FTColdLaunchBridge as ${bridgeName}
|
|
310
|
-
|
|
311
|
-
|
|
779
|
+
`import { FTColdLaunchBridge as ${bridgeName}, FTConsoleLogBridge as ${installBridgeName}` +
|
|
780
|
+
(version.length > 0 ? `, FTSDK as ${sdkName}` : '') + ` } from '${COLD_LAUNCH_BRIDGE_IMPORT}';\n` +
|
|
781
|
+
(launchTypes.needed ? `import { Want as ${launchTypes.want}, AbilityConstant as ${launchTypes.abilityConstant} } ` +
|
|
782
|
+
`from '@kit.AbilityKit';\n` : '');
|
|
783
|
+
const initialization = `\n${installBridgeName}.markCompilerPluginInstalled();\n` +
|
|
784
|
+
(version.length > 0 ? `${sdkName}.registerHvigorPluginVersion(${JSON.stringify(version)});\n` : '') +
|
|
785
|
+
(moduleFallback ? '// Application-init fallback: UIAbility module evaluation without an AbilityStage entry.\n' +
|
|
786
|
+
`${bridgeName}.recordApplicationInit(undefined, ${JSON.stringify(version)});\n` : '');
|
|
787
|
+
injections.push({ position: insertionOffset, content: bridgeImport });
|
|
788
|
+
injections.push({ position: initializationOffset, content: initialization });
|
|
789
|
+
return applyColdLaunchInjections(code, injections);
|
|
312
790
|
}
|