@cubing/dev-config 0.6.5 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -50,6 +50,12 @@ bun add @biomejs/biome @cubing/dev-config
50
50
  bun x @biomejs/biome check
51
51
  ```
52
52
 
53
+ Add to a project using [`repo`](https://github.com/lgarron/repo):
54
+
55
+ ```shell
56
+ repo boilerplate biome add # Or invoke using `npx repo …` / `bun x repo …`
57
+ ```
58
+
53
59
  ### TypeScript
54
60
 
55
61
  #### Check types
@@ -72,6 +78,12 @@ bun add --dev typescript @cubing/dev-config
72
78
  bun x tsc --noEmit --project .
73
79
  ```
74
80
 
81
+ Add to a project using [`repo`](https://github.com/lgarron/repo):
82
+
83
+ ```shell
84
+ repo boilerplate tsconfig add # Or invoke using `npx repo …` / `bun x repo …`
85
+ ```
86
+
75
87
  #### Build types
76
88
 
77
89
  ```jsonc
@@ -95,6 +107,12 @@ bun add --dev typescript @cubing/dev-config
95
107
  bun x tsc --project .
96
108
  ```
97
109
 
110
+ Add to a project using [`repo`](https://github.com/lgarron/repo):
111
+
112
+ ```shell
113
+ repo boilerplate tsconfig add # Or invoke using `npx repo …` / `bun x repo …`
114
+ ```
115
+
98
116
  #### No DOM
99
117
 
100
118
  Use the `no-dom` variant instead:
@@ -106,6 +124,12 @@ Use the `no-dom` variant instead:
106
124
  }
107
125
  ```
108
126
 
127
+ Add to a project using [`repo`](https://github.com/lgarron/repo):
128
+
129
+ ```shell
130
+ repo boilerplate tsconfig add --no-dom # Or invoke using `npx repo …` / `bun x repo …`
131
+ ```
132
+
109
133
  ### `es2024`
110
134
 
111
135
  The following are also available:
@@ -115,6 +139,13 @@ The following are also available:
115
139
 
116
140
  This is useful for features like `Promise.withResolvers(…)`.
117
141
 
142
+ Add to a project using [`repo`](https://github.com/lgarron/repo):
143
+
144
+ ```shell
145
+ repo boilerplate tsconfig add --module es2022 # Or invoke using `npx repo …` / `bun x repo …`
146
+ repo boilerplate tsconfig add --no-dom --module es2022 # Or invoke using `npx repo …` / `bun x repo …`
147
+ ```
148
+
118
149
  ### Checking `package.json` for a project
119
150
 
120
151
  Run as follows:
@@ -41,7 +41,6 @@ It also assumes certain conventions about package structure and maintenance.
41
41
 
42
42
  */
43
43
 
44
- // TODO: support "format" command that corrects some things.
45
44
  // TODO: Schema validation.
46
45
 
47
46
  console.log("Parsing `package.json`:");
@@ -332,7 +331,7 @@ console.log("Checking presence and type of fields:");
332
331
  field(["name"], "string");
333
332
  field(["version"], "string", {
334
333
  additionalChecks: {
335
- "Version cannot be parsed.": (version: string) =>
334
+ "Version must parse successfully.": (version: string) =>
336
335
  semver.order(version, version) === 0,
337
336
  },
338
337
  });
@@ -398,20 +397,24 @@ field(["type"], "string", {
398
397
  'Type must be `"module"`.': (type: string) => type === "module",
399
398
  },
400
399
  });
401
- if ("main" in packageJSON || "types" in packageJSON) {
402
- field(["main"], "string", {
403
- mustBePopulatedMessage: 'Must be populated if "types" is populated.',
404
- });
405
- field(["types"], "string", {
406
- mustBePopulatedMessage: 'Must be populated if "main" is populated.',
407
- });
408
- } else {
409
- console.log("☑️ .main");
410
- console.log("☑️ .types");
411
- }
400
+ const mainOrTypesArePopoulated = (() => {
401
+ if ("main" in packageJSON || "types" in packageJSON) {
402
+ field(["main"], "string", {
403
+ mustBePopulatedMessage: 'Must be populated if "types" is populated.',
404
+ });
405
+ field(["types"], "string", {
406
+ mustBePopulatedMessage: 'Must be populated if "main" is populated.',
407
+ });
408
+ return true;
409
+ } else {
410
+ console.log("☑️ .main");
411
+ console.log("☑️ .types");
412
+ return false;
413
+ }
414
+ })();
412
415
  mustNotBePopulated(["module"]);
413
416
  mustNotBePopulated(["browser"]);
414
- field(["exports"], "object");
417
+ field(["exports"], "object", { optional: !mainOrTypesArePopoulated });
415
418
  field(["bin"], "object", { optional: true });
416
419
  field(["dependencies"], "object", { optional: true });
417
420
  field(["devDependencies"], "object", { optional: true });
@@ -429,8 +432,11 @@ field(["scripts", "prepublishOnly"], "string");
429
432
  console.log("Checking paths of binaries and exports:");
430
433
 
431
434
  const tempDir = await Path.makeTempDir();
432
- await using _ = {
433
- [Symbol.asyncDispose]: () => tempDir.rm_rf(),
435
+ await using tempDirDisposable = {
436
+ [Symbol.asyncDispose]: async () => {
437
+ console.log("Disposing…");
438
+ await tempDir.rm_rf();
439
+ },
434
440
  };
435
441
  const extractionDir = await tempDir.join("extracted").mkdir();
436
442
  // TODO: is there a 100% reliable way to test against paths that *will* be packed?
@@ -514,17 +520,17 @@ function checkPath(
514
520
  // TODO: allow folders (with a required trailing slash)?
515
521
  if (!(await resolvedPath.existsAsFile())) {
516
522
  exitCode = 1;
517
- return `❌ ${breadcrumbString} — Path is not present on disk. — ${value}`;
523
+ return `❌ ${breadcrumbString} — Path must be present in the package. — ${value}`;
518
524
  }
519
525
  if (options.mustBeExecutable) {
520
526
  if (!((await resolvedPath.stat()).mode ^ constants.X_OK)) {
521
527
  // This is not considered fixable because the binary may be the output
522
528
  // of a build process. In that case, the build process is responsible
523
529
  // for marking it as executable.
524
- return `❌ ${breadcrumbString} — File at path must be executable — ${value}`;
530
+ return `❌ ${breadcrumbString} — File at path must be executable. — ${value}`;
525
531
  }
526
532
  }
527
- return `✅ ${breadcrumbString} — OK — ${value}`;
533
+ return `✅ ${breadcrumbString} — Path must be present in the package. — ${value}`;
528
534
  })(),
529
535
  );
530
536
  }
@@ -642,7 +648,7 @@ if (exports) {
642
648
  ...fixingLines,
643
649
  ].join("\n");
644
650
  } else {
645
- return `✅ ${breadcrumbString} — Key set and ordering is okay.`;
651
+ return `✅ ${breadcrumbString} — Key set and ordering is OK.`;
646
652
  }
647
653
  }
648
654
  })(),
@@ -689,4 +695,6 @@ if (subcommand === "format") {
689
695
  console.log();
690
696
  }
691
697
 
698
+ await tempDirDisposable[Symbol.asyncDispose]();
699
+
692
700
  exit(exitCode);
@@ -0,0 +1,152 @@
1
+ import {
2
+ type BuildOptions,
3
+ build,
4
+ type ImportKind,
5
+ type Metafile,
6
+ type Plugin,
7
+ } from "esbuild";
8
+ import { es2022Lib } from "../../src/esbuild/es2022";
9
+
10
+ /**
11
+ * Note:
12
+ * - A file may be matched by any parent path scope key.
13
+ * - Files in a given scope key are allowed to import any other within the same scope.
14
+ */
15
+ export type AllowedImports = {
16
+ [scope: string]: { static?: string[]; dynamic?: string[] };
17
+ };
18
+
19
+ const plugin = {
20
+ name: "mark-bare-imports-as-external",
21
+ setup(build) {
22
+ const filter = /^[^./]|^\.[^./]|^\.\.[^/]/; // Must not start with "/" or "./" or "../"
23
+ build.onResolve({ filter }, (args) => ({
24
+ path: args.path,
25
+ external: true,
26
+ }));
27
+ },
28
+ } satisfies Plugin;
29
+
30
+ export async function checkAllowedImports(
31
+ groups: {
32
+ [description: string]: {
33
+ entryPoints: string[];
34
+ allowedImports: AllowedImports;
35
+ };
36
+ },
37
+ options?: { overrideEsbuildOptions: BuildOptions },
38
+ ): Promise<void> {
39
+ let failure = false;
40
+
41
+ console.log("+ means a new file");
42
+ console.log("- means a valid import for that file");
43
+
44
+ for (const [description, { entryPoints, allowedImports }] of Object.entries(
45
+ groups,
46
+ )) {
47
+ console.log(`# ${description}`);
48
+ // From https://github.com/evanw/esbuild/issues/619#issuecomment-1504100390
49
+
50
+ const { metafile } = await build({
51
+ ...es2022Lib(),
52
+ entryPoints,
53
+ plugins: [plugin],
54
+ ...options?.overrideEsbuildOptions,
55
+ write: false,
56
+ metafile: true,
57
+ });
58
+
59
+ // Starts with the path and then keeps chopping off from the right.
60
+ function* pathPrefixes(path: string) {
61
+ const pathParts = path.split("/");
62
+ for (let n = pathParts.length; n > 0; n--) {
63
+ yield pathParts.slice(0, n).join("/");
64
+ }
65
+ }
66
+
67
+ function matchingPathPrefix(matchPrefixes: string[], path: string) {
68
+ for (const pathPrefix of pathPrefixes(path)) {
69
+ if (matchPrefixes.includes(pathPrefix)) {
70
+ return pathPrefix;
71
+ }
72
+ }
73
+ return false;
74
+ }
75
+
76
+ const importKindMap: Partial<Record<ImportKind, "static" | "dynamic">> = {
77
+ "import-statement": "static",
78
+ "dynamic-import": "dynamic",
79
+ } as const;
80
+
81
+ function checkImport(
82
+ sourcePath: string,
83
+ importInfo: {
84
+ path: string;
85
+ kind: ImportKind;
86
+ external?: boolean;
87
+ original?: string;
88
+ },
89
+ allowedImports: AllowedImports,
90
+ ) {
91
+ const importKind = importKindMap[importInfo.kind];
92
+ if (!importKind) {
93
+ throw new Error("Unexpected import kind!");
94
+ }
95
+ for (const sourcePathPrefix of pathPrefixes(sourcePath)) {
96
+ const matchingSourcePathPrefix = matchingPathPrefix(
97
+ Object.keys(allowedImports),
98
+ sourcePathPrefix,
99
+ );
100
+ if (matchingSourcePathPrefix) {
101
+ const allowedImportsForKind =
102
+ allowedImports[matchingSourcePathPrefix][importKind];
103
+ if (
104
+ typeof allowedImportsForKind !== "undefined" &&
105
+ !Array.isArray(allowedImportsForKind)
106
+ ) {
107
+ throw new Error(
108
+ `Expected a string list for ${importKind} imports under the scope "${matchingSourcePathPrefix}"`,
109
+ );
110
+ }
111
+ if (
112
+ matchingPathPrefix(
113
+ [
114
+ matchingSourcePathPrefix, // allow importing from any source group to itself.
115
+ ...(allowedImportsForKind ?? []),
116
+ ],
117
+ importInfo.path,
118
+ )
119
+ ) {
120
+ process.stdout.write("-");
121
+ return;
122
+ }
123
+ }
124
+ }
125
+ failure = true;
126
+ console.error(`\n❌ File has disallowed ${importKind} import:`);
127
+ console.error(`From file: ${sourcePath}`);
128
+ console.error(`Importing: ${importInfo.path}`);
129
+ }
130
+
131
+ async function checkImports(
132
+ metafile: Metafile,
133
+ allowedImports: AllowedImports,
134
+ ) {
135
+ for (const [filePath, importInfoList] of Object.entries(
136
+ metafile.inputs,
137
+ )) {
138
+ process.stdout.write("+");
139
+ for (const importInfo of importInfoList.imports) {
140
+ checkImport(filePath, importInfo, allowedImports);
141
+ }
142
+ }
143
+ console.log();
144
+ }
145
+
146
+ await checkImports(metafile, allowedImports);
147
+ }
148
+
149
+ if (failure) {
150
+ throw new Error("Failure");
151
+ }
152
+ }
@@ -0,0 +1,4 @@
1
+ export {
2
+ type AllowedImports,
3
+ checkAllowedImports,
4
+ } from "./checkAllowedImports";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cubing/dev-config",
3
- "version": "0.6.5",
3
+ "version": "0.7.0",
4
4
  "description": "Common dev configs for projects.",
5
5
  "author": {
6
6
  "name": "Lucas Garron",
@@ -17,6 +17,9 @@
17
17
  "./esbuild/es2022": {
18
18
  "types": "./esbuild/es2022/index.d.ts",
19
19
  "import": "./esbuild/es2022/index.js"
20
+ },
21
+ "./check-allowed-imports": {
22
+ "default": "./lib/check-allowed-imports/index.ts"
20
23
  }
21
24
  },
22
25
  "bin": {
@@ -37,6 +40,7 @@
37
40
  "files": [
38
41
  "./biome/",
39
42
  "./esbuild/",
43
+ "./lib",
40
44
  "./ts/"
41
45
  ],
42
46
  "scripts": {