@embroider/macros
Version:
Standardized build-time macros for ember apps.
394 lines • 17.4 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = __importDefault(require("fs"));
const path_1 = require("path");
const crypto_1 = __importDefault(require("crypto"));
const find_up_1 = __importDefault(require("find-up"));
const shared_internals_1 = require("@embroider/shared-internals");
const ast_transform_1 = require("./glimmer/ast-transform");
const partition_1 = __importDefault(require("lodash/partition"));
// this is a module-scoped cache. If multiple callers ask _this copy_ of
// @embroider/macros for a shared MacrosConfig, they'll all get the same one.
// And if somebody asks a *different* copy of @embroider/macros for the shared
// MacrosConfig, it will have its own instance with its own code, but will still
// share the GlobalSharedState beneath.
let localSharedState = new WeakMap();
function gatherAddonCacheKeyWorker(item, memo) {
item.addons.forEach((addon) => {
let key = `${addon.pkg.name}@${addon.pkg.version}`;
memo.add(key);
gatherAddonCacheKeyWorker(addon, memo);
});
}
let addonCacheKey = new WeakMap();
// creates a string representing all addons and their versions
// (foo@1.0.0|bar@2.0.0) to use as a cachekey
function gatherAddonCacheKey(project) {
let cacheKey = addonCacheKey.get(project);
if (cacheKey) {
return cacheKey;
}
let memo = new Set();
project.addons.forEach((addon) => {
let key = `${addon.pkg.name}@${addon.pkg.version}`;
memo.add(key);
gatherAddonCacheKeyWorker(addon, memo);
});
cacheKey = [...memo].join('|');
addonCacheKey.set(project, cacheKey);
return cacheKey;
}
const babelCacheBustingPluginPath = require.resolve('@embroider/shared-internals/src/babel-plugin-cache-busting');
class MacrosConfig {
static for(key, appRoot) {
let found = localSharedState.get(key);
if (found) {
return found;
}
let g = global;
if (!g.__embroider_macros_global__) {
g.__embroider_macros_global__ = new WeakMap();
}
let shared = g.__embroider_macros_global__.get(key);
if (shared) {
// if an earlier version of @embroider/macros created the shared state, it
// would have configSources.
if (!shared.configSources) {
shared.configSources = new WeakMap();
}
// earlier versions did not include this -- we may need to upgrade the
// format here
if (!shared.globalConfigs) {
shared.globalConfigs = {};
}
}
else {
shared = {
configs: new Map(),
globalConfigs: {},
configSources: new WeakMap(),
mergers: new Map(),
};
g.__embroider_macros_global__.set(key, shared);
}
let config = new MacrosConfig(appRoot, shared);
localSharedState.set(key, config);
return config;
}
enableRuntimeMode() {
if (this.mode !== 'run-time') {
if (!this._configWritable) {
throw new Error(`[Embroider:MacrosConfig] attempted to enableRuntimeMode after configs have been finalized`);
}
this.mode = 'run-time';
}
}
enablePackageDevelopment(packageRoot) {
if (!this.isDevelopingPackageRoots.has(packageRoot)) {
if (!this._configWritable) {
throw new Error(`[Embroider:MacrosConfig] attempted to enablePackageDevelopment after configs have been finalized`);
}
this.isDevelopingPackageRoots.add(packageRoot);
}
}
get importSyncImplementation() {
return this._importSyncImplementation;
}
set importSyncImplementation(value) {
if (!this._configWritable) {
throw new Error(`[Embroider:MacrosConfig] attempted to set importSyncImplementation after configs have been finalized`);
}
this._importSyncImplementation = value;
}
constructor(origAppRoot, shared) {
this.origAppRoot = origAppRoot;
this.mode = 'compile-time';
this.isDevelopingPackageRoots = new Set();
this._importSyncImplementation = 'cjs';
this._configWritable = true;
this.configs = shared.configs;
this.globalConfig = shared.globalConfigs;
this.configSources = shared.configSources;
this.mergers = shared.mergers;
// this uses globalConfig because these things truly are global. Even if a
// package doesn't have a dep or peerDep on @embroider/macros, it's legit
// for them to want to know the answer to these questions, and there is only
// one answer throughout the whole dependency graph.
this.globalConfig['@embroider/macros'] = {
// this powers the `isTesting` macro. It always starts out false here,
// because:
// - if this is a production build, we will evaluate all macros at build
// time and isTesting will stay false, so test-only code will not be
// included.
// - if this is a dev build, we evaluate macros at runtime, which allows
// both "I'm running my app in development" and "I'm running my test
// suite" to coexist within a single build. When you run the test
// suite, early in the runtime boot process we can flip isTesting to
// true to distinguish the two.
isTesting: false,
};
}
get packageCache() {
return shared_internals_1.RewrittenPackageCache.shared('embroider', this.origAppRoot);
}
get appRoot() {
return this.origAppRoot;
}
// Registers a new source of configuration to be given to the named package.
// Your config type must be json-serializable. You must always set fromPath to
// `__filename`.
setConfig(fromPath, packageName, config) {
return this.internalSetConfig(fromPath, packageName, config);
}
// Registers a new source of configuration to be given to your own package.
// Your config type must be json-serializable. You must always set fromPath to
// `__filename`.
setOwnConfig(fromPath, config) {
return this.internalSetConfig(fromPath, undefined, config);
}
// Registers a new source of configuration to be shared globally within the
// app. USE GLOBALS SPARINGLY! Prefer setConfig or setOwnConfig instead,
// unless your state is truly, necessarily global.
//
// Include a relevant package name in your key to help avoid collisions.
//
// Your value must be json-serializable. You must always set fromPath to
// `__filename`.
setGlobalConfig(fromPath, key, value) {
if (!this._configWritable) {
throw new Error(`[Embroider:MacrosConfig] attempted to set global config after configs have been finalized from: '${fromPath}'`);
}
this.globalConfig[key] = value;
}
internalSetConfig(fromPath, packageName, config) {
if (!this._configWritable) {
throw new Error(`[Embroider:MacrosConfig] attempted to set config after configs have been finalized from: '${fromPath}'`);
}
if (!isSerializable(config)) {
throw new Error(`[Embroider:MacrosConfig] the given config from '${fromPath}' for packageName '${packageName}' is not JSON serializable.`);
}
let targetPackage = this.resolvePackage(fromPath, packageName);
let peers = (0, shared_internals_1.getOrCreate)(this.configs, targetPackage.root, () => []);
peers.push(config);
this.configSources.set(config, fromPath);
}
// Allows you to set the merging strategy used for your package's config. The
// merging strategy applies when multiple other packages all try to send
// configuration to you.
useMerger(fromPath, merger) {
if (this._configWritable) {
throw new Error(`[Embroider:MacrosConfig] attempted to call useMerger after configs have been finalized`);
}
let targetPackage = this.resolvePackage(fromPath, undefined);
let other = this.mergers.get(targetPackage.root);
if (other) {
throw new Error(`[Embroider:MacrosConfig] conflicting mergers registered for package ${targetPackage.name} at ${targetPackage.root}. See ${other.fromPath} and ${fromPath}.`);
}
this.mergers.set(targetPackage.root, { merger, fromPath });
}
get userConfigs() {
if (this._configWritable) {
throw new Error('[Embroider:MacrosConfig] cannot read userConfigs until MacrosConfig has been finalized.');
}
if (!this.cachedUserConfigs) {
let userConfigs = {};
let sourceOfConfig = this.makeConfigSourcer(this.configSources);
for (let [pkgRoot, configs] of this.configs) {
let combined;
if (configs.length > 1) {
combined = this.mergerFor(pkgRoot)(configs, { sourceOfConfig });
}
else {
combined = configs[0];
}
userConfigs[pkgRoot] = combined;
}
this.cachedUserConfigs = userConfigs;
}
return this.cachedUserConfigs;
}
makeConfigSourcer(configSources) {
return config => {
let fromPath = configSources.get(config);
if (!fromPath) {
throw new Error(`unknown object passed to sourceOfConfig(). You can only pass back the configs you were given.`);
}
let maybePkg = this.packageCache.ownerOfFile(fromPath);
if (!maybePkg) {
throw new Error(`bug: unexpected error, we always check that fromPath is owned during internalSetConfig so this should never happen`);
}
// our configs all deal in the original locations of packages, even if
// embroider is rewriting some of them
let pkg = this.packageCache.original(maybePkg);
return {
get name() {
return pkg.name;
},
get version() {
return pkg.version;
},
get root() {
return pkg.root;
},
};
};
}
// to be called from within your build system. Returns the thing you should
// push into your babel plugins list.
//
// owningPackageRoot is needed when the files you will process (1) all belongs
// to one package, (2) will not be located in globally correct paths such that
// normal node_modules resolution can find their dependencies. In other words,
// owningPackageRoot is needed when you use this inside classic ember-cli, and
// it's not appropriate inside embroider.
babelPluginConfig(appOrAddonInstance) {
let self = this;
let owningPackageRoot = appOrAddonInstance ? appOrAddonInstance.root || appOrAddonInstance.project.root : null;
let opts = {
// this is deliberately lazy because we want to allow everyone to finish
// setting config before we generate the userConfigs
get userConfigs() {
return self.userConfigs;
},
get globalConfig() {
return self.globalConfig;
},
owningPackageRoot,
isDevelopingPackageRoots: [...this.isDevelopingPackageRoots],
get appPackageRoot() {
return self.appRoot;
},
// This is used as a signature so we can detect ourself among the plugins
// emitted from v1 addons.
embroiderMacrosConfigMarker: true,
get mode() {
return self.mode;
},
importSyncImplementation: this.importSyncImplementation,
};
let lockFilePath = find_up_1.default.sync(['yarn.lock', 'package-lock.json', 'pnpm-lock.yaml'], { cwd: self.appRoot });
if (!lockFilePath) {
lockFilePath = find_up_1.default.sync('package.json', { cwd: opts.appPackageRoot });
}
let lockFileBuffer = lockFilePath ? fs_1.default.readFileSync(lockFilePath) : 'no-cache-key';
// @embroider/macros provides a macro called dependencySatisfies which checks if a given
// package name satisfies a given semver version range. Due to the way babel caches this can
// cause a problem where the macro plugin does not run (because it has been cached) but the version
// of the dependency being checked for changes (due to installing a different version). This will lead to
// the old evaluated state being used which might be invalid. This cache busting plugin keeps track of a
// hash representing the lock file of the app and if it ever changes forces babel to rerun its plugins.
// more information in issue #906
let hash = crypto_1.default.createHash('sha256');
hash = hash.update(lockFileBuffer);
if (appOrAddonInstance) {
// ensure that the actual running addon names and versions are accounted
// for in the cache key; this ensures that we still invalidate the cache
// when linking another project (e.g. ember-source) which would normally
// not cause the lockfile to change;
hash = hash.update(gatherAddonCacheKey(appOrAddonInstance.project));
}
let cacheKey = hash.digest('hex');
return [
[(0, path_1.join)(__dirname, 'babel', 'macros-babel-plugin.js'), opts],
[babelCacheBustingPluginPath, { version: cacheKey }, `/macros cache buster: ${owningPackageRoot}`],
];
}
// provides the ast plugins that implement the macro system, in reverse order
// for compatibility with the classic build, which historically always ran ast
// plugins in backwards order.
static astPlugins(owningPackageRoot) {
let result = this.transforms(owningPackageRoot);
result.plugins.reverse();
return result;
}
// todo: type adjuments here
// provides the ast plugins that implement the macro system
static transforms(owningPackageRoot) {
let configs;
let lazyParams = {
// this is deliberately lazy because we want to allow everyone to finish
// setting config before we generate the userConfigs
get configs() {
if (!configs) {
throw new Error(`Bug: /macros ast-transforms were not plugged into a MacrosConfig`);
}
return configs.userConfigs;
},
packageRoot: owningPackageRoot,
get appRoot() {
if (!configs) {
throw new Error(`Bug: /macros ast-transforms were not plugged into a MacrosConfig`);
}
return configs.appRoot;
},
};
let plugins = [(0, ast_transform_1.makeFirstTransform)(lazyParams), (0, ast_transform_1.makeSecondTransform)()];
function setConfig(c) {
configs = c;
}
return { plugins, setConfig, lazyParams };
}
mergerFor(pkgRoot) {
let entry = this.mergers.get(pkgRoot);
if (entry) {
return entry.merger;
}
return defaultMergerFor(pkgRoot);
}
resolvePackage(fromPath, packageName) {
let us = this.packageCache.ownerOfFile(fromPath);
if (!us) {
throw new Error(`[Embroider:MacrosConfig] unable to determine which npm package owns the file ${fromPath}`);
}
if (packageName) {
let target = this.packageCache.resolve(packageName, us);
return this.packageCache.original(target);
}
else {
return this.packageCache.original(us);
}
}
finalize() {
this._configWritable = false;
}
}
exports.default = MacrosConfig;
function defaultMergerFor(pkgRoot) {
return function defaultMerger(configs, { sourceOfConfig }) {
let [ownConfigs, otherConfigs] = (0, partition_1.default)(configs, c => sourceOfConfig(c).root === pkgRoot);
return Object.assign({}, ...ownConfigs, ...otherConfigs);
};
}
function isSerializable(obj) {
if (isScalar(obj)) {
return true;
}
if (Array.isArray(obj)) {
return !obj.some((arrayItem) => !isSerializable(arrayItem));
}
if (isPlainObject(obj)) {
for (let property in obj) {
let value = obj[property];
if (!isSerializable(value)) {
return false;
}
}
return true;
}
console.error('non serializable item found in config:', obj);
return false;
}
function isScalar(val) {
return (typeof val === 'undefined' ||
typeof val === 'string' ||
typeof val === 'boolean' ||
typeof val === 'number' ||
val === null);
}
function isPlainObject(obj) {
return typeof obj === 'object' && obj.constructor === Object && obj.toString() === '[object Object]';
}
//# sourceMappingURL=macros-config.js.map