UNPKG

@warp-drive/build-config

Version:

Provides Build Configuration for projects using WarpDrive

711 lines (675 loc) 21.8 kB
'use strict'; Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); const EmbroiderMacros = require('@embroider/macros/src/node.js'); const semver = require('semver'); const fs = require('fs'); const path = require('path'); const url = require('url'); require('@embroider/macros/src/babel.js'); var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null; function getEnv(forceMode) { const FORCE_TESTING = forceMode === 'testing' || forceMode === 'development' || forceMode === 'debug'; const FORCE_DEBUG = forceMode === 'development' || forceMode === 'debug'; const FORCE_PRODUCTION = forceMode === 'production'; const { EMBER_ENV, IS_TESTING, EMBER_CLI_TEST_COMMAND, NODE_ENV, CI, IS_RECORDING } = process.env; const PRODUCTION = FORCE_PRODUCTION || EMBER_ENV === 'production' || !EMBER_ENV && NODE_ENV === 'production'; const DEBUG = FORCE_DEBUG || !PRODUCTION; const TESTING = FORCE_TESTING || DEBUG || Boolean(EMBER_ENV === 'test' || IS_TESTING || EMBER_CLI_TEST_COMMAND); const SHOULD_RECORD = Boolean(!CI || IS_RECORDING); return { TESTING, PRODUCTION, DEBUG, IS_RECORDING: Boolean(IS_RECORDING), IS_CI: Boolean(CI), SHOULD_RECORD }; } // ======================== // FOR CONTRIBUTING AUTHORS // // Deprecations here should also have guides PR'd to the emberjs deprecation app // // github: https://github.com/ember-learn/deprecation-app // website: https://deprecations.emberjs.com // // Each deprecation should also be given an associated URL pointing to the // relevant guide. // // URLs should be of the form: https://deprecations.emberjs.com/v<major>.x#toc_<fileName> // where <major> is the major version of the deprecation and <fileName> is the // name of the markdown file in the guides repo. // // ======================== // const DEPRECATE_CATCH_ALL = '99.0'; const DEPRECATE_NON_STRICT_TYPES = '5.3'; const DEPRECATE_NON_STRICT_ID = '5.3'; const DEPRECATE_COMPUTED_CHAINS = '7.0'; const DEPRECATE_LEGACY_IMPORTS = '5.3'; const DEPRECATE_NON_UNIQUE_PAYLOADS = '5.3'; const DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE = '5.3'; const DEPRECATE_MANY_ARRAY_DUPLICATES = '5.3'; const DEPRECATE_STORE_EXTENDS_EMBER_OBJECT = '5.4'; const ENABLE_LEGACY_SCHEMA_SERVICE = '5.4'; const DEPRECATE_EMBER_INFLECTOR = '5.3'; const DISABLE_7X_DEPRECATIONS = '7.0'; const DEPRECATE_TRACKING_PACKAGE = '5.5'; const ENABLE_LEGACY_REQUEST_METHODS = '5.6'; const CURRENT_DEPRECATIONS = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({ __proto__: null, DEPRECATE_CATCH_ALL, DEPRECATE_COMPUTED_CHAINS, DEPRECATE_EMBER_INFLECTOR, DEPRECATE_LEGACY_IMPORTS, DEPRECATE_MANY_ARRAY_DUPLICATES, DEPRECATE_NON_STRICT_ID, DEPRECATE_NON_STRICT_TYPES, DEPRECATE_NON_UNIQUE_PAYLOADS, DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE, DEPRECATE_STORE_EXTENDS_EMBER_OBJECT, DEPRECATE_TRACKING_PACKAGE, DISABLE_7X_DEPRECATIONS, ENABLE_LEGACY_REQUEST_METHODS, ENABLE_LEGACY_SCHEMA_SERVICE }, Symbol.toStringTag, { value: 'Module' })); function deprecationIsResolved(deprecatedSince, compatVersion) { return semver.lte(semver.minVersion(deprecatedSince), semver.minVersion(compatVersion)); } const NextMajorVersion = '6.'; function deprecationIsNextMajorCycle(deprecatedSince) { return deprecatedSince.startsWith(NextMajorVersion); } function getDeprecations(compatVersion, deprecations) { const flags = {}; const keys = Object.keys(CURRENT_DEPRECATIONS); const DISABLE_7X_DEPRECATIONS = deprecations?.DISABLE_7X_DEPRECATIONS ?? true; keys.forEach(flag => { const deprecatedSince = CURRENT_DEPRECATIONS[flag]; const isDeactivatedDeprecationNotice = DISABLE_7X_DEPRECATIONS && deprecationIsNextMajorCycle(deprecatedSince); let flagState = true; // default to no code-stripping if (!isDeactivatedDeprecationNotice) { // if we have a specific flag setting, use it if (typeof deprecations?.[flag] === 'boolean') { flagState = deprecations?.[flag]; } else if (compatVersion) { // if we are told we are compatible with a version // we check if we can strip this flag const isResolved = deprecationIsResolved(deprecatedSince, compatVersion); // if we've resolved, we strip (by setting the flag to false) /* if (DEPRECATED_FEATURE) { // deprecated code path } else { // if needed a non-deprecated code path } */ flagState = !isResolved; } } // console.log(`${flag}=${flagState} (${deprecatedSince} <= ${compatVersion})`); flags[flag] = flagState; }); return flags; } /** * * # Canary Features <Badge type="warning" text="requires canary" /> * * ***Warp*Drive** allows users to test upcoming features that are implemented * but not yet activated in canary builds. * * Typically these features represent work that carries higher risk of breaking * changes, or are not yet fully ready for production use. * * Such features have their implementations guarded by a "feature flag", and the * flag is only activated once the core-data team is prepared to ship the work * in a canary release, beginning the process of it landing in a stable release. * * ### Installing Canary * * ::: warning To test a feature guarded behind a flag, you MUST be using a canary build. * ::: * * Canary builds are published to `npm` and can be installed using a precise tag * (such as `@warp-drive/core@5.6.0-alpha.1`) or by installing the latest dist-tag * published to the `canary` channel. * * Because ***Warp*Drive** packages operate on a strict lockstep policy with each other, * you must install the matching canary version of all ***Warp*Drive** packages. * * Below is an example of installing the latest canary version of all the core * packages that are part of the ***Warp*Drive** project when using EmberJS. * * Add/remove packages from this list to match your project. * * ::: code-group * * ```sh [pnpm] * pnpm add -E @warp-drive/core@canary \ * @warp-drive/json-api@canary \ * @warp-drive/ember@canary; * ``` * * ```sh [npm] * npm add -E @warp-drive/core@canary \ * @warp-drive/json-api@canary \ * @warp-drive/ember@canary; * ``` * * ```sh [yarn] * yarn add -E @warp-drive/core@canary \ * @warp-drive/json-api@canary \ * @warp-drive/ember@canary; * ``` * * ```sh [bun] * bun add --exact @warp-drive/core@canary \ * @warp-drive/json-api@canary \ * @warp-drive/ember@canary; * ``` * * ::: * * ### Activating a Feature * * Once you have installed canary, feature-flags can be activated at build-time * * ```ts * setConfig(app, __dirname, { * features: { * FEATURE_A: false, // utilize existing behavior * FEATURE_B: true // utilize the new behavior * } * }) * ``` * * by setting an environment variable: * * ```sh * # Activate a single flag * export WARP_DRIVE_FEATURE_OVERRIDE=SOME_FLAG; * * # Activate multiple flags by separating with commas * export WARP_DRIVE_FEATURE_OVERRIDE=SOME_FLAG,OTHER_FLAG; * * # Activate all flags * export WARP_DRIVE_FEATURE_OVERRIDE=ENABLE_ALL_OPTIONAL; * ``` * * ::: warning To test a feature guarded behind a flag, you MUST be running a development build. * ::: * * * ### Preparing a Project to use a Canary Feature * * For most projects and features, simple version detection should be enough. * * Using the provided version compatibility helpers from [embroider-macros](https://github.com/embroider-build/embroider/tree/main/packages/macros#readme) * the following can be done: * * ```js * if (macroCondition(dependencySatisfies('@warp-drive/core', '5.6'))) { * // do thing * } * ``` * * For more complex projects and migrations, configure {@link @warp-drive/core!build-config/babel-macros | @warp-drive/core/build-config/babel-macros} * * The current list of features used at build time for canary releases is defined below. * * ::: tip 💡 If empty there are no features currently gated by feature flags. * ::: * * The valid values are: * * - `true` | The feature is **enabled** at all times, and cannot be disabled. * - `false` | The feature is **disabled** at all times, and cannot be enabled. * - `null` | The feature is **disabled by default**, but can be enabled via configuration. * * @module * @public */ /** * We use this for some tests etc. * * @private */ const SAMPLE_FEATURE_FLAG = null; /** * This upcoming feature adds a validation step to payloads received * by the JSONAPICache implementation. * * When a request completes and the result is given to the cache via * `cache.put`, the cache will validate the payload against registered * schemas as well as the JSON:API spec. * * @since 5.4 * @public */ const JSON_API_CACHE_VALIDATION_ERRORS = false; /** * This upcoming feature adds a validation step when `schema.fields({ type })` * is called for the first time for a resource. * * When active, if any trait specified by the resource or one of its traits is * missing an error will be thrown in development. * * @since 5.7 * @public */ const ENFORCE_STRICT_RESOURCE_FINALIZATION = false; const CURRENT_FEATURES = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({ __proto__: null, ENFORCE_STRICT_RESOURCE_FINALIZATION, JSON_API_CACHE_VALIDATION_ERRORS, SAMPLE_FEATURE_FLAG }, Symbol.toStringTag, { value: 'Module' })); const dirname = typeof __dirname !== 'undefined' ? __dirname : url.fileURLToPath(new URL(".", (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cjs-set-config.cjs', document.baseURI).href)))); const relativePkgPath = path.join(dirname, '../package.json'); const version = JSON.parse(fs.readFileSync(relativePkgPath, 'utf-8')).version; const isCanary = version.includes('alpha'); function getFeatures(isProd) { const features = Object.assign({}, CURRENT_FEATURES); const keys = Object.keys(features); if (!isCanary) { // disable all features with a current value of `null` for (const feature of keys) { let featureValue = features[feature]; if (featureValue === null) { features[feature] = false; } } return features; } const FEATURE_OVERRIDES = process.env.WARP_DRIVE_FEATURE_OVERRIDE; if (FEATURE_OVERRIDES === 'ENABLE_ALL_OPTIONAL') { // enable all features with a current value of `null` for (const feature of keys) { let featureValue = features[feature]; if (featureValue === null) { features[feature] = true; } } } else if (FEATURE_OVERRIDES === 'DISABLE_ALL') { // disable all features, including those with a value of `true` for (const feature of keys) { features[feature] = false; } } else if (FEATURE_OVERRIDES) { // enable only the specific features listed in the environment // variable (comma separated) const forcedFeatures = FEATURE_OVERRIDES.split(','); for (let i = 0; i < forcedFeatures.length; i++) { let featureName = forcedFeatures[i]; if (!keys.includes(featureName)) { throw new Error(`Unknown feature flag: ${featureName}`); } features[featureName] = true; } } if (isProd) { // disable all features with a current value of `null` for (const feature of keys) { let featureValue = features[feature]; if (featureValue === null) { features[feature] = false; } } } return features; } /** * # Log Instrumentation <Badge type="tip" text="debug only" /> * * Many portions of the internals are helpfully instrumented with logging. * This instrumentation is always removed from production builds. * * Log instrumentation is "regionalized" to specific concepts and concerns * to enable you to enable/disable just the areas you are interested in. * * To activate a particular group of logs set the appropriate flag to `true` * either in your build config or via the runtime helper. * * * ## Runtime Activation * * ::: tip 💡 Just Works in browser Dev Tools! * No import is needed, and the logging config is preserved when the page is refreshed * ::: * * ```ts * setWarpDriveLogging({ * LOG_CACHE: true, * LOG_REQUESTS: true, * }) * ``` * * A runtime helper is attached to `globalThis` to enable activation of the logs * from anywhere in your application including from the devtools panel. * * The runtime helper overrides any build config settings for the given flag * for the current browser tab. It stores the configuration you give it in * `sessionStorage` so that it persists across page reloads of the current tab, * but not across browser tabs or windows. * * If you need to deactivate the logging, you can call the helper again with the * same flag set to `false` or just open a new tab/window. * * ## Buildtime Activation * * ```ts * setConfig(__dirname, app, { * debug: { * LOG_CACHE: true, * LOG_REQUESTS: false, * LOG_NOTIFICATIONS: true, * } * }); * ``` * * The build config settings are used to set the default values for the * logging flags. Any logging flag that is not set in the build config * will default to `false`. * * @module */ /** * log cache updates for both local * and remote state. Note in some older versions * this was called `LOG_PAYLOADS` and was one * of three flags that controlled logging of * cache updates. This is now the only flag. * * The others were `LOG_OPERATIONS` and `LOG_MUTATIONS`. * * @public * @since 5.5 */ const LOG_CACHE = false; /** * <Badge type="danger" text="removed" /> * * This flag no longer has any effect. * * Use {@link LOG_CACHE} instead. * * @deprecated removed in version 5.5 * @public */ const LOG_PAYLOADS = false; /** * <Badge type="danger" text="removed" /> * * This flag no longer has any effect. * * Use {@link LOG_CACHE} instead. * * @deprecated removed in version 5.5 * @public */ const LOG_OPERATIONS = false; /** * <Badge type="danger" text="removed" /> * * This flag no longer has any effect. * * Use {@link LOG_CACHE} instead. * * @deprecated removed in version 5.5 * @public */ const LOG_MUTATIONS = false; /** * Log decisions made by the Basic CachePolicy * * @public */ const LOG_CACHE_POLICY = false; /** * log notifications received by the NotificationManager * * @public */ const LOG_NOTIFICATIONS = false; /** * log requests issued by the RequestManager * * @public */ const LOG_REQUESTS = false; /** * log updates to requests the store has issued to * the network (adapter) to fulfill. * * @public */ const LOG_REQUEST_STATUS = false; /** * log peek, generation and updates to * Record Identifiers. * * @public */ const LOG_IDENTIFIERS = false; /** * log updates received by the graph (relationship pointer storage) * * @public */ const LOG_GRAPH = false; /** * log creation/removal of RecordData and Record * instances. * * @public */ const LOG_INSTANCE_CACHE = false; /** * Log key count metrics, useful for performance * debugging. * * @public */ const LOG_METRIC_COUNTS = false; /** * Helps when debugging causes of a change notification * when processing an update to a hasMany relationship. * * @public */ const DEBUG_RELATIONSHIP_NOTIFICATIONS = false; /** * A private flag to enable logging of the native Map/Set * constructor and method calls. * * EXTREMELY MALPERFORMANT * * LOG_METRIC_COUNTS must also be enabled. * * @private */ const __INTERNAL_LOG_NATIVE_MAP_SET_COUNTS = false; /** * Helps when debugging React specific reactivity issues. */ const LOG_REACT_SIGNAL_INTEGRATION = false; const LOGGING = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({ __proto__: null, DEBUG_RELATIONSHIP_NOTIFICATIONS, LOG_CACHE, LOG_CACHE_POLICY, LOG_GRAPH, LOG_IDENTIFIERS, LOG_INSTANCE_CACHE, LOG_METRIC_COUNTS, LOG_MUTATIONS, LOG_NOTIFICATIONS, LOG_OPERATIONS, LOG_PAYLOADS, LOG_REACT_SIGNAL_INTEGRATION, LOG_REQUESTS, LOG_REQUEST_STATUS, __INTERNAL_LOG_NATIVE_MAP_SET_COUNTS }, Symbol.toStringTag, { value: 'Module' })); function createLoggingConfig(env, debug) { const config = {}; const keys = Object.keys(LOGGING); for (const key of keys) { if (env.DEBUG || env.TESTING) { config[key] = true; } else { config[key] = debug[key] || false; } } return config; } /** * This package provides a build-plugin that enables configuration of deprecations, * optional features, development/testing support and debug logging. * * This configuration is done using `setConfig` in `ember-cli-build`. * * ```ts [ember-cli-build.js] * 'use strict'; * * const EmberApp = require('ember-cli/lib/broccoli/ember-app'); * * module.exports = async function (defaults) { * const { setConfig } = await import('@warp-drive/build-config'); // [!code focus] * * const app = new EmberApp(defaults, {}); * * setConfig(app, __dirname, { // [!code focus:3] * // settings here * }); * * const { buildOnce } = await import('@embroider/vite'); * const { compatBuild } = await import('@embroider/compat'); * * return compatBuild(app, buildOnce); * }; * * ``` * * Available settings include: * * - {@link LOGGING | debugging} * - {@link DEPRECATIONS | deprecations} * - {@link FEATURES | features} * - {@link WarpDriveConfig.polyfillUUID | polyfillUUID} * - {@link WarpDriveConfig.includeDataAdapterInProduction | includeDataAdapterInProduction} * - {@link WarpDriveConfig.compatWith | compatWith} * * @module */ const _MacrosConfig = EmbroiderMacros.MacrosConfig; /** * Build Configuration options for WarpDrive that * allow adjusting logging, deprecations, canary features * and optional features. */ function recastMacrosConfig(macros) { if (!('globalConfig' in macros)) { throw new Error('Expected MacrosConfig to have a globalConfig property'); } return macros; } /** * Sets the build configuration for WarpDrive that ensures * environment specific behaviors are activated/deactivated * and enables adjusting log instrumentation, removing code * that supports deprecated features, enabling canary features * and enabling/disabling optional features. * * The library uses [@embroider/macros](https://www.npmjs.com/package/@embroider/macros) * to perform this final configuration code transform. * * This is a low level API for configuring WarpDrive. If your * project does not use `@embroider/macros` then you should use * {@link babelPlugin} instead of this function. * * ### Example * * ```ts * import { setConfig } from '@warp-drive/core/build-config'; * import { buildMacros } from '@embroider/macros/babel'; * * const Macros = buildMacros({ * configure: (config) => { * setConfig(config, { * compatWith: '5.6' * }); * }, * }); * * export default { * plugins: [ * // babel-plugin-debug-macros is temporarily needed * // to convert deprecation/warn calls into console.warn * [ * 'babel-plugin-debug-macros', * { * flags: [], * * debugTools: { * isDebug: true, * source: '@ember/debug', * assertPredicateIndex: 1, * }, * }, * 'ember-data-specific-macros-stripping-test', * ], * ...Macros.babelMacros, * ], * }; * ``` */ function setConfig(context, appRootOrConfig, config) { const isEmberClassicUsage = arguments.length === 3; const macros = recastMacrosConfig(isEmberClassicUsage ? _MacrosConfig.for(context, appRootOrConfig) : context); const userConfig = isEmberClassicUsage ? config : appRootOrConfig; const isLegacySupport = userConfig.___legacy_support; const hasDeprecatedConfig = isLegacySupport && Object.keys(userConfig).length > 1; const hasInitiatedConfig = macros.globalConfig['WarpDrive']; // setConfig called by user prior to legacy support called if (isLegacySupport && hasInitiatedConfig) { if (hasDeprecatedConfig) { throw new Error('You have provided a config object to setConfig, but are also using the legacy emberData options key in ember-cli-build. Please remove the emberData key from options.'); } return; } // included hooks run during class initialization of the EmberApp instance // so our hook will run before the user has a chance to call setConfig // else we could print a useful message here // else if (isLegacySupport) { // console.warn( // `WarpDrive requires your ember-cli-build file to set a base configuration for the project.\n\nUsage:\n\t\`import { setConfig } from '@warp-drive/build-config';\n\tsetConfig(app, __dirname, {});\`` // ); // } const debugOptions = Object.assign({}, LOGGING, userConfig.debug); const env = getEnv(userConfig.forceMode); const DEPRECATIONS = getDeprecations(userConfig.compatWith || null, userConfig.deprecations); const FEATURES = getFeatures(env.PRODUCTION); const includeDataAdapterInProduction = typeof userConfig.includeDataAdapterInProduction === 'boolean' ? userConfig.includeDataAdapterInProduction : true; const includeDataAdapter = env.PRODUCTION ? includeDataAdapterInProduction : true; const finalizedConfig = { debug: debugOptions, polyfillUUID: userConfig.polyfillUUID ?? false, includeDataAdapter, compatWith: userConfig.compatWith ?? null, deprecations: DEPRECATIONS, features: FEATURES, activeLogging: createLoggingConfig(env, debugOptions), env }; macros.setGlobalConfig(undefined, 'WarpDrive', finalizedConfig); } exports.setConfig = setConfig;