diff --git a/package-lock.json b/package-lock.json index b73899e5..21cbadcb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1489,6 +1489,10 @@ "resolved": "recipes/http-classes-with-new", "link": true }, + "node_modules/@nodejs/http2-priority-signaling": { + "resolved": "recipes/http2-priority-signaling", + "link": true + }, "node_modules/@nodejs/import-assertions-to-attributes": { "resolved": "recipes/import-assertions-to-attributes", "link": true @@ -4354,6 +4358,17 @@ "@codemod.com/jssg-types": "^1.0.9" } }, + "recipes/http2-priority-signaling": { + "name": "@nodejs/http2-priority-signaling", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@nodejs/codemod-utils": "*" + }, + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.9" + } + }, "recipes/import-assertions-to-attributes": { "name": "@nodejs/import-assertions-to-attributes", "version": "1.0.0", diff --git a/recipes/http2-priority-signaling/README.md b/recipes/http2-priority-signaling/README.md new file mode 100644 index 00000000..9e0bc5a7 --- /dev/null +++ b/recipes/http2-priority-signaling/README.md @@ -0,0 +1,53 @@ +# HTTP/2 Priority Signaling Removal - DEP0194 + +This recipe removes HTTP/2 priority-related options and methods since priority signaling has been deprecated. + +See [DEP0194](https://nodejs.org/api/deprecations.html#DEP0194). + + +## What this codemod does + +- Removes the `priority` property from `http2.connect()` call options +- Removes the `priority` property from `session.request()` call options +- Removes entire `stream.priority()` method call statements +- Removes the `priority` property from `client.settings()` call options +- Handles both CommonJS (`require()`) and ESM (`import`) imports + +## Examples + +**Before:** + +```js +// CommonJS usage +const http2 = require("node:http2"); +const session = http2.connect("https://example.com", { + priority: { weight: 16, parent: 0, exclusive: false } +}); +const stream = session.request({ + ":path": "/api/data", + priority: { weight: 32 } +}); +stream.priority({ exclusive: true, parent: 0, weight: 128 }); + +// ESM usage +import http2 from "node:http2"; +const client = http2.connect("https://example.com"); +client.settings({ enablePush: false, priority: true }); +``` + +**After:** + +```js +// CommonJS usage +const http2 = require("node:http2"); +const session = http2.connect("https://example.com"); +const stream = session.request({ + ":path": "/api/data" +}); +// stream.priority() removed + +// ESM usage +import http2 from "node:http2"; +const client = http2.connect("https://example.com"); +client.settings({ enablePush: false }); +``` diff --git a/recipes/http2-priority-signaling/codemod.yaml b/recipes/http2-priority-signaling/codemod.yaml new file mode 100644 index 00000000..dd78010e --- /dev/null +++ b/recipes/http2-priority-signaling/codemod.yaml @@ -0,0 +1,23 @@ +schema_version: "1.0" +name: "@nodejs/http2-priority-signaling" +version: 1.0.0 +description: Handle DEP0194 via removing HTTP/2 priority-related options and methods. +author: Augustin Mauroy +license: MIT +workflow: workflow.yaml +category: migration + +targets: + languages: + - javascript + - typescript + +keywords: + - transformation + - migration + - http2 + - deprecation + +registry: + access: public + visibility: public diff --git a/recipes/http2-priority-signaling/package.json b/recipes/http2-priority-signaling/package.json new file mode 100644 index 00000000..477c0091 --- /dev/null +++ b/recipes/http2-priority-signaling/package.json @@ -0,0 +1,24 @@ +{ + "name": "@nodejs/http2-priority-signaling", + "version": "1.0.0", + "description": "Handle DEP0194 via removing HTTP/2 priority-related options and methods", + "type": "module", + "scripts": { + "test": "npx codemod jssg test -l typescript ./src/workflow.ts ./" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nodejs/userland-migrations.git", + "directory": "recipes/http2-priority-signaling", + "bugs": "https://github.com/nodejs/userland-migrations/issues" + }, + "author": "Augustin Mauroy", + "license": "MIT", + "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/http2-priority-signaling/README.md", + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.9" + }, + "dependencies": { + "@nodejs/codemod-utils": "*" + } +} diff --git a/recipes/http2-priority-signaling/src/workflow.ts b/recipes/http2-priority-signaling/src/workflow.ts new file mode 100644 index 00000000..ba870e7c --- /dev/null +++ b/recipes/http2-priority-signaling/src/workflow.ts @@ -0,0 +1,519 @@ +import type { Edit, Range, SgNode, SgRoot } from '@codemod.com/jssg-types/main'; +import type Js from '@codemod.com/jssg-types/langs/javascript'; +import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call'; +import { + getNodeImportStatements, + getNodeImportCalls, +} from '@nodejs/codemod-utils/ast-grep/import-statement'; +import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path'; +import { removeLines } from '@nodejs/codemod-utils/ast-grep/remove-lines'; + +function getLexicalScope(node: SgNode): SgNode { + let current = node.parent(); + let candidate: SgNode | undefined; + while (current) { + const kind = current.kind(); + if (kind === 'block' || kind === 'program') { + candidate = current; + } + if (kind === 'program') break; + current = current.parent(); + } + if (candidate) return candidate; + // Ascend to top-most ancestor (program) + let top: SgNode = node; + while (top.parent()) top = top.parent() as SgNode; + return top; +} + +/* + * Transforms HTTP/2 priority-related options and methods. + * + * Steps: + * + * 1. Find all http2 imports and require calls + * 2. Find and remove priority property from connect() options + * 3. Find and remove priority property from request() options + * 4. Find and remove complete stream.priority() calls + * 5. Find and remove priority property from settings() options + */ +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const edits: Edit[] = []; + const linesToRemove: Range[] = []; + + // Gather all http2 import/require statements/calls + const dynamicHttp2Imports = getNodeImportCalls(root, 'http2'); + const http2Statements = [ + ...getNodeImportStatements(root, 'http2'), + ...getNodeRequireCalls(root, 'http2'), + ...dynamicHttp2Imports, + ]; + + // If any import do nothing + if (!http2Statements.length) return null; + + // Resolve all local callee names for http2.connect (handles namespace, default, named, alias, require/import) + const connectCallees = new Set(); + for (const stmt of http2Statements) { + if (stmt.kind() === 'expression_statement') continue; // skip dynamic imports + + const resolved = resolveBindingPath(stmt, '$.connect'); + if (resolved) connectCallees.add(resolved); + } + + // Discover session variables created via http2.connect or destructured connect calls by + // locating call expressions and climbing to the variable declarator. + const sessionVars: { name: string; decl: SgNode; scope: SgNode }[] = + []; + const connectCalls: SgNode[] = [ + ...rootNode.findAll({ rule: { pattern: '$HTTP2.connect($$$_ARGS)' } }), + // Also include direct calls when `connect` is imported as a named binding or alias (e.g., `connect(...)` or `bar(...)`). + ...Array.from(connectCallees).flatMap((callee) => { + // If callee already includes a dot (e.g., http2.connect), the pattern above already matches it. + if (callee.includes('.')) return [] as SgNode[]; + return rootNode.findAll({ rule: { pattern: `${callee}($$$_ARGS)` } }); + }), + ]; + + for (const call of connectCalls) { + let n: SgNode | undefined = call; + while (n && n.kind() !== 'variable_declarator') { + n = n.parent(); + } + if (!n) continue; + const nameNode = (n as SgNode).field('name'); + if (!nameNode) continue; + sessionVars.push({ + name: nameNode.text(), + decl: n, + scope: getLexicalScope(n), + }); + } + + // Handle dynamic imports of http2 + for (const importNode of dynamicHttp2Imports) { + const binding = importNode.field('name'); + if (binding) { + sessionVars.push({ + name: binding.text(), + decl: importNode, + scope: getLexicalScope(importNode), + }); + } + } + + // Case 1: Remove priority object from http2.connect() options (direct call sites) + edits.push(...removeConnectPriority(rootNode)); + + // Case 2: Remove priority from session.request() options scoped to discovered session vars + chained connect().request. + edits.push(...removeRequestPriority(rootNode, sessionVars)); + + // Determine stream variables created from session.request() or connect().request(). + const streamVars = collectStreamVars(rootNode, sessionVars); + + // Case 3: Remove entire stream.priority() calls only for: + // - chained request().priority() + // - variables assigned from session.request/connect().request + const result3 = removePriorityMethodCalls(rootNode, streamVars); + edits.push(...result3.edits); + linesToRemove.push(...result3.linesToRemove); + + // Case 4: Remove priority property from session.settings() options scoped to session vars + chained connect().settings. + edits.push(...removeSettingsPriority(rootNode, sessionVars)); + + if (!edits.length && !linesToRemove.length) return null; + + const sourceCode = rootNode.commitEdits(edits); + + return removeLines(sourceCode, linesToRemove); +} + +/** + * Remove priority property from http2.connect() call options + */ +function removeConnectPriority(rootNode: SgNode): Edit[] { + const edits: Edit[] = []; + + // Match any connect() call + const connectCalls = rootNode.findAll({ + rule: { + pattern: '$HTTP2.connect($$$ARGS)', + }, + }); + + for (const call of connectCalls) { + const objects = call.findAll({ + rule: { + kind: 'object', + }, + }); + + for (const obj of objects) { + // Check if object only contains priority properties + // Get immediate children pairs only (not nested) + const pairs = obj.children().filter((child) => child.kind() === 'pair'); + + let hasPriority = false; + let allPriority = true; + + for (const pair of pairs) { + const keyNode = pair.find({ + rule: { + kind: 'property_identifier', + regex: '^priority$', + }, + }); + + if (keyNode) { + hasPriority = true; + } else { + allPriority = false; + } + } + + if (allPriority && hasPriority) { + // Remove the entire object argument from the call + const callText = call.text(); + const objText = obj.text(); + // Use s flag to match across newlines (including the multiline object) + const cleanedCall = callText.replace( + new RegExp(`(,\\s*)?${escapeRegex(objText)}(,\\s*)?`, 's'), + '', + ); + const finalCall = cleanedCall.replace(/,\s*\)/, ')'); + + if (finalCall !== callText) { + edits.push(call.replace(finalCall)); + } + } else if (hasPriority) { + // Object has other properties, so just remove priority pair + edits.push(...removePriorityPairFromObject(obj)); + } + } + } + + return edits; +} + +/** + * Remove priority property from session.request() call options + */ +function removeRequestPriority( + rootNode: SgNode, + sessionVars: { name: string; scope: SgNode }[], +): Edit[] { + const edits: Edit[] = []; + + // Chained connect().request(...) still safe regardless of variable binding. + const chained = rootNode.findAll({ + rule: { pattern: '$HTTP2.connect($$$_ARGS).request($$ARGS)' }, + }); + const allCalls: SgNode[] = [...chained]; + + // Scoped session.request calls based on discovered session variables. + for (const sess of sessionVars) { + const calls = sess.scope.findAll({ + rule: { + kind: 'call_expression', + has: { + field: 'function', + kind: 'member_expression', + pattern: `${sess.name}.request`, + }, + }, + }); + allCalls.push(...calls); + } + + for (const call of allCalls) { + const objects = call.findAll({ + rule: { + kind: 'object', + }, + }); + for (const obj of objects) { + const pairs = obj.children().filter((child) => child.kind() === 'pair'); + + let hasPriority = false; + let allPriority = true; + + for (const pair of pairs) { + const keyNode = pair.find({ + rule: { kind: 'property_identifier', regex: '^priority$' }, + }); + if (keyNode) hasPriority = true; + else allPriority = false; + } + if (allPriority && hasPriority) { + const callText = call.text(); + const objText = obj.text(); + // Remove object argument (with potential surrounding commas) + let cleanedCall = callText; + cleanedCall = cleanedCall.replace(objText, ''); + cleanedCall = cleanedCall.replace(/,\s*\)/, ')'); + cleanedCall = cleanedCall.replace(/\(\s*,/, '('); + const finalCall = cleanedCall; + if (finalCall !== callText) edits.push(call.replace(finalCall)); + } else if (hasPriority) { + edits.push(...removePriorityPairFromObject(obj)); + } + } + } + return edits; +} + +/** + * Remove entire stream.priority() method calls + */ +function removePriorityMethodCalls( + rootNode: SgNode, + streamVars: { name: string; scope: SgNode }[], +): { edits: Edit[]; linesToRemove: Range[] } { + const edits: Edit[] = []; + const linesToRemove: Range[] = []; + + // Chained request(...).priority(...) directly (session or connect chains) + const chained = rootNode.findAll({ + rule: { pattern: '$SESSION.request($$$_ARGS).priority($$$ARGS)' }, + }); + const chainedConnect = rootNode.findAll({ + rule: { + pattern: '$HTTP2.connect($$$_ARGS).request($$ARGS).priority($$$ARGS)', + }, + }); + + const safeCalls = new Set>([...chained, ...chainedConnect]); + + // Priority on identified stream variable names within their scope. + for (const stream of streamVars) { + const calls = stream.scope.findAll({ + rule: { + kind: 'call_expression', + has: { + field: 'function', + kind: 'member_expression', + pattern: `${stream.name}.priority`, + }, + }, + }); + for (const c of calls) safeCalls.add(c); + } + + // Remove expression statements containing safe priority calls. + for (const call of safeCalls) { + let node: SgNode | undefined = call; + + while (node) { + const parent = node.parent(); + + if (parent?.kind() === 'expression_statement') { + linesToRemove.push(parent.range()); + break; + } + node = parent; + } + } + return { edits, linesToRemove }; +} + +/** + * Remove priority from settings() call + */ +function removeSettingsPriority( + rootNode: SgNode, + sessionVars: { name: string; scope: SgNode }[], +): Edit[] { + const edits: Edit[] = []; + // Chained connect().settings(...) + const chained = rootNode.findAll({ + rule: { pattern: '$HTTP2.connect($$$_ARGS).settings($$$_ARGS)' }, + }); + + const safeCalls = new Set>([...chained]); + + // Scoped session.settings calls for discovered session variables. + for (const sess of sessionVars) { + const calls = sess.scope.findAll({ + rule: { + kind: 'call_expression', + has: { + field: 'function', + kind: 'member_expression', + pattern: `${sess.name}.settings`, + }, + }, + }); + for (const c of calls) safeCalls.add(c); + } + + for (const call of safeCalls) { + const objects = call.findAll({ rule: { kind: 'object' } }); + + for (const obj of objects) { + const pairs = obj.children().filter((child) => child.kind() === 'pair'); + + let hasPriority = false; + let allPriority = true; + + for (const pair of pairs) { + const keyNode = pair.find({ + rule: { kind: 'property_identifier', regex: '^priority$' }, + }); + if (keyNode) hasPriority = true; + else allPriority = false; + } + if (allPriority && hasPriority) { + const callText = call.text(); + const objText = obj.text(); + let cleanedCall = callText; + cleanedCall = cleanedCall.replace(objText, ''); + cleanedCall = cleanedCall.replace(/,\s*\)/, ')'); + cleanedCall = cleanedCall.replace(/\(\s*,/, '('); + const finalCall = cleanedCall; + if (finalCall !== callText) edits.push(call.replace(finalCall)); + } else if (hasPriority) { + edits.push(...removePriorityPairFromObject(obj)); + } + } + } + return edits; +} + +// Collect stream variables created from session.request() or connect().request() patterns. +function collectStreamVars( + rootNode: SgNode, + sessionVars: { name: string; scope: SgNode }[], +): { name: string; scope: SgNode }[] { + const streamVars: { name: string; scope: SgNode }[] = []; + // From sessionVar.request(...) + for (const sess of sessionVars) { + const decls = sess.scope.findAll({ + rule: { + kind: 'variable_declarator', + has: { + field: 'value', + kind: 'call_expression', + has: { + field: 'function', + kind: 'member_expression', + pattern: `${sess.name}.request`, + }, + }, + }, + }); + for (const d of decls) { + const nameNode = (d as SgNode).field('name'); + streamVars.push({ name: nameNode.text(), scope: getLexicalScope(d) }); + } + } + // From connect().request(...) chained assignments. + const chainedDecls = rootNode.findAll({ + rule: { + kind: 'variable_declarator', + has: { + field: 'value', + kind: 'call_expression', + has: { + field: 'function', + kind: 'member_expression', + pattern: 'request', // we will validate parent chain text + }, + }, + }, + }); + for (const d of chainedDecls) { + const valueText = (d as SgNode) + .field('value') + .text(); + // Quick heuristic: contains ".connect(" before ".request(". + if (/connect\s*\([^)]*\).*\.request\s*\(/.test(valueText)) { + const nameNode = (d as SgNode).field('name'); + streamVars.push({ name: nameNode.text(), scope: getLexicalScope(d) }); + } + } + return streamVars; +} + +/** + * Find and remove priority pair from an object, handling commas properly + */ +function removePriorityPairFromObject(obj: SgNode): Edit[] { + const edits: Edit[] = []; + + const pairs = obj.findAll({ + rule: { + kind: 'pair', + }, + }); + + // Find all priority pairs + const priorityPairs: SgNode[] = []; + for (const pair of pairs) { + const keyNode = pair.find({ + rule: { + kind: 'property_identifier', + regex: '^priority$', + }, + }); + + if (keyNode) { + priorityPairs.push(pair); + } + } + + if (priorityPairs.length === 0) { + return edits; + } + + // If all pairs are priority, remove the entire object + if (priorityPairs.length === pairs.length) { + edits.push(obj.replace('')); + return edits; + } + + // Otherwise, we need to remove pairs and clean up commas + // Strategy: replace the object with a cleaned version + const objText = obj.text(); + let result = objText; + + // For each priority pair, remove it along with associated comma + for (const pair of priorityPairs) { + const pairText = pair.text(); + + // Try to match and remove: ", priority: {...}" or similar + // First try with leading comma + const leadingCommaPattern = `,\\s*${escapeRegex(pairText)}`; + if (result.includes(',') && result.includes(pairText)) { + result = result.replace(new RegExp(leadingCommaPattern), ''); + } + + // If still not removed, try with trailing comma + if (result.includes(pairText)) { + const trailingCommaPattern = `${escapeRegex(pairText)},`; + result = result.replace(new RegExp(trailingCommaPattern), ''); + } + + // If still not removed, just remove the pair + if (result.includes(pairText)) { + result = result.replace(pairText, ''); + } + } + + // Clean up any resulting spacing issues + result = result.replace(/,\s*,/g, ','); + result = result.replace(/{\s*,/g, '{'); + result = result.replace(/,\s*}/g, '}'); + result = result.replace(/{(\S)/g, '{ $1'); + result = result.replace(/(\S)}/g, '$1 }'); + + if (result !== objText) { + edits.push(obj.replace(result)); + } + + return edits; +} + +function escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} diff --git a/recipes/http2-priority-signaling/tests/expected/case1-connect-priority.js b/recipes/http2-priority-signaling/tests/expected/case1-connect-priority.js new file mode 100644 index 00000000..2e9c114d --- /dev/null +++ b/recipes/http2-priority-signaling/tests/expected/case1-connect-priority.js @@ -0,0 +1,2 @@ +const http2 = require("node:http2"); +const session = http2.connect("https://example.com"); diff --git a/recipes/http2-priority-signaling/tests/expected/case10-import-namespace.mjs b/recipes/http2-priority-signaling/tests/expected/case10-import-namespace.mjs new file mode 100644 index 00000000..55c03124 --- /dev/null +++ b/recipes/http2-priority-signaling/tests/expected/case10-import-namespace.mjs @@ -0,0 +1,5 @@ +import * as foo from "node:http2"; +const session = foo.connect("https://example.com"); +session.settings({ + enablePush: true +}); diff --git a/recipes/http2-priority-signaling/tests/expected/case11-import-connect.mjs b/recipes/http2-priority-signaling/tests/expected/case11-import-connect.mjs new file mode 100644 index 00000000..d9970988 --- /dev/null +++ b/recipes/http2-priority-signaling/tests/expected/case11-import-connect.mjs @@ -0,0 +1,5 @@ +import { connect } from "node:http2"; +const session = connect("https://example.com"); +session.settings({ + enablePush: true +}); diff --git a/recipes/http2-priority-signaling/tests/expected/case12-import-connect-alias.mjs b/recipes/http2-priority-signaling/tests/expected/case12-import-connect-alias.mjs new file mode 100644 index 00000000..55380e67 --- /dev/null +++ b/recipes/http2-priority-signaling/tests/expected/case12-import-connect-alias.mjs @@ -0,0 +1,5 @@ +import { connect as bar } from "node:http2"; +const session = bar("https://example.com"); +session.settings({ + enablePush: true +}); diff --git a/recipes/http2-priority-signaling/tests/expected/case2-request-priority.js b/recipes/http2-priority-signaling/tests/expected/case2-request-priority.js new file mode 100644 index 00000000..971e5117 --- /dev/null +++ b/recipes/http2-priority-signaling/tests/expected/case2-request-priority.js @@ -0,0 +1,5 @@ +const http2 = require("node:http2"); +const session = http2.connect("https://example.com"); +const stream = session.request({ + ":path": "/api/data" +}); diff --git a/recipes/http2-priority-signaling/tests/expected/case3-stream-priority-method.js b/recipes/http2-priority-signaling/tests/expected/case3-stream-priority-method.js new file mode 100644 index 00000000..e21ea359 --- /dev/null +++ b/recipes/http2-priority-signaling/tests/expected/case3-stream-priority-method.js @@ -0,0 +1,3 @@ +const http2 = require("node:http2"); +const session = http2.connect("https://example.com"); +const stream = session.request({ ":path": "/" }); diff --git a/recipes/http2-priority-signaling/tests/expected/case4-settings-priority-esm.mjs b/recipes/http2-priority-signaling/tests/expected/case4-settings-priority-esm.mjs new file mode 100644 index 00000000..d14019cf --- /dev/null +++ b/recipes/http2-priority-signaling/tests/expected/case4-settings-priority-esm.mjs @@ -0,0 +1,3 @@ +import http2 from "node:http2"; +const client = http2.connect("https://example.com"); +client.settings({ enablePush: false }); diff --git a/recipes/http2-priority-signaling/tests/expected/case5-dynamic-import.mjs b/recipes/http2-priority-signaling/tests/expected/case5-dynamic-import.mjs new file mode 100644 index 00000000..d12f1455 --- /dev/null +++ b/recipes/http2-priority-signaling/tests/expected/case5-dynamic-import.mjs @@ -0,0 +1,6 @@ +import("node:http2").then((http2) => { + const session = http2.connect("https://example.com"); + session.settings({ + enablePush: true + }); +}); \ No newline at end of file diff --git a/recipes/http2-priority-signaling/tests/expected/case6-dynamic-import-await.mjs b/recipes/http2-priority-signaling/tests/expected/case6-dynamic-import-await.mjs new file mode 100644 index 00000000..903aa689 --- /dev/null +++ b/recipes/http2-priority-signaling/tests/expected/case6-dynamic-import-await.mjs @@ -0,0 +1,5 @@ +const http2 = await import("node:http2"); +const session = http2.connect("https://example.com"); +session.settings({ + enablePush: true +}); diff --git a/recipes/http2-priority-signaling/tests/expected/case7-require-myHttp2.js b/recipes/http2-priority-signaling/tests/expected/case7-require-myHttp2.js new file mode 100644 index 00000000..864b4111 --- /dev/null +++ b/recipes/http2-priority-signaling/tests/expected/case7-require-myHttp2.js @@ -0,0 +1,5 @@ +const myHttp2 = require("http2"); +const session = myHttp2.connect("https://example.com"); +session.settings({ + enablePush: true +}); diff --git a/recipes/http2-priority-signaling/tests/expected/case8-require-connect.js b/recipes/http2-priority-signaling/tests/expected/case8-require-connect.js new file mode 100644 index 00000000..b2eb4cd2 --- /dev/null +++ b/recipes/http2-priority-signaling/tests/expected/case8-require-connect.js @@ -0,0 +1,5 @@ +const { connect } = require("http2"); +const session = connect("https://example.com"); +session.settings({ + enablePush: true +}); diff --git a/recipes/http2-priority-signaling/tests/expected/case9-require-connect-alias.js b/recipes/http2-priority-signaling/tests/expected/case9-require-connect-alias.js new file mode 100644 index 00000000..96525956 --- /dev/null +++ b/recipes/http2-priority-signaling/tests/expected/case9-require-connect-alias.js @@ -0,0 +1,5 @@ +const { connect: bar } = require("http2"); +const session = bar("https://example.com"); +session.settings({ + enablePush: true +}); diff --git a/recipes/http2-priority-signaling/tests/input/case1-connect-priority.js b/recipes/http2-priority-signaling/tests/input/case1-connect-priority.js new file mode 100644 index 00000000..de8e7bad --- /dev/null +++ b/recipes/http2-priority-signaling/tests/input/case1-connect-priority.js @@ -0,0 +1,8 @@ +const http2 = require("node:http2"); +const session = http2.connect("https://example.com", { + priority: { + weight: 16, + parent: 0, + exclusive: false + } +}); diff --git a/recipes/http2-priority-signaling/tests/input/case10-import-namespace.mjs b/recipes/http2-priority-signaling/tests/input/case10-import-namespace.mjs new file mode 100644 index 00000000..5a034232 --- /dev/null +++ b/recipes/http2-priority-signaling/tests/input/case10-import-namespace.mjs @@ -0,0 +1,6 @@ +import * as foo from "node:http2"; +const session = foo.connect("https://example.com"); +session.settings({ + enablePush: true, + priority: { weight: 16 } +}); diff --git a/recipes/http2-priority-signaling/tests/input/case11-import-connect.mjs b/recipes/http2-priority-signaling/tests/input/case11-import-connect.mjs new file mode 100644 index 00000000..719abde2 --- /dev/null +++ b/recipes/http2-priority-signaling/tests/input/case11-import-connect.mjs @@ -0,0 +1,6 @@ +import { connect } from "node:http2"; +const session = connect("https://example.com"); +session.settings({ + enablePush: true, + priority: { weight: 16 } +}); diff --git a/recipes/http2-priority-signaling/tests/input/case12-import-connect-alias.mjs b/recipes/http2-priority-signaling/tests/input/case12-import-connect-alias.mjs new file mode 100644 index 00000000..f2678568 --- /dev/null +++ b/recipes/http2-priority-signaling/tests/input/case12-import-connect-alias.mjs @@ -0,0 +1,6 @@ +import { connect as bar } from "node:http2"; +const session = bar("https://example.com"); +session.settings({ + enablePush: true, + priority: { weight: 16 } +}); diff --git a/recipes/http2-priority-signaling/tests/input/case2-request-priority.js b/recipes/http2-priority-signaling/tests/input/case2-request-priority.js new file mode 100644 index 00000000..f2d57dc8 --- /dev/null +++ b/recipes/http2-priority-signaling/tests/input/case2-request-priority.js @@ -0,0 +1,6 @@ +const http2 = require("node:http2"); +const session = http2.connect("https://example.com"); +const stream = session.request({ + ":path": "/api/data", + priority: { weight: 32 } +}); diff --git a/recipes/http2-priority-signaling/tests/input/case3-stream-priority-method.js b/recipes/http2-priority-signaling/tests/input/case3-stream-priority-method.js new file mode 100644 index 00000000..e7cc4bad --- /dev/null +++ b/recipes/http2-priority-signaling/tests/input/case3-stream-priority-method.js @@ -0,0 +1,8 @@ +const http2 = require("node:http2"); +const session = http2.connect("https://example.com"); +const stream = session.request({ ":path": "/" }); +stream.priority({ + exclusive: true, + parent: 0, + weight: 128 +}); diff --git a/recipes/http2-priority-signaling/tests/input/case4-settings-priority-esm.mjs b/recipes/http2-priority-signaling/tests/input/case4-settings-priority-esm.mjs new file mode 100644 index 00000000..47b8911c --- /dev/null +++ b/recipes/http2-priority-signaling/tests/input/case4-settings-priority-esm.mjs @@ -0,0 +1,3 @@ +import http2 from "node:http2"; +const client = http2.connect("https://example.com"); +client.settings({ enablePush: false, priority: true }); diff --git a/recipes/http2-priority-signaling/tests/input/case5-dynamic-import.mjs b/recipes/http2-priority-signaling/tests/input/case5-dynamic-import.mjs new file mode 100644 index 00000000..9b1cd0d2 --- /dev/null +++ b/recipes/http2-priority-signaling/tests/input/case5-dynamic-import.mjs @@ -0,0 +1,7 @@ +import("node:http2").then((http2) => { + const session = http2.connect("https://example.com"); + session.settings({ + enablePush: true, + priority: { weight: 16 } + }); +}); \ No newline at end of file diff --git a/recipes/http2-priority-signaling/tests/input/case6-dynamic-import-await.mjs b/recipes/http2-priority-signaling/tests/input/case6-dynamic-import-await.mjs new file mode 100644 index 00000000..52085605 --- /dev/null +++ b/recipes/http2-priority-signaling/tests/input/case6-dynamic-import-await.mjs @@ -0,0 +1,6 @@ +const http2 = await import("node:http2"); +const session = http2.connect("https://example.com"); +session.settings({ + enablePush: true, + priority: { weight: 16 } +}); diff --git a/recipes/http2-priority-signaling/tests/input/case7-require-myHttp2.js b/recipes/http2-priority-signaling/tests/input/case7-require-myHttp2.js new file mode 100644 index 00000000..f8aa4670 --- /dev/null +++ b/recipes/http2-priority-signaling/tests/input/case7-require-myHttp2.js @@ -0,0 +1,6 @@ +const myHttp2 = require("http2"); +const session = myHttp2.connect("https://example.com"); +session.settings({ + enablePush: true, + priority: { weight: 16 } +}); diff --git a/recipes/http2-priority-signaling/tests/input/case8-require-connect.js b/recipes/http2-priority-signaling/tests/input/case8-require-connect.js new file mode 100644 index 00000000..8ee32803 --- /dev/null +++ b/recipes/http2-priority-signaling/tests/input/case8-require-connect.js @@ -0,0 +1,6 @@ +const { connect } = require("http2"); +const session = connect("https://example.com"); +session.settings({ + enablePush: true, + priority: { weight: 16 } +}); diff --git a/recipes/http2-priority-signaling/tests/input/case9-require-connect-alias.js b/recipes/http2-priority-signaling/tests/input/case9-require-connect-alias.js new file mode 100644 index 00000000..45b0f136 --- /dev/null +++ b/recipes/http2-priority-signaling/tests/input/case9-require-connect-alias.js @@ -0,0 +1,6 @@ +const { connect: bar } = require("http2"); +const session = bar("https://example.com"); +session.settings({ + enablePush: true, + priority: { weight: 16 } +}); diff --git a/recipes/http2-priority-signaling/workflow.yaml b/recipes/http2-priority-signaling/workflow.yaml new file mode 100644 index 00000000..4d7dac7f --- /dev/null +++ b/recipes/http2-priority-signaling/workflow.yaml @@ -0,0 +1,25 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod-com/codemod/refs/heads/main/schemas/workflow.json + +version: "1" + +nodes: + - id: apply-transforms + name: Apply AST Transformations + type: automatic + steps: + - name: Handle DEP0194 via removing HTTP/2 priority-related options and methods + js-ast-grep: + js_file: src/workflow.ts + base_path: . + include: + - "**/*.cjs" + - "**/*.cts" + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript diff --git a/utils/src/ast-grep/resolve-binding-path.ts b/utils/src/ast-grep/resolve-binding-path.ts index 42a06391..8363a538 100644 --- a/utils/src/ast-grep/resolve-binding-path.ts +++ b/utils/src/ast-grep/resolve-binding-path.ts @@ -42,7 +42,7 @@ export function resolveBindingPath(node: SgNode, path: string) { if (!supportedKinds.includes(rootKind.toString())) { throw Error( - `Invalid node kind. To resolve binding path, one of these types must be provided: ${supportedKinds.join(', ')}`, + `Invalid node kind. To resolve binding path, one of these types must be provided: ${supportedKinds.join(', ')}\n received: ${rootKind}`, ); }