Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | 4x 35x 35x 9x 26x 26x 1x 1x 1x 25x 4x 4x 21x 21x 21x 35x 54x 126x 126x 26x 28x 9x 126x 28x 28x 1x 1x 1x 21x 2x 21x 8x 21x 5x 7x 21x 17x 17x | /**
* ConfigLoader
* Loads configuration from project config files, read through the port.
*/
import { dirname, isAbsolute, join, resolve } from "node:path";
import IFileConfig from "./types/IFileConfig";
import PathNormalizer from "./PathNormalizer";
import NodeFileSystem from "../PARSE/1-Discover/NodeFileSystem";
import IFileSystem from "../types/IFileSystem";
/**
* Searched in this order in each directory. `cnext --help` documents these
* three names and JSON as the only format.
*/
const SEARCH_PLACES = ["cnext.config.json", ".cnext.json", ".cnextrc"];
/**
* Load configuration from project directory
*/
class ConfigLoader {
/**
* Load config from project directory, searching up the directory tree
* @param startDir - Directory to start searching from
* @returns Loaded configuration (empty object if no config found)
*/
static load(
startDir: string,
fs: IFileSystem = NodeFileSystem.instance,
): IFileConfig {
const found = ConfigLoader.find(resolve(startDir), fs);
if (!found) {
return {}; // No config found
}
let config: unknown;
try {
config = JSON.parse(found.content);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`Warning: Failed to parse config: ${message}`);
return {};
}
if (!config || typeof config !== "object" || Array.isArray(config)) {
console.error(
`Warning: Config ${found.path} is not a JSON object; ignored`,
);
return {};
}
const fileConfig = config as IFileConfig;
fileConfig._path = found.path;
return ConfigLoader.anchorPaths(fileConfig, dirname(found.path));
}
/**
* #1653: the first non-empty config file, from `startDir` up to the root.
*
* This replaces cosmiconfig, for two reasons. First, cosmiconfig read the
* disk itself, outside the injected port, where no gate could see it. Second,
* it read more than `cnext --help` documents. With `stopDir` set, cosmiconfig
* 9 switches to its "global" strategy, so after the upward walk it also
* searched the OS config directory for `config`, `config.json`,
* `config.yaml`, `config.js`, `config.ts` and more, and a JavaScript one
* would be executed. Only the documented search remains: the three names,
* JSON, and the upward walk.
*
* An empty file is skipped and the search continues, as cosmiconfig's
* default `ignoreEmptySearchPlaces` did. So is a file that cannot be read,
* as cosmiconfig skipped EACCES, now with a warning (#1826 review: letting
* the error escape aborted every run under an unreadable ancestor config).
*/
private static find(
startDir: string,
fs: IFileSystem,
): { path: string; content: string } | null {
for (let dir = startDir; ; dir = dirname(dir)) {
for (const place of SEARCH_PLACES) {
const content = ConfigLoader.readConfig(join(dir, place), fs);
if (content !== null && content.trim() !== "") {
return { path: join(dir, place), content };
}
}
if (dirname(dir) === dir) {
return null;
}
}
}
/** A config file's text, or null when there is none or it cannot be read. */
private static readConfig(path: string, fs: IFileSystem): string | null {
if (!fs.isFile(path)) return null;
try {
return fs.readFile(path);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`Warning: Failed to read config ${path}: ${message}`);
return null;
}
}
/**
* Issue #1547: anchor every path a config file declares to that config file's
* own directory.
*
* The search goes UPWARD from the start directory, so the config that is
* found is routinely not in the directory the user is standing in. A relative
* path read out of it and left relative is therefore resolved against the CWD
* by whoever consumes it, which makes the destination slide with the shell:
* the same project, config and entry file wrote the header to `proj/include`,
* `proj/src/include` or `proj/include/include` depending only on where the
* run started. `include` entries failed more quietly still -- an unresolvable
* directory is dropped by PathNormalizer.expandRecursive, so the path simply
* vanished from the effective config with nothing reported.
*
* The config file's directory is the project root by definition: it is the
* thing the upward search was looking for. Anchoring here rather than at each
* consumer also keeps CLI flags correct by construction -- everything this
* class returns came from the file, so there is no path by which a flag
* (typed at the shell, where CWD *is* the right anchor) can reach this code.
*/
private static anchorPaths(
config: IFileConfig,
configDir: string,
): IFileConfig {
if (config.output) {
config.output = ConfigLoader.anchor(config.output, configDir);
}
if (config.headerOut) {
config.headerOut = ConfigLoader.anchor(config.headerOut, configDir);
}
if (config.include) {
// Empty entries are filtered rather than anchored: `resolve(configDir, "")`
// is `configDir`, so anchoring one would silently put the whole project
// root on the header search path. Same reason `output` and `headerOut`
// above are guarded -- an empty `output` must keep meaning "(same dir as
// input)" rather than becoming the project root.
config.include = config.include
.filter(Boolean)
.map((path) => ConfigLoader.anchor(path, configDir));
}
return config;
}
/**
* Resolve one config-declared path against `configDir`. A `~` path is the
* user's own absolute spelling and an already-absolute path is already
* anchored, so neither is relative to anything and both are left alone.
*/
private static anchor(path: string, configDir: string): string {
const expanded = PathNormalizer.expandTilde(path);
return isAbsolute(expanded) ? expanded : resolve(configDir, expanded);
}
}
export default ConfigLoader;
|