@warp-drive/build-config
Version:
Provides Build Configuration for projects using WarpDrive or EmberData
629 lines (600 loc) • 19.5 kB
JavaScript
'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');
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 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_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 [@warp-drive/build-config/babel-macros](./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.
*
* @internal
*/
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;
const CURRENT_FEATURES = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
__proto__: null,
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.
*
* @internal
*/
const __INTERNAL_LOG_NATIVE_MAP_SET_COUNTS = 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_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 { Webpack } = require('@embroider/webpack');
* return require('@embroider/compat').compatBuild(app, Webpack, {});
* };
*
* ```
*
* 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;
function recastMacrosConfig(macros) {
if (!('globalConfig' in macros)) {
throw new Error('Expected MacrosConfig to have a globalConfig property');
}
return macros;
}
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;