Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions sdks/typescript/src/step.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { emitV0RemovedWarning } from './util/v0-deprecation-warning';

export * from './legacy/step';

console.warn(
'\x1b[31mDeprecation warning: The v0 sdk, including the step module has been deprecated and has been removed in release v1.14.0.\x1b[0m'
);
console.warn(
'\x1b[32mPlease migrate to v1 SDK instead: https://docs.hatchet.run/home/v1-sdk-improvements\x1b[0m'
);
console.warn('--------------------------------');
emitV0RemovedWarning('step');
117 changes: 117 additions & 0 deletions sdks/typescript/src/util/v0-deprecation-warning.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import {
V0_DEPRECATION_CODE,
_resetEmittedV0Warnings,
emitV0RemovedWarning,
} from './v0-deprecation-warning';

describe('emitV0RemovedWarning', () => {
let emitWarningSpy: jest.SpyInstance;

beforeEach(() => {
_resetEmittedV0Warnings();
emitWarningSpy = jest.spyOn(process, 'emitWarning').mockImplementation(() => {});
});

afterEach(() => {
emitWarningSpy.mockRestore();
});

it('emits via process.emitWarning with the stable HATCHET_V0_REMOVED code', () => {
emitV0RemovedWarning('workflow');

expect(emitWarningSpy).toHaveBeenCalledTimes(1);
const [[message, opts]] = emitWarningSpy.mock.calls;
expect(message).toContain('workflow module');
expect(message).toContain('v1.14.0');
expect(message).toContain('https://docs.hatchet.run/home/v1-sdk-improvements');
expect(opts).toMatchObject({
type: 'DeprecationWarning',
code: V0_DEPRECATION_CODE,
});
});

it('passes the optional detail string through to process.emitWarning', () => {
emitV0RemovedWarning('workflow', 'ConcurrencyLimitStrategy has moved.');

expect(emitWarningSpy).toHaveBeenCalledTimes(1);
const [[, opts]] = emitWarningSpy.mock.calls;
expect(opts.detail).toBe('ConcurrencyLimitStrategy has moved.');
});

it('deduplicates per submodule across repeated calls', () => {
emitV0RemovedWarning('workflow');
emitV0RemovedWarning('workflow');
emitV0RemovedWarning('workflow');

expect(emitWarningSpy).toHaveBeenCalledTimes(1);
});

it('emits separate warnings for different submodules', () => {
emitV0RemovedWarning('workflow');
emitV0RemovedWarning('step');

expect(emitWarningSpy).toHaveBeenCalledTimes(2);
expect(emitWarningSpy.mock.calls[0][0]).toContain('workflow module');
expect(emitWarningSpy.mock.calls[1][0]).toContain('step module');
});

it('routes to console.warn when process.throwDeprecation is set, bypassing emitWarning', () => {
// Under --throw-deprecation or process.throwDeprecation=true, Node queues
// `throw warning` on the next tick AFTER emitWarning returns, so a
// try/catch around the call cannot intercept it. The helper must check
// the flag up front and avoid calling emitWarning at all.
const originalThrowDeprecation = process.throwDeprecation;
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
process.throwDeprecation = true;

try {
expect(() => emitV0RemovedWarning('workflow')).not.toThrow();
expect(emitWarningSpy).not.toHaveBeenCalled();
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
expect(consoleWarnSpy.mock.calls[0][0]).toContain(V0_DEPRECATION_CODE);
expect(consoleWarnSpy.mock.calls[0][0]).toContain('workflow module');
} finally {
process.throwDeprecation = originalThrowDeprecation;
consoleWarnSpy.mockRestore();
}
});

it('falls back to console.warn if emitWarning throws synchronously (non-Node hosts)', () => {
// Defense-in-depth: a polyfilled `process.emitWarning` in a non-Node
// host could throw synchronously. Real Node throws asynchronously under
// --throw-deprecation; that path is exercised by the test above.
emitWarningSpy.mockImplementation(() => {
throw new Error('synchronous emitWarning failure');
});
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});

try {
expect(() => emitV0RemovedWarning('workflow')).not.toThrow();
expect(emitWarningSpy).toHaveBeenCalledTimes(1);
expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
expect(consoleWarnSpy.mock.calls[0][0]).toContain(V0_DEPRECATION_CODE);
} finally {
consoleWarnSpy.mockRestore();
}
});

it('falls back to console.warn when process.emitWarning is unavailable', () => {
emitWarningSpy.mockRestore();
const original = process.emitWarning;
// Simulate a runtime that doesn't expose emitWarning.
(process as unknown as { emitWarning?: typeof process.emitWarning }).emitWarning =
undefined as unknown as typeof process.emitWarning;
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});

