@cubing/dev-config 0.6.6 → 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
  });
@@ -433,8 +432,11 @@ field(["scripts", "prepublishOnly"], "string");
433
432
  console.log("Checking paths of binaries and exports:");
434
433
 
435
434
  const tempDir = await Path.makeTempDir();
436
- await using _ = {
437
- [Symbol.asyncDispose]: () => tempDir.rm_rf(),
435
+ await using tempDirDisposable = {
436
+ [Symbol.asyncDispose]: async () => {
437
+ console.log("Disposing…");
438
+ await tempDir.rm_rf();
439
+ },
438
440
  };
439
441
  const extractionDir = await tempDir.join("extracted").mkdir();
440
442
  // TODO: is there a 100% reliable way to test against paths that *will* be packed?
@@ -518,17 +520,17 @@ function checkPath(
518
520
  // TODO: allow folders (with a required trailing slash)?
519
521
  if (!(await resolvedPath.existsAsFile())) {
520
522
  exitCode = 1;
521
- return `❌ ${breadcrumbString} — Path is not present on disk. — ${value}`;
523
+ return `❌ ${breadcrumbString} — Path must be present in the package. — ${value}`;
522
524
  }
523
525
  if (options.mustBeExecutable) {
524
526
  if (!((await resolvedPath.stat()).mode ^ constants.X_OK)) {
525
527
  // This is not considered fixable because the binary may be the output
526
528
  // of a build process. In that case, the build process is responsible
527
529
  // for marking it as executable.
528
- return `❌ ${breadcrumbString} — File at path must be executable — ${value}`;
530
+ return `❌ ${breadcrumbString} — File at path must be executable. — ${value}`;
529
531
  }
530
532
  }
531
- return `✅ ${breadcrumbString} — OK — ${value}`;
533
+ return `✅ ${breadcrumbString} — Path must be present in the package. — ${value}`;
532
534
  })(),
533
535
  );
534
536
  }
@@ -693,4 +695,6 @@ if (subcommand === "format") {
693
695
  console.log();
694
696
  }
695
697
 
698
+ await tempDirDisposable[Symbol.asyncDispose]();
699
+
696
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.6",
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": {