@ankhorage/devtools 1.6.0 → 1.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
@@ -101,7 +101,7 @@ The synchronized package scripts are:
101
101
  "lint:fix": "ankhorage-eslint . --fix --max-warnings=0",
102
102
  "format": "ankhorage-prettier --write .",
103
103
  "format:check": "ankhorage-prettier --check .",
104
- "knip": "ankhorage-knip"
104
+ "knip:check": "ankhorage-knip"
105
105
  }
106
106
  }
107
107
  ```
@@ -154,7 +154,7 @@ Synchronization is deterministic and idempotent:
154
154
  - missing managed artifacts are created
155
155
  - outdated centrally owned artifacts are updated
156
156
  - the managed Bun runtime version is applied consistently to package metadata and workflows
157
- - package changes are followed by `bun install`, keeping installed dependencies and `bun.lock` synchronized
157
+ - package changes are followed by `bun install` after all managed files have been written, keeping installed dependencies and `bun.lock` synchronized without invalidating the running sync
158
158
  - current artifacts are left untouched
159
159
  - unrelated files and package fields are preserved
160
160
  - repeated sync produces only `unchanged` results
@@ -232,7 +232,7 @@ Use `eslint.local.config.mjs` for narrow repository-specific flat-config overrid
232
232
 
233
233
  ## Prettier
234
234
 
235
- `ankh devtools prettier sync` owns `.prettierrc.js` and emits the correct ESM or CommonJS delegate based on the repository's `package.json` module type.
235
+ `ankh devtools prettier sync` owns `.prettierrc.js`, emits the correct ESM or CommonJS wrapper based on the repository's `package.json` module type, and creates `prettier.local.config.js` once for narrow repository-specific options.
236
236
 
237
237
  The consumer delegates formatting policy to:
238
238
 
@@ -240,6 +240,8 @@ The consumer delegates formatting policy to:
240
240
  @ankhorage/devtools/prettier
241
241
  ```
242
242
 
243
+ The wrapper merges shared and local `overrides` in that order. On first synchronization, an existing non-canonical `.prettierrc.js` is preserved as `prettier.local.config.js`; former shared-only Devtools delegates become an empty local config. Later synchronization never overwrites the local file.
244
+
243
245
  ## Knip
244
246
 
245
247
  `ankh devtools knip sync` bootstraps `knip.config.ts` with:
@@ -289,10 +291,13 @@ When this managed package contract changes, synchronization runs `bun install`.
289
291
 
290
292
  ```text
291
293
  .github/workflows/ci.yml
294
+ .github/workflows/renovate.yml
292
295
  .github/workflows/release.yml
293
296
  ```
294
297
 
295
- Both workflows render their `bun-version` from the same managed Bun runtime policy used for `package.json`. The CI workflow installs that Bun version with the frozen lockfile, builds before repository-provider validation, runs `bunx @ankhorage/ankh doctor validate .`, and conditionally runs lint, formatting, Knip, tests, typecheck, and Changesets checks.
298
+ CI and Release render their `bun-version` from the same managed Bun runtime policy used for `package.json`. The CI workflow installs that Bun version with the frozen lockfile, builds before repository-provider validation, runs `bunx @ankhorage/ankh doctor validate .`, and conditionally runs lint, formatting, Knip, tests, typecheck, and Changesets checks.
299
+
300
+ The Renovate workflow accepts only same-repository branches created by `renovate[bot]`. It calls the SHA-pinned `ankhorage/renovate` workflow to add a release Changeset for runtime Ankhorage dependency updates or an empty Changeset for dev-only updates, then dispatches CI for that bot-authored commit. It never checks out or executes pull-request code in the privileged `pull_request_target` context.
296
301
 
297
302
  ## Managed VS Code configuration
298
303
 
@@ -52,20 +52,22 @@ async function runStatus(scope, targetDirectory, devtoolsVersion, context) {
52
52
  }
53
53
  async function runSync(scope, targetDirectory, devtoolsVersion, dryRun, context) {
54
54
  const results = [];
55
+ let packageDependenciesChanged = false;
55
56
  if (scope === 'all' || scope === 'package') {
56
57
  const packageResult = await syncPackageManifest(targetDirectory, devtoolsVersion, { dryRun });
57
58
  results.push(packageResult);
58
- if (packageResult.action !== 'unchanged') {
59
- if (dryRun) {
60
- results.push(planBunDependencySync(targetDirectory));
61
- }
62
- else {
63
- const syncDependencies = context.syncDependencies ?? syncBunDependencies;
64
- results.push(await syncDependencies(targetDirectory));
65
- }
66
- }
59
+ packageDependenciesChanged = packageResult.action !== 'unchanged';
67
60
  }
68
61
  results.push(...(await syncManagedFiles(targetDirectory, getManagedFiles(scope), { dryRun })));
62
+ if (packageDependenciesChanged) {
63
+ if (dryRun) {
64
+ results.push(planBunDependencySync(targetDirectory));
65
+ }
66
+ else {
67
+ const syncDependencies = context.syncDependencies ?? syncBunDependencies;
68
+ results.push(await syncDependencies(targetDirectory));
69
+ }
70
+ }
69
71
  writeSyncOutput(results, context);
70
72
  return { exitCode: 0 };
71
73
  }
