upstreamfix 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +73 -2
- package/dist/cli.js +167 -0
- package/dist/core/diagnostic-diff.js +41 -0
- package/dist/core/process-cleanup.js +77 -0
- package/dist/core/report.js +131 -0
- package/dist/core/runner.js +826 -0
- package/dist/core/safety.js +7 -0
- package/dist/core/types.js +1 -0
- package/dist/core/verify.js +422 -0
- package/dist/core/write-branch.js +296 -0
- package/dist/migrations/firebase-admin-v14/index.js +597 -0
- package/dist/migrations/firebase-admin-v14/mapping.js +80 -0
- package/dist/migrations/index.js +46 -0
- package/dist/migrations/stripe-shared.js +393 -0
- package/dist/migrations/stripe-v22/index.js +47 -0
- package/dist/migrations/stripe-v23/index.js +55 -0
- package/package.json +49 -4
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
import { Node, SyntaxKind, } from 'ts-morph';
|
|
2
|
+
import { APP_MODULE, CREDENTIAL_FACTORIES, REMOVED_SERVICES, ROOT_TYPES, SERVICES, } from './mapping.js';
|
|
3
|
+
const PKG = 'firebase-admin';
|
|
4
|
+
const NODE_RUNTIME_GUIDANCE = 'firebase-admin v14 requires Node.js 22 or later; package.json does not declare engines.node. Add `"engines": { "node": ">=22" }` or use `--node-runtime 22` after confirming the deployment runtime.';
|
|
5
|
+
export const firebaseAdminV14 = {
|
|
6
|
+
id: 'firebase-admin@14',
|
|
7
|
+
packageName: PKG,
|
|
8
|
+
fromMajor: 13,
|
|
9
|
+
toMajor: 14,
|
|
10
|
+
releasedAt: '2026-06-08',
|
|
11
|
+
title: 'firebase-admin v13 → v14 (namespaced API removed)',
|
|
12
|
+
guideUrl: 'https://firebase.google.com/docs/admin/migrate-node-v10',
|
|
13
|
+
rules: ['service-getter', 'service-static', 'credential-factory', 'app-lifecycle', 'type'],
|
|
14
|
+
inspectManifest(manifest) {
|
|
15
|
+
const notes = [];
|
|
16
|
+
const nodeRange = manifest.engines?.node;
|
|
17
|
+
if (nodeRange === undefined)
|
|
18
|
+
notes.push(NODE_RUNTIME_GUIDANCE);
|
|
19
|
+
else if (typeof nodeRange !== 'string' || !supportsNode22(nodeRange)) {
|
|
20
|
+
notes.push(`firebase-admin v14 requires Node.js 22 or later; review \`engines.node\` (${String(nodeRange)}).`);
|
|
21
|
+
}
|
|
22
|
+
const functionsTest = [manifest.dependencies, manifest.devDependencies, manifest.peerDependencies, manifest.optionalDependencies]
|
|
23
|
+
.map((dependencies) => dependencies?.['firebase-functions-test'])
|
|
24
|
+
.find((value) => typeof value === 'string');
|
|
25
|
+
if (functionsTest)
|
|
26
|
+
notes.push(`firebase-functions-test "${functionsTest}" is installed. In a benchmarked project, the v14 typecheck failed because this package's declarations import firebase-admin exports removed in v14 (including app and firestore). Check whether a compatible release is available; otherwise update the test setup, then rerun the tests before upgrading.`);
|
|
27
|
+
return notes;
|
|
28
|
+
},
|
|
29
|
+
transform(project) {
|
|
30
|
+
const result = { changes: [], manual: [], filesChanged: [], fileUpdates: [] };
|
|
31
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
32
|
+
const parseDiagnostics = sourceFile.compilerNode.parseDiagnostics ?? [];
|
|
33
|
+
if (parseDiagnostics.length > 0) {
|
|
34
|
+
result.manual.push({
|
|
35
|
+
file: sourceFile.getFilePath(),
|
|
36
|
+
line: 1,
|
|
37
|
+
reason: 'This source file has syntax errors and could not be analyzed safely.',
|
|
38
|
+
snippet: sourceFile.getFullText().split(/\r?\n/, 1)[0] ?? '',
|
|
39
|
+
});
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (migrateFile(sourceFile, result))
|
|
43
|
+
result.filesChanged.push(sourceFile.getFilePath());
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
},
|
|
47
|
+
updateManifest(manifest) {
|
|
48
|
+
const notes = [];
|
|
49
|
+
const engine = manifest.engines?.node;
|
|
50
|
+
const runtimeReady = typeof engine === 'string' && supportsNode22(engine);
|
|
51
|
+
for (const field of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
52
|
+
const deps = manifest[field];
|
|
53
|
+
const current = deps?.[PKG];
|
|
54
|
+
if (!current)
|
|
55
|
+
continue;
|
|
56
|
+
const major = current.match(/(?:^|[^\d])(\d+)(?:\.|$)/)?.[1];
|
|
57
|
+
if (!major) {
|
|
58
|
+
notes.push(`Could not safely interpret \`${field}.${PKG}\` range \`${current}\`; update it to \`^14.0.0\` manually if appropriate.`);
|
|
59
|
+
}
|
|
60
|
+
else if (Number(major) === 13 && /^\s*[~^]?13(?:\.(?:\d+|x|\*)){0,2}\s*$/i.test(current)) {
|
|
61
|
+
if (runtimeReady) {
|
|
62
|
+
notes.push(`\`${field}.${PKG}\`: \`${current}\` → \`^14.0.0\``);
|
|
63
|
+
deps[PKG] = '^14.0.0';
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
notes.push(`\`${field}.${PKG}\` was left unchanged: firebase-admin v14 requires Node.js 22 or newer. Add \`"engines": { "node": ">=22" }\` to package.json, or use \`--node-runtime 22\` to confirm your deployment runtime and include it in the proposed change.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (Number(major) < 13) {
|
|
70
|
+
notes.push(`\`${field}.${PKG}\` is on major ${major}; this migration targets v13 → v14. Upgrade to v13 first, then rerun.`);
|
|
71
|
+
}
|
|
72
|
+
else if (Number(major) === 13) {
|
|
73
|
+
notes.push(`Could not safely interpret \`${field}.${PKG}\` range \`${current}\`; review it and update to \`^14.0.0\` manually if appropriate.`);
|
|
74
|
+
}
|
|
75
|
+
else if (Number(major) > 14) {
|
|
76
|
+
notes.push(`\`${field}.${PKG}\` is already on major ${major}; it was left unchanged to avoid downgrading it to v14.`);
|
|
77
|
+
}
|
|
78
|
+
else if (Number(major) === 14) {
|
|
79
|
+
// Already on the target major; leave the range as declared.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!engine) {
|
|
83
|
+
notes.push(NODE_RUNTIME_GUIDANCE);
|
|
84
|
+
}
|
|
85
|
+
else if (!runtimeReady) {
|
|
86
|
+
notes.push(`firebase-admin v14 requires Node.js 22 or later; review \`engines.node\` (${engine}).`);
|
|
87
|
+
}
|
|
88
|
+
const functionsVersion = manifest.dependencies?.['firebase-functions'];
|
|
89
|
+
if (functionsVersion) {
|
|
90
|
+
notes.push(`firebase-functions \`${functionsVersion}\` is installed: check that its firebase-admin peer range accepts v14 (firebase-functions#1902).`);
|
|
91
|
+
}
|
|
92
|
+
return notes;
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
function supportsNode22(range) {
|
|
96
|
+
const normalized = range.trim();
|
|
97
|
+
return /^(?:>=\s*22(?:\.\d+){0,2}|(?:\^|~)\s*22(?:\.\d+){0,2}|22(?:\.\d+){0,2})$/.test(normalized);
|
|
98
|
+
}
|
|
99
|
+
function migrateFile(sourceFile, result) {
|
|
100
|
+
const file = sourceFile.getFilePath();
|
|
101
|
+
const manualBefore = result.manual.length;
|
|
102
|
+
reportRemovedNamedImports(sourceFile, result.manual);
|
|
103
|
+
reportUnsupportedReferences(sourceFile, result.manual);
|
|
104
|
+
const aliases = findAliases(sourceFile);
|
|
105
|
+
if (aliases.length === 0)
|
|
106
|
+
return false;
|
|
107
|
+
const edits = [];
|
|
108
|
+
const aliasesWithManualUses = new Set();
|
|
109
|
+
for (const alias of aliases) {
|
|
110
|
+
const variableDeclaration = alias.nameNode.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
|
|
111
|
+
const variableStatement = variableDeclaration?.getVariableStatement();
|
|
112
|
+
if (variableStatement?.getModifiers().some((modifier) => modifier.getKind() === SyntaxKind.ExportKeyword)) {
|
|
113
|
+
aliasesWithManualUses.add(alias.name);
|
|
114
|
+
result.manual.push({
|
|
115
|
+
file,
|
|
116
|
+
line: alias.nameNode.getStartLineNumber(),
|
|
117
|
+
reason: 'This firebase-admin namespace alias is exported. Keep the public export stable, migrate its callers separately, and remove the alias only after checking downstream imports.',
|
|
118
|
+
snippet: snippetOf(alias.nameNode),
|
|
119
|
+
});
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (variableStatement && variableStatement.getDeclarations().length > 1) {
|
|
123
|
+
aliasesWithManualUses.add(alias.name);
|
|
124
|
+
result.manual.push({
|
|
125
|
+
file,
|
|
126
|
+
line: alias.nameNode.getStartLineNumber(),
|
|
127
|
+
reason: 'A firebase-admin require shares a declaration with other variables; split the declaration before migrating it.',
|
|
128
|
+
snippet: snippetOf(alias.nameNode),
|
|
129
|
+
});
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const references = alias.nameNode
|
|
133
|
+
.findReferencesAsNodes()
|
|
134
|
+
.filter(Node.isIdentifier)
|
|
135
|
+
.filter((reference) => reference.getSourceFile() === sourceFile)
|
|
136
|
+
.filter((reference) => reference !== alias.nameNode);
|
|
137
|
+
for (const ref of references) {
|
|
138
|
+
const classification = classify(ref);
|
|
139
|
+
const line = ref.getStartLineNumber();
|
|
140
|
+
if ('reason' in classification) {
|
|
141
|
+
aliasesWithManualUses.add(alias.name);
|
|
142
|
+
result.manual.push({ file, line, reason: classification.reason, snippet: snippetOf(ref) });
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const collision = classification.imports.find((imp) => hasImportCollision(sourceFile, imp));
|
|
146
|
+
if (collision) {
|
|
147
|
+
aliasesWithManualUses.add(alias.name);
|
|
148
|
+
result.manual.push({
|
|
149
|
+
file,
|
|
150
|
+
line,
|
|
151
|
+
reason: `\`${collision.name}\` is already declared in this file: rename it, then rerun.`,
|
|
152
|
+
snippet: snippetOf(ref),
|
|
153
|
+
});
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
edits.push({
|
|
157
|
+
start: classification.node.getStart(),
|
|
158
|
+
end: classification.node.getEnd(),
|
|
159
|
+
text: classification.text,
|
|
160
|
+
rule: classification.rule,
|
|
161
|
+
before: classification.node.getText(),
|
|
162
|
+
line,
|
|
163
|
+
imports: classification.imports.map((imp) => ({ ...imp, typeOnly: imp.typeOnly && alias.kind === 'esm' })),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (edits.length === 0) {
|
|
168
|
+
if (result.manual.length > manualBefore)
|
|
169
|
+
sortManual(result.manual, manualBefore);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
const textEdits = edits.map((edit) => ({ start: edit.start, end: edit.end, text: edit.text }));
|
|
173
|
+
for (const edit of edits) {
|
|
174
|
+
result.changes.push({ file, line: edit.line, rule: edit.rule, before: edit.before, after: edit.text });
|
|
175
|
+
}
|
|
176
|
+
const importMap = mergeImports(edits.flatMap((edit) => edit.imports));
|
|
177
|
+
const kind = aliases.some((alias) => alias.kind === 'esm') ? 'esm' : 'cjs';
|
|
178
|
+
const importText = renderImports(sourceFile, kind, importMap);
|
|
179
|
+
if (importText) {
|
|
180
|
+
const insertion = safeImportInsertionPosition(sourceFile);
|
|
181
|
+
textEdits.push({ start: insertion, end: insertion, text: importText });
|
|
182
|
+
}
|
|
183
|
+
textEdits.push(...aliasDeclarationRemovals(sourceFile, aliases, aliasesWithManualUses));
|
|
184
|
+
const transformed = applyTextEdits(sourceFile.getFullText(), textEdits);
|
|
185
|
+
if (transformed !== sourceFile.getFullText())
|
|
186
|
+
result.fileUpdates?.push({ file, contents: transformed });
|
|
187
|
+
if (result.manual.length > manualBefore)
|
|
188
|
+
sortManual(result.manual, manualBefore);
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
function classify(ref) {
|
|
192
|
+
const parent = ref.getParent();
|
|
193
|
+
if (Node.isPropertyAccessExpression(parent) && parent.getExpression() === ref)
|
|
194
|
+
return classifyAccess(parent);
|
|
195
|
+
if (Node.isQualifiedName(parent) && parent.getLeft() === ref)
|
|
196
|
+
return classifyQualified(parent);
|
|
197
|
+
return { reason: 'The firebase-admin namespace is used as a value (passed around or destructured).' };
|
|
198
|
+
}
|
|
199
|
+
function classifyAccess(access) {
|
|
200
|
+
const name = access.getName();
|
|
201
|
+
const outer = access.getParent();
|
|
202
|
+
const isCallee = Node.isCallExpression(outer) && outer.getExpression() === access;
|
|
203
|
+
const memberOf = Node.isPropertyAccessExpression(outer) && outer.getExpression() === access ? outer : undefined;
|
|
204
|
+
const service = Object.hasOwn(SERVICES, name) ? SERVICES[name] : undefined;
|
|
205
|
+
if (service) {
|
|
206
|
+
if (isCallee) {
|
|
207
|
+
return {
|
|
208
|
+
node: access,
|
|
209
|
+
text: service.getter,
|
|
210
|
+
rule: 'service-getter',
|
|
211
|
+
imports: [{ module: service.module, name: service.getter, typeOnly: false }],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (memberOf && service.statics.includes(memberOf.getName())) {
|
|
215
|
+
const member = memberOf.getName();
|
|
216
|
+
return {
|
|
217
|
+
node: memberOf,
|
|
218
|
+
text: member,
|
|
219
|
+
rule: 'service-static',
|
|
220
|
+
imports: [{ module: service.module, name: member, typeOnly: false }],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return { reason: `\`${access.getText()}\` is not a supported pattern for this service.` };
|
|
224
|
+
}
|
|
225
|
+
if (Object.hasOwn(REMOVED_SERVICES, name))
|
|
226
|
+
return { reason: REMOVED_SERVICES[name] };
|
|
227
|
+
if (name === 'credential' && memberOf && CREDENTIAL_FACTORIES.includes(memberOf.getName())) {
|
|
228
|
+
const factory = memberOf.getName();
|
|
229
|
+
return {
|
|
230
|
+
node: memberOf,
|
|
231
|
+
text: factory,
|
|
232
|
+
rule: 'credential-factory',
|
|
233
|
+
imports: [{ module: APP_MODULE, name: factory, typeOnly: false }],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
if (name === 'initializeApp' && isCallee) {
|
|
237
|
+
return { node: access, text: 'initializeApp', rule: 'app-lifecycle', imports: [appImport('initializeApp')] };
|
|
238
|
+
}
|
|
239
|
+
if (name === 'app' && isCallee) {
|
|
240
|
+
const appCall = outer;
|
|
241
|
+
if (Node.isCallExpression(appCall)) {
|
|
242
|
+
if (appCall.getArguments().length > 0) {
|
|
243
|
+
return { reason: 'Named-app calls such as admin.app(name) need a manual modular API mapping.' };
|
|
244
|
+
}
|
|
245
|
+
const serviceAccess = appCall.getParent();
|
|
246
|
+
const serviceCall = serviceAccess?.getParent();
|
|
247
|
+
if (Node.isPropertyAccessExpression(serviceAccess) &&
|
|
248
|
+
serviceAccess.getExpression() === appCall &&
|
|
249
|
+
Node.isCallExpression(serviceCall) &&
|
|
250
|
+
serviceCall.getExpression() === serviceAccess) {
|
|
251
|
+
const serviceName = serviceAccess.getName();
|
|
252
|
+
const nestedService = Object.hasOwn(SERVICES, serviceName) ? SERVICES[serviceName] : undefined;
|
|
253
|
+
if (nestedService) {
|
|
254
|
+
return {
|
|
255
|
+
node: serviceCall,
|
|
256
|
+
text: `${nestedService.getter}()`,
|
|
257
|
+
rule: 'app-service-getter',
|
|
258
|
+
imports: [{ module: nestedService.module, name: nestedService.getter, typeOnly: false }],
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return { node: access, text: 'getApp', rule: 'app-lifecycle', imports: [appImport('getApp')] };
|
|
264
|
+
}
|
|
265
|
+
if (name === 'apps') {
|
|
266
|
+
return { node: access, text: 'getApps()', rule: 'app-lifecycle', imports: [appImport('getApps')] };
|
|
267
|
+
}
|
|
268
|
+
return { reason: `\`${access.getText()}\` has no automatic mapping yet.` };
|
|
269
|
+
}
|
|
270
|
+
function classifyQualified(qualified) {
|
|
271
|
+
const name = qualified.getRight().getText();
|
|
272
|
+
const outer = qualified.getParent();
|
|
273
|
+
if (ROOT_TYPES.includes(name)) {
|
|
274
|
+
return { node: qualified, text: name, rule: 'type', imports: [{ module: PKG, name, typeOnly: true }] };
|
|
275
|
+
}
|
|
276
|
+
const service = Object.hasOwn(SERVICES, name) ? SERVICES[name] : undefined;
|
|
277
|
+
if (service && Node.isQualifiedName(outer) && outer.getLeft() === qualified) {
|
|
278
|
+
const typeName = outer.getRight().getText();
|
|
279
|
+
if (service.removedTypes?.includes(typeName)) {
|
|
280
|
+
return { reason: `\`${outer.getText()}\` was removed in v14 (legacy FCM API): use \`Message\` with \`send()\`.` };
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
node: outer,
|
|
284
|
+
text: typeName,
|
|
285
|
+
rule: 'type',
|
|
286
|
+
imports: [{ module: service.module, name: typeName, typeOnly: !service.statics.includes(typeName) }],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
return { reason: `Type \`${qualified.getText()}\` has no automatic mapping yet.` };
|
|
290
|
+
}
|
|
291
|
+
function appImport(name) {
|
|
292
|
+
return { module: APP_MODULE, name, typeOnly: false };
|
|
293
|
+
}
|
|
294
|
+
function findAliases(sourceFile) {
|
|
295
|
+
const aliases = [];
|
|
296
|
+
for (const decl of sourceFile.getImportDeclarations()) {
|
|
297
|
+
if (decl.getModuleSpecifierValue() !== PKG)
|
|
298
|
+
continue;
|
|
299
|
+
const namespaceImport = decl.getNamespaceImport();
|
|
300
|
+
const defaultImport = decl.getDefaultImport();
|
|
301
|
+
if (namespaceImport)
|
|
302
|
+
aliases.push({ name: namespaceImport.getText(), kind: 'esm', nameNode: namespaceImport });
|
|
303
|
+
if (defaultImport)
|
|
304
|
+
aliases.push({ name: defaultImport.getText(), kind: 'esm', nameNode: defaultImport });
|
|
305
|
+
}
|
|
306
|
+
for (const decl of sourceFile.getDescendantsOfKind(SyntaxKind.ImportEqualsDeclaration)) {
|
|
307
|
+
const moduleReference = decl.getModuleReference();
|
|
308
|
+
if (Node.isExternalModuleReference(moduleReference)) {
|
|
309
|
+
const expression = moduleReference.getExpression();
|
|
310
|
+
if (Node.isStringLiteral(expression) && expression.getLiteralValue() === PKG) {
|
|
311
|
+
aliases.push({ name: decl.getName(), kind: 'esm', nameNode: decl.getNameNode() });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
for (const decl of sourceFile.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
|
|
316
|
+
const init = decl.getInitializer();
|
|
317
|
+
const nameNode = decl.getNameNode();
|
|
318
|
+
if (!Node.isIdentifier(nameNode) || !init || !Node.isCallExpression(init))
|
|
319
|
+
continue;
|
|
320
|
+
if (init.getExpression().getText() !== 'require')
|
|
321
|
+
continue;
|
|
322
|
+
const [arg] = init.getArguments();
|
|
323
|
+
if (Node.isStringLiteral(arg) && arg.getLiteralValue() === PKG) {
|
|
324
|
+
aliases.push({ name: nameNode.getText(), kind: 'cjs', nameNode });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return aliases;
|
|
328
|
+
}
|
|
329
|
+
/** `import { firestore } from 'firebase-admin'` relied on namespace exports that no longer exist. */
|
|
330
|
+
function reportRemovedNamedImports(sourceFile, manual) {
|
|
331
|
+
for (const decl of sourceFile.getImportDeclarations()) {
|
|
332
|
+
if (decl.getModuleSpecifierValue() !== PKG)
|
|
333
|
+
continue;
|
|
334
|
+
for (const spec of decl.getNamedImports()) {
|
|
335
|
+
const name = spec.getName();
|
|
336
|
+
if (Object.hasOwn(SERVICES, name) ||
|
|
337
|
+
name === 'credential' ||
|
|
338
|
+
Object.hasOwn(REMOVED_SERVICES, name) ||
|
|
339
|
+
name === 'app' ||
|
|
340
|
+
name === 'apps') {
|
|
341
|
+
manual.push({
|
|
342
|
+
file: sourceFile.getFilePath(),
|
|
343
|
+
line: spec.getStartLineNumber(),
|
|
344
|
+
reason: `\`${name}\` is no longer exported by firebase-admin v14: import from the modular entry point instead.`,
|
|
345
|
+
snippet: decl.getText(),
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function reportUnsupportedReferences(sourceFile, manual) {
|
|
352
|
+
const file = sourceFile.getFilePath();
|
|
353
|
+
const knownRequireAliases = new Set(findAliases(sourceFile).filter((alias) => alias.kind === 'cjs').map((alias) => alias.name));
|
|
354
|
+
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
355
|
+
if (declaration.getModuleSpecifierValue() !== PKG)
|
|
356
|
+
continue;
|
|
357
|
+
const specs = declaration.getNamedImports();
|
|
358
|
+
if ((declaration.getDefaultImport() || declaration.getNamespaceImport()) && specs.length > 0) {
|
|
359
|
+
manual.push({
|
|
360
|
+
file,
|
|
361
|
+
line: declaration.getStartLineNumber(),
|
|
362
|
+
reason: 'A mixed default/namespace and named import from firebase-admin is not rewritten automatically.',
|
|
363
|
+
snippet: declaration.getText(),
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
const unsupportedNamedImport = specs.some((spec) => {
|
|
367
|
+
const name = spec.getName();
|
|
368
|
+
return !(ROOT_TYPES.includes(name) ||
|
|
369
|
+
Object.hasOwn(SERVICES, name) ||
|
|
370
|
+
name === 'credential' ||
|
|
371
|
+
Object.hasOwn(REMOVED_SERVICES, name) ||
|
|
372
|
+
name === 'app' ||
|
|
373
|
+
name === 'apps');
|
|
374
|
+
});
|
|
375
|
+
if ((specs.length === 0 && !declaration.getDefaultImport() && !declaration.getNamespaceImport()) ||
|
|
376
|
+
unsupportedNamedImport) {
|
|
377
|
+
manual.push({
|
|
378
|
+
file,
|
|
379
|
+
line: declaration.getStartLineNumber(),
|
|
380
|
+
reason: 'This import form from the firebase-admin root package is not migrated automatically; review it against the v14 modular API.',
|
|
381
|
+
snippet: declaration.getText(),
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
for (const declaration of sourceFile.getExportDeclarations()) {
|
|
386
|
+
if (declaration.getModuleSpecifierValue() !== PKG)
|
|
387
|
+
continue;
|
|
388
|
+
manual.push({
|
|
389
|
+
file,
|
|
390
|
+
line: declaration.getStartLineNumber(),
|
|
391
|
+
reason: 'Re-exports from the firebase-admin root package are not migrated automatically.',
|
|
392
|
+
snippet: declaration.getText(),
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
396
|
+
const expression = call.getExpression();
|
|
397
|
+
const [argument] = call.getArguments();
|
|
398
|
+
const moduleSpecifier = Node.isStringLiteral(argument) || Node.isNoSubstitutionTemplateLiteral(argument)
|
|
399
|
+
? argument.getLiteralValue()
|
|
400
|
+
: undefined;
|
|
401
|
+
if (moduleSpecifier !== PKG)
|
|
402
|
+
continue;
|
|
403
|
+
if (Node.isIdentifier(expression) && expression.getText() === 'require') {
|
|
404
|
+
const declaration = call.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
|
|
405
|
+
const name = declaration?.getNameNode();
|
|
406
|
+
if (Node.isIdentifier(name) && knownRequireAliases.has(name.getText()))
|
|
407
|
+
continue;
|
|
408
|
+
manual.push({
|
|
409
|
+
file,
|
|
410
|
+
line: call.getStartLineNumber(),
|
|
411
|
+
reason: 'Inline or destructured require("firebase-admin") calls are not migrated automatically.',
|
|
412
|
+
snippet: snippetOf(call),
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
else if (expression.getKind() === SyntaxKind.ImportKeyword) {
|
|
416
|
+
manual.push({
|
|
417
|
+
file,
|
|
418
|
+
line: call.getStartLineNumber(),
|
|
419
|
+
reason: 'Dynamic import("firebase-admin") calls are not migrated automatically.',
|
|
420
|
+
snippet: snippetOf(call),
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function hasImportCollision(sourceFile, needed) {
|
|
426
|
+
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
427
|
+
const module = declaration.getModuleSpecifierValue();
|
|
428
|
+
const defaultImport = declaration.getDefaultImport();
|
|
429
|
+
const namespaceImport = declaration.getNamespaceImport();
|
|
430
|
+
if (defaultImport?.getText() === needed.name || namespaceImport?.getText() === needed.name) {
|
|
431
|
+
if (module !== needed.module)
|
|
432
|
+
return true;
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
for (const specifier of declaration.getNamedImports()) {
|
|
436
|
+
const localName = (specifier.getAliasNode() ?? specifier.getNameNode()).getText();
|
|
437
|
+
if (localName !== needed.name)
|
|
438
|
+
continue;
|
|
439
|
+
if (module !== needed.module)
|
|
440
|
+
return true;
|
|
441
|
+
if (!needed.typeOnly && (declaration.isTypeOnly() || specifier.isTypeOnly()))
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return sourceFile.getDescendantsOfKind(SyntaxKind.Identifier).some((identifier) => {
|
|
446
|
+
if (identifier.getText() !== needed.name)
|
|
447
|
+
return false;
|
|
448
|
+
const parent = identifier.getParent();
|
|
449
|
+
if (Node.isImportClause(parent) || Node.isNamespaceImport(parent) || Node.isImportSpecifier(parent))
|
|
450
|
+
return false;
|
|
451
|
+
if (Node.isVariableDeclaration(parent))
|
|
452
|
+
return parent.getNameNode() === identifier;
|
|
453
|
+
if (Node.isParameterDeclaration(parent))
|
|
454
|
+
return parent.getNameNode() === identifier;
|
|
455
|
+
if (Node.isFunctionDeclaration(parent) || Node.isFunctionExpression(parent))
|
|
456
|
+
return parent.getNameNode() === identifier;
|
|
457
|
+
if (Node.isClassDeclaration(parent) || Node.isInterfaceDeclaration(parent) || Node.isTypeAliasDeclaration(parent) || Node.isEnumDeclaration(parent)) {
|
|
458
|
+
return parent.getNameNode() === identifier;
|
|
459
|
+
}
|
|
460
|
+
if (Node.isImportClause(parent))
|
|
461
|
+
return parent.getDefaultImport() === identifier;
|
|
462
|
+
if (Node.isNamespaceImport(parent))
|
|
463
|
+
return parent.getNameNode() === identifier;
|
|
464
|
+
if (Node.isImportSpecifier(parent))
|
|
465
|
+
return (parent.getAliasNode() ?? parent.getNameNode()) === identifier;
|
|
466
|
+
if (Node.isBindingElement(parent))
|
|
467
|
+
return parent.getNameNode() === identifier;
|
|
468
|
+
if (Node.isTypeParameterDeclaration(parent))
|
|
469
|
+
return parent.getNameNode() === identifier;
|
|
470
|
+
return false;
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
function applyTextEdits(source, edits) {
|
|
474
|
+
let result = source;
|
|
475
|
+
for (const edit of [...edits].sort((a, b) => b.start - a.start || b.end - a.end)) {
|
|
476
|
+
result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
|
|
477
|
+
}
|
|
478
|
+
return result;
|
|
479
|
+
}
|
|
480
|
+
function mergeImports(imports) {
|
|
481
|
+
const byModule = new Map();
|
|
482
|
+
for (const imp of imports) {
|
|
483
|
+
const names = byModule.get(imp.module) ?? new Map();
|
|
484
|
+
// A name used both as a value and as a type must be a value import.
|
|
485
|
+
names.set(imp.name, (names.get(imp.name) ?? true) && imp.typeOnly);
|
|
486
|
+
byModule.set(imp.module, names);
|
|
487
|
+
}
|
|
488
|
+
return byModule;
|
|
489
|
+
}
|
|
490
|
+
function renderImports(sourceFile, kind, byModule) {
|
|
491
|
+
const quote = importQuote(sourceFile);
|
|
492
|
+
const lines = [];
|
|
493
|
+
for (const module of [...byModule.keys()].sort()) {
|
|
494
|
+
const names = byModule.get(module);
|
|
495
|
+
const missing = [...names.entries()]
|
|
496
|
+
.filter(([name]) => !hasExactNamedImport(sourceFile, module, name))
|
|
497
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
498
|
+
if (missing.length === 0)
|
|
499
|
+
continue;
|
|
500
|
+
if (kind === 'cjs') {
|
|
501
|
+
lines.push(`const { ${missing.map(([name]) => name).join(', ')} } = require(${quote}${module}${quote});`);
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
const values = missing.filter(([, typeOnly]) => !typeOnly).map(([name]) => name);
|
|
505
|
+
const types = missing.filter(([, typeOnly]) => typeOnly).map(([name]) => name);
|
|
506
|
+
if (values.length > 0)
|
|
507
|
+
lines.push(`import { ${values.join(', ')} } from ${quote}${module}${quote};`);
|
|
508
|
+
if (types.length > 0)
|
|
509
|
+
lines.push(`import type { ${types.join(', ')} } from ${quote}${module}${quote};`);
|
|
510
|
+
}
|
|
511
|
+
if (lines.length === 0)
|
|
512
|
+
return '';
|
|
513
|
+
const fullText = sourceFile.getFullText();
|
|
514
|
+
const position = safeImportInsertionPosition(sourceFile);
|
|
515
|
+
const newline = fullText.includes('\r\n') ? '\r\n' : '\n';
|
|
516
|
+
const before = fullText.slice(0, position);
|
|
517
|
+
const after = fullText.slice(position);
|
|
518
|
+
const prefix = before.endsWith('\n') ? '' : newline;
|
|
519
|
+
const suffix = after.startsWith('\n') ? '' : newline;
|
|
520
|
+
return `${prefix}${lines.join(newline)}${suffix}`;
|
|
521
|
+
}
|
|
522
|
+
function safeImportInsertionPosition(sourceFile) {
|
|
523
|
+
const imports = sourceFile.getImportDeclarations();
|
|
524
|
+
if (imports.length > 0)
|
|
525
|
+
return imports[0].getStart();
|
|
526
|
+
const statements = sourceFile.getStatements();
|
|
527
|
+
let index = 0;
|
|
528
|
+
while (index < statements.length) {
|
|
529
|
+
const statement = statements[index];
|
|
530
|
+
if (Node.isExpressionStatement(statement) && Node.isStringLiteral(statement.getExpression()))
|
|
531
|
+
index++;
|
|
532
|
+
else
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
if (index > 0)
|
|
536
|
+
return statements[index - 1].getEnd();
|
|
537
|
+
return statements[0]?.getStart() ?? sourceFile.getEnd();
|
|
538
|
+
}
|
|
539
|
+
function importQuote(sourceFile) {
|
|
540
|
+
const specifier = sourceFile.getImportDeclarations()[0]?.getModuleSpecifier().getText();
|
|
541
|
+
if (specifier?.startsWith("'"))
|
|
542
|
+
return "'";
|
|
543
|
+
if (specifier?.startsWith('"'))
|
|
544
|
+
return '"';
|
|
545
|
+
const text = sourceFile.getFullText();
|
|
546
|
+
const singleCount = (text.match(/'/g) ?? []).length;
|
|
547
|
+
const doubleCount = (text.match(/"/g) ?? []).length;
|
|
548
|
+
return singleCount >= doubleCount ? "'" : '"';
|
|
549
|
+
}
|
|
550
|
+
function hasExactNamedImport(sourceFile, module, name) {
|
|
551
|
+
return sourceFile.getImportDeclarations().some((declaration) => declaration.getModuleSpecifierValue() === module &&
|
|
552
|
+
declaration.getNamedImports().some((specifier) => {
|
|
553
|
+
const local = specifier.getAliasNode() ?? specifier.getNameNode();
|
|
554
|
+
return local.getText() === name && specifier.getName() === name;
|
|
555
|
+
}));
|
|
556
|
+
}
|
|
557
|
+
function aliasDeclarationRemovals(sourceFile, aliases, aliasesWithManualUses) {
|
|
558
|
+
const removals = [];
|
|
559
|
+
const seen = new Set();
|
|
560
|
+
for (const alias of aliases) {
|
|
561
|
+
if (aliasesWithManualUses.has(alias.name))
|
|
562
|
+
continue;
|
|
563
|
+
const importDecl = alias.nameNode.getFirstAncestorByKind(SyntaxKind.ImportDeclaration);
|
|
564
|
+
if (importDecl) {
|
|
565
|
+
const declarationAliases = aliases.filter((candidate) => candidate.nameNode.getFirstAncestorByKind(SyntaxKind.ImportDeclaration) === importDecl);
|
|
566
|
+
if (declarationAliases.length > 0 &&
|
|
567
|
+
declarationAliases.every((candidate) => !aliasesWithManualUses.has(candidate.name)) &&
|
|
568
|
+
importDecl.getNamedImports().length === 0) {
|
|
569
|
+
const key = `${importDecl.getStart()}:${importDecl.getEnd()}`;
|
|
570
|
+
if (!seen.has(key))
|
|
571
|
+
removals.push({ start: importDecl.getStart(), end: importDecl.getEnd(), text: '' });
|
|
572
|
+
seen.add(key);
|
|
573
|
+
}
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
const importEquals = alias.nameNode.getFirstAncestorByKind(SyntaxKind.ImportEqualsDeclaration);
|
|
577
|
+
const variable = alias.nameNode.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
|
|
578
|
+
const statement = variable?.getVariableStatement();
|
|
579
|
+
const removable = importEquals ?? (statement?.getDeclarations().length === 1 ? statement : undefined);
|
|
580
|
+
if (removable) {
|
|
581
|
+
const key = `${removable.getStart()}:${removable.getEnd()}`;
|
|
582
|
+
if (!seen.has(key))
|
|
583
|
+
removals.push({ start: removable.getStart(), end: removable.getEnd(), text: '' });
|
|
584
|
+
seen.add(key);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return removals;
|
|
588
|
+
}
|
|
589
|
+
function snippetOf(node) {
|
|
590
|
+
const statement = node.getFirstAncestor((n) => Node.isStatement(n)) ?? node;
|
|
591
|
+
const text = statement.getText().split('\n')[0].trim();
|
|
592
|
+
return text.length > 120 ? text.slice(0, 117) + '...' : text;
|
|
593
|
+
}
|
|
594
|
+
function sortManual(manual, from) {
|
|
595
|
+
const tail = manual.splice(from).sort((a, b) => a.line - b.line);
|
|
596
|
+
manual.push(...tail);
|
|
597
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Namespaced API → modular API, from https://firebase.google.com/docs/admin/migrate-node-v10
|
|
2
|
+
// The namespaced API was removed in firebase-admin v14.0.0 (firebase-admin-node#3164).
|
|
3
|
+
export const SERVICES = {
|
|
4
|
+
firestore: {
|
|
5
|
+
getter: 'getFirestore',
|
|
6
|
+
module: 'firebase-admin/firestore',
|
|
7
|
+
statics: [
|
|
8
|
+
'FieldValue',
|
|
9
|
+
'Timestamp',
|
|
10
|
+
'FieldPath',
|
|
11
|
+
'GeoPoint',
|
|
12
|
+
'Filter',
|
|
13
|
+
'AggregateField',
|
|
14
|
+
'DocumentReference',
|
|
15
|
+
'CollectionReference',
|
|
16
|
+
'Query',
|
|
17
|
+
'WriteBatch',
|
|
18
|
+
'Transaction',
|
|
19
|
+
'BulkWriter',
|
|
20
|
+
'setLogFunction',
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
auth: { getter: 'getAuth', module: 'firebase-admin/auth', statics: [] },
|
|
24
|
+
messaging: {
|
|
25
|
+
getter: 'getMessaging',
|
|
26
|
+
module: 'firebase-admin/messaging',
|
|
27
|
+
statics: [],
|
|
28
|
+
// Legacy FCM types removed in firebase-admin-node#3157.
|
|
29
|
+
removedTypes: [
|
|
30
|
+
'MessagingPayload',
|
|
31
|
+
'MessagingOptions',
|
|
32
|
+
'DataMessagePayload',
|
|
33
|
+
'NotificationMessagePayload',
|
|
34
|
+
'MessagingDevicesResponse',
|
|
35
|
+
'MessagingDeviceResult',
|
|
36
|
+
'MessagingDeviceGroupResponse',
|
|
37
|
+
'MessagingTopicResponse',
|
|
38
|
+
'MessagingConditionResponse',
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
storage: { getter: 'getStorage', module: 'firebase-admin/storage', statics: [] },
|
|
42
|
+
database: {
|
|
43
|
+
getter: 'getDatabase',
|
|
44
|
+
module: 'firebase-admin/database',
|
|
45
|
+
statics: ['ServerValue', 'enableLogging'],
|
|
46
|
+
},
|
|
47
|
+
appCheck: { getter: 'getAppCheck', module: 'firebase-admin/app-check', statics: [] },
|
|
48
|
+
remoteConfig: { getter: 'getRemoteConfig', module: 'firebase-admin/remote-config', statics: [] },
|
|
49
|
+
securityRules: { getter: 'getSecurityRules', module: 'firebase-admin/security-rules', statics: [] },
|
|
50
|
+
projectManagement: {
|
|
51
|
+
getter: 'getProjectManagement',
|
|
52
|
+
module: 'firebase-admin/project-management',
|
|
53
|
+
statics: [],
|
|
54
|
+
},
|
|
55
|
+
machineLearning: {
|
|
56
|
+
getter: 'getMachineLearning',
|
|
57
|
+
module: 'firebase-admin/machine-learning',
|
|
58
|
+
statics: [],
|
|
59
|
+
},
|
|
60
|
+
installations: { getter: 'getInstallations', module: 'firebase-admin/installations', statics: [] },
|
|
61
|
+
functions: { getter: 'getFunctions', module: 'firebase-admin/functions', statics: [] },
|
|
62
|
+
eventarc: { getter: 'getEventarc', module: 'firebase-admin/eventarc', statics: [] },
|
|
63
|
+
};
|
|
64
|
+
export const APP_MODULE = 'firebase-admin/app';
|
|
65
|
+
/** `admin.credential.<name>()` → `<name>()` from firebase-admin/app. */
|
|
66
|
+
export const CREDENTIAL_FACTORIES = ['cert', 'applicationDefault', 'refreshToken'];
|
|
67
|
+
/** Types still exported by the `firebase-admin` entry point in v14. */
|
|
68
|
+
export const ROOT_TYPES = [
|
|
69
|
+
'App',
|
|
70
|
+
'AppOptions',
|
|
71
|
+
'Credential',
|
|
72
|
+
'ServiceAccount',
|
|
73
|
+
'GoogleOAuthAccessToken',
|
|
74
|
+
'FirebaseError',
|
|
75
|
+
'FirebaseArrayIndexError',
|
|
76
|
+
'ErrorInfo',
|
|
77
|
+
];
|
|
78
|
+
export const REMOVED_SERVICES = {
|
|
79
|
+
instanceId: 'Instance ID was removed in v14: migrate to getInstallations() from firebase-admin/installations.',
|
|
80
|
+
};
|