@pob/root 27.5.0 → 27.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/bin/oxlint.js ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createRequire } from "node:module";
4
+ import pkg from "oxlint/package.json" with { type: "json" };
5
+
6
+ // oxlint looks for tsgolint (type-aware rules) in the project's
7
+ // node_modules/.bin, which does not contain the bins of @pob/root dependencies
8
+ if (!process.env.OXLINT_TSGOLINT_PATH) {
9
+ const require = createRequire(
10
+ import.meta.resolve("oxlint-tsgolint/package.json"),
11
+ );
12
+ try {
13
+ process.env.OXLINT_TSGOLINT_PATH = require.resolve(
14
+ `@oxlint-tsgolint/${process.platform}-${process.arch}/tsgolint${process.platform === "win32" ? ".exe" : ""}`,
15
+ );
16
+ } catch {
17
+ // unsupported platform: oxlint reports the missing tsgolint when needed
18
+ }
19
+ }
20
+
21
+ await import(
22
+ new URL(
23
+ typeof pkg.bin === "string" ? pkg.bin : pkg.bin.oxlint,
24
+ import.meta.resolve("oxlint/package.json"),
25
+ ).href
26
+ );
@@ -56,16 +56,18 @@ for (const [pattern, commandFnOrArray] of Object.entries(config)) {
56
56
  const matchBase = !pattern.includes("/");
57
57
  if (!picomatch.isMatch(relFile, pattern, { matchBase })) continue;
58
58
 
59
- let commands;
60
- if (typeof commandFnOrArray === "function") {
61
- const result = commandFnOrArray([file]);
62
- commands = Array.isArray(result) ? result : [result];
63
- } else {
64
- const arr = Array.isArray(commandFnOrArray)
65
- ? commandFnOrArray
66
- : [commandFnOrArray];
67
- commands = arr.map((cmd) => `${cmd} ${JSON.stringify(file)}`);
68
- }
59
+ // Function tasks generate their own commands and get no filename appended,
60
+ // arrays are sequential tasks and nested arrays parallel ones.
61
+ const resolveCommands = (command) => {
62
+ if (typeof command === "function") {
63
+ const result = command([file]);
64
+ return Array.isArray(result) ? result : [result];
65
+ }
66
+ if (Array.isArray(command)) return command.flatMap(resolveCommands);
67
+ return [`${command} ${JSON.stringify(file)}`];
68
+ };
69
+
70
+ const commands = resolveCommands(commandFnOrArray);
69
71
 
70
72
  for (const cmd of commands.filter(Boolean)) {
71
73
  if (shouldSkip(cmd)) continue;
@@ -10,6 +10,7 @@ lintStaged({
10
10
  process.exitCode = passed ? 0 : 1;
11
11
  })
12
12
  // eslint-disable-next-line unicorn/prefer-top-level-await
13
- .catch(() => {
13
+ .catch((error) => {
14
+ console.error(error);
14
15
  process.exitCode = 1;
15
16
  });
@@ -1,8 +1,10 @@
1
- name: Run Prettier on renovate update
1
+ name: Run format on renovate update
2
2
 
3
3
  on:
4
4
  push:
5
5
  branches:
6
+ - "renovate/oxfmt-**"
7
+ - "renovate/major-oxfmt"
6
8
  - "renovate/prettier-**"
7
9
  - "renovate/major-prettier"
8
10
 
@@ -10,7 +12,7 @@ permissions:
10
12
  contents: write
11
13
 
12
14
  jobs:
13
- run-prettier:
15
+ run-format:
14
16
  runs-on: ubuntu-latest
15
17
  steps:
16
18
  - uses: actions/checkout@v7
@@ -26,8 +28,8 @@ jobs:
26
28
  - name: Install Dependencies
27
29
  run: $installOnCICommand$
28
30
 
29
- - name: Run Prettier
30
- run: $pmExec$ prettier --write .
31
+ - name: Run format
32
+ run: $pmRun$ format
31
33
 
32
34
  - name: Check for added or modified files
33
35
  id: git-check-modified
@@ -39,5 +41,5 @@ jobs:
39
41
  git add .
40
42
  git -c user.name="Christophe Hurpeau" \
41
43
  -c user.email="christophehurpeau@users.noreply.github.com" \
42
- commit -m "style: prettier format" --author="github-actions[bot] <actions@github.com>"
44
+ commit -m "style: format" --author="github-actions[bot] <actions@github.com>"
43
45
  git push --no-verify
@@ -1,9 +1,23 @@
1
+ import { execFileSync } from "node:child_process";
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import { assertYarnBerry } from "../../lib/assert-yarn-berry.js";
5
6
  import { getPackageManagerCommands } from "./packageManagerHelpers.js";
6
7
 
8
+ // this workflow only exists to commit the build output back to the branch.
9
+ const hasBuildOutputInGit = () => {
10
+ try {
11
+ return execFileSync("git", ["ls-files", "--", "*dist/*", "*build/*"], {
12
+ encoding: "utf8",
13
+ })
14
+ .split("\n")
15
+ .some((file) => /(?:^|\/)(?:dist|build)\//.test(file));
16
+ } catch {
17
+ return false;
18
+ }
19
+ };
20
+
7
21
  const ensureWorkflowUninstalled = (workflowName) => {
8
22
  try {
9
23
  fs.unlinkSync(path.resolve(`.github/workflows/${workflowName}.yml`));
@@ -12,7 +26,7 @@ const ensureWorkflowUninstalled = (workflowName) => {
12
26
 
13
27
  const installWorkflow = (
14
28
  workflowName,
15
- { pmExec, installOnCICommand, installMutableCommand, ciPreStep },
29
+ { pmRun, pmExec, installOnCICommand, installMutableCommand, ciPreStep },
16
30
  condition = true,
17
31
  ) => {
18
32
  if (condition) {
@@ -26,6 +40,7 @@ const installWorkflow = (
26
40
  ),
27
41
  { encoding: "utf8" },
28
42
  )
43
+ .replaceAll("$pmRun$", pmRun)
29
44
  .replaceAll("$pmExec$", pmExec)
30
45
  .replaceAll("$ciPreStep$", ciPreStep)
31
46
  .replaceAll("$installOnCICommand$", installOnCICommand)
@@ -43,14 +58,16 @@ export default function installGithubWorkflows({ pkg, pm }) {
43
58
 
44
59
  if (fs.existsSync(".github")) {
45
60
  installWorkflow("push-renovate-pob_root", pmCommands);
46
- installWorkflow("push-renovate-prettier", pmCommands);
61
+ installWorkflow("push-renovate-format", pmCommands);
62
+ ensureWorkflowUninstalled("push-renovate-prettier");
47
63
  ensureWorkflowUninstalled("push-renovate-typedoc");
48
64
  if (
49
65
  pkg.devDependencies &&
50
66
  (pkg.devDependencies.rollup ||
51
67
  pkg.devDependencies["@pob/rollup-esbuild"] ||
52
68
  pkg.devDependencies["@pob/rollup-typescript"]) &&
53
- pkg.scripts?.build
69
+ pkg.scripts?.build &&
70
+ hasBuildOutputInGit()
54
71
  ) {
55
72
  installWorkflow("push-renovate-build", pmCommands);
56
73
  } else {
@@ -1,5 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { defineConfig } from "lint-staged/config";
4
+ import picomatch from "picomatch";
3
5
  import { assertYarnBerry } from "./lib/assert-yarn-berry.js";
4
6
  import { whichPmRuns } from "./lib/which-pm-runs.js";
5
7
 
@@ -56,23 +58,110 @@ const { lockfile, configfile, installAndDedupe } = (() => {
56
58
  const pkg = JSON.parse(fs.readFileSync(path.resolve("package.json")));
57
59
  const workspaces = pkg.workspaces || false;
58
60
 
61
+ const workspacesPattern = (() => {
62
+ if (!workspaces) return undefined;
63
+ return workspaces.length === 1 ? workspaces[0] : `{${workspaces.join(",")}}`;
64
+ })();
65
+
66
+ const OXFMT = "oxfmt --no-error-on-unmatched-pattern";
67
+ // Above this many files, commands run on the whole project instead.
68
+ const MAX_FILENAMES = 100;
69
+ const ESLINT_FIX = "eslint --fix --quiet";
70
+ const OXLINT_FIX = "oxlint --fix --quiet --no-error-on-unmatched-pattern";
71
+
72
+ // projects not yet migrated by pob have no oxlint config
73
+ const hasOxlint = fs.existsSync(path.resolve(".oxlintrc.json"));
74
+ const isOxlintSupportedFile = (filename) =>
75
+ /\.[cm]?[jt]sx?$/.test(filename) && !filename.endsWith(".d.ts");
76
+
77
+ const CODE_COMMANDS = [OXFMT, hasOxlint && OXLINT_FIX, ESLINT_FIX].filter(
78
+ Boolean,
79
+ );
80
+
59
81
  const getSrcDirectories = () => {
60
- if (workspaces) {
61
- return `${workspaces.length === 1 ? workspaces[0] : `{${workspaces.join(",")}}`}/{src,lib}`;
82
+ if (workspacesPattern) {
83
+ return `${workspacesPattern}/{src,lib}`;
62
84
  }
63
85
 
64
86
  return "{src,lib}";
65
87
  };
66
88
 
67
- export default function createLintStagedConfig() {
68
- const srcDirectories = getSrcDirectories();
89
+ // Changing the lockfile, the package manager config or any package.json
90
+ // requires a reinstall before anything else runs.
91
+ const getPmFilesPattern = () =>
92
+ `{${lockfile},${configfile ? `${configfile},` : ""}package.json${
93
+ workspaces
94
+ ? `,${workspaces.map((workspacePath) => `${workspacePath}/package.json`).join(",")}`
95
+ : ""
96
+ }}`;
97
+
98
+ // tsc must also run when the typescript project layout or the dependencies
99
+ // change, not only when a source file changes.
100
+ const getTscTriggerPattern = (srcDirectories) =>
101
+ `{${[
102
+ `${srcDirectories}/**/*.{ts,tsx}`,
103
+ "tsconfig*.json",
104
+ "package.json",
105
+ workspacesPattern && `${workspacesPattern}/tsconfig*.json`,
106
+ workspacesPattern && `${workspacesPattern}/package.json`,
107
+ ]
108
+ .filter(Boolean)
109
+ .join(",")}}`;
110
+
111
+ const getGitAddPmFilesCommand = () =>
112
+ `git add ${lockfile}${configfile ? ` ${configfile}` : ""}${pm.name === "yarn" ? " .yarn" : ""}`;
69
113
 
70
- return {
71
- [`{${lockfile},${configfile ? `${configfile},` : ""}package.json${
72
- workspaces
73
- ? `,${workspaces.map((workspacePath) => `${workspacePath}/package.json`).join(",")}`
74
- : ""
75
- }}`]: (filenames) => {
114
+ /**
115
+ * Matches a file against a list of patterns, with the same picomatch options
116
+ * lint-staged itself uses (see lint-staged/lib/matchFiles.js), so both config
117
+ * flavors match the exact same files.
118
+ */
119
+ const createMatcher = (patterns) => {
120
+ const matchers = patterns.map((pattern) =>
121
+ picomatch(pattern, {
122
+ dot: true,
123
+ // If the pattern doesn't look like a path, match against the basename in
124
+ // every directory.
125
+ matchBase: !pattern.includes("/"),
126
+ posixSlashes: true,
127
+ strictBrackets: true,
128
+ }),
129
+ );
130
+
131
+ return (filename) => matchers.some((isMatch) => isMatch(filename));
132
+ };
133
+
134
+ // lint-staged passes absolute paths unless `relative: true` is set.
135
+ const toRelativePosixPath = (filename) =>
136
+ (path.isAbsolute(filename)
137
+ ? path.relative(process.cwd(), filename)
138
+ : filename
139
+ )
140
+ .split(path.sep)
141
+ .join("/");
142
+
143
+ const withFilenames = (command, filenames) =>
144
+ `${command} "${filenames.join('" "')}"`;
145
+
146
+ /**
147
+ * Legacy: one entry per pattern group, each running its own oxfmt and eslint on
148
+ * the files it matched. Kept behind POB_LEGACY_LINT_STAGED_CONFIG.
149
+ */
150
+ const createLegacyConfig = ({
151
+ buildTasks,
152
+ cssPattern,
153
+ otherJsonPattern,
154
+ pmFilesPattern,
155
+ rootConfigPattern,
156
+ rootFilesPattern,
157
+ configDirsCodePattern,
158
+ srcCodePattern,
159
+ srcDocsPattern,
160
+ tscTriggerPattern,
161
+ workspaceConfigPattern,
162
+ }) =>
163
+ defineConfig({
164
+ [pmFilesPattern]: (filenames) => {
76
165
  const packagejsonFilenames = filenames.filter((filename) =>
77
166
  filename.endsWith(".json"),
78
167
  );
@@ -82,32 +171,147 @@ export default function createLintStagedConfig() {
82
171
  pkg.scripts?.checks ? `${pm.name} run checks` : undefined,
83
172
  packagejsonFilenames.length === 0
84
173
  ? undefined
85
- : `oxfmt "${packagejsonFilenames.join('" "')}"`,
86
- "eslint --fix --quiet",
87
- `git add ${lockfile}${configfile ? ` ${configfile}` : ""}${pm.name === "yarn" ? " .yarn" : ""}`,
174
+ : withFilenames("oxfmt", packagejsonFilenames),
175
+ ESLINT_FIX,
176
+ getGitAddPmFilesCommand(),
88
177
  ].filter(Boolean);
89
178
  },
90
- "!(package|package-lock|.eslintrc).json": [
91
- "oxfmt --no-error-on-unmatched-pattern",
92
- ],
93
- [`{.github,scripts,config,${srcDirectories}}/**/*.{yml,yaml,md}`]: [
94
- "oxfmt --no-error-on-unmatched-pattern",
95
- ],
96
- "./*.{yml,yaml,md,jsonc}": ["oxfmt --no-error-on-unmatched-pattern"],
97
- [`${srcDirectories}/**/*.{js,ts,tsx}`]: [
98
- "oxfmt --no-error-on-unmatched-pattern",
99
- "eslint --fix --quiet",
100
- ],
101
- "{scripts,config,.storyboook}/**/*.{js,mjs,cjs}": [
102
- "oxfmt --no-error-on-unmatched-pattern",
103
- "eslint --fix --quiet",
104
- ],
105
- [`{.storybook,${srcDirectories}}/**/*.css`]: [
106
- "oxfmt --no-error-on-unmatched-pattern",
179
+ [otherJsonPattern]: [OXFMT],
180
+ [srcDocsPattern]: [OXFMT],
181
+ [rootFilesPattern]: [OXFMT],
182
+ [srcCodePattern]: CODE_COMMANDS,
183
+ [configDirsCodePattern]: CODE_COMMANDS,
184
+ [rootConfigPattern]: CODE_COMMANDS,
185
+ ...(workspaceConfigPattern && {
186
+ [workspaceConfigPattern]: CODE_COMMANDS,
187
+ }),
188
+ [cssPattern]: [OXFMT],
189
+ // Tasks are declared as functions so that lint-staged does not append the
190
+ // matched filenames: both commands build the whole project.
191
+ [tscTriggerPattern]: buildTasks,
192
+ });
193
+
194
+ /**
195
+ * A single entry, which builds the command list itself: oxfmt and eslint each
196
+ * run once on all the files they should handle, instead of once per pattern
197
+ * group, and the build runs after them instead of alongside.
198
+ *
199
+ * oxfmt receives every staged file: it skips the files it does not support and
200
+ * the ones ignored by its config, so no file it can format is left out.
201
+ */
202
+ const createConfig = ({
203
+ hasRollup,
204
+ pmFilesPattern,
205
+ rootConfigPattern,
206
+ configDirsCodePattern,
207
+ srcCodePattern,
208
+ tscTriggerPattern,
209
+ workspaceConfigPattern,
210
+ }) => {
211
+ const matchesPmFiles = createMatcher([pmFilesPattern]);
212
+ const matchesEslint = createMatcher(
213
+ [
214
+ pmFilesPattern,
215
+ srcCodePattern,
216
+ configDirsCodePattern,
217
+ rootConfigPattern,
218
+ workspaceConfigPattern,
219
+ ].filter(Boolean),
220
+ );
221
+ const matchesTscTrigger = createMatcher([tscTriggerPattern]);
222
+
223
+ // Build tasks generate their own command so that lint-staged does not append
224
+ // the matched filenames: they build the whole project. They return no command
225
+ // at all when nothing they depend on changed.
226
+ const createBuildTask = (command) => (allStagedFiles) =>
227
+ allStagedFiles.map(toRelativePosixPath).some(matchesTscTrigger)
228
+ ? command
229
+ : [];
230
+
231
+ return defineConfig({
232
+ "*": [
233
+ (allStagedFiles) => {
234
+ const filenames = allStagedFiles.map(toRelativePosixPath);
235
+ const hasPmFiles = filenames.some(matchesPmFiles);
236
+ const eslintFilenames = filenames.filter(matchesEslint);
237
+
238
+ // Running on the whole project is simpler and avoids too long command
239
+ // lines when many files are staged.
240
+ const withFilenamesOrWholeProject = (command, commandFilenames) => {
241
+ if (commandFilenames.length === 0) return undefined;
242
+ if (commandFilenames.length > MAX_FILENAMES) return command;
243
+ return withFilenames(command, commandFilenames);
244
+ };
245
+
246
+ const getOxlintCommand = () => {
247
+ if (!hasOxlint) return undefined;
248
+ return withFilenamesOrWholeProject(
249
+ OXLINT_FIX,
250
+ eslintFilenames.filter(isOxlintSupportedFile),
251
+ );
252
+ };
253
+
254
+ const getEslintCommand = () => {
255
+ // eslint runs on the whole project when a package.json, the lockfile
256
+ // or the package manager config changed, like in the legacy config,
257
+ // as lint rules depend on the dependencies themselves.
258
+ if (hasPmFiles) return ESLINT_FIX;
259
+ return withFilenamesOrWholeProject(ESLINT_FIX, eslintFilenames);
260
+ };
261
+
262
+ return [
263
+ ...(hasPmFiles ? installAndDedupe : []),
264
+ hasPmFiles && pkg.scripts?.checks
265
+ ? `${pm.name} run checks`
266
+ : undefined,
267
+ withFilenamesOrWholeProject(OXFMT, filenames),
268
+ getOxlintCommand(),
269
+ getEslintCommand(),
270
+ hasPmFiles ? getGitAddPmFilesCommand() : undefined,
271
+ ].filter(Boolean);
272
+ },
273
+ // Runs after the tasks above, so that the build sees the formatted and
274
+ // fixed files. Both commands build the whole project, in parallel.
275
+ hasRollup
276
+ ? [
277
+ createBuildTask("rollup --config rollup.config.mjs"),
278
+ createBuildTask("tsc -b"),
279
+ ]
280
+ : createBuildTask("tsc"),
107
281
  ],
108
- [`${srcDirectories}/**/*.{ts,tsx}`]: () =>
109
- pkg.devDependencies && pkg.devDependencies["@pob/rollup-esbuild"]
110
- ? ["rollup --config rollup.config.mjs", "tsc -b"]
111
- : ["tsc"],
282
+ });
283
+ };
284
+
285
+ export default function createLintStagedConfig() {
286
+ const srcDirectories = getSrcDirectories();
287
+
288
+ const hasRollup = Boolean(
289
+ pkg.devDependencies && pkg.devDependencies["@pob/rollup-esbuild"],
290
+ );
291
+
292
+ const rootConfigPattern = "./*.{js,mjs,cjs,ts}";
293
+ const workspaceConfigPattern = workspacesPattern
294
+ ? `${workspacesPattern}/*.{js,mjs,cjs,ts}`
295
+ : undefined;
296
+
297
+ const patterns = {
298
+ buildTasks: hasRollup
299
+ ? [[() => "rollup --config rollup.config.mjs", () => "tsc -b"]] // run in parallel
300
+ : [() => "tsc"],
301
+ cssPattern: `{.storybook,${srcDirectories}}/**/*.css`,
302
+ hasRollup,
303
+ otherJsonPattern: "!(package|package-lock|.eslintrc).json",
304
+ pmFilesPattern: getPmFilesPattern(),
305
+ rootConfigPattern,
306
+ rootFilesPattern: "./*.{yml,yaml,md,jsonc}",
307
+ configDirsCodePattern: "{scripts,config,.storybook}/**/*.{js,mjs,cjs}",
308
+ srcCodePattern: `${srcDirectories}/**/*.{js,ts,tsx}`,
309
+ srcDocsPattern: `{.github,scripts,config,${srcDirectories}}/**/*.{yml,yaml,md}`,
310
+ tscTriggerPattern: getTscTriggerPattern(srcDirectories),
311
+ workspaceConfigPattern,
112
312
  };
313
+
314
+ return process.env.POB_LEGACY_LINT_STAGED_CONFIG
315
+ ? createLegacyConfig(patterns)
316
+ : createConfig(patterns);
113
317
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pob/root",
3
- "version": "27.5.0",
3
+ "version": "27.7.0",
4
4
  "description": "root package",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/christophehurpeau/pob",
@@ -16,6 +16,7 @@
16
16
  },
17
17
  "bin": {
18
18
  "oxfmt": "./bin/oxfmt.cjs",
19
+ "oxlint": "./bin/oxlint.js",
19
20
  "pob-check-commit-msg": "./bin/pob-check-commit-msg.js",
20
21
  "pob-claude-run-lint-staged": "./bin/pob-claude-run-lint-staged.js",
21
22
  "pob-root-lint-staged": "./bin/pob-root-lint-staged.js",
@@ -68,19 +69,22 @@
68
69
  "conventional-changelog-writer": "^8.2.0",
69
70
  "conventional-commits-parser": "^6.2.1",
70
71
  "json5": "^2.2.3",
71
- "lint-staged": "17.3.0",
72
+ "lint-staged": "17.4.1",
72
73
  "oxfmt": "0.54.0",
73
- "picomatch": "^4.0.5",
74
+ "oxlint": "1.85.0",
75
+ "oxlint-tsgolint": "7.0.2002",
76
+ "picomatch": "^4.0.7",
74
77
  "semver": "^7.7.3",
75
- "@pob/version": "6.0.1",
76
- "pob-dependencies": "27.4.0"
78
+ "@pob/version": "6.2.0",
79
+ "pob-dependencies": "27.6.0"
77
80
  },
78
81
  "engines": {
79
82
  "node": ">=22.18.0"
80
83
  },
81
84
  "pob": {},
82
85
  "scripts": {
83
- "lint": "pnpm run lint:eslint",
84
- "lint:eslint": "pnpm run --dir '../..' eslint --quiet '@pob/root'"
86
+ "lint": "pnpm run lint:oxlint && pnpm run lint:eslint",
87
+ "lint:eslint": "pnpm --dir '../..' exec eslint --quiet '@pob/root'",
88
+ "lint:oxlint": "pnpm --dir '../..' exec oxlint --quiet '@pob/root'"
85
89
  }
86
90
  }