Flatten, process, and cache incoming options by key.
This commit is contained in:
parent
a599c49436
commit
fc448ca8f2
@ -9,6 +9,8 @@ const debug = buildDebug("babel:config:config-chain");
|
||||
|
||||
import { findConfigs, loadConfig, type ConfigFile } from "./loading/files";
|
||||
|
||||
import { makeWeakCache, makeStrongCache } from "./caching";
|
||||
|
||||
type ConfigItem = {
|
||||
type: "options" | "arguments",
|
||||
options: {},
|
||||
@ -17,6 +19,13 @@ type ConfigItem = {
|
||||
loc: string,
|
||||
};
|
||||
|
||||
type ConfigRaw = {
|
||||
type: "options" | "arguments",
|
||||
options: {},
|
||||
alias: string,
|
||||
dirname: string,
|
||||
};
|
||||
|
||||
export default function buildConfigChain(opts: {}): Array<ConfigItem> | null {
|
||||
if (typeof opts.filename !== "string" && opts.filename != null) {
|
||||
throw new Error(".filename must be a string, null, or undefined");
|
||||
@ -54,33 +63,231 @@ class ConfigChainBuilder {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
mergeConfigArguments(opts, dirname, envKey: string) {
|
||||
this.mergeConfig(
|
||||
{
|
||||
type: "arguments",
|
||||
options: opts,
|
||||
alias: "base",
|
||||
dirname,
|
||||
},
|
||||
envKey,
|
||||
mergeConfigArguments(opts: {}, dirname: string, envKey: string) {
|
||||
flattenArgumentsOptionsParts(opts, dirname, envKey).forEach(part =>
|
||||
this._processConfigPart(part, envKey),
|
||||
);
|
||||
}
|
||||
|
||||
mergeConfigFile(file: ConfigFile, envKey: string) {
|
||||
const { filepath, dirname, options } = file;
|
||||
|
||||
this.mergeConfig(
|
||||
{
|
||||
type: "options",
|
||||
options,
|
||||
alias: filepath,
|
||||
dirname,
|
||||
},
|
||||
envKey,
|
||||
flattenFileOptionsParts(file)(envKey).forEach(part =>
|
||||
this._processConfigPart(part, envKey),
|
||||
);
|
||||
}
|
||||
|
||||
mergeConfig({ type, options: rawOpts, alias, dirname }, envKey: string) {
|
||||
_processConfigPart(part: ConfigPart, envKey: string) {
|
||||
if (part.part === "config") {
|
||||
const { ignore, only } = part;
|
||||
|
||||
// Bail out ASAP if this file is ignored so that we run as little logic as possible on ignored files.
|
||||
if (
|
||||
this.file &&
|
||||
this.file.shouldIgnore(ignore, only, part.config.dirname)
|
||||
) {
|
||||
// TODO(logan): This is a really gross way to bail out. Avoid this in rewrite.
|
||||
throw Object.assign((new Error("This file has been ignored."): any), {
|
||||
code: "BABEL_IGNORED_FILE",
|
||||
});
|
||||
}
|
||||
|
||||
this.configs.push(part.config);
|
||||
} else {
|
||||
const extendsConfig = loadConfig(part.path, part.dirname);
|
||||
|
||||
const existingConfig = this.configs.some(config => {
|
||||
return config.alias === extendsConfig.filepath;
|
||||
});
|
||||
if (!existingConfig) {
|
||||
this.mergeConfigFile(extendsConfig, envKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the root config object passed to Babel, split it into the separate
|
||||
* config parts. The resulting config objects in the 'ConfigPart' have their
|
||||
* object identity preserved between calls so that they can be used for caching.
|
||||
*/
|
||||
function flattenArgumentsOptionsParts(
|
||||
opts: {},
|
||||
dirname: string,
|
||||
envKey: string,
|
||||
): Array<ConfigPart> {
|
||||
const raw = [];
|
||||
|
||||
const env = typeof opts.env === "object" ? opts.env : null;
|
||||
const plugins = Array.isArray(opts.plugins) ? opts.plugins : null;
|
||||
const presets = Array.isArray(opts.presets) ? opts.presets : null;
|
||||
const passPerPreset =
|
||||
typeof opts.passPerPreset === "boolean" ? opts.passPerPreset : false;
|
||||
|
||||
if (env) {
|
||||
raw.push(...flattenArgumentsEnvOptionsParts(env)(dirname)(envKey));
|
||||
}
|
||||
|
||||
const innerOpts = Object.assign({}, opts);
|
||||
// If the env, plugins, and presets values on the object aren't arrays or
|
||||
// objects, leave them in the base opts so that normal options validation
|
||||
// will throw errors on them later.
|
||||
if (env) delete innerOpts.env;
|
||||
if (plugins) delete innerOpts.plugins;
|
||||
if (presets) {
|
||||
delete innerOpts.presets;
|
||||
delete innerOpts.passPerPreset;
|
||||
}
|
||||
delete innerOpts.extends;
|
||||
|
||||
if (Object.keys(innerOpts).length > 0) {
|
||||
raw.push(
|
||||
...flattenOptionsParts({
|
||||
type: "arguments",
|
||||
options: innerOpts,
|
||||
alias: "base",
|
||||
dirname,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (plugins) {
|
||||
raw.push(...flattenArgumentsPluginsOptionsParts(plugins)(dirname));
|
||||
}
|
||||
if (presets) {
|
||||
raw.push(
|
||||
...flattenArgumentsPresetsOptionsParts(presets)(passPerPreset)(dirname),
|
||||
);
|
||||
}
|
||||
|
||||
if (opts.extends != null) {
|
||||
raw.push(
|
||||
...flattenOptionsParts(
|
||||
buildArgumentsRawConfig({ extends: opts.extends }, dirname),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* For the top-level 'options' object, we cache the env list based on
|
||||
* the object identity of the 'env' object.
|
||||
*/
|
||||
const flattenArgumentsEnvOptionsParts = makeWeakCache((env: {}) => {
|
||||
const options = { env };
|
||||
|
||||
return makeStrongCache((dirname: string) =>
|
||||
flattenOptionsPartsLookup(buildArgumentsRawConfig(options, dirname)),
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* For the top-level 'options' object, we cache the plugin list based on
|
||||
* the object identity of the 'plugins' object.
|
||||
*/
|
||||
const flattenArgumentsPluginsOptionsParts = makeWeakCache(
|
||||
(plugins: Array<mixed>) => {
|
||||
const options = { plugins };
|
||||
|
||||
return makeStrongCache((dirname: string) =>
|
||||
flattenOptionsParts(buildArgumentsRawConfig(options, dirname)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* For the top-level 'options' object, we cache the preset list based on
|
||||
* the object identity of the 'presets' object.
|
||||
*/
|
||||
const flattenArgumentsPresetsOptionsParts = makeWeakCache(
|
||||
(presets: Array<mixed>) =>
|
||||
makeStrongCache((passPerPreset: ?boolean) => {
|
||||
// The concept of passPerPreset is integrally tied to the preset list
|
||||
// so unfortunately we need to copy both values here, adding an extra
|
||||
// layer of caching functions.
|
||||
const options = { presets, passPerPreset };
|
||||
|
||||
return makeStrongCache((dirname: string) =>
|
||||
flattenOptionsParts(buildArgumentsRawConfig(options, dirname)),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
function buildArgumentsRawConfig(options: {}, dirname: string): ConfigRaw {
|
||||
return {
|
||||
type: "arguments",
|
||||
options,
|
||||
alias: "base",
|
||||
dirname,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a config from a specific file, return a list of ConfigPart objects
|
||||
* with object identity preserved for all 'config' part objects for use
|
||||
* with caching later in config processing.
|
||||
*/
|
||||
const flattenFileOptionsParts = makeWeakCache((file: ConfigFile) => {
|
||||
return flattenOptionsPartsLookup({
|
||||
type: "options",
|
||||
options: file.options,
|
||||
alias: file.filepath,
|
||||
dirname: file.dirname,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Given a config, create a function that will return the config parts for
|
||||
* the environment passed as the first argument.
|
||||
*/
|
||||
function flattenOptionsPartsLookup(
|
||||
config: ConfigRaw,
|
||||
): (string | null) => Array<ConfigPart> {
|
||||
const parts = flattenOptionsParts(config);
|
||||
|
||||
const def = parts.filter(part => part.activeEnv === null);
|
||||
const lookup = new Map();
|
||||
|
||||
parts.forEach(part => {
|
||||
if (part.activeEnv !== null) lookup.set(part.activeEnv, []);
|
||||
});
|
||||
|
||||
for (const [activeEnv, values] of lookup) {
|
||||
parts.forEach(part => {
|
||||
if (part.activeEnv === null || part.activeEnv === activeEnv) {
|
||||
values.push(part);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return envKey => lookup.get(envKey) || def;
|
||||
}
|
||||
|
||||
type ConfigPart =
|
||||
| {
|
||||
part: "config",
|
||||
config: ConfigItem,
|
||||
ignore: ?Array<mixed>,
|
||||
only: ?Array<mixed>,
|
||||
activeEnv: string | null,
|
||||
}
|
||||
| {
|
||||
part: "extends",
|
||||
path: string,
|
||||
dirname: string,
|
||||
activeEnv: string | null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a generic config object, flatten it into its various parts so that
|
||||
* then can be cached and processed later.
|
||||
*/
|
||||
function flattenOptionsParts(
|
||||
rawConfig: ConfigRaw,
|
||||
activeEnv: string | null = null,
|
||||
): Array<ConfigPart> {
|
||||
const { type, options: rawOpts, alias, dirname } = rawConfig;
|
||||
|
||||
if (rawOpts.ignore != null && !Array.isArray(rawOpts.ignore)) {
|
||||
throw new Error(
|
||||
`.ignore should be an array, ${JSON.stringify(rawOpts.ignore)} given`,
|
||||
@ -91,21 +298,10 @@ class ConfigChainBuilder {
|
||||
`.only should be an array, ${JSON.stringify(rawOpts.only)} given`,
|
||||
);
|
||||
}
|
||||
const ignore = rawOpts.ignore || null;
|
||||
const only = rawOpts.only || null;
|
||||
|
||||
// Bail out ASAP if this file is ignored so that we run as little logic as possible on ignored files.
|
||||
if (
|
||||
this.file &&
|
||||
this.file.shouldIgnore(rawOpts.ignore, rawOpts.only, dirname)
|
||||
) {
|
||||
// TODO(logan): This is a really cross way to bail out. Avoid this in rewrite.
|
||||
throw Object.assign((new Error("This file has been ignored."): any), {
|
||||
code: "BABEL_IGNORED_FILE",
|
||||
});
|
||||
}
|
||||
|
||||
const options = Object.assign({}, rawOpts);
|
||||
delete options.env;
|
||||
delete options.extends;
|
||||
const parts = [];
|
||||
|
||||
if (
|
||||
rawOpts.env != null &&
|
||||
@ -114,7 +310,14 @@ class ConfigChainBuilder {
|
||||
throw new Error(".env block must be an object, null, or undefined");
|
||||
}
|
||||
|
||||
const envOpts = rawOpts.env && rawOpts.env[envKey];
|
||||
const rawEnv = rawOpts.env || {};
|
||||
|
||||
Object.keys(rawEnv).forEach(envKey => {
|
||||
const envOpts = rawEnv[envKey];
|
||||
|
||||
if (envOpts !== undefined && activeEnv !== null && activeEnv !== envKey) {
|
||||
throw new Error(`Unreachable .env[${envKey}] block detected`);
|
||||
}
|
||||
|
||||
if (
|
||||
envOpts != null &&
|
||||
@ -124,40 +327,52 @@ class ConfigChainBuilder {
|
||||
}
|
||||
|
||||
if (envOpts) {
|
||||
this.mergeConfig(
|
||||
parts.push(
|
||||
...flattenOptionsParts(
|
||||
{
|
||||
type,
|
||||
options: envOpts,
|
||||
alias: `${alias}.env.${envKey}`,
|
||||
dirname: dirname,
|
||||
alias: alias + `.env.${envKey}`,
|
||||
dirname,
|
||||
},
|
||||
envKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.configs.push({
|
||||
const options = Object.assign({}, rawOpts);
|
||||
delete options.env;
|
||||
delete options.extends;
|
||||
|
||||
parts.push({
|
||||
part: "config",
|
||||
config: {
|
||||
type,
|
||||
options,
|
||||
alias,
|
||||
loc: alias,
|
||||
dirname,
|
||||
},
|
||||
ignore,
|
||||
only,
|
||||
activeEnv,
|
||||
});
|
||||
|
||||
if (rawOpts.extends) {
|
||||
if (rawOpts.extends != null) {
|
||||
if (typeof rawOpts.extends !== "string") {
|
||||
throw new Error(".extends must be a string");
|
||||
}
|
||||
|
||||
const extendsConfig = loadConfig(rawOpts.extends, dirname);
|
||||
|
||||
const existingConfig = this.configs.some(config => {
|
||||
return config.alias === extendsConfig.filepath;
|
||||
parts.push({
|
||||
part: "extends",
|
||||
path: rawOpts.extends,
|
||||
dirname,
|
||||
activeEnv,
|
||||
});
|
||||
if (!existingConfig) {
|
||||
this.mergeConfigFile(extendsConfig, envKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -31,7 +31,7 @@ export function makeStrongCache<ArgT, ResultT>(
|
||||
* configures its caching behavior. Cached values are stored weakly and the function argument must be
|
||||
* an object type.
|
||||
*/
|
||||
export function makeWeakCache<ArgT: {}, ResultT>(
|
||||
export function makeWeakCache<ArgT: {} | Array<*>, ResultT>(
|
||||
handler: (ArgT, CacheConfigurator) => ResultT,
|
||||
autoPermacache?: boolean,
|
||||
): ArgT => ResultT {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import assert from "assert";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import buildConfigChain from "../lib/config/build-config-chain";
|
||||
|
||||
@ -52,6 +53,234 @@ describe("buildConfigChain", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("caching", function() {
|
||||
describe("programmatic options", function() {
|
||||
it("should not cache the input options by identity", () => {
|
||||
const comments = false;
|
||||
|
||||
const chain1 = buildConfigChain({ comments });
|
||||
const chain2 = buildConfigChain({ comments });
|
||||
|
||||
assert.equal(chain1.length, 1);
|
||||
assert.equal(chain2.length, 1);
|
||||
assert.notStrictEqual(chain1[0], chain2[0]);
|
||||
});
|
||||
|
||||
it("should cache the env options by identity", () => {
|
||||
process.env.NODE_ENV = "foo";
|
||||
const env = {
|
||||
foo: {
|
||||
comments: false,
|
||||
},
|
||||
};
|
||||
|
||||
const chain1 = buildConfigChain({ env });
|
||||
const chain2 = buildConfigChain({ env });
|
||||
|
||||
assert.equal(chain1.length, 2);
|
||||
assert.equal(chain2.length, 2);
|
||||
assert.strictEqual(chain1[0], chain2[0]);
|
||||
assert.strictEqual(chain1[1], chain2[1]);
|
||||
});
|
||||
|
||||
it("should cache the plugin options by identity", () => {
|
||||
const plugins = [];
|
||||
|
||||
const chain1 = buildConfigChain({ plugins });
|
||||
const chain2 = buildConfigChain({ plugins });
|
||||
|
||||
assert.equal(chain1.length, 1);
|
||||
assert.equal(chain2.length, 1);
|
||||
assert.strictEqual(chain1[0], chain2[0]);
|
||||
});
|
||||
|
||||
it("should cache the presets options by identity", () => {
|
||||
const presets = [];
|
||||
|
||||
const chain1 = buildConfigChain({ presets });
|
||||
const chain2 = buildConfigChain({ presets });
|
||||
|
||||
assert.equal(chain1.length, 1);
|
||||
assert.equal(chain2.length, 1);
|
||||
assert.strictEqual(chain1[0], chain2[0]);
|
||||
});
|
||||
|
||||
it("should not cache the presets options with passPerPreset", () => {
|
||||
const presets = [];
|
||||
|
||||
const chain1 = buildConfigChain({ presets });
|
||||
const chain2 = buildConfigChain({ presets, passPerPreset: true });
|
||||
const chain3 = buildConfigChain({ presets, passPerPreset: false });
|
||||
|
||||
assert.equal(chain1.length, 1);
|
||||
assert.equal(chain2.length, 1);
|
||||
assert.equal(chain3.length, 1);
|
||||
assert.notStrictEqual(chain1[0], chain2[0]);
|
||||
assert.strictEqual(chain1[0], chain3[0]);
|
||||
assert.notStrictEqual(chain2[0], chain3[0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("config file options", function() {
|
||||
function touch(filepath) {
|
||||
const s = fs.statSync(filepath);
|
||||
fs.utimesSync(
|
||||
filepath,
|
||||
s.atime,
|
||||
s.mtime + Math.random() > 0.5 ? 1 : -1,
|
||||
);
|
||||
}
|
||||
|
||||
it("should cache package.json files by mtime", () => {
|
||||
const filename = fixture(
|
||||
"complex-plugin-config",
|
||||
"config-identity",
|
||||
"pkg",
|
||||
"src.js",
|
||||
);
|
||||
const pkgJSON = fixture(
|
||||
"complex-plugin-config",
|
||||
"config-identity",
|
||||
"pkg",
|
||||
"package.json",
|
||||
);
|
||||
|
||||
const chain1 = buildConfigChain({ filename });
|
||||
const chain2 = buildConfigChain({ filename });
|
||||
|
||||
touch(pkgJSON);
|
||||
|
||||
const chain3 = buildConfigChain({ filename });
|
||||
const chain4 = buildConfigChain({ filename });
|
||||
|
||||
assert.equal(chain1.length, 3);
|
||||
assert.equal(chain2.length, 3);
|
||||
assert.equal(chain3.length, 3);
|
||||
assert.equal(chain4.length, 3);
|
||||
assert.equal(chain1[1].alias, pkgJSON);
|
||||
assert.equal(chain2[1].alias, pkgJSON);
|
||||
assert.equal(chain3[1].alias, pkgJSON);
|
||||
assert.equal(chain4[1].alias, pkgJSON);
|
||||
assert.strictEqual(chain1[1], chain2[1]);
|
||||
|
||||
// Identity changed after touch().
|
||||
assert.notStrictEqual(chain3[1], chain1[1]);
|
||||
assert.strictEqual(chain3[1], chain4[1]);
|
||||
});
|
||||
|
||||
it("should cache .babelrc files by mtime", () => {
|
||||
const filename = fixture(
|
||||
"complex-plugin-config",
|
||||
"config-identity",
|
||||
"babelrc",
|
||||
"src.js",
|
||||
);
|
||||
const babelrcFile = fixture(
|
||||
"complex-plugin-config",
|
||||
"config-identity",
|
||||
"babelrc",
|
||||
".babelrc",
|
||||
);
|
||||
|
||||
const chain1 = buildConfigChain({ filename });
|
||||
const chain2 = buildConfigChain({ filename });
|
||||
|
||||
touch(babelrcFile);
|
||||
|
||||
const chain3 = buildConfigChain({ filename });
|
||||
const chain4 = buildConfigChain({ filename });
|
||||
|
||||
assert.equal(chain1.length, 3);
|
||||
assert.equal(chain2.length, 3);
|
||||
assert.equal(chain3.length, 3);
|
||||
assert.equal(chain4.length, 3);
|
||||
assert.equal(chain1[1].alias, babelrcFile);
|
||||
assert.equal(chain2[1].alias, babelrcFile);
|
||||
assert.equal(chain3[1].alias, babelrcFile);
|
||||
assert.equal(chain4[1].alias, babelrcFile);
|
||||
assert.strictEqual(chain1[1], chain2[1]);
|
||||
|
||||
// Identity changed after touch().
|
||||
assert.notStrictEqual(chain3[1], chain1[1]);
|
||||
assert.strictEqual(chain3[1], chain4[1]);
|
||||
});
|
||||
|
||||
it("should cache .babelignore files by mtime", () => {
|
||||
const filename = fixture(
|
||||
"complex-plugin-config",
|
||||
"config-identity",
|
||||
"babelignore",
|
||||
"src.js",
|
||||
);
|
||||
const babelignoreFile = fixture(
|
||||
"complex-plugin-config",
|
||||
"config-identity",
|
||||
"babelignore",
|
||||
".babelignore",
|
||||
);
|
||||
|
||||
const chain1 = buildConfigChain({ filename });
|
||||
const chain2 = buildConfigChain({ filename });
|
||||
|
||||
touch(babelignoreFile);
|
||||
|
||||
const chain3 = buildConfigChain({ filename });
|
||||
const chain4 = buildConfigChain({ filename });
|
||||
|
||||
assert.equal(chain1.length, 6);
|
||||
assert.equal(chain2.length, 6);
|
||||
assert.equal(chain3.length, 6);
|
||||
assert.equal(chain4.length, 6);
|
||||
assert.equal(chain1[4].alias, babelignoreFile);
|
||||
assert.equal(chain2[4].alias, babelignoreFile);
|
||||
assert.equal(chain3[4].alias, babelignoreFile);
|
||||
assert.equal(chain4[4].alias, babelignoreFile);
|
||||
assert.strictEqual(chain1[4], chain2[4]);
|
||||
|
||||
// Identity changed after touch().
|
||||
assert.notStrictEqual(chain3[4], chain1[4]);
|
||||
assert.strictEqual(chain3[4], chain4[4]);
|
||||
});
|
||||
|
||||
it("should cache .babelrc.js files programmable behavior", () => {
|
||||
const filename = fixture(
|
||||
"complex-plugin-config",
|
||||
"config-identity",
|
||||
"babelrc-js",
|
||||
"src.js",
|
||||
);
|
||||
const babelrcFile = fixture(
|
||||
"complex-plugin-config",
|
||||
"config-identity",
|
||||
"babelrc-js",
|
||||
".babelrc.js",
|
||||
);
|
||||
|
||||
const chain1 = buildConfigChain({ filename });
|
||||
const chain2 = buildConfigChain({ filename });
|
||||
|
||||
process.env.NODE_ENV = "new-env";
|
||||
|
||||
const chain3 = buildConfigChain({ filename });
|
||||
const chain4 = buildConfigChain({ filename });
|
||||
|
||||
assert.equal(chain1.length, 3);
|
||||
assert.equal(chain2.length, 3);
|
||||
assert.equal(chain3.length, 3);
|
||||
assert.equal(chain4.length, 3);
|
||||
assert.equal(chain1[1].alias, babelrcFile);
|
||||
assert.equal(chain2[1].alias, babelrcFile);
|
||||
assert.equal(chain3[1].alias, babelrcFile);
|
||||
assert.equal(chain4[1].alias, babelrcFile);
|
||||
assert.strictEqual(chain1[1], chain2[1]);
|
||||
|
||||
// Identity changed after changing the NODE_ENV.
|
||||
assert.notStrictEqual(chain3[1], chain1[1]);
|
||||
assert.strictEqual(chain3[1], chain4[1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("dir1", function() {
|
||||
const chain = buildConfigChain({
|
||||
filename: fixture("dir1", "src.js"),
|
||||
|
||||
@ -0,0 +1 @@
|
||||
fake-file.js
|
||||
@ -0,0 +1,7 @@
|
||||
module.exports = function(api) {
|
||||
api.env();
|
||||
|
||||
return {
|
||||
comments: false,
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
{
|
||||
comments: false,
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
{
|
||||
"babel": {
|
||||
"comments": false
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user