{
22 | const sections = getRelevantSections(options);
23 |
24 | const context: GenerateReadmeContext = {
25 | ...options,
26 | sections,
27 | str: options.existingReadme ?? ""
28 | };
29 |
30 | if (sections.has("logo")) {
31 | generateLogoSection(context);
32 | }
33 |
34 | if (sections.has("description_short")) {
35 | generateDescriptionShortSection(context);
36 | }
37 |
38 | if (sections.has("badges")) {
39 | generateBadgesSection(context);
40 | }
41 |
42 | if (sections.has("description_long")) {
43 | generateDescriptionLongSection(context);
44 | }
45 |
46 | if (sections.has("features")) {
47 | generateFeaturesSection(context);
48 | }
49 |
50 | if (sections.has("feature_image")) {
51 | generateFeatureImageSection(context);
52 | }
53 |
54 | if (sections.has("backers")) {
55 | generateBackersSection(context);
56 | }
57 |
58 | if (sections.has("toc")) {
59 | generateTableOfContentsSection(context, true);
60 | }
61 |
62 | if (sections.has("install")) {
63 | generateInstallSection(context);
64 | }
65 |
66 | if (sections.has("usage")) {
67 | generateUsageSection(context);
68 | }
69 |
70 | if (sections.has("contributing")) {
71 | generateContributingSection(context);
72 | }
73 |
74 | if (sections.has("maintainers")) {
75 | generateMaintainersSection(context);
76 | }
77 |
78 | if (sections.has("faq")) {
79 | generateFaqSection(context);
80 | }
81 |
82 | if (sections.has("license")) {
83 | await generateLicenseSection(context);
84 | }
85 |
86 | if (sections.has("toc")) {
87 | generateTableOfContentsSection(context, false);
88 | }
89 |
90 | // Stringify the StringBuilder
91 | return options.prettier.format(context.str, {
92 | ...options.config.prettier,
93 | parser: "markdown"
94 | });
95 | }
96 |
97 | /**
98 | * Sets the given content with the given content within the context
99 | */
100 | function setSection(context: GenerateReadmeContext, sectionKind: SectionKind, content: string, outro?: string): void {
101 | const startMark = getShadowSectionMark(sectionKind, "start");
102 | const endMark = getShadowSectionMark(sectionKind, "end");
103 | const startMarkIndex = context.str.indexOf(startMark);
104 | const endMarkIndex = context.str.indexOf(endMark);
105 | const markedContent = startMark + "\n\n" + content + "\n\n" + endMark + "\n\n" + (outro == null ? "" : outro + "\n\n");
106 |
107 | if (startMarkIndex >= 0 && endMarkIndex >= 0) {
108 | const before = context.str.slice(0, startMarkIndex);
109 | const after = context.str.slice(endMarkIndex + endMark.length);
110 | context.str = before + markedContent + after;
111 | } else {
112 | context.str += markedContent;
113 | }
114 | }
115 |
116 | /**
117 | * Generates the logo section of the README
118 | */
119 | function generateLogoSection(context: GenerateReadmeContext): void {
120 | setSection(
121 | context,
122 | "logo",
123 | // Don't proceed if there is no logo to generate an image for
124 | context.config.logo.url == null
125 | ? ""
126 | : `${formatImage({
127 | url: context.config.logo.url,
128 | alt: "Logo",
129 | height: context.config.logo.height
130 | })}
`
131 | );
132 | }
133 |
134 | /**
135 | * Generates the feature image section of the README
136 | */
137 | function generateFeatureImageSection(context: GenerateReadmeContext): void {
138 | setSection(
139 | context,
140 | "feature_image",
141 | // Don't proceed if there is no feature image to generate an image for
142 | context.config.featureImage.url == null
143 | ? ""
144 | : `${formatImage({
145 | url: context.config.featureImage.url,
146 | alt: "Feature image",
147 | height: context.config.featureImage.height
148 | })}
`
149 | );
150 | }
151 |
152 | /**
153 | * Generates the Table Of Contents section of the README
154 | */
155 | function generateTableOfContentsSection(context: GenerateReadmeContext, reserveOnly = false): void {
156 | setSection(
157 | context,
158 | "toc",
159 | `## Table of Contents\n\n` +
160 | (reserveOnly
161 | ? // Only reserve the spot within the README with an empty placeholder that can be replaced later on
162 | ``
163 | : toc(context.str).content)
164 | );
165 | }
166 |
167 | /**
168 | * Generates the badges section of the README
169 | */
170 | async function generateBadgesSection(context: GenerateReadmeContext): Promise {
171 | const badges = await getBadges(context);
172 |
173 | const content = Object.values(badges)
174 | .map(value => {
175 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
176 | if (value == null) return "";
177 | return value.join("\n");
178 | })
179 | .join("\n");
180 | setSection(context, "badges", content);
181 | }
182 |
183 | /**
184 | * Generates the short description section of the README
185 | */
186 | function generateDescriptionShortSection(context: GenerateReadmeContext): void {
187 | // Don't proceed if the package has no description
188 | if (context.pkg.description == null) return;
189 |
190 | setSection(context, "description_short", `> ${context.pkg.description}`);
191 | }
192 |
193 | /**
194 | * Generates the long description section of the README
195 | */
196 | function generateDescriptionLongSection(context: GenerateReadmeContext): void {
197 | setSection(context, "description_long", `## Description`);
198 | }
199 |
200 | /**
201 | * Generates the features section of the README
202 | */
203 | function generateFeaturesSection(context: GenerateReadmeContext): void {
204 | setSection(context, "features", `### Features\n\n`);
205 | }
206 |
207 | /**
208 | * Generates the FAQ section of the README
209 | */
210 | function generateFaqSection(context: GenerateReadmeContext): void {
211 | setSection(context, "faq", `## FAQ\n\n`);
212 | }
213 |
214 | function generateNpxStep(binName: string, requiredPeerDependencies: string[], context: GenerateReadmeContext): string {
215 | const canUseShorthand = binName === context.pkg.name;
216 | const simpleCommand = "```\n" + `$ npx ${canUseShorthand ? `${context.pkg.name}` : `-p ${context.pkg.name} ${binName}`}\n` + "```";
217 |
218 | if (requiredPeerDependencies.length < 1) {
219 | return simpleCommand;
220 | } else if (canUseShorthand) {
221 | return (
222 | `First, add ${requiredPeerDependencies.length === 1 ? "the peer dependency" : "the peer dependencies"} ${listFormat(
223 | requiredPeerDependencies,
224 | "and",
225 | element => `\`${element}\``
226 | )} as${requiredPeerDependencies.length === 1 ? " a" : ""}${context.config.isDevelopmentPackage ? " development " : ""}${
227 | requiredPeerDependencies.length === 1 ? " dependency" : "dependencies"
228 | } to the package from which you're going to run \`${binName}\`. Alternatively, if you want to run it from _anywhere_, you can also install ${
229 | requiredPeerDependencies.length === 1 ? "it" : "them"
230 | } globally: \`npm i -g ${requiredPeerDependencies.join(" ")}\`. Now, you can simply run:\n` +
231 | simpleCommand +
232 | "\n" +
233 | `You can also run \`${binName}\` along with its peer dependencies in one combined command:\n` +
234 | "```\n" +
235 | `$ npx${requiredPeerDependencies.map(requiredPeerDependency => ` -p ${requiredPeerDependency}`).join("")} -p ${context.pkg.name} ${binName}\n` +
236 | "```\n"
237 | );
238 | } else {
239 | return "```\n" + `$ npx${requiredPeerDependencies.map(requiredPeerDependency => ` -p ${requiredPeerDependency}`).join("")} -p ${context.pkg.name} ${binName}\n` + "```\n";
240 | }
241 | }
242 |
243 | /**
244 | * Generates the install section of the README
245 | */
246 | function generateInstallSection(context: GenerateReadmeContext): void {
247 | // Don't proceed if the package has no name
248 | if (context.pkg.name == null) return;
249 | const peerDependencies =
250 | context.pkg.peerDependencies == null
251 | ? []
252 | : Object.keys(context.pkg.peerDependencies).map(peerDependency => ({
253 | peerDependency,
254 | optional: Boolean(context.pkg.peerDependenciesMeta?.[peerDependency]?.optional)
255 | }));
256 |
257 | const requiredPeerDependencies = peerDependencies.filter(({optional}) => !optional).map(({peerDependency}) => peerDependency);
258 | const optionalPeerDependencies = peerDependencies.filter(({optional}) => optional).map(({peerDependency}) => peerDependency);
259 |
260 | const firstBinName = context.pkg.bin == null ? undefined : Object.keys(context.pkg.bin)[0];
261 |
262 | setSection(
263 | context,
264 | "install",
265 | `## Install\n\n` +
266 | `### npm\n\n` +
267 | "```\n" +
268 | `$ npm install ${context.pkg.name}${context.config.isDevelopmentPackage ? ` --save-dev` : ``}\n` +
269 | "```\n\n" +
270 | `### Yarn\n\n` +
271 | "```\n" +
272 | `$ yarn add ${context.pkg.name}${context.config.isDevelopmentPackage ? ` --dev` : ``}\n` +
273 | "```\n\n" +
274 | `### pnpm\n\n` +
275 | "```\n" +
276 | `$ pnpm add ${context.pkg.name}${context.config.isDevelopmentPackage ? ` --save-dev` : ``}\n` +
277 | "```" +
278 | (firstBinName == null ? "" : `\n\n` + `### Run once with npx\n\n` + generateNpxStep(firstBinName, requiredPeerDependencies, context)) +
279 | (peerDependencies.length < 1
280 | ? ""
281 | : "\n\n" +
282 | `### Peer Dependencies\n\n` +
283 | (requiredPeerDependencies.length < 1
284 | ? ""
285 | : `\`${context.pkg.name}\` depends on ${listFormat(requiredPeerDependencies, "and", element => `\`${element}\``)}, so you need to manually install ${
286 | requiredPeerDependencies.length === 1 ? "this" : "these"
287 | }${context.config.isDevelopmentPackage ? ` as ${requiredPeerDependencies.length === 1 ? "a development dependency" : "development dependencies"}` : ``} as well.`) +
288 | (optionalPeerDependencies.length < 1
289 | ? ""
290 | : (requiredPeerDependencies.length < 1 ? `You may` : `\n\nYou may also`) +
291 | ` need to install ${
292 | optionalPeerDependencies.length < 2
293 | ? `\`${optionalPeerDependencies[0]}\``
294 | : `${requiredPeerDependencies.length < 1 ? "" : "additional "}peer dependencies such as ${listFormat(
295 | optionalPeerDependencies,
296 | "or",
297 | element => `\`${element}\``
298 | )}`
299 | } depending on the features you are going to use. Refer to the documentation for the specific cases where ${
300 | optionalPeerDependencies.length < 2 ? "it" : "any of these"
301 | } may be relevant.`))
302 | );
303 | }
304 |
305 | /**
306 | * Generates the usage section of the README
307 | */
308 | function generateUsageSection(context: GenerateReadmeContext): void {
309 | setSection(context, "usage", `## Usage`);
310 | }
311 |
312 | /**
313 | * Generates the contributing section of the README
314 | */
315 | function generateContributingSection(context: GenerateReadmeContext): void {
316 | // Only add the contributing section if a CONTRIBUTING.md file exists
317 | const contributingFilePath = path.join(context.root, CONSTANT.codeOfConductFilename);
318 | const nativeContributingFilePath = path.native.normalize(contributingFilePath);
319 |
320 | setSection(
321 | context,
322 | "contributing",
323 | !context.fs.existsSync(nativeContributingFilePath)
324 | ? ""
325 | : `## Contributing\n\n` + `Do you want to contribute? Awesome! Please follow [these recommendations](./${CONSTANT.contributingFilename}).`
326 | );
327 | }
328 |
329 | function generateContributorTable(contributors: Contributor[]): string {
330 | let str = "\n|";
331 |
332 | contributors.forEach(contributor => {
333 | const inner =
334 | contributor.imageUrl == null
335 | ? undefined
336 | : formatImage({
337 | alt: contributor.name,
338 | url: contributor.imageUrl,
339 | height: CONSTANT.contributorImageUrlHeight
340 | });
341 |
342 | const formattedImageWithUrl =
343 | inner == null
344 | ? ""
345 | : contributor.email != null
346 | ? formatUrl({url: `mailto:${contributor.email}`, inner})
347 | : contributor.url != null
348 | ? formatUrl({url: contributor.url, inner})
349 | : inner;
350 |
351 | str += formattedImageWithUrl;
352 | str += "|";
353 | });
354 | str += "\n|";
355 | contributors.forEach(() => {
356 | str += "-----------|";
357 | });
358 |
359 | str += "\n|";
360 |
361 | contributors.forEach(contributor => {
362 | if (contributor.name != null) {
363 | if (contributor.email != null) {
364 | str += `[${contributor.name}](mailto:${contributor.email})`;
365 | } else if (contributor.url != null) {
366 | str += `[${contributor.name}](${contributor.url})`;
367 | } else {
368 | str += contributor.name;
369 | }
370 | }
371 |
372 | if (contributor.twitter != null) {
373 | str += `
Twitter: [@${contributor.twitter}](https://twitter.com/${contributor.twitter})`;
374 | }
375 |
376 | if (contributor.github != null) {
377 | str += `
Github: [@${contributor.github}](https://github.com/${contributor.github})`;
378 | }
379 |
380 | // Also add the role of the contributor, if available
381 | if (contributor.role != null) {
382 | str += `
_${contributor.role}_`;
383 | }
384 | str += "|";
385 | });
386 |
387 | return str;
388 | }
389 |
390 | /**
391 | * Generates the maintainers section of the README
392 | */
393 | function generateMaintainersSection(context: GenerateReadmeContext): void {
394 | const contributors = getContributorsFromPackage(context.pkg);
395 |
396 | setSection(context, "maintainers", contributors.length < 1 ? "" : `## Maintainers\n\n` + generateContributorTable(contributors));
397 | }
398 |
399 | function guessPreferredFundingUrlForOtherDonations(context: GenerateReadmeContext): string | undefined {
400 | // If a funding url is given, that should take precedence.
401 | if (context.config.donate.other.fundingUrl != null) {
402 | return context.config.donate.other.fundingUrl;
403 | }
404 |
405 | // Otherwise, it might be provided from a "funding" property in the package.json file
406 | if (context.pkg.funding != null) {
407 | if (typeof context.pkg.funding === "string") {
408 | return context.pkg.funding;
409 | } else if (context.pkg.funding.url != null) {
410 | return context.pkg.funding.url;
411 | }
412 | }
413 |
414 | // Otherwise, there's no way to know.
415 | return undefined;
416 | }
417 |
418 | /**
419 | * Generates the backers section of the README
420 | */
421 | function generateBackersSection(context: GenerateReadmeContext): void {
422 | let content = "";
423 |
424 | if (context.config.donate.other.donors.length > 0) {
425 | const preferredFundingUrl = guessPreferredFundingUrlForOtherDonations(context);
426 | content +=
427 | (preferredFundingUrl != null ? `[Become a sponsor/backer](${preferredFundingUrl}) and get your logo listed here.\n\n` : "") +
428 | generateContributorTable(context.config.donate.other.donors) +
429 | "\n\n";
430 | }
431 |
432 | if (context.config.donate.openCollective.project != null) {
433 | content +=
434 | `### Open Collective\n\n` +
435 | `[Become a sponsor/backer](${CONSTANT.openCollectiveDonateUrl(context.config.donate.openCollective.project)}) and get your logo listed here.\n\n` +
436 | `#### Sponsors\n\n` +
437 | formatUrl({
438 | url: CONSTANT.openCollectiveContributorsUrl(context.config.donate.openCollective.project),
439 | inner: formatImage({
440 | url: CONSTANT.openCollectiveSponsorsBadgeUrl(context.config.donate.openCollective.project),
441 | alt: "Sponsors on Open Collective",
442 | width: 500
443 | })
444 | }) +
445 | "\n\n" +
446 | `#### Backers\n\n` +
447 | formatUrl({
448 | url: CONSTANT.openCollectiveContributorsUrl(context.config.donate.openCollective.project),
449 | inner: formatImage({
450 | url: CONSTANT.openCollectiveBackersBadgeUrl(context.config.donate.openCollective.project),
451 | alt: "Backers on Open Collective"
452 | })
453 | }) +
454 | "\n\n";
455 | }
456 |
457 | if (context.config.donate.patreon.userId != null && context.config.donate.patreon.username != null) {
458 | content +=
459 | `### Patreon\n\n` +
460 | formatUrl({
461 | url: CONSTANT.patreonDonateUrl(context.config.donate.patreon.userId),
462 | inner: formatImage({
463 | url: CONSTANT.patreonBadgeUrl(context.config.donate.patreon.username),
464 | alt: "Patrons on Patreon",
465 | width: 200
466 | })
467 | }) +
468 | "\n\n";
469 | }
470 |
471 | setSection(context, "backers", context.config.donate.patreon.userId == null && context.config.donate.openCollective.project == null ? "" : `## Backers\n\n` + content);
472 | }
473 |
474 | async function generateLicenseSection(context: GenerateReadmeContext): Promise {
475 | const license = await findLicense(context);
476 | const contributors = getContributorsFromPackage(context.pkg);
477 | const licenseFilePath = path.join(context.root, CONSTANT.licenseFilename);
478 | const nativeLicenseFilePath = path.native.normalize(licenseFilePath);
479 |
480 | setSection(
481 | context,
482 | "license",
483 | license == null || !context.fs.existsSync(nativeLicenseFilePath)
484 | ? ""
485 | : `## License\n\n` +
486 | `${license} © ${listFormat(
487 | contributors.map(contributor => formatContributor(contributor, "markdown")),
488 | "and"
489 | )}`
490 | );
491 | }
492 |
--------------------------------------------------------------------------------
/src/readme/get-shadow-section-mark/get-shadow-section-mark.ts:
--------------------------------------------------------------------------------
1 | import type {SectionKind} from "../../section/section-kind.js";
2 |
3 | /**
4 | * Gets a Section mark.
5 | *
6 | * @param kind
7 | * @param startOrEnd
8 | */
9 | export function getShadowSectionMark(kind: SectionKind, startOrEnd: "start" | "end"): string {
10 | return ``.toUpperCase();
11 | }
12 |
--------------------------------------------------------------------------------
/src/section/ensure-section-kind/ensure-section-kind.ts:
--------------------------------------------------------------------------------
1 | import type {SectionKind} from "../section-kind.js";
2 | import {SECTION_KINDS} from "../section-kind.js";
3 | import {listFormat} from "../../util/list-format/list-format.js";
4 |
5 | /**
6 | * Ensures that the given input is a proper SectionKind
7 | */
8 | export function ensureSectionKind(sectionKind: string): SectionKind {
9 | if (typeof sectionKind !== "string") return sectionKind;
10 | if (SECTION_KINDS.some(key => key === sectionKind)) {
11 | return sectionKind as SectionKind;
12 | } else {
13 | throw new TypeError(`Could not parse string: '${sectionKind}' as a SectionKind. Possible values: ${listFormat(SECTION_KINDS, "and")}`);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/section/get-relevant-sections/get-relevant-sections-options.ts:
--------------------------------------------------------------------------------
1 | import type {SandhogConfig} from "../../config/sandhog-config.js";
2 |
3 | export interface GetRelevantSectionsOptions {
4 | config: SandhogConfig;
5 | }
6 |
--------------------------------------------------------------------------------
/src/section/get-relevant-sections/get-relevant-sections.ts:
--------------------------------------------------------------------------------
1 | import type {GetRelevantSectionsOptions} from "./get-relevant-sections-options.js";
2 | import type {SectionKind} from "../section-kind.js";
3 | import {SECTION_KINDS} from "../section-kind.js";
4 |
5 | /**
6 | * Gets all those sections that are relevant in relation to the given options
7 | */
8 | export function getRelevantSections({config}: GetRelevantSectionsOptions): Set {
9 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
10 | const excluded = new Set(config.readme.sections.exclude ?? []);
11 |
12 | // Prepare the baseline of sections. This will be the sections that will be included always (except for the excluded ones).
13 | return new Set(SECTION_KINDS.filter(value => !excluded.has(value)));
14 | }
15 |
--------------------------------------------------------------------------------
/src/section/section-kind.ts:
--------------------------------------------------------------------------------
1 | import type {ElementOf} from "helpertypes";
2 |
3 | export const SECTION_KINDS = [
4 | "toc",
5 | "logo",
6 | "badges",
7 | "description_short",
8 | "description_long",
9 | "features",
10 | "feature_image",
11 | "usage",
12 | "install",
13 | "contributing",
14 | "maintainers",
15 | "faq",
16 | "backers",
17 | "license"
18 | ] as const;
19 |
20 | export type SectionKind = ElementOf;
21 |
--------------------------------------------------------------------------------
/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | declare module "@effect/markdown-toc" {
2 | interface Result {
3 | content: string;
4 | }
5 |
6 | export default function (content: string): Result;
7 | }
8 |
--------------------------------------------------------------------------------
/src/util/list-format/list-format.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Formats the given iterable of strings in a list format (in the English locale)
3 | */
4 | export function listFormat(elements: Iterable, andOrOr: "and" | "or", mapper: (element: string) => string = element => element): string {
5 | const arr = [...elements];
6 | if (arr.length === 0) return "";
7 | else if (arr.length === 1) return mapper(arr[0]!);
8 | else if (arr.length === 2) {
9 | const [first, last] = arr;
10 | return `${mapper(first!)} ${andOrOr} ${mapper(last!)}`;
11 | } else {
12 | const head = arr.slice(0, arr.length - 1).map(mapper);
13 | const last = mapper(arr.slice(-1)[0]!);
14 | return `${head.join(", ")}, ${andOrOr} ${last}`;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/util/path/is-lib.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns true if the given path represents a library
3 | *
4 | * @param path
5 | * @returns
6 | */
7 | export function isLib(path: string): boolean {
8 | return !path.startsWith(".") && !path.startsWith("/");
9 | }
10 |
--------------------------------------------------------------------------------
/src/util/path/strip-leading-if-matched.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Strips the leading part of the given string if it is equal to the given match.
3 | *
4 | * @param str
5 | * @param match
6 | * @returns
7 | */
8 | export function stripLeadingIfMatched(str: string, match: string): string {
9 | const encodedMatch = encodeURIComponent(match);
10 | if (str.startsWith(match)) return str.slice(match.length);
11 | else if (str.startsWith(encodedMatch)) return str.slice(encodedMatch.length);
12 | return str;
13 | }
14 |
--------------------------------------------------------------------------------
/src/util/prompt/confirm.ts:
--------------------------------------------------------------------------------
1 | import {confirm as inquirerConfirm} from "@inquirer/prompts";
2 |
3 | /**
4 | * Prints a 'confirm' prompt in the terminal
5 | */
6 | export async function confirm(message: string, defaultValue?: boolean): Promise {
7 | const answer = await inquirerConfirm({
8 | message,
9 | default: defaultValue
10 | });
11 |
12 | return answer;
13 | }
14 |
--------------------------------------------------------------------------------
/src/util/prompt/radio.ts:
--------------------------------------------------------------------------------
1 | import {select} from "@inquirer/prompts";
2 |
3 | /**
4 | * Provides a "radio button group" of potential options the user may pick
5 | */
6 | export async function radio(message: string, items: T[] | readonly T[]): Promise {
7 | const answer = await select({
8 | message,
9 | choices: items.map(item => ({name: item, value: item}))
10 | });
11 |
12 | return answer;
13 | }
14 |
--------------------------------------------------------------------------------
/test/config.test.ts:
--------------------------------------------------------------------------------
1 | import test from "node:test";
2 | import assert from "node:assert";
3 | import {Logger} from "../src/logger/logger.js";
4 | import {LogLevelKind} from "../src/logger/log-level-kind.js";
5 | import {getConfig} from "../src/config/get-config/get-config.js";
6 |
7 | test("Can sanitize a SandhogConfig", async () => {
8 | const config = await getConfig({
9 | root: process.cwd(),
10 | logger: new Logger(LogLevelKind.NONE)
11 | });
12 |
13 | // Verify that it has no optional keys
14 | assert(
15 | "donate" in config &&
16 | "patreon" in config.donate &&
17 | "userId" in config.donate.patreon &&
18 | "openCollective" in config.donate &&
19 | "project" in config.donate.openCollective &&
20 | "prettier" in config &&
21 | "logo" in config &&
22 | "url" in config.logo &&
23 | "height" in config.logo &&
24 | "featureImage" in config &&
25 | "url" in config.featureImage &&
26 | "height" in config.featureImage &&
27 | "readme" in config &&
28 | "badges" in config.readme &&
29 | "sections" in config.readme &&
30 | "exclude" in config.readme.badges &&
31 | "exclude" in config.readme.sections
32 | );
33 | });
34 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./node_modules/@wessberg/ts-config/tsconfig.json",
3 | "include": ["src/**/*.*", "test/**/*.*", "loader.cjs", "sandhog.config.js"],
4 | "exclude": ["dist/*.*"],
5 | "compilerOptions": {
6 | "importHelpers": false
7 | }
8 | }
9 |
--------------------------------------------------------------------------------