@@ -5,7 +5,7 @@
5
5
  * as a development dependency for normal consumers, while `@ankhorage/ankh` keeps devtools as a
6
6
  * runtime dependency because the CLI loads it as a bundled core provider. Individually installed
7
7
  * toolchain packages that devtools now owns are removed, and the canonical `lint`, `lint:fix`,
8
- * `format`, `format:check`, and `knip` scripts are written.
8
+ * `format`, `format:check`, and `knip:check` scripts are written.
9
9
  * Unrelated manifest fields, scripts, dependencies, and metadata are preserved.
10
10
  *
11
11
  * The Bun runtime policy is shared by every repository, including devtools itself. Devtools skips
@@ -30,7 +30,7 @@ const STANDARD_SCRIPTS = {
30
30
  'lint:fix': 'ankhorage-eslint . --fix --max-warnings=0',
31
31
  format: 'ankhorage-prettier --write .',
32
32
  'format:check': 'ankhorage-prettier --check .',
33
- knip: 'ankhorage-knip',
33
+ 'knip:check': 'ankhorage-knip',
34
34
  };
35
35
  const DEVTOOLS_OWNED_DEV_DEPENDENCIES = [
36
36
  '@eslint/js',
@@ -82,7 +82,9 @@ export function applyManagedPackageContract(manifest, devtoolsVersion) {
82
82
  if (manifest.name === DEVTOOLS_PACKAGE_NAME) {
83
83
  return applyBunRuntimePolicy(manifest);
84
84
  }
85
- const scripts = { ...toRecord(manifest.scripts), ...STANDARD_SCRIPTS };
85
+ const scripts = { ...toRecord(manifest.scripts) };
86
+ delete scripts.knip;
87
+ Object.assign(scripts, STANDARD_SCRIPTS);
86
88
  const devDependencies = removeOwnedDependencies(toRecord(manifest.devDependencies));
87
89
  const dependencies = toRecord(manifest.dependencies);
88
90
  applyDevtoolsDependencyPlacement(manifest, dependencies, devDependencies, devtoolsVersion);
@@ -175,7 +177,8 @@ function normalizedDependencies(manifest, dependencies) {
175
177
  : { dependencies };
176
178
  }
177
179
  function hasStandardScripts(scripts) {
178
- return Object.entries(STANDARD_SCRIPTS).every(([name, command]) => scripts[name] === command);
180
+ return (scripts.knip === undefined &&
181
+ Object.entries(STANDARD_SCRIPTS).every(([name, command]) => scripts[name] === command));
179
182
  }
180
183
  function serializePackageManifest(manifest) {
181
184
  return `${JSON.stringify(manifest, null, 2)}\n`;
@@ -1,6 +1,11 @@
1
1
  export declare const prettierManagedFiles: readonly [{
2
+ readonly relativePath: "prettier.local.config.js";
3
+ readonly render: typeof renderInitialLocalConfig;
4
+ readonly mode: "create-only";
5
+ }, {
2
6
  readonly relativePath: ".prettierrc.js";
3
7
  readonly render: typeof renderPrettierConfig;
4
8
  }];
5
9
  declare function renderPrettierConfig(targetDirectory: string): Promise<string>;
10
+ declare function renderInitialLocalConfig(targetDirectory: string): Promise<string>;
6
11
  export {};
@@ -1,10 +1,37 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { resolve } from 'node:path';
3
- const ESM_CONFIG = `export { default } from '@ankhorage/devtools/prettier';
3
+ const ESM_CONFIG = `import sharedConfig from '@ankhorage/devtools/prettier';
4
+ import localConfig from './prettier.local.config.js';
5
+
6
+ export default {
7
+ ...sharedConfig,
8
+ ...localConfig,
9
+ overrides: [...(sharedConfig.overrides ?? []), ...(localConfig.overrides ?? [])],
10
+ };
4
11
  `;
5
- const COMMONJS_CONFIG = `module.exports = require('@ankhorage/devtools/prettier');
12
+ const COMMONJS_CONFIG = `const sharedConfig = require('@ankhorage/devtools/prettier');
13
+ const localConfig = require('./prettier.local.config.js');
14
+
15
+ module.exports = {
16
+ ...sharedConfig,
17
+ ...localConfig,
18
+ overrides: [...(sharedConfig.overrides ?? []), ...(localConfig.overrides ?? [])],
19
+ };
20
+ `;
21
+ const EMPTY_ESM_LOCAL_CONFIG = `export default {};
22
+ `;
23
+ const EMPTY_COMMONJS_LOCAL_CONFIG = `module.exports = {};
24
+ `;
25
+ const LEGACY_ESM_CONFIG = `export { default } from '@ankhorage/devtools/prettier';
26
+ `;
27
+ const LEGACY_COMMONJS_CONFIG = `module.exports = require('@ankhorage/devtools/prettier');
6
28
  `;
7
29
  export const prettierManagedFiles = [
30
+ {
31
+ relativePath: 'prettier.local.config.js',
32
+ render: renderInitialLocalConfig,
33
+ mode: 'create-only',
34
+ },
8
35
  {
9
36
  relativePath: '.prettierrc.js',
10
37
  render: renderPrettierConfig,
@@ -13,6 +40,19 @@ export const prettierManagedFiles = [
13
40
  async function renderPrettierConfig(targetDirectory) {
14
41
  return (await readPackageType(targetDirectory)) === 'module' ? ESM_CONFIG : COMMONJS_CONFIG;
15
42
  }
43
+ async function renderInitialLocalConfig(targetDirectory) {
44
+ const isModule = (await readPackageType(targetDirectory)) === 'module';
45
+ try {
46
+ const existingConfig = await readFile(resolve(targetDirectory, '.prettierrc.js'), 'utf8');
47
+ if (!isSharedOnlyConfig(existingConfig))
48
+ return existingConfig;
49
+ }
50
+ catch (error) {
51
+ if (!isNodeError(error) || error.code !== 'ENOENT')
52
+ throw error;
53
+ }
54
+ return isModule ? EMPTY_ESM_LOCAL_CONFIG : EMPTY_COMMONJS_LOCAL_CONFIG;
55
+ }
16
56
  async function readPackageType(targetDirectory) {
17
57
  try {
18
58
  const contents = await readFile(resolve(targetDirectory, 'package.json'), 'utf8');
@@ -29,6 +69,9 @@ async function readPackageType(targetDirectory) {
29
69
  function isRecord(value) {
30
70
  return typeof value === 'object' && value !== null && !Array.isArray(value);
31
71
  }
72
+ function isSharedOnlyConfig(value) {
73
+ return [ESM_CONFIG, COMMONJS_CONFIG, LEGACY_ESM_CONFIG, LEGACY_COMMONJS_CONFIG].includes(value);
74
+ }
32
75
  function isNodeError(error) {
33
76
  return error instanceof Error && 'code' in error;
34
77
  }
@@ -5,6 +5,7 @@ on:
5
5
  push:
6
6
  branches:
7
7
  - main
8
+ workflow_dispatch:
8
9
 
9
10
  permissions:
10
11
  contents: read
@@ -61,10 +62,10 @@ jobs:
61
62
 
62
63
  - name: Run Knip
63
64
  run: |
64
- if node -e "const p=require('./package.json'); process.exit(p.scripts?.knip ? 0 : 1)"; then
65
- bun run knip
65
+ if node -e "const p=require('./package.json'); process.exit(p.scripts?.['knip:check'] ? 0 : 1)"; then
66
+ bun run knip:check
66
67
  else
67
- echo "No knip script found; skipping."
68
+ echo "No knip:check script found; skipping."
68
69
  fi
69
70
 
70
71
  - name: Run tests
@@ -0,0 +1,21 @@
1
+ name: Renovate
2
+
3
+ on:
4
+ pull_request_target:
5
+ types:
6
+ - opened
7
+ - reopened
8
+ - synchronize
9
+
10
+ permissions:
11
+ actions: write
12
+ contents: write
13
+ pull-requests: read
14
+
15
+ jobs:
16
+ changeset:
17
+ if: >-
18
+ github.actor == 'renovate[bot]' &&
19
+ github.event.pull_request.head.repo.full_name == github.repository &&
20
+ startsWith(github.event.pull_request.head.ref, 'renovate/')
21
+ uses: ankhorage/renovate/.github/workflows/changeset.yml@b7305e8f17f9b07238f6b827bbc9f866fd498a0f
@@ -1,2 +1,2 @@
1
1
  import type { ManagedFileDefinition } from '../shared/managedFiles.js';
2
- export declare const workflowManagedFiles: readonly [ManagedFileDefinition, ManagedFileDefinition];
2
+ export declare const workflowManagedFiles: readonly [ManagedFileDefinition, ManagedFileDefinition, ManagedFileDefinition];
@@ -5,6 +5,7 @@ const NODE_VERSION_TOKEN = '__ANKH_NODE_VERSION__';
5
5
  export const workflowManagedFiles = [
6
6
  createWorkflowDefinition('.github/workflows/ci.yml', './files/ci.yml'),
7
7
  createWorkflowDefinition('.github/workflows/release.yml', './files/release.yml'),
8
+ createWorkflowDefinition('.github/workflows/renovate.yml', './files/renovate.yml'),
8
9
  ];
9
10
  function createWorkflowDefinition(relativePath, sourcePath) {
10
11
  const sourceUrl = new URL(sourcePath, import.meta.url);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ankhorage/devtools",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Shared development tools and repository standards for Ankhorage",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/ankhorage/devtools#readme",