diff --git a/package-lock.json b/package-lock.json index 944bf453..5bd12c70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1513,6 +1513,10 @@ "resolved": "recipes/rmdir", "link": true }, + "node_modules/@nodejs/timers-deprecations": { + "resolved": "recipes/timers-deprecations", + "link": true + }, "node_modules/@nodejs/tmpdir-to-tmpdir": { "resolved": "recipes/tmpdir-to-tmpdir", "link": true @@ -4414,6 +4418,17 @@ "@codemod.com/jssg-types": "^1.0.9" } }, + "recipes/timers-deprecations": { + "name": "@nodejs/timers-deprecations", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@nodejs/codemod-utils": "*" + }, + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.9" + } + }, "recipes/tmpdir-to-tmpdir": { "name": "@nodejs/tmpdir-to-tmpdir", "version": "1.0.0", diff --git a/recipes/timers-deprecations/README.md b/recipes/timers-deprecations/README.md new file mode 100644 index 00000000..6c10ce04 --- /dev/null +++ b/recipes/timers-deprecations/README.md @@ -0,0 +1,39 @@ +# Node.js Timers Deprecations + +This recipe migrates deprecated internals from `node:timers` to the supported public timers API. It replaces usages of `timers.enroll()`, `timers.unenroll()`, `timers.active()`, and `timers._unrefActive()` with standard constructs built on top of `setTimeout()`, `clearTimeout()`, and `Timer#unref()`. + +See the upstream notices: [DEP0095](https://nodejs.org/api/deprecations.html#DEP0095), [DEP0096](https://nodejs.org/api/deprecations.html#DEP0096), [DEP0126](https://nodejs.org/api/deprecations.html#DEP0126), and [DEP0127](https://nodejs.org/api/deprecations.html#DEP0127). + +## Example + +### Replace `timers.enroll()` + +```diff +- const timers = require('node:timers'); +- const resource = { _idleTimeout: 1500 }; +- timers.enroll(resource, 1500); ++ const resource = { timeout: setTimeout(() => { ++ // timeout handler ++ }, 1500) }; +``` + +### Replace `timers.unenroll()` + +```diff +- timers.unenroll(resource); ++ clearTimeout(resource.timeout); +``` + +### Replace `timers.active()` and `timers._unrefActive()` + +```diff +- const timers = require('node:timers'); +- timers.active(resource); +- timers._unrefActive(resource); ++ const handle = setTimeout(onTimeout, delay); ++ handle.unref(); +``` + +## Caveats + +The legacy APIs exposed internal timer bookkeeping fields such as `_idleStart` or `_idleTimeout`. Those internals have no public equivalent. The codemod focuses on migrating the control flow to modern timers and leaves application specific bookkeeping to the developer. Carefully review the transformed code to ensure that any custom metadata is still updated as expected. diff --git a/recipes/timers-deprecations/codemod.yaml b/recipes/timers-deprecations/codemod.yaml new file mode 100644 index 00000000..434e593e --- /dev/null +++ b/recipes/timers-deprecations/codemod.yaml @@ -0,0 +1,21 @@ +schema_version: "1.0" +name: "@nodejs/timers-deprecations" +version: 1.0.0 +description: Migrate deprecated node:timers APIs to public timer functions. +author: Augustin Mauroy +license: MIT +workflow: workflow.yaml +category: migration + +targets: + languages: + - javascript + - typescript + +keywords: + - migration + - timers + +registry: + access: public + visibility: public diff --git a/recipes/timers-deprecations/package.json b/recipes/timers-deprecations/package.json new file mode 100644 index 00000000..19e2fd84 --- /dev/null +++ b/recipes/timers-deprecations/package.json @@ -0,0 +1,29 @@ +{ + "name": "@nodejs/timers-deprecations", + "version": "1.0.0", + "description": "Migrate deprecated node:timers APIs to public timer functions.", + "type": "module", + "scripts": { + "test": "node --run test:enroll && node --run test:unenroll && node --run test:active && node --run test:unref && node --run test:imports", + "test:enroll": "npx codemod jssg test -l typescript ./src/enroll-to-set-timeout.ts ./tests/ --filter dep0095", + "test:unenroll": "npx codemod jssg test -l typescript ./src/unenroll-to-clear-timer.ts ./tests/ --filter dep0096", + "test:active": "npx codemod jssg test -l typescript ./src/active-to-standard-timer.ts ./tests/ --filter active", + "test:unref": "npx codemod jssg test -l typescript ./src/unref-active-to-unref.ts ./tests/ --filter unref", + "test:imports": "npx codemod jssg test -l typescript ./src/cleanup-imports.ts ./tests/ --filter imports" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nodejs/userland-migrations.git", + "directory": "recipes/timers-deprecations", + "bugs": "https://github.com/nodejs/userland-migrations/issues" + }, + "author": "Augustin Mauroy", + "license": "MIT", + "homepage": "https://github.com/nodejs/userland-migrations/tree/main/recipes/timers-deprecations#readme", + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.9" + }, + "dependencies": { + "@nodejs/codemod-utils": "*" + } +} diff --git a/recipes/timers-deprecations/src/active-to-standard-timer.ts b/recipes/timers-deprecations/src/active-to-standard-timer.ts new file mode 100644 index 00000000..70434da5 --- /dev/null +++ b/recipes/timers-deprecations/src/active-to-standard-timer.ts @@ -0,0 +1,82 @@ +import { EOL } from 'node:os'; +import { + getNodeImportStatements, + getNodeImportCalls, +} from '@nodejs/codemod-utils/ast-grep/import-statement'; +import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call'; +import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path'; +import { + findParentStatement, + isSafeResourceTarget, +} from '@nodejs/codemod-utils/ast-grep/general'; +import { + detectIndentUnit, + getLineIndent, +} from '@nodejs/codemod-utils/ast-grep/indent'; +import type { Edit, SgRoot } from '@codemod.com/jssg-types/main'; +import type Js from '@codemod.com/jssg-types/langs/javascript'; + +const TARGET_METHOD = 'active'; + +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const sourceCode = rootNode.text(); + const indentUnit = detectIndentUnit(sourceCode); + const edits: Edit[] = []; + const handledStatements = new Set(); + + const importNodes = [ + ...getNodeRequireCalls(root, 'timers'), + ...getNodeImportStatements(root, 'timers'), + ...getNodeImportCalls(root, 'timers'), + ]; + + for (const importNode of importNodes) { + if (importNode.kind() === 'expression_statement') continue; + const bindingPath = resolveBindingPath(importNode, `$.${TARGET_METHOD}`); + if (!bindingPath) continue; + + const matches = rootNode.findAll({ + rule: { + any: [ + { pattern: `${bindingPath}($RESOURCE)` }, + { pattern: `${bindingPath}($RESOURCE, $$$REST)` }, + ], + }, + }); + + for (const match of matches) { + const resourceNode = match.getMatch('RESOURCE'); + if (!resourceNode) continue; + + if (!isSafeResourceTarget(resourceNode)) continue; + + const statement = findParentStatement(match); + if (!statement) continue; + + if (handledStatements.has(statement.id())) continue; + handledStatements.add(statement.id()); + + const indent = getLineIndent(sourceCode, statement.range().start.index); + const resourceText = resourceNode.text(); + const childIndent = indent + indentUnit; + const innerIndent = childIndent + indentUnit; + + const replacement = + `if (${resourceText}.timeout != null) {${EOL}` + + `${childIndent}clearTimeout(${resourceText}.timeout);${EOL}` + + `${indent}}${EOL}${EOL}` + + `${indent}${resourceText}.timeout = setTimeout(() => {${EOL}` + + `${childIndent}if (typeof ${resourceText}._onTimeout === "function") {${EOL}` + + `${innerIndent}${resourceText}._onTimeout();${EOL}` + + `${childIndent}}${EOL}` + + `${indent}}, ${resourceText}._idleTimeout);`; + + edits.push(statement.replace(replacement)); + } + } + + if (!edits.length) return null; + + return rootNode.commitEdits(edits); +} diff --git a/recipes/timers-deprecations/src/cleanup-imports.ts b/recipes/timers-deprecations/src/cleanup-imports.ts new file mode 100644 index 00000000..ab5563e1 --- /dev/null +++ b/recipes/timers-deprecations/src/cleanup-imports.ts @@ -0,0 +1,196 @@ +import { + getNodeImportStatements, + getDefaultImportIdentifier, + getNodeImportCalls, +} from '@nodejs/codemod-utils/ast-grep/import-statement'; +import { + getNodeRequireCalls, + getRequireNamespaceIdentifier, +} from '@nodejs/codemod-utils/ast-grep/require-call'; +import { removeBinding } from '@nodejs/codemod-utils/ast-grep/remove-binding'; +import { removeLines } from '@nodejs/codemod-utils/ast-grep/remove-lines'; +import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path'; +import type { Edit, Range, SgNode, SgRoot } from '@codemod.com/jssg-types/main'; +import type Js from '@codemod.com/jssg-types/langs/javascript'; + +const DEPRECATED_METHODS = [ + 'enroll', + 'unenroll', + 'active', + '_unrefActive', +] as const; +const DEPRECATED_SET = new Set(DEPRECATED_METHODS); + +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const edits: Edit[] = []; + const linesToRemove: Range[] = []; + + const statements = [ + ...getNodeRequireCalls(root, 'timers'), + ...getNodeImportStatements(root, 'timers'), + ...getNodeImportCalls(root, 'timers'), + ]; + + for (const statement of statements) { + if (statement.kind() === 'expression_statement') { + continue; + } + if (shouldRemoveEntireStatement(statement)) { + linesToRemove.push(statement.range()); + continue; + } + + let statementMarkedForRemoval = false; + const removedBindings = new Set(); + + for (const method of DEPRECATED_METHODS) { + const bindingPath = resolveBindingPath(statement, `$.${method}`); + if (!bindingPath) continue; + + const localBinding = bindingPath.split('.').at(-1); + if (!localBinding || removedBindings.has(localBinding)) continue; + + if (isBindingStillUsed(rootNode, statement, localBinding)) continue; + + const removal = removeBinding(statement, localBinding); + if (!removal) continue; + + if (removal.edit) edits.push(removal.edit); + if (removal.lineToRemove) { + linesToRemove.push(removal.lineToRemove); + removedBindings.add(localBinding); + statementMarkedForRemoval = true; + break; + } + + removedBindings.add(localBinding); + } + + if (statementMarkedForRemoval) { + continue; + } + + const namespaceIdentifier = getNamespaceIdentifier(statement); + if (!namespaceIdentifier) continue; + + const nsName = namespaceIdentifier.text(); + if (removedBindings.has(nsName)) continue; + if (isBindingStillUsed(rootNode, statement, nsName)) continue; + + const removal = removeBinding(statement, nsName); + if (!removal) continue; + + if (removal.edit) edits.push(removal.edit); + if (removal.lineToRemove) linesToRemove.push(removal.lineToRemove); + } + + if (!edits.length && !linesToRemove.length) { + return null; + } + + let source = edits.length ? rootNode.commitEdits(edits) : rootNode.text(); + + if (linesToRemove.length) { + source = removeLines(source, linesToRemove); + } + + return source.replace(/^\s*\n/, ''); +} + +function isBindingStillUsed( + rootNode: SgNode, + statement: SgNode, + binding: string, +): boolean { + const occurrences = rootNode.findAll({ rule: { pattern: binding } }); + for (const occurrence of occurrences) { + if (isInsideNode(occurrence, statement)) continue; + return true; + } + return false; +} + +function isInsideNode(node: SgNode, container: SgNode): boolean { + for (const ancestor of node.ancestors()) { + if (ancestor.id() === container.id()) return true; + } + return false; +} + +function getNamespaceIdentifier(statement: SgNode): SgNode | null { + const requireIdent = getRequireNamespaceIdentifier(statement); + if (requireIdent) return requireIdent; + + const namespaceImport = statement.find({ + rule: { + kind: 'identifier', + inside: { kind: 'namespace_import' }, + }, + }); + if (namespaceImport) return namespaceImport; + + const dynamicImportIdentifier = statement.find({ + rule: { + kind: 'identifier', + inside: { kind: 'variable_declarator' }, + not: { inside: { kind: 'object_pattern' } }, + }, + }); + if (dynamicImportIdentifier) return dynamicImportIdentifier; + + return getDefaultImportIdentifier(statement); +} + +function shouldRemoveEntireStatement(statement: SgNode): boolean { + const objectPattern = statement.find({ rule: { kind: 'object_pattern' } }); + if (objectPattern) { + const propertyNames = new Set(); + for (const shorthand of objectPattern.findAll({ + rule: { kind: 'shorthand_property_identifier_pattern' }, + })) { + propertyNames.add(shorthand.text()); + } + for (const pair of objectPattern.findAll({ + rule: { kind: 'pair_pattern' }, + })) { + const property = pair.find({ rule: { kind: 'property_identifier' } }); + if (!property) return false; + propertyNames.add(property.text()); + } + if (!propertyNames.size) return false; + for (const name of propertyNames) { + if (!DEPRECATED_SET.has(name)) return false; + } + return true; + } + + const namedImports = statement.find({ rule: { kind: 'named_imports' } }); + if (!namedImports) return false; + + const importClause = statement.find({ rule: { kind: 'import_clause' } }); + if (!importClause) return false; + + if (importClause.find({ rule: { kind: 'namespace_import' } })) return false; + + const defaultImport = importClause.find({ + rule: { + kind: 'identifier', + not: { inside: { kind: 'named_imports' } }, + }, + }); + if (defaultImport) return false; + + let hasSpecifier = false; + for (const specifier of namedImports.findAll({ + rule: { kind: 'import_specifier' }, + })) { + hasSpecifier = true; + const identifiers = specifier.findAll({ rule: { kind: 'identifier' } }); + if (!identifiers.length) return false; + const importedName = identifiers[0]?.text(); + if (!importedName || !DEPRECATED_SET.has(importedName)) return false; + } + + return hasSpecifier; +} diff --git a/recipes/timers-deprecations/src/enroll-to-set-timeout.ts b/recipes/timers-deprecations/src/enroll-to-set-timeout.ts new file mode 100644 index 00000000..3676f18b --- /dev/null +++ b/recipes/timers-deprecations/src/enroll-to-set-timeout.ts @@ -0,0 +1,77 @@ +import { EOL } from 'node:os'; +import { + getNodeImportStatements, + getNodeImportCalls, +} from '@nodejs/codemod-utils/ast-grep/import-statement'; +import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call'; +import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path'; +import { + findParentStatement, + isSafeResourceTarget, +} from '@nodejs/codemod-utils/ast-grep/general'; +import { + detectIndentUnit, + getLineIndent, +} from '@nodejs/codemod-utils/ast-grep/indent'; +import type { Edit, SgRoot } from '@codemod.com/jssg-types/main'; +import type Js from '@codemod.com/jssg-types/langs/javascript'; + +const TARGET_METHOD = 'enroll'; + +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const sourceCode = rootNode.text(); + const indentUnit = detectIndentUnit(sourceCode); + const edits: Edit[] = []; + const handledStatements = new Set(); + + const importNodes = [ + ...getNodeRequireCalls(root, 'timers'), + ...getNodeImportStatements(root, 'timers'), + ...getNodeImportCalls(root, 'timers'), + ]; + + for (const importNode of importNodes) { + if (importNode.kind() === 'expression_statement') continue; + const bindingPath = resolveBindingPath(importNode, `$.${TARGET_METHOD}`); + if (!bindingPath) continue; + + const matches = rootNode.findAll({ + rule: { pattern: `${bindingPath}($RESOURCE, $TIMEOUT)` }, + }); + + for (const match of matches) { + const resourceNode = match.getMatch('RESOURCE'); + const timeoutNode = match.getMatch('TIMEOUT'); + if (!resourceNode || !timeoutNode) continue; + + if (!isSafeResourceTarget(resourceNode)) continue; + + const statement = findParentStatement(match); + if (!statement) continue; + + if (handledStatements.has(statement.id())) continue; + handledStatements.add(statement.id()); + + const indent = getLineIndent(sourceCode, statement.range().start.index); + const resourceText = resourceNode.text(); + const timeoutText = timeoutNode.text(); + const childIndent = indent + indentUnit; + const innerIndent = childIndent + indentUnit; + + const replacement = + `${resourceText}._idleTimeout = ${timeoutText};${EOL}` + + `${indent}${resourceText}.timeout = setTimeout(() => {${EOL}` + + `${childIndent}if (typeof ${resourceText}._onTimeout === "function") {${EOL}` + + `${innerIndent}${resourceText}._onTimeout();${EOL}` + + `${childIndent}}${EOL}` + + `${indent}}, ${timeoutText});`; + + edits.push(statement.replace(replacement)); + } + } + + if (!edits.length) return null; + + return rootNode.commitEdits(edits); +} diff --git a/recipes/timers-deprecations/src/unenroll-to-clear-timer.ts b/recipes/timers-deprecations/src/unenroll-to-clear-timer.ts new file mode 100644 index 00000000..071c9a61 --- /dev/null +++ b/recipes/timers-deprecations/src/unenroll-to-clear-timer.ts @@ -0,0 +1,70 @@ +import { EOL } from 'node:os'; +import { + getNodeImportStatements, + getNodeImportCalls, +} from '@nodejs/codemod-utils/ast-grep/import-statement'; +import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call'; +import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path'; +import { + findParentStatement, + isSafeResourceTarget, +} from '@nodejs/codemod-utils/ast-grep/general'; +import { getLineIndent } from '@nodejs/codemod-utils/ast-grep/indent'; +import type { Edit, SgRoot } from '@codemod.com/jssg-types/main'; +import type Js from '@codemod.com/jssg-types/langs/javascript'; + +const TARGET_METHOD = 'unenroll'; + +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const sourceCode = rootNode.text(); + const edits: Edit[] = []; + const handledStatements = new Set(); + + const importNodes = [ + ...getNodeRequireCalls(root, 'timers'), + ...getNodeImportStatements(root, 'timers'), + ...getNodeImportCalls(root, 'timers'), + ]; + + for (const importNode of importNodes) { + if (importNode.kind() === 'expression_statement') continue; + const bindingPath = resolveBindingPath(importNode, `$.${TARGET_METHOD}`); + if (!bindingPath) continue; + + const matches = rootNode.findAll({ + rule: { + any: [ + { pattern: `${bindingPath}($RESOURCE)` }, + { pattern: `${bindingPath}($RESOURCE, $$$REST)` }, + ], + }, + }); + + for (const match of matches) { + const resourceNode = match.getMatch('RESOURCE'); + if (!resourceNode) continue; + + if (!isSafeResourceTarget(resourceNode)) continue; + + const statement = findParentStatement(match); + if (!statement) continue; + + if (handledStatements.has(statement.id())) continue; + handledStatements.add(statement.id()); + + const indent = getLineIndent(sourceCode, statement.range().start.index); + const resourceText = resourceNode.text(); + + const replacement = + `clearTimeout(${resourceText}.timeout);${EOL}` + + `${indent}delete ${resourceText}.timeout;`; + + edits.push(statement.replace(replacement)); + } + } + + if (!edits.length) return null; + + return rootNode.commitEdits(edits); +} diff --git a/recipes/timers-deprecations/src/unref-active-to-unref.ts b/recipes/timers-deprecations/src/unref-active-to-unref.ts new file mode 100644 index 00000000..cdd71fbb --- /dev/null +++ b/recipes/timers-deprecations/src/unref-active-to-unref.ts @@ -0,0 +1,83 @@ +import { EOL } from 'node:os'; +import { + getNodeImportStatements, + getNodeImportCalls, +} from '@nodejs/codemod-utils/ast-grep/import-statement'; +import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call'; +import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path'; +import { + findParentStatement, + isSafeResourceTarget, +} from '@nodejs/codemod-utils/ast-grep/general'; +import { + detectIndentUnit, + getLineIndent, +} from '@nodejs/codemod-utils/ast-grep/indent'; +import type { Edit, SgRoot } from '@codemod.com/jssg-types/main'; +import type Js from '@codemod.com/jssg-types/langs/javascript'; + +const TARGET_METHOD = '_unrefActive'; + +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const sourceCode = rootNode.text(); + const indentUnit = detectIndentUnit(sourceCode); + const edits: Edit[] = []; + const handledStatements = new Set(); + + const importNodes = [ + ...getNodeRequireCalls(root, 'timers'), + ...getNodeImportStatements(root, 'timers'), + ...getNodeImportCalls(root, 'timers'), + ]; + + for (const importNode of importNodes) { + if (importNode.kind() === 'expression_statement') continue; + const bindingPath = resolveBindingPath(importNode, `$.${TARGET_METHOD}`); + if (!bindingPath) continue; + + const matches = rootNode.findAll({ + rule: { + any: [ + { pattern: `${bindingPath}($RESOURCE)` }, + { pattern: `${bindingPath}($RESOURCE, $$$REST)` }, + ], + }, + }); + + for (const match of matches) { + const resourceNode = match.getMatch('RESOURCE'); + if (!resourceNode) continue; + + if (!isSafeResourceTarget(resourceNode)) continue; + + const statement = findParentStatement(match); + if (!statement) continue; + + if (handledStatements.has(statement.id())) continue; + handledStatements.add(statement.id()); + + const indent = getLineIndent(sourceCode, statement.range().start.index); + const resourceText = resourceNode.text(); + const childIndent = indent + indentUnit; + const innerIndent = childIndent + indentUnit; + + const replacement = + `if (${resourceText}.timeout != null) {${EOL}` + + `${childIndent}clearTimeout(${resourceText}.timeout);${EOL}` + + `${indent}}${EOL}${EOL}` + + `${indent}${resourceText}.timeout = setTimeout(() => {${EOL}` + + `${childIndent}if (typeof ${resourceText}._onTimeout === "function") {${EOL}` + + `${innerIndent}${resourceText}._onTimeout();${EOL}` + + `${childIndent}}${EOL}` + + `${indent}}, ${resourceText}._idleTimeout);${EOL}` + + `${indent}${resourceText}.timeout.unref?.();`; + + edits.push(statement.replace(replacement)); + } + } + + if (!edits.length) return null; + + return rootNode.commitEdits(edits); +} diff --git a/recipes/timers-deprecations/tests/active/expected/active_import-variants.js b/recipes/timers-deprecations/tests/active/expected/active_import-variants.js new file mode 100644 index 00000000..8e69f9ac --- /dev/null +++ b/recipes/timers-deprecations/tests/active/expected/active_import-variants.js @@ -0,0 +1,92 @@ +const timersNamespace = require("node:timers"); +const { active: activeAlias } = require("node:timers"); + +import timersDefault from "node:timers"; +import { active as markActive } from "node:timers"; +import * as timersESMNamespace from "node:timers"; + +async function fromNamespace(resource) { + if (resource.target.timeout != null) { + clearTimeout(resource.target.timeout); + } + + resource.target.timeout = setTimeout(() => { + if (typeof resource.target._onTimeout === "function") { + resource.target._onTimeout(); + } + }, resource.target._idleTimeout); +} + +function fromCjsAlias(resource) { + if (resource.target.timeout != null) { + clearTimeout(resource.target.timeout); + } + + resource.target.timeout = setTimeout(() => { + if (typeof resource.target._onTimeout === "function") { + resource.target._onTimeout(); + } + }, resource.target._idleTimeout); +} + +function fromEsmDefault(resource) { + if (resource.timeout != null) { + clearTimeout(resource.timeout); + } + + resource.timeout = setTimeout(() => { + if (typeof resource._onTimeout === "function") { + resource._onTimeout(); + } + }, resource._idleTimeout); +} + +function fromEsmNamed(resource) { + if (resource.item.timeout != null) { + clearTimeout(resource.item.timeout); + } + + resource.item.timeout = setTimeout(() => { + if (typeof resource.item._onTimeout === "function") { + resource.item._onTimeout(); + } + }, resource.item._idleTimeout); +} + +function fromEsmNamespace(resource) { + if (resource.node.timeout != null) { + clearTimeout(resource.node.timeout); + } + + resource.node.timeout = setTimeout(() => { + if (typeof resource.node._onTimeout === "function") { + resource.node._onTimeout(); + } + }, resource.node._idleTimeout); +} + +async function fromDynamic(resource) { + const { active } = await import("node:timers"); + if (resource.session.timeout != null) { + clearTimeout(resource.session.timeout); + } + + resource.session.timeout = setTimeout(() => { + if (typeof resource.session._onTimeout === "function") { + resource.session._onTimeout(); + } + }, resource.session._idleTimeout); +} + +async function fromDynamicAlias(resource) { + const { active: activateDynamic } = await import("node:timers"); + if (resource.session.timeout != null) { + clearTimeout(resource.session.timeout); + } + + resource.session.timeout = setTimeout(() => { + if (typeof resource.session._onTimeout === "function") { + resource.session._onTimeout(); + } + }, resource.session._idleTimeout); +} diff --git a/recipes/timers-deprecations/tests/active/expected/basic.js b/recipes/timers-deprecations/tests/active/expected/basic.js new file mode 100644 index 00000000..dbfce297 --- /dev/null +++ b/recipes/timers-deprecations/tests/active/expected/basic.js @@ -0,0 +1,19 @@ +const timers = require("node:timers"); + +const resource = { + _idleTimeout: 500, + timeout: setTimeout(() => { }, 500), + _onTimeout() { + console.log("again"); + }, +}; + +if (resource.timeout != null) { + clearTimeout(resource.timeout); +} + +resource.timeout = setTimeout(() => { + if (typeof resource._onTimeout === "function") { + resource._onTimeout(); + } +}, resource._idleTimeout); diff --git a/recipes/timers-deprecations/tests/active/expected/destructured.js b/recipes/timers-deprecations/tests/active/expected/destructured.js new file mode 100644 index 00000000..fd1991a5 --- /dev/null +++ b/recipes/timers-deprecations/tests/active/expected/destructured.js @@ -0,0 +1,19 @@ +const { active } = require("node:timers"); + +const handle = { + _idleTimeout: 750, + timeout: setTimeout(() => { }, 750), + _onTimeout() { + console.log("tick"); + }, +}; + +if (handle.timeout != null) { + clearTimeout(handle.timeout); +} + +handle.timeout = setTimeout(() => { + if (typeof handle._onTimeout === "function") { + handle._onTimeout(); + } +}, handle._idleTimeout); diff --git a/recipes/timers-deprecations/tests/active/input/active_import-variants.js b/recipes/timers-deprecations/tests/active/input/active_import-variants.js new file mode 100644 index 00000000..3c179a3a --- /dev/null +++ b/recipes/timers-deprecations/tests/active/input/active_import-variants.js @@ -0,0 +1,36 @@ +const timersNamespace = require("node:timers"); +const { active: activeAlias } = require("node:timers"); + +import timersDefault from "node:timers"; +import { active as markActive } from "node:timers"; +import * as timersESMNamespace from "node:timers"; + +async function fromNamespace(resource) { + timersNamespace.active(resource.target); +} + +function fromCjsAlias(resource) { + activeAlias(resource.target); +} + +function fromEsmDefault(resource) { + timersDefault.active(resource); +} + +function fromEsmNamed(resource) { + markActive(resource.item); +} + +function fromEsmNamespace(resource) { + timersESMNamespace.active(resource.node); +} + +async function fromDynamic(resource) { + const { active } = await import("node:timers"); + active(resource.session); +} + +async function fromDynamicAlias(resource) { + const { active: activateDynamic } = await import("node:timers"); + activateDynamic(resource.session); +} diff --git a/recipes/timers-deprecations/tests/active/input/basic.js b/recipes/timers-deprecations/tests/active/input/basic.js new file mode 100644 index 00000000..04d5897c --- /dev/null +++ b/recipes/timers-deprecations/tests/active/input/basic.js @@ -0,0 +1,11 @@ +const timers = require("node:timers"); + +const resource = { + _idleTimeout: 500, + timeout: setTimeout(() => { }, 500), + _onTimeout() { + console.log("again"); + }, +}; + +timers.active(resource); diff --git a/recipes/timers-deprecations/tests/active/input/destructured.js b/recipes/timers-deprecations/tests/active/input/destructured.js new file mode 100644 index 00000000..d6a51681 --- /dev/null +++ b/recipes/timers-deprecations/tests/active/input/destructured.js @@ -0,0 +1,11 @@ +const { active } = require("node:timers"); + +const handle = { + _idleTimeout: 750, + timeout: setTimeout(() => { }, 750), + _onTimeout() { + console.log("tick"); + }, +}; + +active(handle); diff --git a/recipes/timers-deprecations/tests/enroll/expected/dep0095-basic.js b/recipes/timers-deprecations/tests/enroll/expected/dep0095-basic.js new file mode 100644 index 00000000..7510abdc --- /dev/null +++ b/recipes/timers-deprecations/tests/enroll/expected/dep0095-basic.js @@ -0,0 +1,14 @@ +const timers = require("node:timers"); + +const resource = { + _onTimeout() { + console.log("done"); + }, +}; + +resource._idleTimeout = 1000; +resource.timeout = setTimeout(() => { + if (typeof resource._onTimeout === "function") { + resource._onTimeout(); + } +}, 1000); diff --git a/recipes/timers-deprecations/tests/enroll/expected/dep0095-destructured.js b/recipes/timers-deprecations/tests/enroll/expected/dep0095-destructured.js new file mode 100644 index 00000000..39cc0a20 --- /dev/null +++ b/recipes/timers-deprecations/tests/enroll/expected/dep0095-destructured.js @@ -0,0 +1,14 @@ +const { enroll } = require("node:timers"); + +const scope = { + _onTimeout() { + console.log("refresh"); + }, +}; + +scope._idleTimeout = 250; +scope.timeout = setTimeout(() => { + if (typeof scope._onTimeout === "function") { + scope._onTimeout(); + } +}, 250); diff --git a/recipes/timers-deprecations/tests/enroll/expected/dep0095-import-variants.js b/recipes/timers-deprecations/tests/enroll/expected/dep0095-import-variants.js new file mode 100644 index 00000000..f94fc98e --- /dev/null +++ b/recipes/timers-deprecations/tests/enroll/expected/dep0095-import-variants.js @@ -0,0 +1,75 @@ +const timersNamespace = require("node:timers"); +const { enroll: enrollAlias } = require("node:timers"); + +import timersDefault from "node:timers"; +import { enroll as enrollRenamed } from "node:timers"; +import * as timersESMNamespace from "node:timers"; + +async function fromNamespace(resource) { + resource.target._idleTimeout = resource.delay; + resource.target.timeout = setTimeout(() => { + if (typeof resource.target._onTimeout === "function") { + resource.target._onTimeout(); + } + }, resource.delay); +} + +function fromCjsAlias(resource) { + resource.target._idleTimeout = resource.delay + 5; + resource.target.timeout = setTimeout(() => { + if (typeof resource.target._onTimeout === "function") { + resource.target._onTimeout(); + } + }, resource.delay + 5); +} + +function fromEsmDefault(resource) { + resource._idleTimeout = 100; + resource.timeout = setTimeout(() => { + if (typeof resource._onTimeout === "function") { + resource._onTimeout(); + } + }, 100); +} + +function fromEsmNamed(resource) { + resource._idleTimeout = getDelay(); + resource.timeout = setTimeout(() => { + if (typeof resource._onTimeout === "function") { + resource._onTimeout(); + } + }, getDelay()); +} + +function fromEsmNamespace(resource) { + resource.item._idleTimeout = resource.timeout; + resource.item.timeout = setTimeout(() => { + if (typeof resource.item._onTimeout === "function") { + resource.item._onTimeout(); + } + }, resource.timeout); +} + +async function fromDynamic(resource) { + const { enroll } = await import("node:timers"); + resource.node._idleTimeout = resource.delay; + resource.node.timeout = setTimeout(() => { + if (typeof resource.node._onTimeout === "function") { + resource.node._onTimeout(); + } + }, resource.delay); +} + +async function fromDynamicAlias(resource) { + const { enroll: load } = await import("node:timers"); + resource.node._idleTimeout = 50; + resource.node.timeout = setTimeout(() => { + if (typeof resource.node._onTimeout === "function") { + resource.node._onTimeout(); + } + }, 50); +} + +function getDelay() { + return 300; +} diff --git a/recipes/timers-deprecations/tests/enroll/input/dep0095-basic.js b/recipes/timers-deprecations/tests/enroll/input/dep0095-basic.js new file mode 100644 index 00000000..05508c87 --- /dev/null +++ b/recipes/timers-deprecations/tests/enroll/input/dep0095-basic.js @@ -0,0 +1,9 @@ +const timers = require("node:timers"); + +const resource = { + _onTimeout() { + console.log("done"); + }, +}; + +timers.enroll(resource, 1000); diff --git a/recipes/timers-deprecations/tests/enroll/input/dep0095-destructured.js b/recipes/timers-deprecations/tests/enroll/input/dep0095-destructured.js new file mode 100644 index 00000000..e4ae6657 --- /dev/null +++ b/recipes/timers-deprecations/tests/enroll/input/dep0095-destructured.js @@ -0,0 +1,9 @@ +const { enroll } = require("node:timers"); + +const scope = { + _onTimeout() { + console.log("refresh"); + }, +}; + +enroll(scope, 250); diff --git a/recipes/timers-deprecations/tests/enroll/input/dep0095-import-variants.js b/recipes/timers-deprecations/tests/enroll/input/dep0095-import-variants.js new file mode 100644 index 00000000..7894f561 --- /dev/null +++ b/recipes/timers-deprecations/tests/enroll/input/dep0095-import-variants.js @@ -0,0 +1,40 @@ +const timersNamespace = require("node:timers"); +const { enroll: enrollAlias } = require("node:timers"); + +import timersDefault from "node:timers"; +import { enroll as enrollRenamed } from "node:timers"; +import * as timersESMNamespace from "node:timers"; + +async function fromNamespace(resource) { + timersNamespace.enroll(resource.target, resource.delay); +} + +function fromCjsAlias(resource) { + enrollAlias(resource.target, resource.delay + 5); +} + +function fromEsmDefault(resource) { + timersDefault.enroll(resource, 100); +} + +function fromEsmNamed(resource) { + enrollRenamed(resource, getDelay()); +} + +function fromEsmNamespace(resource) { + timersESMNamespace.enroll(resource.item, resource.timeout); +} + +async function fromDynamic(resource) { + const { enroll } = await import("node:timers"); + enroll(resource.node, resource.delay); +} + +async function fromDynamicAlias(resource) { + const { enroll: load } = await import("node:timers"); + load(resource.node, 50); +} + +function getDelay() { + return 300; +} diff --git a/recipes/timers-deprecations/tests/imports/expected/basic.js b/recipes/timers-deprecations/tests/imports/expected/basic.js new file mode 100644 index 00000000..dbf3e5f4 --- /dev/null +++ b/recipes/timers-deprecations/tests/imports/expected/basic.js @@ -0,0 +1,17 @@ +const resource = { + _idleTimeout: 100, + timeout: setTimeout(() => { }, 100), + _onTimeout() { + console.log("cleanup"); + }, +}; + +if (resource.timeout != null) { + clearTimeout(resource.timeout); +} + +resource.timeout = setTimeout(() => { + if (typeof resource._onTimeout === "function") { + resource._onTimeout(); + } +}, resource._idleTimeout); diff --git a/recipes/timers-deprecations/tests/imports/expected/imports_dynamic.js b/recipes/timers-deprecations/tests/imports/expected/imports_dynamic.js new file mode 100644 index 00000000..8abc284f --- /dev/null +++ b/recipes/timers-deprecations/tests/imports/expected/imports_dynamic.js @@ -0,0 +1,39 @@ +async function cleanupEnroll(resource) { + resource._idleTimeout = resource.delay; + resource.timeout = setTimeout(() => { + if (typeof resource._onTimeout === "function") { + resource._onTimeout(); + } + }, resource.delay); +} + +async function cleanupAlias(resource) { + if (resource.timeout != null) { + clearTimeout(resource.timeout); + } + + resource.timeout = setTimeout(() => { + if (typeof resource._onTimeout === "function") { + resource._onTimeout(); + } + }, resource._idleTimeout); +} + +async function cleanupDefault(resource) { + const timersModule = await import("node:timers"); + resource.timeout = setTimeout(() => { + if (typeof resource._onTimeout === "function") { + resource._onTimeout(); + } + }, resource._idleTimeout); +} + +async function preserveNamespace(resource) { + const timersModule = await import("node:timers"); + resource.timeout = setTimeout(() => { + if (typeof resource._onTimeout === "function") { + resource._onTimeout(); + } + }, resource._idleTimeout); + return timersModule; +} diff --git a/recipes/timers-deprecations/tests/imports/expected/named.js b/recipes/timers-deprecations/tests/imports/expected/named.js new file mode 100644 index 00000000..674b9638 --- /dev/null +++ b/recipes/timers-deprecations/tests/imports/expected/named.js @@ -0,0 +1,6 @@ +function setup(resource) { + resource._idleTimeout = 42; + resource.timeout = setTimeout(() => { }, 42); +} + +setup({}); diff --git a/recipes/timers-deprecations/tests/imports/input/basic.js b/recipes/timers-deprecations/tests/imports/input/basic.js new file mode 100644 index 00000000..3387e713 --- /dev/null +++ b/recipes/timers-deprecations/tests/imports/input/basic.js @@ -0,0 +1,19 @@ +import timers from "node:timers"; + +const resource = { + _idleTimeout: 100, + timeout: setTimeout(() => { }, 100), + _onTimeout() { + console.log("cleanup"); + }, +}; + +if (resource.timeout != null) { + clearTimeout(resource.timeout); +} + +resource.timeout = setTimeout(() => { + if (typeof resource._onTimeout === "function") { + resource._onTimeout(); + } +}, resource._idleTimeout); diff --git a/recipes/timers-deprecations/tests/imports/input/imports_dynamic.js b/recipes/timers-deprecations/tests/imports/input/imports_dynamic.js new file mode 100644 index 00000000..c078a18d --- /dev/null +++ b/recipes/timers-deprecations/tests/imports/input/imports_dynamic.js @@ -0,0 +1,41 @@ +async function cleanupEnroll(resource) { + const { enroll } = await import("node:timers"); + resource._idleTimeout = resource.delay; + resource.timeout = setTimeout(() => { + if (typeof resource._onTimeout === "function") { + resource._onTimeout(); + } + }, resource.delay); +} + +async function cleanupAlias(resource) { + const { active: activate } = await import("node:timers"); + if (resource.timeout != null) { + clearTimeout(resource.timeout); + } + + resource.timeout = setTimeout(() => { + if (typeof resource._onTimeout === "function") { + resource._onTimeout(); + } + }, resource._idleTimeout); +} + +async function cleanupDefault(resource) { + const timersModule = await import("node:timers"); + resource.timeout = setTimeout(() => { + if (typeof resource._onTimeout === "function") { + resource._onTimeout(); + } + }, resource._idleTimeout); +} + +async function preserveNamespace(resource) { + const timersModule = await import("node:timers"); + resource.timeout = setTimeout(() => { + if (typeof resource._onTimeout === "function") { + resource._onTimeout(); + } + }, resource._idleTimeout); + return timersModule; +} diff --git a/recipes/timers-deprecations/tests/imports/input/named.js b/recipes/timers-deprecations/tests/imports/input/named.js new file mode 100644 index 00000000..8c0c76e1 --- /dev/null +++ b/recipes/timers-deprecations/tests/imports/input/named.js @@ -0,0 +1,8 @@ +const { enroll, active } = require("node:timers"); + +function setup(resource) { + resource._idleTimeout = 42; + resource.timeout = setTimeout(() => { }, 42); +} + +setup({}); diff --git a/recipes/timers-deprecations/tests/unenroll/expected/dep0096-basic.js b/recipes/timers-deprecations/tests/unenroll/expected/dep0096-basic.js new file mode 100644 index 00000000..5cf62448 --- /dev/null +++ b/recipes/timers-deprecations/tests/unenroll/expected/dep0096-basic.js @@ -0,0 +1,8 @@ +const timers = require("node:timers"); + +const resource = { + timeout: setTimeout(() => { }, 1000), +}; + +clearTimeout(resource.timeout); +delete resource.timeout; diff --git a/recipes/timers-deprecations/tests/unenroll/expected/dep0096-destructured.js b/recipes/timers-deprecations/tests/unenroll/expected/dep0096-destructured.js new file mode 100644 index 00000000..edbefa3f --- /dev/null +++ b/recipes/timers-deprecations/tests/unenroll/expected/dep0096-destructured.js @@ -0,0 +1,8 @@ +const { unenroll } = require("node:timers"); + +const queue = { + timeout: setTimeout(() => { }, 200), +}; + +clearTimeout(queue.timeout); +delete queue.timeout; diff --git a/recipes/timers-deprecations/tests/unenroll/expected/dep0096-import-variants.js b/recipes/timers-deprecations/tests/unenroll/expected/dep0096-import-variants.js new file mode 100644 index 00000000..8406a392 --- /dev/null +++ b/recipes/timers-deprecations/tests/unenroll/expected/dep0096-import-variants.js @@ -0,0 +1,43 @@ +const timersNamespace = require("node:timers"); +const { unenroll: cancelAlias } = require("node:timers"); + +import timersDefault from "node:timers"; +import { unenroll as release } from "node:timers"; +import * as timersESMNamespace from "node:timers"; + +async function fromNamespace(resource) { + clearTimeout(resource.target.timeout); + delete resource.target.timeout; +} + +function fromCjsAlias(resource) { + clearTimeout(resource.target.timeout); + delete resource.target.timeout; +} + +function fromEsmDefault(resource) { + clearTimeout(resource.timeout); + delete resource.timeout; +} + +function fromEsmNamed(resource) { + clearTimeout(resource.item.timeout); + delete resource.item.timeout; +} + +function fromEsmNamespace(resource) { + clearTimeout(resource.node.timeout); + delete resource.node.timeout; +} + +async function fromDynamic(resource) { + const { unenroll } = await import("node:timers"); + clearTimeout(resource.session.timeout); + delete resource.session.timeout; +} + +async function fromDynamicAlias(resource) { + const { unenroll: cancel } = await import("node:timers"); + clearTimeout(resource.session.timeout); + delete resource.session.timeout; +} diff --git a/recipes/timers-deprecations/tests/unenroll/input/dep0096-basic.js b/recipes/timers-deprecations/tests/unenroll/input/dep0096-basic.js new file mode 100644 index 00000000..e51a0d2e --- /dev/null +++ b/recipes/timers-deprecations/tests/unenroll/input/dep0096-basic.js @@ -0,0 +1,7 @@ +const timers = require("node:timers"); + +const resource = { + timeout: setTimeout(() => { }, 1000), +}; + +timers.unenroll(resource); diff --git a/recipes/timers-deprecations/tests/unenroll/input/dep0096-destructured.js b/recipes/timers-deprecations/tests/unenroll/input/dep0096-destructured.js new file mode 100644 index 00000000..52652231 --- /dev/null +++ b/recipes/timers-deprecations/tests/unenroll/input/dep0096-destructured.js @@ -0,0 +1,7 @@ +const { unenroll } = require("node:timers"); + +const queue = { + timeout: setTimeout(() => { }, 200), +}; + +unenroll(queue); diff --git a/recipes/timers-deprecations/tests/unenroll/input/dep0096-import-variants.js b/recipes/timers-deprecations/tests/unenroll/input/dep0096-import-variants.js new file mode 100644 index 00000000..44c55a43 --- /dev/null +++ b/recipes/timers-deprecations/tests/unenroll/input/dep0096-import-variants.js @@ -0,0 +1,36 @@ +const timersNamespace = require("node:timers"); +const { unenroll: cancelAlias } = require("node:timers"); + +import timersDefault from "node:timers"; +import { unenroll as release } from "node:timers"; +import * as timersESMNamespace from "node:timers"; + +async function fromNamespace(resource) { + timersNamespace.unenroll(resource.target); +} + +function fromCjsAlias(resource) { + cancelAlias(resource.target); +} + +function fromEsmDefault(resource) { + timersDefault.unenroll(resource); +} + +function fromEsmNamed(resource) { + release(resource.item); +} + +function fromEsmNamespace(resource) { + timersESMNamespace.unenroll(resource.node); +} + +async function fromDynamic(resource) { + const { unenroll } = await import("node:timers"); + unenroll(resource.session); +} + +async function fromDynamicAlias(resource) { + const { unenroll: cancel } = await import("node:timers"); + cancel(resource.session); +} diff --git a/recipes/timers-deprecations/tests/unref/expected/basic.js b/recipes/timers-deprecations/tests/unref/expected/basic.js new file mode 100644 index 00000000..188d52b7 --- /dev/null +++ b/recipes/timers-deprecations/tests/unref/expected/basic.js @@ -0,0 +1,20 @@ +const timers = require("node:timers"); + +const resource = { + _idleTimeout: 60, + timeout: setTimeout(() => { }, 60), + _onTimeout() { + console.log("cleanup"); + }, +}; + +if (resource.timeout != null) { + clearTimeout(resource.timeout); +} + +resource.timeout = setTimeout(() => { + if (typeof resource._onTimeout === "function") { + resource._onTimeout(); + } +}, resource._idleTimeout); +resource.timeout.unref?.(); diff --git a/recipes/timers-deprecations/tests/unref/expected/destructured.js b/recipes/timers-deprecations/tests/unref/expected/destructured.js new file mode 100644 index 00000000..85a8cc76 --- /dev/null +++ b/recipes/timers-deprecations/tests/unref/expected/destructured.js @@ -0,0 +1,20 @@ +const { _unrefActive } = require("node:timers"); + +const task = { + _idleTimeout: 90, + timeout: setTimeout(() => { }, 90), + _onTimeout() { + console.log("idle"); + }, +}; + +if (task.timeout != null) { + clearTimeout(task.timeout); +} + +task.timeout = setTimeout(() => { + if (typeof task._onTimeout === "function") { + task._onTimeout(); + } +}, task._idleTimeout); +task.timeout.unref?.(); diff --git a/recipes/timers-deprecations/tests/unref/expected/unref_import-variants.js b/recipes/timers-deprecations/tests/unref/expected/unref_import-variants.js new file mode 100644 index 00000000..b8c0c4fd --- /dev/null +++ b/recipes/timers-deprecations/tests/unref/expected/unref_import-variants.js @@ -0,0 +1,99 @@ +const timersNamespace = require("node:timers"); +const { _unrefActive: unrefAlias } = require("node:timers"); + +import timersDefault from "node:timers"; +import { _unrefActive as unref } from "node:timers"; +import * as timersESMNamespace from "node:timers"; + +async function fromNamespace(resource) { + if (resource.target.timeout != null) { + clearTimeout(resource.target.timeout); + } + + resource.target.timeout = setTimeout(() => { + if (typeof resource.target._onTimeout === "function") { + resource.target._onTimeout(); + } + }, resource.target._idleTimeout); + resource.target.timeout.unref?.(); +} + +function fromCjsAlias(resource) { + if (resource.target.timeout != null) { + clearTimeout(resource.target.timeout); + } + + resource.target.timeout = setTimeout(() => { + if (typeof resource.target._onTimeout === "function") { + resource.target._onTimeout(); + } + }, resource.target._idleTimeout); + resource.target.timeout.unref?.(); +} + +function fromEsmDefault(resource) { + if (resource.timeout != null) { + clearTimeout(resource.timeout); + } + + resource.timeout = setTimeout(() => { + if (typeof resource._onTimeout === "function") { + resource._onTimeout(); + } + }, resource._idleTimeout); + resource.timeout.unref?.(); +} + +function fromEsmNamed(resource) { + if (resource.item.timeout != null) { + clearTimeout(resource.item.timeout); + } + + resource.item.timeout = setTimeout(() => { + if (typeof resource.item._onTimeout === "function") { + resource.item._onTimeout(); + } + }, resource.item._idleTimeout); + resource.item.timeout.unref?.(); +} + +function fromEsmNamespace(resource) { + if (resource.node.timeout != null) { + clearTimeout(resource.node.timeout); + } + + resource.node.timeout = setTimeout(() => { + if (typeof resource.node._onTimeout === "function") { + resource.node._onTimeout(); + } + }, resource.node._idleTimeout); + resource.node.timeout.unref?.(); +} + +async function fromDynamic(resource) { + const { _unrefActive } = await import("node:timers"); + if (resource.session.timeout != null) { + clearTimeout(resource.session.timeout); + } + + resource.session.timeout = setTimeout(() => { + if (typeof resource.session._onTimeout === "function") { + resource.session._onTimeout(); + } + }, resource.session._idleTimeout); + resource.session.timeout.unref?.(); +} + +async function fromDynamicAlias(resource) { + const { _unrefActive: unrefDynamic } = await import("node:timers"); + if (resource.session.timeout != null) { + clearTimeout(resource.session.timeout); + } + + resource.session.timeout = setTimeout(() => { + if (typeof resource.session._onTimeout === "function") { + resource.session._onTimeout(); + } + }, resource.session._idleTimeout); + resource.session.timeout.unref?.(); +} diff --git a/recipes/timers-deprecations/tests/unref/input/basic.js b/recipes/timers-deprecations/tests/unref/input/basic.js new file mode 100644 index 00000000..1c48ca25 --- /dev/null +++ b/recipes/timers-deprecations/tests/unref/input/basic.js @@ -0,0 +1,11 @@ +const timers = require("node:timers"); + +const resource = { + _idleTimeout: 60, + timeout: setTimeout(() => { }, 60), + _onTimeout() { + console.log("cleanup"); + }, +}; + +timers._unrefActive(resource); diff --git a/recipes/timers-deprecations/tests/unref/input/destructured.js b/recipes/timers-deprecations/tests/unref/input/destructured.js new file mode 100644 index 00000000..b1ec9493 --- /dev/null +++ b/recipes/timers-deprecations/tests/unref/input/destructured.js @@ -0,0 +1,11 @@ +const { _unrefActive } = require("node:timers"); + +const task = { + _idleTimeout: 90, + timeout: setTimeout(() => { }, 90), + _onTimeout() { + console.log("idle"); + }, +}; + +_unrefActive(task); diff --git a/recipes/timers-deprecations/tests/unref/input/unref_import-variants.js b/recipes/timers-deprecations/tests/unref/input/unref_import-variants.js new file mode 100644 index 00000000..d9f4aad8 --- /dev/null +++ b/recipes/timers-deprecations/tests/unref/input/unref_import-variants.js @@ -0,0 +1,36 @@ +const timersNamespace = require("node:timers"); +const { _unrefActive: unrefAlias } = require("node:timers"); + +import timersDefault from "node:timers"; +import { _unrefActive as unref } from "node:timers"; +import * as timersESMNamespace from "node:timers"; + +async function fromNamespace(resource) { + timersNamespace._unrefActive(resource.target); +} + +function fromCjsAlias(resource) { + unrefAlias(resource.target); +} + +function fromEsmDefault(resource) { + timersDefault._unrefActive(resource); +} + +function fromEsmNamed(resource) { + unref(resource.item); +} + +function fromEsmNamespace(resource) { + timersESMNamespace._unrefActive(resource.node); +} + +async function fromDynamic(resource) { + const { _unrefActive } = await import("node:timers"); + _unrefActive(resource.session); +} + +async function fromDynamicAlias(resource) { + const { _unrefActive: unrefDynamic } = await import("node:timers"); + unrefDynamic(resource.session); +} diff --git a/recipes/timers-deprecations/workflow.yaml b/recipes/timers-deprecations/workflow.yaml new file mode 100644 index 00000000..6c55e026 --- /dev/null +++ b/recipes/timers-deprecations/workflow.yaml @@ -0,0 +1,91 @@ +# 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 + runtime: + type: direct + steps: + - name: Replace `timers.enroll()` with `setTimeout()` + js-ast-grep: + js_file: src/enroll-to-set-timeout.ts + base_path: . + include: + - "**/*.cjs" + - "**/*.cts" + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript + - name: Replace `timers.unenroll()` with standard clear APIs + js-ast-grep: + js_file: src/unenroll-to-clear-timer.ts + base_path: . + include: + - "**/*.cjs" + - "**/*.cts" + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript + - name: Remove usages of `timers.active()` + js-ast-grep: + js_file: src/active-to-standard-timer.ts + base_path: . + include: + - "**/*.cjs" + - "**/*.cts" + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript + - name: Replace `timers._unrefActive()` with `setTimeout().unref()` + js-ast-grep: + js_file: src/unref-active-to-unref.ts + base_path: . + include: + - "**/*.cjs" + - "**/*.cts" + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript + - name: Clean up `node:timers` imports and requires + js-ast-grep: + js_file: src/cleanup-imports.ts + base_path: . + include: + - "**/*.cjs" + - "**/*.cts" + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript diff --git a/utils/src/ast-grep/general.test.ts b/utils/src/ast-grep/general.test.ts new file mode 100644 index 00000000..fa09f195 --- /dev/null +++ b/utils/src/ast-grep/general.test.ts @@ -0,0 +1,61 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import astGrep from '@ast-grep/napi'; +import { findParentStatement, isSafeResourceTarget } from './general.ts'; // ts ext is needed + +describe('findParentStatement', () => { + it('should find the parent expression_statement', () => { + const code = 'function test() { x + 1; }'; + const root = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = root.root().find({ + rule: { kind: 'binary_expression' }, + }); + + const parentStatement = findParentStatement(node); + assert.ok(parentStatement); + assert.strictEqual(parentStatement?.kind(), 'expression_statement'); + }); + + it('should return null if no parent statement is found', () => { + const code = 'const x = 5;'; + const root = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = root.root().find({ + rule: { kind: 'identifier' }, + }); + + const parentStatement = findParentStatement(node); + assert.strictEqual(parentStatement, null); + }); +}); + +describe('isSafeResourceTarget', () => { + it('should return true for an identifier', () => { + const code = 'function test() { const x = 5; }'; + const root = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = root.root().find({ + rule: { kind: 'identifier' }, + }); + + assert.strictEqual(isSafeResourceTarget(node), true); + }); + + it('should return true for a member_expression', () => { + const code = 'function test() { obj.prop = 5; }'; + const root = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = root.root().find({ + rule: { kind: 'member_expression' }, + }); + + assert.strictEqual(isSafeResourceTarget(node), true); + }); + + it('should return false for other node types', () => { + const code = 'function test() { 5 + 3; }'; + const root = astGrep.parse(astGrep.Lang.JavaScript, code); + const node = root.root().find({ + rule: { kind: 'binary_expression' }, + }); + + assert.strictEqual(isSafeResourceTarget(node), false); + }); +}); diff --git a/utils/src/ast-grep/general.ts b/utils/src/ast-grep/general.ts new file mode 100644 index 00000000..9f183b2f --- /dev/null +++ b/utils/src/ast-grep/general.ts @@ -0,0 +1,15 @@ +import type { SgNode } from '@codemod.com/jssg-types/main'; +import type Js from '@codemod.com/jssg-types/langs/javascript'; + +export function findParentStatement(node: SgNode): SgNode | null { + for (const ancestor of node.ancestors()) { + if (ancestor.kind() === 'expression_statement') { + return ancestor; + } + } + return null; +} + +export function isSafeResourceTarget(node: SgNode): boolean { + return node.is('identifier') || node.is('member_expression'); +} diff --git a/utils/src/ast-grep/indent.test.ts b/utils/src/ast-grep/indent.test.ts new file mode 100644 index 00000000..b5ecc41f --- /dev/null +++ b/utils/src/ast-grep/indent.test.ts @@ -0,0 +1,89 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { detectIndentUnit, getLineIndent } from './indent.ts'; + +describe('detectIndentUnit', () => { + it('should detect tab indentation', () => { + const source = '\tfunction test() {\n\t\tconsole.log("Hello");\n\t}'; + assert.equal(detectIndentUnit(source), '\t'); + }); + + it('should detect space indentation', () => { + const source = + ' function test() {\n console.log("Hello");\n }'; + assert.equal(detectIndentUnit(source), ' '); + }); + + it('should detect mixed indentation and return the most common one', () => { + const source = + ' function test() {\n console.log("Hello");\n\tconsole.log("World");\n }'; + assert.equal(detectIndentUnit(source), '\t'); + }); + + it('should return empty string if no indentation is found', () => { + const source = 'function test() {\nconsole.log("Hello");\n}'; + assert.equal(detectIndentUnit(source), '\t'); + }); + + it('should handle empty source', () => { + const source = ''; + assert.equal(detectIndentUnit(source), '\t'); + }); + + it('should handle lines with only whitespace', () => { + const source = ' \n\t\t\n '; + assert.equal(detectIndentUnit(source), '\t'); + }); + + it('should handle inconsistent indentation', () => { + const source = ' \t \t\n \t\n\t'; + assert.equal(detectIndentUnit(source), '\t'); + }); + + it('should handle no indentation at all', () => { + const source = 'function test() {\nconsole.log("Hello");\n}'; + assert.equal(detectIndentUnit(source), '\t'); + }); + + it('should handle a single line of code', () => { + const source = 'console.log("Hello");'; + assert.equal(detectIndentUnit(source), '\t'); + }); +}); + +describe('getLineIndent', () => { + it('should return the correct indentation for a given line index', () => { + const source = + 'function test() {\n console.log("Hello");\n\tconsole.log("World");\n}'; + assert.equal(getLineIndent(source, 25), ' '); + assert.equal(getLineIndent(source, 50), '\t'); + assert.equal(getLineIndent(source, 0), ''); + }); + + it('should handle lines with no indentation', () => { + const source = 'function test() {\nconsole.log("Hello");\n}'; + assert.equal(getLineIndent(source, 20), ''); + }); + + it('should handle empty source', () => { + const source = ''; + assert.equal(getLineIndent(source, 0), ''); + }); +}); + +describe('getLineIndent - additional tests', () => { + it('should handle index out of bounds', () => { + const source = 'function test() {\n console.log("Hello");\n}'; + assert.equal(getLineIndent(source, 100), ''); + }); + + it('should handle lines with only whitespace', () => { + const source = ' \n\t\t\n '; + assert.equal(getLineIndent(source, 1), ' '); + }); + + it('should handle an empty string', () => { + const source = ''; + assert.equal(getLineIndent(source, 0), ''); + }); +}); diff --git a/utils/src/ast-grep/indent.ts b/utils/src/ast-grep/indent.ts new file mode 100644 index 00000000..205a37e8 --- /dev/null +++ b/utils/src/ast-grep/indent.ts @@ -0,0 +1,48 @@ +import { gcd } from '../math.ts'; + +export function detectIndentUnit(source: string): string { + let tabIndent = ''; + const spaceIndents: number[] = []; + const lines = source.split(/\r?\n/); + + for (const line of lines) { + const match = line.match(/^(\s+)/); + if (!match) continue; + const leading = match[1]; + if (leading.includes('\t')) { + tabIndent = '\t'; + break; + } + spaceIndents.push(leading.length); + } + + if (tabIndent) return tabIndent; + if (!spaceIndents.length) return '\t'; + + const unit = spaceIndents.reduce((acc, len) => gcd(acc, len)); + return ' '.repeat(unit || spaceIndents[0]); +} + +export function getLineIndent(source: string, index: number): string { + let cursor = index; + while ( + cursor > 0 && + source[cursor - 1] !== '\n' && + source[cursor - 1] !== '\r' + ) { + cursor--; + } + + let indent = ''; + while (cursor < source.length) { + const char = source[cursor]; + if (char === ' ' || char === '\t') { + indent += char; + cursor++; + continue; + } + break; + } + + return indent; +} diff --git a/utils/src/math.test.ts b/utils/src/math.test.ts new file mode 100644 index 00000000..74682588 --- /dev/null +++ b/utils/src/math.test.ts @@ -0,0 +1,32 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { gcd } from './math.ts'; + +describe('gcd', () => { + it('should return the greatest common divisor of two numbers', () => { + assert.equal(gcd(48, 18), 6); + assert.equal(gcd(56, 98), 14); + assert.equal(gcd(101, 10), 1); + assert.equal(gcd(-48, 18), 6); + assert.equal(gcd(48, -18), 6); + assert.equal(gcd(-48, -18), 6); + assert.equal(gcd(0, 5), 5); + assert.equal(gcd(5, 0), 5); + assert.equal(gcd(0, 0), 0); + }); + + it('should throw TypeError if arguments are not numbers', () => { + assert.throws(() => gcd('48' as unknown as number, 18), { + name: 'TypeError', + message: 'Both arguments must be numbers.', + }); + assert.throws(() => gcd(48, null as unknown as number), { + name: 'TypeError', + message: 'Both arguments must be numbers.', + }); + assert.throws(() => gcd(undefined as unknown as number, 18), { + name: 'TypeError', + message: 'Both arguments must be numbers.', + }); + }); +}); diff --git a/utils/src/math.ts b/utils/src/math.ts new file mode 100644 index 00000000..9957a9a1 --- /dev/null +++ b/utils/src/math.ts @@ -0,0 +1,14 @@ +export function gcd(a: number, b: number): number { + if (typeof a !== 'number' || typeof b !== 'number') { + throw new TypeError('Both arguments must be numbers.'); + } + + let x = Math.abs(a); + let y = Math.abs(b); + while (y !== 0) { + const temp = y; + y = x % y; + x = temp; + } + return x; +}