try {
emitV0RemovedWarning('step');

expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
expect(consoleWarnSpy.mock.calls[0][0]).toContain(V0_DEPRECATION_CODE);
expect(consoleWarnSpy.mock.calls[0][0]).toContain('step module');
} finally {
consoleWarnSpy.mockRestore();
(process as unknown as { emitWarning: typeof process.emitWarning }).emitWarning = original;
}
});
});
84 changes: 84 additions & 0 deletions sdks/typescript/src/util/v0-deprecation-warning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* v0 SDK root-import deprecation warnings.
*
* The legacy `workflow` and `step` re-exports under the root specifier need
* to keep nagging consumers to migrate to v1, but the original implementation
* used `console.warn` at module evaluation time, which:
* - cannot be silenced by Node's standard `--no-deprecation`,
* `--no-warnings`, or `--no-warnings=DeprecationWarning` flags;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-blocking suggestion: I don’t think this source comment needs to enumerate the exact Node CLI flags. Since those are Node-owned behavior and can vary by version, I would slightly prefer keeping the source comment focused on the reason for process.emitWarning: it gives consumers structured warning metadata and a stable HATCHET_V0_REMOVED code that standard Node warning controls can filter or suppress. That would also avoid teaching the undocumented --no-warnings=DeprecationWarning form in source.

* - has no stable `code` for `process.on('warning', ...)` handlers; and
* - is duplicated when both submodules are loaded (which happens for
* anyone importing from the root, since `index.ts` re-exports both).
*
* Switching to `process.emitWarning` with a fixed code makes the warnings
* filterable, dedupable, and consistent with the rest of Node's deprecation
* surface, while still being visible by default.
*/

export const V0_DEPRECATION_CODE = 'HATCHET_V0_REMOVED';
const MIGRATION_URL = 'https://docs.hatchet.run/home/v1-sdk-improvements';

const emittedSubmodules = new Set<string>();

/** Reset hook for tests. Not part of the public API. */
export function _resetEmittedV0Warnings(): void {
emittedSubmodules.clear();
}

function fallbackConsoleWarn(message: string, detail?: string): void {
console.warn(`[${V0_DEPRECATION_CODE}] ${message}${detail ? `\n${detail}` : ''}`);
}

/**
* Emit a deduplicated v0-removal deprecation warning for a given submodule.
*
* Each unique `submodule` is emitted at most once per process. Uses
* `process.emitWarning` when available so consumers can suppress or filter
* via standard Node mechanisms.
*
* Importing the SDK must never abort module evaluation, since the root
* specifier still re-exports v0 `workflow` and `step` for transitive
* consumers who only use v1 APIs. Two cases would otherwise crash them:
*
* 1. `process.throwDeprecation` (set by `--throw-deprecation` or directly).
* Node queues a `throw warning` on the next tick after `emitWarning`
* returns, so a `try`/`catch` around the call would not catch it. We
* check the flag up front and route to `console.warn` instead.
* 2. Runtimes that don't expose `process.emitWarning` at all (older
* browsers, certain bundler shims). Same fallback applies.
*
* The remaining `try`/`catch` is belt-and-suspenders for non-Node hosts
* where a polyfilled `emitWarning` could throw synchronously.
*
* @param submodule - The legacy v0 submodule being imported (e.g. "workflow", "step").
* @param detail - Optional follow-up sentence appended to the warning detail.
*/
export function emitV0RemovedWarning(submodule: string, detail?: string): void {
if (emittedSubmodules.has(submodule)) {
return;
}
emittedSubmodules.add(submodule);

const message =
`The v0 SDK, including the ${submodule} module, has been deprecated and was removed in v1.14.0. ` +
`Please migrate to the v1 SDK: ${MIGRATION_URL}`;

const hasProcess = typeof process !== 'undefined';
const hasEmitWarning = hasProcess && typeof process.emitWarning === 'function';
const willThrowAsync = hasProcess && process.throwDeprecation === true;

if (!hasEmitWarning || willThrowAsync) {
fallbackConsoleWarn(message, detail);
return;
}

try {
process.emitWarning(message, {
type: 'DeprecationWarning',
code: V0_DEPRECATION_CODE,
detail,
});
} catch {
fallbackConsoleWarn(message, detail);
}
}
13 changes: 4 additions & 9 deletions sdks/typescript/src/workflow.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import { ConcurrencyLimitStrategy, StickyStrategy } from '@hatchet/v1';
import { emitV0RemovedWarning } from './util/v0-deprecation-warning';

export * from './legacy/workflow';

export { ConcurrencyLimitStrategy, StickyStrategy };

console.warn(
'\x1b[31mDeprecation warning: The v0 sdk, including the workflow module has been deprecated and has been removed in release v1.14.0.\x1b[0m'
emitV0RemovedWarning(
'workflow',
'ConcurrencyLimitStrategy and StickyStrategy have been moved to @hatchet-dev/typescript-sdk/v1.'
);
console.warn(
'\x1b[31mPlease migrate to v1 SDK instead: https://docs.hatchet.run/home/v1-sdk-improvements\x1b[0m'
);
console.warn(
'ConcurrencyLimitStrategy, StickyStrategy have been moved to @hatchet-dev/typescript-sdk/v1'
);
console.warn('--------------------------------');