@cloudcare/hvigor-ohos-plugin 0.1.0 → 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 +14 -2
- package/README.md +32 -17
- package/hvigorCompile/ft-cold-launch-transform.ts +595 -192
- package/hvigorCompile/ft-console-log-transform.ts +72 -176
- 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 +135 -0
- package/hvigorCompile/ft-transform-context.ts +36 -0
- package/hvigorCompile/ft-web-replay-transform.ts +51 -0
- package/index.js +7 -7
- package/package.json +13 -3
|
@@ -5,41 +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 } 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;
|
|
17
18
|
|
|
18
|
-
interface
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
interface
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
interface LaunchParameterTypes {
|
|
31
|
+
want: string;
|
|
32
|
+
abilityConstant: string;
|
|
33
|
+
needed: boolean;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
interface ColdLaunchInjection {
|
|
33
37
|
position: number;
|
|
38
|
+
end?: number;
|
|
34
39
|
content: string;
|
|
35
40
|
}
|
|
36
41
|
|
|
42
|
+
interface ColdLaunchInjections {
|
|
43
|
+
injections: ColdLaunchInjection[];
|
|
44
|
+
hasUIAbility: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
37
47
|
const COLD_LAUNCH_BRIDGE_IMPORT = '@guancecloud/ft_sdk/Index';
|
|
38
48
|
const COLD_LAUNCH_TRANSFORM_MARKER = '/* guance-cold-launch-transform:instrumented */';
|
|
39
|
-
const SOURCE_TRANSFORM_REGISTRY_KEY = '__guanceHvigorSourceTransformRegistry';
|
|
40
|
-
const TRANSFORM_ID = 'guance-cold-launch-transform';
|
|
41
|
-
|
|
42
|
-
let registeredFileSystem: any = null;
|
|
43
49
|
|
|
44
50
|
export function isApplicationArkTsSource(id: string): boolean {
|
|
45
51
|
const normalizedId = id.replace(/\\/g, '/');
|
|
@@ -53,27 +59,6 @@ function visit(ts: TypeScriptApi, node: any, visitor: (current: any) => void): v
|
|
|
53
59
|
ts.forEachChild(node, (child: any) => visit(ts, child, visitor));
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
function collectIdentifierNames(ts: TypeScriptApi, sourceFile: any): Set<string> {
|
|
57
|
-
const identifiers = new Set<string>();
|
|
58
|
-
visit(ts, sourceFile, (node: any) => {
|
|
59
|
-
if (ts.isIdentifier(node)) {
|
|
60
|
-
identifiers.add(node.text);
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
return identifiers;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function selectBridgeName(identifiers: Set<string>): string {
|
|
67
|
-
const baseName = '__ftColdLaunchBridge';
|
|
68
|
-
let bridgeName = baseName;
|
|
69
|
-
let suffix = 1;
|
|
70
|
-
while (identifiers.has(bridgeName)) {
|
|
71
|
-
bridgeName = `${baseName}${suffix}`;
|
|
72
|
-
suffix++;
|
|
73
|
-
}
|
|
74
|
-
return bridgeName;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
62
|
function getPluginVersion(): string {
|
|
78
63
|
try {
|
|
79
64
|
const fileSystem = require.main.require('fs');
|
|
@@ -100,7 +85,57 @@ function findImportInsertionOffset(ts: TypeScriptApi, sourceFile: any): number {
|
|
|
100
85
|
return offset;
|
|
101
86
|
}
|
|
102
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
|
+
|
|
103
132
|
function classifyAbilityBaseName(name: string): AbilityClassKind | null {
|
|
133
|
+
if (name === 'InsightIntentEntryExecutor') {
|
|
134
|
+
return 'intent';
|
|
135
|
+
}
|
|
136
|
+
if (name === 'InsightIntentExecutor') {
|
|
137
|
+
return 'legacyIntent';
|
|
138
|
+
}
|
|
104
139
|
if (name === 'AbilityStage') {
|
|
105
140
|
return 'stage';
|
|
106
141
|
}
|
|
@@ -113,54 +148,218 @@ function classifyAbilityBaseName(name: string): AbilityClassKind | null {
|
|
|
113
148
|
return null;
|
|
114
149
|
}
|
|
115
150
|
|
|
116
|
-
function
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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;
|
|
126
163
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
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;
|
|
132
168
|
}
|
|
133
169
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
+
}
|
|
142
187
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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;
|
|
146
203
|
}
|
|
147
204
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
+
}
|
|
151
246
|
return null;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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 };
|
|
156
255
|
}
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
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;
|
|
160
263
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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;
|
|
164
363
|
}
|
|
165
364
|
|
|
166
365
|
function methodIndentation(code: string, method: any): string {
|
|
@@ -184,6 +383,50 @@ function blockIndentation(code: string, block: any): string {
|
|
|
184
383
|
return (match ? match[0] : '') + ' ';
|
|
185
384
|
}
|
|
186
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
|
+
|
|
187
430
|
function collectWindowStageParameterNames(ts: TypeScriptApi, sourceFile: any, method: any): Set<string> {
|
|
188
431
|
const names = new Set<string>();
|
|
189
432
|
method.parameters.forEach((parameter: any) => {
|
|
@@ -199,65 +442,221 @@ function collectWindowStageParameterNames(ts: TypeScriptApi, sourceFile: any, me
|
|
|
199
442
|
}
|
|
200
443
|
|
|
201
444
|
function createFirstFrameRetryInjections(ts: TypeScriptApi, sourceFile: any, code: string,
|
|
202
|
-
method: any, bridgeName: string, windowStageNames: Set<string
|
|
203
|
-
|
|
445
|
+
method: any, bridgeName: string, windowStageNames: Set<string>, identifiers: Set<string>,
|
|
446
|
+
resolve: (location: any, name: string) => any | undefined): ColdLaunchInjection[] {
|
|
447
|
+
const candidates: any[] = [];
|
|
204
448
|
visit(ts, method.body, (node: any) => {
|
|
205
|
-
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
449
|
+
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression) ||
|
|
450
|
+
(ts.isOptionalChain && ts.isOptionalChain(node))) return;
|
|
208
451
|
const delegate = node.expression.expression;
|
|
209
452
|
if (!ts.isIdentifier(delegate) || !windowStageNames.has(delegate.text) ||
|
|
210
|
-
node.expression.name.text !== 'loadContent' || node.arguments.length
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
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;
|
|
214
456
|
const callback = node.arguments[node.arguments.length - 1];
|
|
215
|
-
if ((!ts.isArrowFunction(callback) && !ts.isFunctionExpression(callback)) || !ts.isBlock(callback.body))
|
|
216
|
-
|
|
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);
|
|
217
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;
|
|
218
480
|
const indentation = blockIndentation(code, callback.body);
|
|
219
|
-
const
|
|
481
|
+
const error = callback.parameters.length > 0 && ts.isIdentifier(callback.parameters[0].name)
|
|
220
482
|
? callback.parameters[0].name.text : null;
|
|
221
|
-
const retry = `${bridgeName}.observeFirstFrame(${
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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) }));
|
|
229
503
|
}
|
|
230
504
|
|
|
231
505
|
function createColdLaunchInjections(ts: TypeScriptApi, sourceFile: any, code: string,
|
|
232
|
-
bridgeName: string, pluginVersion: string
|
|
506
|
+
bridgeName: string, pluginVersion: string, identifiers: Set<string>,
|
|
507
|
+
launchTypes: LaunchParameterTypes, readSource?: SourceReader): ColdLaunchInjections {
|
|
233
508
|
const injections: ColdLaunchInjection[] = [];
|
|
234
|
-
|
|
509
|
+
let hasUIAbility = false;
|
|
510
|
+
const resolve = createLexicalResolver(ts, sourceFile);
|
|
511
|
+
const resolveClass = createAbilityClassResolver(ts, sourceFile, readSource);
|
|
235
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
|
+
|
|
236
551
|
visit(ts, sourceFile, (node: any) => {
|
|
237
552
|
if (!ts.isClassDeclaration(node)) {
|
|
238
553
|
return;
|
|
239
554
|
}
|
|
240
|
-
const
|
|
555
|
+
const classification = resolveClass(node);
|
|
556
|
+
const kind = classification.kind;
|
|
241
557
|
if (!kind) {
|
|
242
558
|
return;
|
|
243
559
|
}
|
|
560
|
+
if (kind === 'ui') {
|
|
561
|
+
hasUIAbility = true;
|
|
562
|
+
}
|
|
244
563
|
let hasOnCreate = false;
|
|
245
|
-
|
|
246
|
-
|
|
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();
|
|
247
612
|
return;
|
|
248
613
|
}
|
|
249
|
-
|
|
250
|
-
const position = member.body.getStart(sourceFile) + 1;
|
|
251
|
-
if (member.name.text === 'onCreate') {
|
|
614
|
+
if (name === 'onCreate') {
|
|
252
615
|
hasOnCreate = true;
|
|
253
|
-
let content =
|
|
616
|
+
let content = kind === 'ui' ? '' :
|
|
617
|
+
`\n${indentation}${bridgeName}.recordApplicationInit(this.context${pluginVersionArgument});`;
|
|
254
618
|
if (kind === 'ui') {
|
|
255
|
-
|
|
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);`;
|
|
256
639
|
}
|
|
257
640
|
injections.push({ position, content });
|
|
641
|
+
finish();
|
|
258
642
|
return;
|
|
259
643
|
}
|
|
260
|
-
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) {
|
|
261
660
|
return;
|
|
262
661
|
}
|
|
263
662
|
const windowStageParameter = member.parameters[0];
|
|
@@ -267,121 +666,125 @@ function createColdLaunchInjections(ts: TypeScriptApi, sourceFile: any, code: st
|
|
|
267
666
|
const windowStageName = windowStageParameter.name.text;
|
|
268
667
|
injections.push({
|
|
269
668
|
position,
|
|
270
|
-
content: `\n${indentation}${bridgeName}.observeFirstFrame(${windowStageName})
|
|
669
|
+
content: `\n${indentation}if (${windowStageName}) { ${bridgeName}.observeFirstFrame(${windowStageName}); }`
|
|
271
670
|
});
|
|
671
|
+
finish();
|
|
272
672
|
});
|
|
273
673
|
if (kind === 'ui') {
|
|
274
|
-
node.members.forEach((
|
|
275
|
-
if (
|
|
276
|
-
|
|
277
|
-
|
|
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;
|
|
278
678
|
const windowStageNames = collectWindowStageParameterNames(ts, sourceFile, member);
|
|
279
679
|
if (windowStageNames.size > 0) {
|
|
280
680
|
injections.push(...createFirstFrameRetryInjections(
|
|
281
|
-
ts, sourceFile, code, member, bridgeName, windowStageNames));
|
|
681
|
+
ts, sourceFile, code, member, bridgeName, windowStageNames, identifiers, resolve));
|
|
282
682
|
}
|
|
283
683
|
});
|
|
284
684
|
}
|
|
285
685
|
if (kind === 'stage' && !hasOnCreate) {
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
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
|
+
}
|
|
293
722
|
}
|
|
294
723
|
});
|
|
295
|
-
return injections;
|
|
724
|
+
return { injections, hasUIAbility };
|
|
296
725
|
}
|
|
297
726
|
|
|
298
727
|
function applyColdLaunchInjections(code: string, injections: ColdLaunchInjection[]): string {
|
|
299
728
|
const contentsByPosition = new Map<number, string[]>();
|
|
729
|
+
const ends = new Map<number, number>();
|
|
300
730
|
injections.forEach((injection: ColdLaunchInjection) => {
|
|
301
731
|
const contents = contentsByPosition.get(injection.position) || [];
|
|
302
732
|
contents.push(injection.content);
|
|
303
733
|
contentsByPosition.set(injection.position, contents);
|
|
734
|
+
ends.set(injection.position, Math.max(ends.get(injection.position) || injection.position, injection.end || injection.position));
|
|
304
735
|
});
|
|
305
736
|
const positions = Array.from(contentsByPosition.keys()).sort((left: number, right: number) => right - left);
|
|
306
737
|
let transformedCode = code;
|
|
307
738
|
positions.forEach((position: number) => {
|
|
308
739
|
const content = (contentsByPosition.get(position) || []).join('');
|
|
309
|
-
transformedCode = transformedCode.slice(0, position) + content + transformedCode.slice(position);
|
|
740
|
+
transformedCode = transformedCode.slice(0, position) + content + transformedCode.slice(ends.get(position));
|
|
310
741
|
});
|
|
311
742
|
return transformedCode;
|
|
312
743
|
}
|
|
313
744
|
|
|
314
|
-
function
|
|
315
|
-
|
|
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)) {
|
|
316
749
|
return code;
|
|
317
750
|
}
|
|
318
751
|
const sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
319
|
-
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
|
+
};
|
|
320
759
|
const version = getPluginVersion();
|
|
321
|
-
const
|
|
322
|
-
|
|
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) {
|
|
323
765
|
return code;
|
|
324
766
|
}
|
|
325
|
-
|
|
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');
|
|
326
778
|
const bridgeImport = `\n${COLD_LAUNCH_TRANSFORM_MARKER}\n` +
|
|
327
|
-
`import { FTColdLaunchBridge as ${bridgeName}
|
|
328
|
-
|
|
329
|
-
|
|
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);
|
|
330
790
|
}
|
|
331
|
-
|
|
332
|
-
function installSourceReader(context: CompilePluginContext): void {
|
|
333
|
-
if (registeredFileSystem !== null) {
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
const typeScriptPath = `${context.share.projectConfig.etsLoaderPath}/node_modules/typescript`;
|
|
337
|
-
const ts = require.main.require(typeScriptPath);
|
|
338
|
-
const fileSystem = require.main.require('fs');
|
|
339
|
-
let registry = fileSystem[SOURCE_TRANSFORM_REGISTRY_KEY] as SharedSourceTransformRegistry | undefined;
|
|
340
|
-
if (!registry) {
|
|
341
|
-
registry = {
|
|
342
|
-
originalReadFile: fileSystem.readFileSync as (fileName: string, options?: any) => any,
|
|
343
|
-
buffer: require.main.require('buffer'),
|
|
344
|
-
transforms: new Map<string, (fileName: string, code: string) => string>()
|
|
345
|
-
};
|
|
346
|
-
fileSystem[SOURCE_TRANSFORM_REGISTRY_KEY] = registry;
|
|
347
|
-
fileSystem.readFileSync = (fileName: string, options?: any): any => {
|
|
348
|
-
const content = registry!.originalReadFile(fileName, options);
|
|
349
|
-
if (typeof fileName !== 'string' || !isApplicationArkTsSource(fileName)) {
|
|
350
|
-
return content;
|
|
351
|
-
}
|
|
352
|
-
let transformedCode = typeof content === 'string' ? content : content.toString();
|
|
353
|
-
registry!.transforms.forEach((transform: (id: string, code: string) => string) => {
|
|
354
|
-
transformedCode = transform(fileName, transformedCode);
|
|
355
|
-
});
|
|
356
|
-
return typeof content === 'string' ? transformedCode : registry!.buffer.Buffer.from(transformedCode);
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
registry.transforms.set(TRANSFORM_ID, (fileName: string, code: string) => rewriteSource(ts, fileName, code));
|
|
360
|
-
registeredFileSystem = fileSystem;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function restoreSourceReader(): void {
|
|
364
|
-
if (registeredFileSystem === null) {
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
const registry = registeredFileSystem[SOURCE_TRANSFORM_REGISTRY_KEY] as SharedSourceTransformRegistry | undefined;
|
|
368
|
-
if (registry) {
|
|
369
|
-
registry.transforms.delete(TRANSFORM_ID);
|
|
370
|
-
if (registry.transforms.size === 0) {
|
|
371
|
-
registeredFileSystem.readFileSync = registry.originalReadFile;
|
|
372
|
-
delete registeredFileSystem[SOURCE_TRANSFORM_REGISTRY_KEY];
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
registeredFileSystem = null;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
export default {
|
|
379
|
-
name: 'guance-cold-launch-transform',
|
|
380
|
-
order: 'first',
|
|
381
|
-
buildStart(this: CompilePluginContext): void {
|
|
382
|
-
installSourceReader(this);
|
|
383
|
-
},
|
|
384
|
-
buildEnd(): void {
|
|
385
|
-
restoreSourceReader();
|
|
386
|
-
}
|
|
387
|
-
};
|