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