UNPKG

@atlassian/aui

Version:

Atlassian User Interface library

425 lines (374 loc) 15.1 kB
import globalize from './globalize'; type ArbitraryFunction = () => unknown; type SelectorMap = { selector: string; options?: DeprecationOptions; }; /** * Purely to reflect the existing state of the code, not ideal. */ export type DeprecationOptions = { /** * the version this has been deprecated since */ sinceVersion?: string; /** * the version this will be removed in */ removeInVersion?: string; /** * the name of an alternative to use */ alternativeName?: string; /** * extra information to be printed at the end of the deprecation log */ extraInfo?: string; /** * an extra object that will be printed at the end */ extraObject?: object | string; /** * a human-readable name to show in the deprecation message. If not provided, it is inferred from the function or object being deprecated. */ displayName?: string; /** * type of the deprecation to append to the start of the deprecation message. e.g. JS or CSS */ deprecationType?: string; }; // eslint-disable-next-line @typescript-eslint/unbound-method -- Leaving behaviour identical for now const has = Object.prototype.hasOwnProperty; const deprecationCalls: (string | string[])[] = []; function toSentenceCase(name: string): string { if (!name) { return ''; } name = '' + name; return name.charAt(0).toUpperCase() + name.substring(1); } function getDeprecatedLocation(printFrameOffset: number) { const error = new Error(); let stacktraceString: string | undefined = error.stack ?? // @ts-expect-error -- preserving the legacy, not sure what .stacktrace would exist on, guessing the type (error.stacktrace as string | undefined); stacktraceString = stacktraceString?.replace(/^Error\n/, '') ?? ''; const stacktrace = stacktraceString.split('\n'); return stacktrace[printFrameOffset + 2]; } function logger(...args: unknown[]): void { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- JS test DOM environments if (typeof console !== 'undefined' && console.warn) { Function.prototype.apply.call(console.warn, console, args); } } /** * Return a function that logs a deprecation warning to the console the first time it is called from a certain location. * It will also print the stack frame of the calling function. * * @param {string} displayName the name of the thing being deprecated * @param {DeprecationOptions} options * @return {Function} that logs the warning and stack frame of the calling function. Takes in an optional parameter for the offset of * the stack frame to print, the default is 0. For example, 0 will log it for the line of the calling function, * -1 will print the location the logger was called from */ const getShowDeprecationMessagePublic = ( displayName: string, options: DeprecationOptions ): ArbitraryFunction => { return getShowDeprecationMessageInternal(displayName, options); }; /** * Return a function that logs a deprecation warning to the console the first time it is called from a certain location. * It will also print the stack frame of the calling function. * * @param {string | symbol | number | Function} displayName the name of the thing being deprecated * @param {DeprecationOptions} [options] * @return {Function} that logs the warning and stack frame of the calling function. Takes in an optional parameter for the offset of * the stack frame to print, the default is 0. For example, 0 will log it for the line of the calling function, * -1 will print the location the logger was called from */ function getShowDeprecationMessageInternal( displayName: string | ArbitraryFunction, options?: DeprecationOptions ): ArbitraryFunction { // This can be used internally to pas in a showmessage fn if (typeof displayName === 'function') { return displayName; } let called = false; options = options ?? {}; return function (printFrameOffset?: number): void { let deprecatedLocation = getDeprecatedLocation(printFrameOffset ? printFrameOffset : 1) ?? ''; // Only log once if the stack frame doesn't exist to avoid spamming the console/test output if (!called || !deprecationCalls.includes(deprecatedLocation)) { deprecationCalls.push(deprecatedLocation); called = true; const deprecationType = options.deprecationType ?? ''; let message = 'DEPRECATED ' + deprecationType + '- ' + toSentenceCase(displayName) + ' has been deprecated' + (options.sinceVersion ? ' since ' + options.sinceVersion : '') + ' and will be removed in ' + (options.removeInVersion ?? 'a future release') + '.'; if (options.alternativeName) { message += ' Use ' + options.alternativeName + ' instead. '; } if (options.extraInfo) { message += ' ' + options.extraInfo; } if (deprecatedLocation === '') { deprecatedLocation = ' \n ' + 'No stack trace of the deprecated usage is available in your current browser.'; } else { deprecatedLocation = ' \n ' + String(deprecatedLocation); } if (options.extraObject) { message += '\n'; logger(message, options.extraObject, deprecatedLocation); } else { logger(message, deprecatedLocation); } } }; } function logCssDeprecation(selectorMap: SelectorMap, newNode: Node) { let displayName = selectorMap.options?.displayName; displayName = displayName ? ' (' + displayName + ')' : ''; const options: DeprecationOptions = Object.assign( { deprecationType: 'CSS', extraObject: newNode, }, selectorMap.options ); getShowDeprecationMessageInternal( "'" + selectorMap.selector + "' pattern" + displayName, options )(); } /** * Returns a wrapped version of the function that logs a deprecation warning when the function is used. * @param {Function} fn the fn to wrap * @param {string} displayName the name of the fn to be displayed in the message * @param {DeprecationOptions} options * @return {Function} wrapping the original function */ function deprecateFunctionExpression( fn: ArbitraryFunction, displayName: string, options: DeprecationOptions ): ArbitraryFunction { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Just in case someone outside AUI is using it options = options ?? {}; options.deprecationType = options.deprecationType ?? 'JS'; const showDeprecationMessage = getShowDeprecationMessageInternal( displayName || fn.name || 'this function', options ); return function (...args: unknown[]) { showDeprecationMessage(); // @ts-expect-error Sorry TS, don't want to change this behaviour just yet in case of side effects return fn.apply(this, args); }; } /** * Returns a wrapped version of the constructor that logs a deprecation warning when the constructor is instantiated. * @param {Function} constructorFn the constructor function to wrap * @param {string} displayName the name of the fn to be displayed in the message * @param {DeprecationOptions} options * @return {Function} wrapping the original function */ function deprecateConstructor( constructorFn: ArbitraryFunction, displayName: string, options: DeprecationOptions ): ArbitraryFunction { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Just in case someone outside AUI is using it options = options ?? {}; options.deprecationType = options.deprecationType ?? 'JS'; const deprecatedConstructor = deprecateFunctionExpression(constructorFn, displayName, options); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment deprecatedConstructor.prototype = constructorFn.prototype; Object.assign(deprecatedConstructor, constructorFn); //copy static methods across; return deprecatedConstructor; } /** * Wraps a "value" object property in a deprecation warning in browsers supporting Object.defineProperty * @param {Object} obj the object containing the property * @param {string} prop the name of the property to deprecate * @param {DeprecationOptions} [options] */ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- This is easier to read function deprecateValueProperty<T extends object, K extends keyof T & string>( obj: T, prop: K, options?: DeprecationOptions ) { let oldVal = obj[prop]; options = options ?? {}; options.deprecationType = options.deprecationType ?? 'JS'; const displayNameOrShowMessageFn = options.displayName ?? prop; const showDeprecationMessage = getShowDeprecationMessageInternal( displayNameOrShowMessageFn, options ); Object.defineProperty(obj, prop, { get: function (): T[K] { showDeprecationMessage(); return oldVal; }, set: function (val: T[K]): T[K] { oldVal = val; showDeprecationMessage(); return val; }, }); } /** * Wraps an object property in a deprecation warning, if possible. functions will always log warnings, but other * types of properties will only log in browsers supporting Object.defineProperty * @param {Object} object the object containing the property * @param {string} propertyKey the name of the property to deprecate * @param {DeprecationOptions} options */ function deprecateObjectProperty<T extends object>( object: T, propertyKey: string & keyof T, options: DeprecationOptions ) { if (typeof object[propertyKey] === 'function') { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Just in case someone outside AUI is using it options = options ?? {}; options.deprecationType = options.deprecationType ?? 'JS'; const displayNameOrShowMessageFn = options.displayName ?? propertyKey; // @ts-expect-error -- Maybe a TypeScript wizard can figure out something // better than me so TS is happy. We only care it's a function. object[propertyKey] = deprecateFunctionExpression( object[propertyKey] as ArbitraryFunction, displayNameOrShowMessageFn, options ); } else { deprecateValueProperty(object, propertyKey, options); } } type DeprecationOptionsWithAltNamePrefix = DeprecationOptions & { /** * a prefix for the alternative property name. Used to generate alternativeName per property. */ alternativeNamePrefix?: string; }; /** * Wraps all an objects properties in a deprecation warning, if possible. functions will always log warnings, but other * types of properties will only log in browsers supporting Object.defineProperty * @param {Object} obj the object to be wrapped * @param {string} objDisplayPrefix the object's prefix to be used in logs * @param {DeprecationOptionsWithAltNamePrefix} options */ function deprecateAllProperties( obj: object, objDisplayPrefix: string, options: DeprecationOptionsWithAltNamePrefix ) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Just in case someone outside AUI is using it options = options ?? {}; for (const attr in obj) { if (has.call(obj, attr)) { options.deprecationType = options.deprecationType ?? 'JS'; options.displayName = objDisplayPrefix + attr; options.alternativeName = options.alternativeNamePrefix && options.alternativeNamePrefix + attr; deprecateObjectProperty( obj, // @ts-expect-error -- We're very safely checking this is actually an attribute on the object attr, Object.assign({}, options) ); } } } function matchesSelector(node: Node, selector: string) { if (node instanceof Element) { return node.matches(selector); } return false; } function handleAddingSelector(options?: DeprecationOptions) { return function (selector: string) { const selectorMap: SelectorMap = { selector: selector, options: options, }; // Search if matches have already been added const matches = document.querySelectorAll(selector); for (const match of matches) { logCssDeprecation(selectorMap, match); } observeFutureChange(selectorMap); }; } /** * Return a function that logs a deprecation warning to the console the first time it is called from a certain location. * It will also print the stack frame of the calling function. */ function deprecateCSS(selectors: string | string[], options?: DeprecationOptions): void { if (typeof selectors === 'string') { selectors = [selectors]; } selectors.forEach(handleAddingSelector(options)); } function testAndHandleDeprecation(newNode: Node) { return function (selectorMap: SelectorMap) { if (matchesSelector(newNode, selectorMap.selector)) { logCssDeprecation(selectorMap, newNode); } }; } const deprecatedSelectorMap: SelectorMap[] = []; let observer: MutationObserver | undefined = undefined; function observeFutureChange(selectorMap: SelectorMap) { deprecatedSelectorMap.push(selectorMap); // Lazily instantiate a mutation observer because they're expensive. if (observer === undefined) { observer = new MutationObserver(function (mutations) { mutations.forEach(function (mutation) { // TODO - should this also look at class changes, if possible? const addedNodes = mutation.addedNodes; for (const newNode of addedNodes) { if (newNode.nodeType === 1) { deprecatedSelectorMap.forEach(testAndHandleDeprecation(newNode)); } } }); }); const config = { childList: true, subtree: true, }; observer.observe(document, config); } } globalize('deprecate', { fn: deprecateFunctionExpression, construct: deprecateConstructor, css: deprecateCSS, prop: deprecateObjectProperty, obj: deprecateAllProperties, getMessageLogger: getShowDeprecationMessagePublic, }); export { deprecateFunctionExpression as fn, deprecateConstructor as construct, deprecateCSS as css, deprecateObjectProperty as prop, deprecateAllProperties as obj, getShowDeprecationMessagePublic as getMessageLogger, };