@cloudcare/hvigor-ohos-plugin 0.1.1 → 0.1.2-alpha.1

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