@ethlete/query 6.0.0-next.54 → 6.0.0-next.56

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.
@@ -29,6 +29,16 @@
29
29
  "factory": "./migrate-from-ngrx-toolkit/migration",
30
30
  "schema": "./migrate-from-ngrx-toolkit/schema.json",
31
31
  "description": "Move @tomtomb/ngrx-toolkit features onto v3 query creators and the @ethlete/query/ngrx-toolkit interop"
32
+ },
33
+ "report-legacy-query-apis": {
34
+ "factory": "./report-legacy-query-apis/migration",
35
+ "schema": "./report-legacy-query-apis/schema.json",
36
+ "description": "Write a task to query-v3-migration-tasks.md for every legacy query collection, infinity query, *etQuery template and entity store"
37
+ },
38
+ "move-devtools-contract-imports": {
39
+ "factory": "./move-devtools-contract-imports/migration",
40
+ "schema": "./move-devtools-contract-imports/schema.json",
41
+ "description": "Import the devtools contract from @ethlete/query/devtools-contract instead of @ethlete/query"
32
42
  }
33
43
  }
34
44
  }
@@ -47,14 +47,16 @@ const collectClientEdits = (filePath, sourceFile, report) => {
47
47
  const persistenceProperty = named('persistence');
48
48
  const featuresProperty = named('features');
49
49
  if (featuresProperty) {
50
- report.addManualReview({
51
- title: 'createQueryClient already has a features array',
52
- summary: 'The client was left untouched so an existing `features` array is not overwritten.',
53
- action: 'Fold the removed `multiTabSync` / `persistence` options into the existing `features` array.',
54
- locations: [{ filePath, line }],
55
- source: SOURCE,
56
- dedupeKey: 'existing-features',
57
- });
50
+ if (syncProperty || persistenceProperty) {
51
+ report.addManualReview({
52
+ title: 'createQueryClient already has a features array',
53
+ summary: 'The client was left untouched so an existing `features` array is not overwritten.',
54
+ action: 'Fold the removed `multiTabSync` / `persistence` options into the existing `features` array.',
55
+ locations: [{ filePath, line }],
56
+ source: SOURCE,
57
+ dedupeKey: 'existing-features',
58
+ });
59
+ }
58
60
  }
59
61
  else {
60
62
  for (const property of [syncProperty, persistenceProperty]) {
@@ -350,7 +350,7 @@ const analyzeLegacyQueryCreators = (content, queryClientFiles, filePath, report)
350
350
  report.addFollowUp({
351
351
  title: `Check the entity config carried onto ${toLegacyName(creatorName)}`,
352
352
  summary: `${creatorName} had an \`entity\` config. It was moved onto the \`${toLegacyName(creatorName)}\` interop wrapper, which still writes to the EntityStore; the v3 creator \`${creatorName}\` does not, and v3 has no EntityStore.`,
353
- action: 'Keep the call sites that rely on the store on the legacy wrapper. `set` now only runs on a real response, never on `prepare()` or a failure.',
353
+ action: 'Keep the call sites that rely on the store on the legacy wrapper. `set` now only runs on a real response, never on `prepare()` or a failure. To leave the store, declare `invalidates` on the mutation creators that change the entity and `tags` on the reads that show it, and add `withOptimisticUpdate` where the UI must change before the response - see https://ethlete-sdk-docs.web.app/query/migrating-from-v2#entitystore-becomes-invalidates-and-tags.',
354
354
  locations: [
355
355
  { filePath, line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1 },
356
356
  ],
@@ -109,7 +109,7 @@ export const renderMigrationReportMarkdown = (tasks, scopeLabel) => {
109
109
  '- `Source`: which migration phase raised the task.',
110
110
  '',
111
111
  ].join('\n');
112
- return `${header}${renderTasksSection(tasks, scopeLabel)}\n`;
112
+ return `${header}\n${renderTasksSection(tasks, scopeLabel)}\n`;
113
113
  };
114
114
  const renderTasksSection = (tasks, scopeLabel) => {
115
115
  const heading = scopeLabel ? `## Tasks (scope: ${scopeLabel})` : '## Tasks';
@@ -0,0 +1,155 @@
1
+ import { formatFiles } from '@nx/devkit';
2
+ import * as ts from 'typescript';
3
+ import { createMigrationScope } from '../migrate-to-query-v3/migration-scope.js';
4
+ import { createSourceFile } from '../migrate-to-query-v3/shared.js';
5
+ import { MOVED_DEVTOOLS_CONTRACT_NAMES } from './moved-names.js';
6
+ const QUERY_MODULE = '@ethlete/query';
7
+ const CONTRACT_MODULE = '@ethlete/query/devtools-contract';
8
+ const isMigratableFile = (filePath) => /\.(ts|mts|cts|tsx)$/.test(filePath) && !filePath.endsWith('.d.ts');
9
+ const specifierName = (specifier) => (specifier.propertyName ?? specifier.name).text;
10
+ const isMoved = (specifier) => MOVED_DEVTOOLS_CONTRACT_NAMES.has(specifierName(specifier));
11
+ const moduleOf = (statement) => {
12
+ if (!ts.isImportDeclaration(statement) && !ts.isExportDeclaration(statement))
13
+ return undefined;
14
+ const specifier = statement.moduleSpecifier;
15
+ return specifier && ts.isStringLiteral(specifier) ? specifier : undefined;
16
+ };
17
+ const namedBindingsOf = (statement) => {
18
+ if (ts.isExportDeclaration(statement)) {
19
+ return statement.exportClause && ts.isNamedExports(statement.exportClause) ? statement.exportClause : undefined;
20
+ }
21
+ const bindings = statement.importClause?.namedBindings;
22
+ return bindings && ts.isNamedImports(bindings) ? bindings : undefined;
23
+ };
24
+ const isTypeOnlyDeclaration = (statement) => ts.isExportDeclaration(statement) ? statement.isTypeOnly : !!statement.importClause?.isTypeOnly;
25
+ const declarationKey = (statement) => `${ts.isImportDeclaration(statement) ? 'import' : 'export'}${isTypeOnlyDeclaration(statement) ? ' type' : ''}`;
26
+ const findNamespaceUses = (sourceFile, content) => {
27
+ const uses = [];
28
+ for (const statement of sourceFile.statements) {
29
+ if (!ts.isImportDeclaration(statement) || moduleOf(statement)?.text !== QUERY_MODULE)
30
+ continue;
31
+ const bindings = statement.importClause?.namedBindings;
32
+ if (!bindings || !ts.isNamespaceImport(bindings))
33
+ continue;
34
+ for (const name of MOVED_DEVTOOLS_CONTRACT_NAMES) {
35
+ if (content.includes(`${bindings.name.text}.${name}`))
36
+ uses.push(`${bindings.name.text}.${name}`);
37
+ }
38
+ }
39
+ return uses;
40
+ };
41
+ export const moveDevtoolsContractImportsInFile = (content, filePath) => {
42
+ if (!content.includes(QUERY_MODULE))
43
+ return null;
44
+ const sourceFile = createSourceFile(content, filePath);
45
+ const edits = [];
46
+ const contractTargets = new Map();
47
+ for (const statement of sourceFile.statements) {
48
+ if (!ts.isImportDeclaration(statement) && !ts.isExportDeclaration(statement))
49
+ continue;
50
+ if (moduleOf(statement)?.text !== CONTRACT_MODULE)
51
+ continue;
52
+ const bindings = namedBindingsOf(statement);
53
+ const key = declarationKey(statement);
54
+ if (bindings && !contractTargets.has(key))
55
+ contractTargets.set(key, { bindings, added: [] });
56
+ }
57
+ for (const statement of sourceFile.statements) {
58
+ if (!ts.isImportDeclaration(statement) && !ts.isExportDeclaration(statement))
59
+ continue;
60
+ const moduleSpecifier = moduleOf(statement);
61
+ if (moduleSpecifier?.text !== QUERY_MODULE)
62
+ continue;
63
+ const bindings = namedBindingsOf(statement);
64
+ if (!bindings)
65
+ continue;
66
+ const elements = bindings.elements;
67
+ const moved = elements.filter(isMoved);
68
+ if (moved.length === 0)
69
+ continue;
70
+ const kept = elements.filter((specifier) => !isMoved(specifier));
71
+ const movedText = moved.map((specifier) => specifier.getText(sourceFile));
72
+ const hasDefaultImport = ts.isImportDeclaration(statement) && !!statement.importClause?.name;
73
+ const target = contractTargets.get(declarationKey(statement));
74
+ if (target) {
75
+ target.added.push(...movedText);
76
+ }
77
+ if (kept.length === 0 && !hasDefaultImport) {
78
+ if (target) {
79
+ edits.push({ start: statement.getFullStart(), end: statement.getEnd(), text: '' });
80
+ }
81
+ else {
82
+ const quote = moduleSpecifier.getText(sourceFile)[0];
83
+ edits.push({
84
+ start: moduleSpecifier.getStart(sourceFile),
85
+ end: moduleSpecifier.getEnd(),
86
+ text: `${quote}${CONTRACT_MODULE}${quote}`,
87
+ });
88
+ }
89
+ continue;
90
+ }
91
+ edits.push({
92
+ start: bindings.getStart(sourceFile),
93
+ end: bindings.getEnd(),
94
+ text: kept.length ? `{ ${kept.map((specifier) => specifier.getText(sourceFile)).join(', ')} }` : '{}',
95
+ });
96
+ if (!target) {
97
+ const quote = moduleSpecifier.getText(sourceFile)[0];
98
+ edits.push({
99
+ start: statement.getEnd(),
100
+ end: statement.getEnd(),
101
+ text: `\n${declarationKey(statement)} { ${movedText.join(', ')} } from ${quote}${CONTRACT_MODULE}${quote};`,
102
+ });
103
+ }
104
+ }
105
+ for (const { bindings, added } of contractTargets.values()) {
106
+ if (added.length === 0)
107
+ continue;
108
+ const last = bindings.elements.at(-1);
109
+ const position = last ? last.getEnd() : bindings.getStart(sourceFile) + 1;
110
+ edits.push({ start: position, end: position, text: last ? `, ${added.join(', ')}` : ` ${added.join(', ')} ` });
111
+ }
112
+ if (edits.length === 0)
113
+ return null;
114
+ return edits
115
+ .sort((a, b) => b.start - a.start)
116
+ .reduce((text, edit) => text.slice(0, edit.start) + edit.text + text.slice(edit.end), content);
117
+ };
118
+ export const moveDevtoolsContractImports = (tree, scope) => {
119
+ const touchedFiles = [];
120
+ const manualUses = [];
121
+ scope.visit(tree, (filePath) => {
122
+ if (!isMigratableFile(filePath))
123
+ return;
124
+ const content = tree.read(filePath, 'utf-8');
125
+ if (!content?.includes(QUERY_MODULE))
126
+ return;
127
+ for (const use of findNamespaceUses(createSourceFile(content, filePath), content)) {
128
+ manualUses.push(`${filePath}: ${use}`);
129
+ }
130
+ const next = moveDevtoolsContractImportsInFile(content, filePath);
131
+ if (next === null)
132
+ return;
133
+ tree.write(filePath, next);
134
+ touchedFiles.push(filePath);
135
+ });
136
+ return { touchedFiles, manualUses };
137
+ };
138
+ export default async function migrate(tree, schema) {
139
+ console.log(`\nšŸ”„ Moving devtools contract imports to ${CONTRACT_MODULE}...`);
140
+ const scope = createMigrationScope(tree, schema);
141
+ console.log(` Scope: ${scope.describe()}`);
142
+ const { touchedFiles, manualUses } = moveDevtoolsContractImports(tree, scope);
143
+ console.log(` Rewrote imports in ${touchedFiles.length} file${touchedFiles.length === 1 ? '' : 's'}.`);
144
+ if (manualUses.length > 0) {
145
+ console.log(`\nāš ļø Namespace imports of ${QUERY_MODULE} use names that moved to ${CONTRACT_MODULE}:`);
146
+ for (const use of manualUses) {
147
+ console.log(` ${use}`);
148
+ }
149
+ }
150
+ if (!schema.skipFormat) {
151
+ await formatFiles(tree);
152
+ }
153
+ console.log('\nāœ… Devtools contract imports moved.');
154
+ }
155
+ //# sourceMappingURL=migration.js.map
@@ -0,0 +1,153 @@
1
+ export const MOVED_DEVTOOLS_CONTRACT_NAMES = new Set([
2
+ 'addQueryDevtoolsAuthAccount',
3
+ 'applyQueryDevtoolsOverrides',
4
+ 'applyQueryDevtoolsTokenTtl',
5
+ 'armAllQueryDevtoolsMocks',
6
+ 'armQueryDevtoolsMock',
7
+ 'armQueryDevtoolsOverrideTransfer',
8
+ 'canOverrideQueryDevtoolsTokenTtl',
9
+ 'clearQueryDevtoolsArmedMocks',
10
+ 'clearQueryDevtoolsAuthCredentials',
11
+ 'clearQueryDevtoolsAuthSessions',
12
+ 'clearQueryDevtoolsFaults',
13
+ 'clearQueryDevtoolsMockStore',
14
+ 'clearQueryDevtoolsOverrideStore',
15
+ 'clearQueryDevtoolsStore',
16
+ 'clearQueryDevtoolsTombstones',
17
+ 'clearRestoredQueryDevtoolsOverrides',
18
+ 'collectLeafPaths',
19
+ 'collectQueryDevtoolsSchemaComponents',
20
+ 'countUnresolvedQueryDevtoolsOverrides',
21
+ 'createQueryDevtoolsOverrides',
22
+ 'deleteQueryDevtoolsMock',
23
+ 'DescribableFeature',
24
+ 'detectPaginationShape',
25
+ 'EMPTY_QUERY_DEVTOOLS_FAULT',
26
+ 'forgetQueryDevtoolsAuthSession',
27
+ 'forgetQueryDevtoolsAuthSessionsFor',
28
+ 'generateQueryDevtoolsNumberPreset',
29
+ 'generateQueryDevtoolsSampleNumber',
30
+ 'generateQueryDevtoolsStringPreset',
31
+ 'hasQueryDevtoolsOverridesAtPath',
32
+ 'isDateShapedLeaf',
33
+ 'isQueryDevtoolsEnabled',
34
+ 'isQueryDevtoolsFaultArmed',
35
+ 'isQueryDevtoolsRepositoryLive',
36
+ 'JsonPath',
37
+ 'loadQueryDevtoolsSchema',
38
+ 'loginQueryDevtoolsAuthAccount',
39
+ 'logoutQueryDevtoolsAuthSession',
40
+ 'matchesQueryDevtoolsMockPattern',
41
+ 'matchesQueryDevtoolsMockQuery',
42
+ 'MAX_QUERY_BATCH_TOMBSTONE_BUCKETS',
43
+ 'MAX_QUERY_BATCH_TOMBSTONES',
44
+ 'MAX_QUERY_DEVTOOLS_TOMBSTONES',
45
+ 'measureQueryDevtoolsPayload',
46
+ 'OverrideOp',
47
+ 'parseQueryDevtoolsOverrideTransfer',
48
+ 'QUERY_DEVTOOLS_FAULT_STATUSES',
49
+ 'QUERY_DEVTOOLS_TOKEN_TTL_LIMIT',
50
+ 'queryDevtoolsAbout',
51
+ 'QueryDevtoolsAbout',
52
+ 'queryDevtoolsAllowsLocalAuthSessions',
53
+ 'queryDevtoolsApiEnvIds',
54
+ 'queryDevtoolsApiEnvIsProduction',
55
+ 'queryDevtoolsApiEnvs',
56
+ 'queryDevtoolsApiEnvScope',
57
+ 'queryDevtoolsApiEnvValues',
58
+ 'queryDevtoolsArmedMocks',
59
+ 'queryDevtoolsArmedMocksRestored',
60
+ 'queryDevtoolsAuthAccountsFor',
61
+ 'QueryDevtoolsAuthAccountView',
62
+ 'queryDevtoolsAuthActive',
63
+ 'queryDevtoolsAuthFieldsFor',
64
+ 'QueryDevtoolsAuthLocalAccount',
65
+ 'queryDevtoolsAuthOtherScopeCount',
66
+ 'QueryDevtoolsAuthProviderHandle',
67
+ 'QueryDevtoolsAuthProviderRegistration',
68
+ 'QueryDevtoolsAuthQuery',
69
+ 'QueryDevtoolsAuthSeed',
70
+ 'QueryDevtoolsAuthSession',
71
+ 'queryDevtoolsAuthSessions',
72
+ 'queryDevtoolsAuthSessionsFor',
73
+ 'queryDevtoolsAuthTabLocal',
74
+ 'queryDevtoolsEntries',
75
+ 'QueryDevtoolsEntry',
76
+ 'QueryDevtoolsEntryKind',
77
+ 'QueryDevtoolsEntryMeta',
78
+ 'QueryDevtoolsFault',
79
+ 'queryDevtoolsFaults',
80
+ 'queryDevtoolsFaultsRestored',
81
+ 'QueryDevtoolsFaultTarget',
82
+ 'QueryDevtoolsFeature',
83
+ 'QueryDevtoolsFormField',
84
+ 'QueryDevtoolsFormHandle',
85
+ 'QueryDevtoolsFormLinksHandle',
86
+ 'QueryDevtoolsFormLinksRecorder',
87
+ 'QueryDevtoolsMock',
88
+ 'queryDevtoolsMockId',
89
+ 'queryDevtoolsMocks',
90
+ 'QueryDevtoolsMockTarget',
91
+ 'QueryDevtoolsOverrideEntry',
92
+ 'queryDevtoolsOverridePersistence',
93
+ 'QueryDevtoolsOverridesHandle',
94
+ 'QueryDevtoolsOverridesRecorder',
95
+ 'QueryDevtoolsOverrideTransfer',
96
+ 'QueryDevtoolsOverrideTransferParse',
97
+ 'QueryDevtoolsPayload',
98
+ 'QueryDevtoolsRegistrar',
99
+ 'QueryDevtoolsRegistration',
100
+ 'queryDevtoolsRequestPath',
101
+ 'QueryDevtoolsResolvedFault',
102
+ 'QueryDevtoolsResolvedMock',
103
+ 'queryDevtoolsResponseHistory',
104
+ 'queryDevtoolsRestoredOverridesScope',
105
+ 'QueryDevtoolsRoutePart',
106
+ 'QueryDevtoolsRun',
107
+ 'QueryDevtoolsRunError',
108
+ 'QueryDevtoolsRunStatus',
109
+ 'QueryDevtoolsSchemaComponents',
110
+ 'queryDevtoolsSchemaNames',
111
+ 'QueryDevtoolsSchemaRoute',
112
+ 'queryDevtoolsSchemaRoutes',
113
+ 'QueryDevtoolsSchemaSeed',
114
+ 'queryDevtoolsSchemaState',
115
+ 'QueryDevtoolsSchemaState',
116
+ 'QueryDevtoolsSeedStyle',
117
+ 'queryDevtoolsSettings',
118
+ 'QueryDevtoolsSettings',
119
+ 'QueryDevtoolsStats',
120
+ 'QueryDevtoolsStatsHandle',
121
+ 'QueryDevtoolsStatsRecorder',
122
+ 'queryDevtoolsStorage',
123
+ 'QueryDevtoolsStorageScope',
124
+ 'queryDevtoolsTokenTtls',
125
+ 'readQueryDevtoolsStore',
126
+ 'registerEthleteVersion',
127
+ 'removeQueryDevtoolsAuthAccount',
128
+ 'renameQueryDevtoolsAuthSession',
129
+ 'restoredQueryDevtoolsOverrides',
130
+ 'RestoredQueryDevtoolsOverrides',
131
+ 'saveQueryDevtoolsMock',
132
+ 'seedQueryDevtoolsSchemaBody',
133
+ 'seedQueryDevtoolsSchemaRoute',
134
+ 'serializeQueryDevtoolsOverrideTransfer',
135
+ 'setQueryDevtoolsApiEnv',
136
+ 'setQueryDevtoolsAppInfo',
137
+ 'setQueryDevtoolsArmedMocksScope',
138
+ 'setQueryDevtoolsAuthCredentials',
139
+ 'setQueryDevtoolsAuthTabLocal',
140
+ 'setQueryDevtoolsFault',
141
+ 'setQueryDevtoolsFaultsScope',
142
+ 'setQueryDevtoolsOverridePersistence',
143
+ 'setQueryDevtoolsOverridesScope',
144
+ 'setQueryDevtoolsSettings',
145
+ 'smartDuplicateArrayItem',
146
+ 'snapshotQueryDevtoolsHandle',
147
+ 'stringifyQueryRouteParts',
148
+ 'sumQueryDevtoolsStats',
149
+ 'switchQueryDevtoolsAuthSession',
150
+ 'tombstoneOf',
151
+ 'writeQueryDevtoolsStore',
152
+ ]);
153
+ //# sourceMappingURL=moved-names.js.map
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "$id": "move-devtools-contract-imports",
4
+ "title": "Move devtools contract imports to @ethlete/query/devtools-contract",
5
+ "type": "object",
6
+ "properties": {
7
+ "projects": {
8
+ "type": "array",
9
+ "items": { "type": "string" },
10
+ "description": "Nx projects to migrate. Defaults to every project in the workspace.",
11
+ "alias": "project"
12
+ },
13
+ "include": {
14
+ "type": "array",
15
+ "items": { "type": "string" },
16
+ "description": "Path prefixes to migrate, relative to the workspace root (e.g. 'libs/queries'). Unioned with `projects`. Defaults to the whole workspace.",
17
+ "alias": "includePath"
18
+ },
19
+ "skipFormat": {
20
+ "type": "boolean",
21
+ "description": "Skip formatting files after migration",
22
+ "default": false
23
+ }
24
+ },
25
+ "required": []
26
+ }
@@ -0,0 +1,256 @@
1
+ import { joinPathFragments } from '@nx/devkit';
2
+ import { writeFileSync } from 'fs';
3
+ import { dirname } from 'path';
4
+ import * as ts from 'typescript';
5
+ import { createMigrationScope } from '../migrate-to-query-v3/migration-scope.js';
6
+ import { QUERY_V3_MIGRATION_REPORT_PATH, QueryV3MigrationReport, } from '../migrate-to-query-v3/report.js';
7
+ import { createSourceFile, getLineNumber, getLineNumberFromPosition } from '../migrate-to-query-v3/shared.js';
8
+ const SOURCE = 'report-legacy-query-apis';
9
+ const SCAN_FILE_ENV = 'ETHLETE_SCAN_FILE';
10
+ const QUERY_PACKAGE = '@ethlete/query';
11
+ const DOCS_URL = 'https://ethlete-sdk-docs.web.app';
12
+ const GROUPS_DOCS = `${DOCS_URL}/query/groups`;
13
+ const COLLECTIONS_GUIDE = `${DOCS_URL}/query/migrating-from-v2#query-collections`;
14
+ const INFINITY_GUIDE = `${DOCS_URL}/query/migrating-from-v2#infinity-queries`;
15
+ const TRIGGER_DOCS = `${DOCS_URL}/components/paged-query-trigger`;
16
+ const ENTITY_GUIDE = `${DOCS_URL}/query/migrating-from-v2#entitystore-becomes-invalidates-and-tags`;
17
+ const CACHING_DOCS = `${DOCS_URL}/query/caching`;
18
+ const TEMPLATES_GUIDE = `${DOCS_URL}/query/migrating-from-v2#templates-read-signals-not-directives`;
19
+ const COLLECTION_FNS = ['createQueryCollection', 'createQueryCollectionSignal', 'createQueryCollectionSubject'];
20
+ const COLLECTION_STATE_FN = 'switchQueryCollectionState';
21
+ const INFINITY_CONFIG_FN = 'createInfinityQueryConfig';
22
+ const ENTITY_STORE_CLASS = 'EntityStore';
23
+ const LEGACY_CREATOR_FN = 'createLegacyQueryCreator';
24
+ const INFINITY_DIRECTIVE = 'InfinityQueryDirective';
25
+ const INFINITY_TRIGGER_DIRECTIVE = 'InfinityQueryTriggerDirective';
26
+ const QUERY_DIRECTIVE = 'QueryDirective';
27
+ const QUERY_DIRECTIVE_PATTERN = /\*etQuery(?![\w-])|\[etQuery\](?=\s*=)/g;
28
+ const INFINITY_DIRECTIVE_PATTERN = /\*etInfinityQuery(?![\w-])/g;
29
+ const INFINITY_TRIGGER_PATTERN = /(?<![\w-])etInfinityQueryTrigger(?![\w-])|<et-infinity-query-trigger(?![\w-])/g;
30
+ const ENTITY_ACTION = `Move every write into the store to \`invalidates\` on the mutation creator (${CACHING_DOCS}#invalidating-from-the-mutation) ` +
31
+ `and \`tags\` on the reads it changes (${CACHING_DOCS}#tags), and add \`withOptimisticUpdate\` where the UI must change ` +
32
+ `before the response (${CACHING_DOCS}#optimistic-updates) - see ${ENTITY_GUIDE}.`;
33
+ const isScannableFile = (filePath) => /\.(ts|mts|cts)$/.test(filePath) && !filePath.endsWith('.d.ts');
34
+ const collectQueryImports = (sourceFile) => {
35
+ const named = new Map();
36
+ const namespaces = new Set();
37
+ for (const statement of sourceFile.statements) {
38
+ if (!ts.isImportDeclaration(statement) ||
39
+ !ts.isStringLiteral(statement.moduleSpecifier) ||
40
+ statement.moduleSpecifier.text !== QUERY_PACKAGE) {
41
+ continue;
42
+ }
43
+ const bindings = statement.importClause?.namedBindings;
44
+ if (!bindings)
45
+ continue;
46
+ if (ts.isNamespaceImport(bindings)) {
47
+ namespaces.add(bindings.name.text);
48
+ continue;
49
+ }
50
+ for (const element of bindings.elements) {
51
+ named.set(element.name.text, (element.propertyName ?? element.name).text);
52
+ }
53
+ }
54
+ return { named, namespaces };
55
+ };
56
+ const importedNameOf = (expression, imports) => {
57
+ if (ts.isIdentifier(expression))
58
+ return imports.named.get(expression.text);
59
+ if (ts.isPropertyAccessExpression(expression) &&
60
+ ts.isIdentifier(expression.expression) &&
61
+ imports.namespaces.has(expression.expression.text)) {
62
+ return expression.name.text;
63
+ }
64
+ return undefined;
65
+ };
66
+ const hasImported = (imports, exportName) => imports.namespaces.size > 0 || [...imports.named.values()].includes(exportName);
67
+ const declaredNameOf = (node) => {
68
+ const parent = node.parent;
69
+ if ((ts.isVariableDeclaration(parent) || ts.isPropertyDeclaration(parent) || ts.isPropertyAssignment(parent)) &&
70
+ parent.initializer === node &&
71
+ (ts.isIdentifier(parent.name) || ts.isPrivateIdentifier(parent.name))) {
72
+ return parent.name.text;
73
+ }
74
+ return undefined;
75
+ };
76
+ const propertyNamed = (literal, name) => literal.properties.find((property) => (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) &&
77
+ ts.isIdentifier(property.name) &&
78
+ property.name.text === name);
79
+ const isEntityConfigCall = (call, config, imports) => importedNameOf(call.expression, imports) === LEGACY_CREATOR_FN ||
80
+ (ts.isPropertyAccessExpression(call.expression) && propertyNamed(config, 'route') !== undefined);
81
+ const scanSource = (options) => {
82
+ const { tree, filePath, sourceFile, imports, report } = options;
83
+ const at = (node) => [{ filePath, line: getLineNumber(node, sourceFile) }];
84
+ const keyOf = (kind, node) => `${SOURCE}:${kind}:${filePath}:${node.getStart(sourceFile)}`;
85
+ const visit = (node) => {
86
+ if (ts.isCallExpression(node)) {
87
+ const importedName = importedNameOf(node.expression, imports);
88
+ const name = declaredNameOf(node);
89
+ if (importedName && COLLECTION_FNS.includes(importedName)) {
90
+ report.addFollowUp({
91
+ title: `Replace the query collection ${name ?? importedName}`,
92
+ summary: `${name ? `\`${name}\` is built with` : 'This call is'} \`${importedName}\`, a legacy query collection. v3 has no query collections.`,
93
+ action: `Move an action set where the latest run wins to \`createQueryGroup\` (${GROUPS_DOCS}), a query picked by context to a \`computed\`, and an auth collection to \`createBearerAuthProvider\` - see ${COLLECTIONS_GUIDE}.`,
94
+ locations: at(node),
95
+ source: SOURCE,
96
+ dedupeKey: keyOf('collection', node),
97
+ });
98
+ }
99
+ if (importedName === COLLECTION_STATE_FN) {
100
+ report.addFollowUp({
101
+ title: `Replace switchQueryCollectionState()`,
102
+ summary: `\`switchQueryCollectionState()\` reads the state of the query collection member that ran last.`,
103
+ action: `Read \`succeeded$\`, \`loading()\` or \`error()\` of the query group that replaces the collection (${GROUPS_DOCS}) - see ${COLLECTIONS_GUIDE}.`,
104
+ locations: at(node),
105
+ source: SOURCE,
106
+ dedupeKey: keyOf('collection-state', node),
107
+ });
108
+ }
109
+ if (importedName === INFINITY_CONFIG_FN) {
110
+ report.addFollowUp({
111
+ title: `Replace the infinity query ${name ?? importedName}`,
112
+ summary: `${name ? `\`${name}\` is built with` : 'This call is'} \`createInfinityQueryConfig\`, the legacy infinite query.`,
113
+ action: `Move it to \`createPagedQueryStack\` and bind \`[etPagedQueryTrigger]\` to the stack (${TRIGGER_DOCS}) - see ${INFINITY_GUIDE}.`,
114
+ locations: at(node),
115
+ source: SOURCE,
116
+ dedupeKey: keyOf('infinity-config', node),
117
+ });
118
+ }
119
+ const [config] = node.arguments;
120
+ if (config && ts.isObjectLiteralExpression(config) && isEntityConfigCall(node, config, imports)) {
121
+ const entity = propertyNamed(config, 'entity');
122
+ if (entity) {
123
+ report.addFollowUp({
124
+ title: `Replace the entity config of ${name ?? 'the query creator'}`,
125
+ summary: `${name ? `\`${name}\` has` : 'This query creator has'} an \`entity\` config that writes its response into a legacy EntityStore. v3 has no entity store.`,
126
+ action: ENTITY_ACTION,
127
+ locations: at(entity),
128
+ source: SOURCE,
129
+ dedupeKey: keyOf('entity-config', entity),
130
+ });
131
+ }
132
+ }
133
+ }
134
+ if (ts.isNewExpression(node) && importedNameOf(node.expression, imports) === ENTITY_STORE_CLASS) {
135
+ const name = declaredNameOf(node);
136
+ report.addFollowUp({
137
+ title: `Replace the entity store ${name ?? ENTITY_STORE_CLASS}`,
138
+ summary: `${name ? `\`${name}\` is` : 'This is'} a legacy \`EntityStore\`. v3 has no entity store.`,
139
+ action: ENTITY_ACTION,
140
+ locations: at(node),
141
+ source: SOURCE,
142
+ dedupeKey: keyOf('entity-store', node),
143
+ });
144
+ }
145
+ if (ts.isDecorator(node)) {
146
+ scanComponentTemplate({ tree, filePath, sourceFile, decorator: node, imports, report });
147
+ }
148
+ ts.forEachChild(node, visit);
149
+ };
150
+ visit(sourceFile);
151
+ };
152
+ const scanComponentTemplate = (options) => {
153
+ const { tree, filePath, sourceFile, decorator, imports, report } = options;
154
+ const usesDirective = hasImported(imports, INFINITY_DIRECTIVE);
155
+ const usesTrigger = hasImported(imports, INFINITY_TRIGGER_DIRECTIVE);
156
+ const usesQueryDirective = hasImported(imports, QUERY_DIRECTIVE);
157
+ if (!usesDirective && !usesTrigger && !usesQueryDirective)
158
+ return;
159
+ const call = decorator.expression;
160
+ if (!ts.isCallExpression(call) || !ts.isIdentifier(call.expression) || call.expression.text !== 'Component')
161
+ return;
162
+ const [metadata] = call.arguments;
163
+ if (!metadata || !ts.isObjectLiteralExpression(metadata))
164
+ return;
165
+ const template = propertyNamed(metadata, 'template');
166
+ const templateUrl = propertyNamed(metadata, 'templateUrl');
167
+ let scanned;
168
+ if (template &&
169
+ ts.isPropertyAssignment(template) &&
170
+ (ts.isStringLiteral(template.initializer) || ts.isNoSubstitutionTemplateLiteral(template.initializer))) {
171
+ scanned = { text: template.initializer.text, filePath, firstLine: getLineNumber(template.initializer, sourceFile) };
172
+ }
173
+ if (templateUrl &&
174
+ ts.isPropertyAssignment(templateUrl) &&
175
+ (ts.isStringLiteral(templateUrl.initializer) || ts.isNoSubstitutionTemplateLiteral(templateUrl.initializer))) {
176
+ const templatePath = joinPathFragments(dirname(filePath), templateUrl.initializer.text);
177
+ const text = tree.read(templatePath, 'utf-8');
178
+ if (text !== null)
179
+ scanned = { text, filePath: templatePath, firstLine: 1 };
180
+ }
181
+ if (!scanned)
182
+ return;
183
+ const { text, firstLine } = scanned;
184
+ const templatePath = scanned.filePath;
185
+ const matches = (pattern) => Array.from(text.matchAll(pattern), (match) => ({
186
+ line: firstLine + getLineNumberFromPosition(text, match.index) - 1,
187
+ }));
188
+ if (usesDirective) {
189
+ for (const { line } of matches(INFINITY_DIRECTIVE_PATTERN)) {
190
+ report.addFollowUp({
191
+ title: 'Replace *etInfinityQuery',
192
+ summary: 'The template renders a legacy infinite query through `*etInfinityQuery`.',
193
+ action: `Render the items of the paged query stack that replaces the config with \`@for (item of stack.items(); track item.id)\` - see ${INFINITY_GUIDE}.`,
194
+ locations: [{ filePath: templatePath, line }],
195
+ source: SOURCE,
196
+ dedupeKey: `${SOURCE}:infinity-directive:${templatePath}:${line}`,
197
+ });
198
+ }
199
+ }
200
+ if (usesQueryDirective) {
201
+ for (const { line } of matches(QUERY_DIRECTIVE_PATTERN)) {
202
+ report.addFollowUp({
203
+ title: 'Replace *etQuery',
204
+ summary: 'The template renders a query through `*etQuery`, which accepts only legacy queries. Moving its creator off the interop breaks this template.',
205
+ action: `Read the query's \`loading()\`, \`error()\` and \`response()\` signals in \`@if\` / \`@switch\` blocks - see ${TEMPLATES_GUIDE}.`,
206
+ locations: [{ filePath: templatePath, line }],
207
+ source: SOURCE,
208
+ dedupeKey: `${SOURCE}:query-directive:${templatePath}:${line}`,
209
+ });
210
+ }
211
+ }
212
+ if (usesTrigger) {
213
+ for (const { line } of matches(INFINITY_TRIGGER_PATTERN)) {
214
+ report.addFollowUp({
215
+ title: 'Replace etInfinityQueryTrigger with etPagedQueryTrigger',
216
+ summary: '`etInfinityQueryTrigger` loads the next page of a legacy infinite query.',
217
+ action: `Bind \`[etPagedQueryTrigger]\` to the paged query stack and drop the \`@if (canLoadMore && !loading)\` around it; a trigger on a button becomes \`(click)="stack.fetchNextPage()"\` with \`[etQueryButton]\` - see ${TRIGGER_DOCS}.`,
218
+ locations: [{ filePath: templatePath, line }],
219
+ source: SOURCE,
220
+ dedupeKey: `${SOURCE}:infinity-trigger:${templatePath}:${line}`,
221
+ });
222
+ }
223
+ }
224
+ };
225
+ export const reportLegacyQueryApis = (tree, scope) => {
226
+ const report = new QueryV3MigrationReport();
227
+ scope.visit(tree, (filePath) => {
228
+ if (!isScannableFile(filePath))
229
+ return;
230
+ const content = tree.read(filePath, 'utf-8');
231
+ if (!content || (!content.includes(QUERY_PACKAGE) && !content.includes('entity')))
232
+ return;
233
+ const sourceFile = createSourceFile(content, filePath);
234
+ scanSource({ tree, filePath, sourceFile, imports: collectQueryImports(sourceFile), report });
235
+ });
236
+ return report;
237
+ };
238
+ export default async function migrate(tree, schema) {
239
+ console.log('\nšŸ”„ Reporting legacy query collections, infinity queries, *etQuery templates and entity stores...');
240
+ const scope = createMigrationScope(tree, schema);
241
+ console.log(` Scope: ${scope.describe()}`);
242
+ const report = reportLegacyQueryApis(tree, scope);
243
+ const scanFile = process.env[SCAN_FILE_ENV];
244
+ if (scanFile) {
245
+ const files = new Set(report.tasks.flatMap((task) => task.locations.map((location) => location.filePath)));
246
+ writeFileSync(scanFile, JSON.stringify([...files].sort()), 'utf8');
247
+ return;
248
+ }
249
+ if (report.tasks.length === 0 && !tree.exists(QUERY_V3_MIGRATION_REPORT_PATH)) {
250
+ console.log(' Nothing found.');
251
+ return;
252
+ }
253
+ report.writeToTree(tree, `${SOURCE} in ${scope.describe()}`);
254
+ report.printSummary();
255
+ }
256
+ //# sourceMappingURL=migration.js.map