@savvy-web/silk-effects 0.6.1 → 1.0.1
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 +48 -17
- package/_virtual/_rolldown/runtime.js +18 -0
- package/changesets/api/categories.js +247 -0
- package/changesets/api/changelog.js +134 -0
- package/changesets/api/dependency-table.js +163 -0
- package/changesets/api/linter.js +168 -0
- package/changesets/api/transformer.js +140 -0
- package/changesets/categories/index.js +299 -0
- package/changesets/categories/types.js +66 -0
- package/changesets/changelog/formatting.js +119 -0
- package/changesets/changelog/getDependencyReleaseLine.js +114 -0
- package/changesets/changelog/getReleaseLine.js +122 -0
- package/changesets/changelog/index.js +99 -0
- package/changesets/constants.js +43 -0
- package/changesets/errors.js +305 -0
- package/changesets/index.js +146 -0
- package/changesets/markdownlint/index.js +29 -0
- package/changesets/markdownlint/rules/content-structure.js +98 -0
- package/changesets/markdownlint/rules/dependency-table-format.js +170 -0
- package/changesets/markdownlint/rules/heading-hierarchy.js +61 -0
- package/changesets/markdownlint/rules/required-sections.js +54 -0
- package/changesets/markdownlint/rules/uncategorized-content.js +54 -0
- package/changesets/markdownlint/rules/utils.js +30 -0
- package/changesets/remark/plugins/aggregate-dependency-tables.js +47 -0
- package/changesets/remark/plugins/contributor-footnotes.js +123 -0
- package/changesets/remark/plugins/deduplicate-items.js +30 -0
- package/changesets/remark/plugins/issue-link-refs.js +58 -0
- package/changesets/remark/plugins/merge-sections.js +43 -0
- package/changesets/remark/plugins/normalize-format.js +47 -0
- package/changesets/remark/plugins/reorder-sections.js +34 -0
- package/changesets/remark/presets.js +119 -0
- package/changesets/remark/rules/content-structure.js +22 -0
- package/changesets/remark/rules/dependency-table-format.js +40 -0
- package/changesets/remark/rules/heading-hierarchy.js +19 -0
- package/changesets/remark/rules/required-sections.js +17 -0
- package/changesets/remark/rules/uncategorized-content.js +31 -0
- package/changesets/schemas/changeset.js +146 -0
- package/changesets/schemas/dependency-table.js +189 -0
- package/changesets/schemas/git.js +69 -0
- package/changesets/schemas/github.js +175 -0
- package/changesets/schemas/options.js +182 -0
- package/changesets/schemas/package-scope.js +128 -0
- package/changesets/schemas/primitives.js +72 -0
- package/changesets/schemas/version-files.js +151 -0
- package/changesets/services/branch-analyzer.js +278 -0
- package/changesets/services/changelog.js +50 -0
- package/changesets/services/config-inspector.js +390 -0
- package/changesets/services/github.js +178 -0
- package/changesets/services/markdown.js +106 -0
- package/changesets/services/workspace-snapshot.js +182 -0
- package/changesets/utils/commit-parser.js +80 -0
- package/changesets/utils/dep-diff.js +77 -0
- package/changesets/utils/dependency-table.js +347 -0
- package/changesets/utils/issue-refs.js +101 -0
- package/changesets/utils/jsonpath.js +175 -0
- package/changesets/utils/logger.js +50 -0
- package/changesets/utils/markdown-link.js +57 -0
- package/changesets/utils/publishability.js +39 -0
- package/changesets/utils/remark-pipeline.js +79 -0
- package/changesets/utils/section-parser.js +94 -0
- package/changesets/utils/strip-frontmatter.js +46 -0
- package/changesets/utils/version-blocks.js +108 -0
- package/changesets/utils/version-files.js +336 -0
- package/changesets/utils/worktree-snapshot.js +142 -0
- package/changesets/vendor/github-info.js +55 -0
- package/commitlint/config/factory.js +69 -0
- package/commitlint/config/plugins.js +227 -0
- package/commitlint/config/rules.js +155 -0
- package/commitlint/config/schema.js +46 -0
- package/commitlint/detection/dco.js +53 -0
- package/commitlint/detection/scopes.js +45 -0
- package/commitlint/formatter/format.js +85 -0
- package/commitlint/formatter/messages.js +79 -0
- package/commitlint/hook/diagnostics/branch.js +36 -0
- package/commitlint/hook/diagnostics/cache.js +37 -0
- package/commitlint/hook/diagnostics/commitlint-config.js +36 -0
- package/commitlint/hook/diagnostics/open-issues.js +56 -0
- package/commitlint/hook/diagnostics/package-manager.js +51 -0
- package/commitlint/hook/diagnostics/signing.js +107 -0
- package/commitlint/hook/envelope.js +46 -0
- package/commitlint/hook/output.js +45 -0
- package/commitlint/hook/parse-bash-command.js +105 -0
- package/commitlint/hook/rules/closes-trailer.js +31 -0
- package/commitlint/hook/rules/forbidden-content.js +32 -0
- package/commitlint/hook/rules/plan-leakage.js +36 -0
- package/commitlint/hook/rules/signing-flag-conflict.js +25 -0
- package/commitlint/hook/rules/soft-wrap.js +37 -0
- package/commitlint/hook/rules/types.js +14 -0
- package/commitlint/hook/rules/verbosity.js +31 -0
- package/commitlint/hook/silence-logger.js +39 -0
- package/commitlint/index.js +146 -0
- package/commitlint/prompt/config.js +91 -0
- package/commitlint/prompt/emojis.js +74 -0
- package/commitlint/prompt/prompter.js +135 -0
- package/commitlint/static.js +73 -0
- package/errors/BiomeSyncError.js +21 -0
- package/errors/ChangesetConfigError.js +20 -0
- package/errors/ConfigNotFoundError.js +21 -0
- package/errors/SectionParseError.js +16 -0
- package/errors/SectionValidationError.js +16 -0
- package/errors/SectionWriteError.js +16 -0
- package/errors/TagFormatError.js +20 -0
- package/errors/ToolNotFoundError.js +11 -0
- package/errors/ToolResolutionError.js +11 -0
- package/errors/ToolVersionMismatchError.js +11 -0
- package/errors/VersioningDetectionError.js +20 -0
- package/errors/WorkspaceAnalysisError.js +21 -0
- package/index.d.ts +9743 -8380
- package/index.js +36 -6657
- package/lint/Handler.js +39 -0
- package/lint/cli/sections.js +65 -0
- package/lint/cli/templates/markdownlint.gen.js +183 -0
- package/lint/config/Preset.js +152 -0
- package/lint/config/createConfig.js +89 -0
- package/lint/handlers/Biome.js +179 -0
- package/lint/handlers/Markdown.js +139 -0
- package/lint/handlers/PackageJson.js +130 -0
- package/lint/handlers/PnpmWorkspace.js +141 -0
- package/lint/handlers/ShellScripts.js +58 -0
- package/lint/handlers/TypeScript.js +134 -0
- package/lint/handlers/Yaml.js +167 -0
- package/lint/index.js +52 -0
- package/lint/utils/Command.js +285 -0
- package/lint/utils/Filter.js +100 -0
- package/lint/utils/Workspace.js +86 -0
- package/package.json +52 -63
- package/schemas/CommentStyle.js +16 -0
- package/schemas/ResolvedTool.js +63 -0
- package/schemas/SavvySections.js +113 -0
- package/schemas/SectionBlock.js +70 -0
- package/schemas/SectionDefinition.js +121 -0
- package/schemas/SectionResults.js +12 -0
- package/schemas/TagStrategySchemas.js +18 -0
- package/schemas/ToolDefinition.js +39 -0
- package/schemas/ToolResults.js +14 -0
- package/schemas/VersioningSchemas.js +95 -0
- package/schemas/WorkspaceAnalysisSchemas.js +190 -0
- package/services/BiomeSchemaSync.js +133 -0
- package/services/ChangesetConfig.js +78 -0
- package/services/ChangesetConfigReader.js +106 -0
- package/services/ConfigDiscovery.js +71 -0
- package/services/ManagedSection.js +288 -0
- package/services/SilkPublishability.js +193 -0
- package/services/SilkWorkspaceAnalyzer.js +213 -0
- package/services/TagStrategy.js +54 -0
- package/services/ToolDiscovery.js +229 -0
- package/services/VersioningStrategy.js +67 -0
- package/tsdoc-metadata.json +11 -11
- package/turbo/digest.js +127 -0
- package/turbo/errors.js +48 -0
- package/turbo/index.js +32 -0
- package/turbo/schemas/DryRun.js +57 -0
- package/turbo/schemas/results.js +61 -0
- package/turbo/services/TurboInspector.js +100 -0
- package/utils/ToolCommand.js +40 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { Command } from "../utils/Command.js";
|
|
2
|
+
import { Filter } from "../utils/Filter.js";
|
|
3
|
+
import { getWorkspaceRoot } from "../utils/Workspace.js";
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { format, resolveConfig } from "prettier";
|
|
7
|
+
import { lint } from "yaml-lint";
|
|
8
|
+
|
|
9
|
+
//#region src/lint/handlers/Yaml.ts
|
|
10
|
+
/**
|
|
11
|
+
* Handler for YAML files.
|
|
12
|
+
*
|
|
13
|
+
* Formats with Prettier and validates with yaml-lint, both as bundled dependencies.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Handler for YAML files.
|
|
17
|
+
*
|
|
18
|
+
* Formats with Prettier and validates with yaml-lint, both as bundled dependencies.
|
|
19
|
+
*
|
|
20
|
+
* @remarks
|
|
21
|
+
* Excludes pnpm-lock.yaml and pnpm-workspace.yaml by default.
|
|
22
|
+
* pnpm-workspace.yaml has its own dedicated handler.
|
|
23
|
+
*
|
|
24
|
+
* Uses Prettier for formatting and yaml-lint for validation.
|
|
25
|
+
* Both are bundled dependencies (no CLI spawning required).
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* import { Yaml } from '\@savvy-web/lint-staged';
|
|
30
|
+
*
|
|
31
|
+
* export default {
|
|
32
|
+
* [Yaml.glob]: Yaml.create({
|
|
33
|
+
* exclude: ['pnpm-lock.yaml', 'pnpm-workspace.yaml', 'generated/'],
|
|
34
|
+
* }),
|
|
35
|
+
* };
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
var Yaml = class Yaml {
|
|
39
|
+
/**
|
|
40
|
+
* Glob pattern for matching YAML files.
|
|
41
|
+
* @defaultValue `'**\/*.{yml,yaml}'`
|
|
42
|
+
*/
|
|
43
|
+
static glob = "**/*.{yml,yaml}";
|
|
44
|
+
/**
|
|
45
|
+
* Default patterns to exclude from processing.
|
|
46
|
+
* @defaultValue `['pnpm-lock.yaml', 'pnpm-workspace.yaml', '__test__/fixtures']`
|
|
47
|
+
*/
|
|
48
|
+
static defaultExcludes = [
|
|
49
|
+
"pnpm-lock.yaml",
|
|
50
|
+
"pnpm-workspace.yaml",
|
|
51
|
+
"__test__/fixtures"
|
|
52
|
+
];
|
|
53
|
+
/**
|
|
54
|
+
* Pre-configured handler with default options.
|
|
55
|
+
*/
|
|
56
|
+
static handler = Yaml.create();
|
|
57
|
+
/**
|
|
58
|
+
* Find the yaml-lint config file.
|
|
59
|
+
*
|
|
60
|
+
* Paths are anchored to the workspace root (via {@link getWorkspaceRoot}),
|
|
61
|
+
* falling back to `process.cwd()` when not inside a workspace.
|
|
62
|
+
*
|
|
63
|
+
* Searches in order:
|
|
64
|
+
* 1. `{workspaceRoot}/lib/configs/.yaml-lint.json`
|
|
65
|
+
* 2. `{workspaceRoot}/.yaml-lint.json`
|
|
66
|
+
*
|
|
67
|
+
* @returns The config file path, or undefined if not found
|
|
68
|
+
*/
|
|
69
|
+
static findConfig() {
|
|
70
|
+
const root = getWorkspaceRoot() ?? process.cwd();
|
|
71
|
+
const libPath = join(root, "lib/configs/.yaml-lint.json");
|
|
72
|
+
if (existsSync(libPath)) return libPath;
|
|
73
|
+
const rootPath = join(root, ".yaml-lint.json");
|
|
74
|
+
if (existsSync(rootPath)) return rootPath;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Load the yaml-lint schema from a config file.
|
|
78
|
+
*
|
|
79
|
+
* @param filepath - Path to the yaml-lint config file
|
|
80
|
+
* @returns The schema string, or undefined if not found
|
|
81
|
+
*/
|
|
82
|
+
static loadConfig(filepath) {
|
|
83
|
+
try {
|
|
84
|
+
const content = readFileSync(filepath, "utf-8");
|
|
85
|
+
return JSON.parse(content).schema;
|
|
86
|
+
} catch {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Check if yaml-lint is available.
|
|
92
|
+
*
|
|
93
|
+
* @returns Always `true` since yaml-lint is a bundled dependency
|
|
94
|
+
*/
|
|
95
|
+
static isAvailable() {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Format a YAML file in-place using Prettier.
|
|
100
|
+
*
|
|
101
|
+
* @param filepath - Path to the YAML file
|
|
102
|
+
*/
|
|
103
|
+
static async formatFile(filepath) {
|
|
104
|
+
writeFileSync(filepath, await format(readFileSync(filepath, "utf-8"), {
|
|
105
|
+
...await resolveConfig(filepath),
|
|
106
|
+
filepath,
|
|
107
|
+
parser: "yaml"
|
|
108
|
+
}), "utf-8");
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Validate a YAML file using yaml-lint.
|
|
112
|
+
*
|
|
113
|
+
* @param filepath - Path to the YAML file
|
|
114
|
+
* @param schema - The YAML schema to validate against
|
|
115
|
+
* @throws Error if the YAML is invalid
|
|
116
|
+
*/
|
|
117
|
+
static async validateFile(filepath, schema) {
|
|
118
|
+
await lint(readFileSync(filepath, "utf-8"), schema ? { schema } : void 0);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Create a handler that returns a CLI command to format YAML files.
|
|
122
|
+
*
|
|
123
|
+
* @remarks
|
|
124
|
+
* Unlike {@link create}, this does not modify files in the handler function
|
|
125
|
+
* body. Instead it returns a `savvy-lint fmt yaml` command so lint-staged
|
|
126
|
+
* can detect the modification and auto-stage it.
|
|
127
|
+
* Use this in lint-staged array syntax for sequential execution.
|
|
128
|
+
*
|
|
129
|
+
* @param options - Configuration options
|
|
130
|
+
* @returns A lint-staged compatible handler function
|
|
131
|
+
*/
|
|
132
|
+
static fmtCommand(options = {}) {
|
|
133
|
+
const excludes = options.exclude ?? [...Yaml.defaultExcludes];
|
|
134
|
+
return (filenames) => {
|
|
135
|
+
const filtered = Filter.exclude(filenames, excludes);
|
|
136
|
+
if (filtered.length === 0) return [];
|
|
137
|
+
return `${Command.findSavvyLint()} fmt yaml ${Filter.shellEscape(filtered)}`;
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Create a handler with custom options.
|
|
142
|
+
*
|
|
143
|
+
* @param options - Configuration options
|
|
144
|
+
* @returns A lint-staged compatible handler function
|
|
145
|
+
*/
|
|
146
|
+
static create(options = {}) {
|
|
147
|
+
const excludes = options.exclude ?? [...Yaml.defaultExcludes];
|
|
148
|
+
const skipFormat = options.skipFormat ?? false;
|
|
149
|
+
const skipValidate = options.skipValidate ?? false;
|
|
150
|
+
const configPath = options.config ?? Yaml.findConfig();
|
|
151
|
+
const schema = configPath ? Yaml.loadConfig(configPath) : void 0;
|
|
152
|
+
return async (filenames) => {
|
|
153
|
+
const filtered = Filter.exclude(filenames, excludes);
|
|
154
|
+
if (filtered.length === 0) return [];
|
|
155
|
+
if (!skipFormat) for (const filepath of filtered) await Yaml.formatFile(filepath);
|
|
156
|
+
if (!skipValidate) for (const filepath of filtered) try {
|
|
157
|
+
await Yaml.validateFile(filepath, schema);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
throw new Error(`Invalid YAML in ${filepath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
160
|
+
}
|
|
161
|
+
return [];
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
//#endregion
|
|
167
|
+
export { Yaml };
|
package/lint/index.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { __exportAll } from "../_virtual/_rolldown/runtime.js";
|
|
2
|
+
import { Handler } from "./Handler.js";
|
|
3
|
+
import { Command } from "./utils/Command.js";
|
|
4
|
+
import { Filter } from "./utils/Filter.js";
|
|
5
|
+
import { getWorkspacePackagePaths, getWorkspacePackages, getWorkspaceRoot, isWorkspacePackagePath, resetWorkspaceCache } from "./utils/Workspace.js";
|
|
6
|
+
import { Biome } from "./handlers/Biome.js";
|
|
7
|
+
import { Markdown } from "./handlers/Markdown.js";
|
|
8
|
+
import { PackageJson } from "./handlers/PackageJson.js";
|
|
9
|
+
import { PnpmWorkspace } from "./handlers/PnpmWorkspace.js";
|
|
10
|
+
import { ShellScripts } from "./handlers/ShellScripts.js";
|
|
11
|
+
import { TypeScript } from "./handlers/TypeScript.js";
|
|
12
|
+
import { Yaml } from "./handlers/Yaml.js";
|
|
13
|
+
import { createConfig } from "./config/createConfig.js";
|
|
14
|
+
import { Preset } from "./config/Preset.js";
|
|
15
|
+
import { DEFAULT_CONFIG_PATH, HUSKY_HOOK_PATH, LegacySavvyLintHygieneDef, MARKDOWNLINT_CONFIG_PATH, POST_CHECKOUT_HOOK_PATH, POST_MERGE_HOOK_PATH, SavvyLintSectionDef, generateManagedContent, savvyLintBlock } from "./cli/sections.js";
|
|
16
|
+
import { MARKDOWNLINT_CONFIG, MARKDOWNLINT_SCHEMA, MARKDOWNLINT_TEMPLATE } from "./cli/templates/markdownlint.gen.js";
|
|
17
|
+
|
|
18
|
+
//#region src/lint/index.ts
|
|
19
|
+
var lint_exports = /* @__PURE__ */ __exportAll({
|
|
20
|
+
Biome: () => Biome,
|
|
21
|
+
Command: () => Command,
|
|
22
|
+
DEFAULT_CONFIG_PATH: () => DEFAULT_CONFIG_PATH,
|
|
23
|
+
Filter: () => Filter,
|
|
24
|
+
HUSKY_HOOK_PATH: () => HUSKY_HOOK_PATH,
|
|
25
|
+
Handler: () => Handler,
|
|
26
|
+
LegacySavvyLintHygieneDef: () => LegacySavvyLintHygieneDef,
|
|
27
|
+
MARKDOWNLINT_CONFIG: () => MARKDOWNLINT_CONFIG,
|
|
28
|
+
MARKDOWNLINT_CONFIG_PATH: () => MARKDOWNLINT_CONFIG_PATH,
|
|
29
|
+
MARKDOWNLINT_SCHEMA: () => MARKDOWNLINT_SCHEMA,
|
|
30
|
+
MARKDOWNLINT_TEMPLATE: () => MARKDOWNLINT_TEMPLATE,
|
|
31
|
+
Markdown: () => Markdown,
|
|
32
|
+
POST_CHECKOUT_HOOK_PATH: () => POST_CHECKOUT_HOOK_PATH,
|
|
33
|
+
POST_MERGE_HOOK_PATH: () => POST_MERGE_HOOK_PATH,
|
|
34
|
+
PackageJson: () => PackageJson,
|
|
35
|
+
PnpmWorkspace: () => PnpmWorkspace,
|
|
36
|
+
Preset: () => Preset,
|
|
37
|
+
SavvyLintSectionDef: () => SavvyLintSectionDef,
|
|
38
|
+
ShellScripts: () => ShellScripts,
|
|
39
|
+
TypeScript: () => TypeScript,
|
|
40
|
+
Yaml: () => Yaml,
|
|
41
|
+
createConfig: () => createConfig,
|
|
42
|
+
generateManagedContent: () => generateManagedContent,
|
|
43
|
+
getWorkspacePackagePaths: () => getWorkspacePackagePaths,
|
|
44
|
+
getWorkspacePackages: () => getWorkspacePackages,
|
|
45
|
+
getWorkspaceRoot: () => getWorkspaceRoot,
|
|
46
|
+
isWorkspacePackagePath: () => isWorkspacePackagePath,
|
|
47
|
+
resetWorkspaceCache: () => resetWorkspaceCache,
|
|
48
|
+
savvyLintBlock: () => savvyLintBlock
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
export { lint_exports };
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
//#region src/lint/utils/Command.ts
|
|
6
|
+
/**
|
|
7
|
+
* Utilities for shell command operations.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { Command } from '@savvy-web/lint-staged';
|
|
12
|
+
*
|
|
13
|
+
* if (Command.isAvailable('yq')) {
|
|
14
|
+
* // yq is installed globally
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* // Check for tool available globally OR via package manager
|
|
18
|
+
* const biome = Command.findTool('biome');
|
|
19
|
+
* if (biome.available) {
|
|
20
|
+
* console.log(`Using: ${biome.command}`); // 'biome' or 'pnpm exec biome'
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* // Detect package manager from package.json
|
|
24
|
+
* const pm = Command.detectPackageManager();
|
|
25
|
+
* console.log(pm); // 'pnpm', 'npm', 'yarn', or 'bun'
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
/** Pattern for valid command/tool names (alphanumeric, hyphens, underscores, \@, /) */
|
|
29
|
+
const VALID_COMMAND_PATTERN = /^[\w@/-]+$/;
|
|
30
|
+
/**
|
|
31
|
+
* Validate that a command name is safe for shell execution.
|
|
32
|
+
* Prevents command injection by ensuring only valid characters are used.
|
|
33
|
+
*
|
|
34
|
+
* @param name - The command or tool name to validate
|
|
35
|
+
* @throws Error if the name contains invalid characters
|
|
36
|
+
*/
|
|
37
|
+
function validateCommandName(name) {
|
|
38
|
+
if (!VALID_COMMAND_PATTERN.test(name)) throw new Error(`Invalid command name: "${name}". Only alphanumeric characters, hyphens, underscores, @ and / are allowed.`);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Static utility class for shell command operations.
|
|
42
|
+
*/
|
|
43
|
+
var Command = class Command {
|
|
44
|
+
/** Cached package manager detection result */
|
|
45
|
+
static cachedPackageManager = null;
|
|
46
|
+
/** Cached project root path */
|
|
47
|
+
static cachedRoot = null;
|
|
48
|
+
/**
|
|
49
|
+
* Find the project root directory by walking up from `cwd`.
|
|
50
|
+
*
|
|
51
|
+
* Searches upward for the nearest directory containing a `package.json`.
|
|
52
|
+
* Falls back to the provided `cwd` (or `process.cwd()`) when none is found.
|
|
53
|
+
*
|
|
54
|
+
* @remarks
|
|
55
|
+
* This is more reliable than `process.cwd()` in environments like Husky
|
|
56
|
+
* hooks where the working directory may point to `.husky/` or another
|
|
57
|
+
* subdirectory.
|
|
58
|
+
*
|
|
59
|
+
* @param cwd - Starting directory for the search (defaults to `process.cwd()`)
|
|
60
|
+
* @returns The resolved project root path
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* const root = Command.findRoot();
|
|
65
|
+
* console.log(root); // '/Users/me/my-project'
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
static findRoot(cwd = process.cwd()) {
|
|
69
|
+
if (Command.cachedRoot !== null) return Command.cachedRoot;
|
|
70
|
+
let dir = resolve(cwd);
|
|
71
|
+
while (true) {
|
|
72
|
+
if (existsSync(join(dir, "package.json"))) {
|
|
73
|
+
Command.cachedRoot = dir;
|
|
74
|
+
return dir;
|
|
75
|
+
}
|
|
76
|
+
const parent = dirname(dir);
|
|
77
|
+
if (parent === dir) break;
|
|
78
|
+
dir = parent;
|
|
79
|
+
}
|
|
80
|
+
Command.cachedRoot = cwd;
|
|
81
|
+
return cwd;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Detect the package manager from the root package.json's `packageManager` field.
|
|
85
|
+
*
|
|
86
|
+
* Parses the `packageManager` field (e.g., `pnpm\@9.0.0`) and extracts the manager name.
|
|
87
|
+
* Falls back to "npm" if no packageManager field is found.
|
|
88
|
+
*
|
|
89
|
+
* @param cwd - Directory to search for package.json (defaults to `Command.findRoot()`)
|
|
90
|
+
* @returns The detected package manager
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* const pm = Command.detectPackageManager();
|
|
95
|
+
* console.log(pm); // 'pnpm', 'npm', 'yarn', or 'bun'
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
static detectPackageManager(cwd = Command.findRoot()) {
|
|
99
|
+
if (Command.cachedPackageManager !== null) return Command.cachedPackageManager;
|
|
100
|
+
const packageJsonPath = join(cwd, "package.json");
|
|
101
|
+
if (!existsSync(packageJsonPath)) {
|
|
102
|
+
Command.cachedPackageManager = "npm";
|
|
103
|
+
return "npm";
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const content = readFileSync(packageJsonPath, "utf-8");
|
|
107
|
+
const pkg = JSON.parse(content);
|
|
108
|
+
if (pkg.packageManager) {
|
|
109
|
+
const match = pkg.packageManager.match(/^(npm|pnpm|yarn|bun)@/);
|
|
110
|
+
if (match) {
|
|
111
|
+
Command.cachedPackageManager = match[1];
|
|
112
|
+
return Command.cachedPackageManager;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch {}
|
|
116
|
+
Command.cachedPackageManager = "npm";
|
|
117
|
+
return "npm";
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get the exec command prefix for a package manager.
|
|
121
|
+
*
|
|
122
|
+
* @param packageManager - The package manager name
|
|
123
|
+
* @returns Array of command parts to prefix tool execution
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```typescript
|
|
127
|
+
* Command.getExecPrefix('pnpm'); // ['pnpm', 'exec']
|
|
128
|
+
* Command.getExecPrefix('npm'); // ['npx', '--no']
|
|
129
|
+
* Command.getExecPrefix('yarn'); // ['yarn', 'exec']
|
|
130
|
+
* Command.getExecPrefix('bun'); // ['bun', 'x', "--no-install"]
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
static getExecPrefix(packageManager) {
|
|
134
|
+
switch (packageManager) {
|
|
135
|
+
case "pnpm": return ["pnpm", "exec"];
|
|
136
|
+
case "yarn": return ["yarn", "exec"];
|
|
137
|
+
case "bun": return [
|
|
138
|
+
"bun",
|
|
139
|
+
"x",
|
|
140
|
+
"--no-install"
|
|
141
|
+
];
|
|
142
|
+
default: return ["npx", "--no"];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Clear the cached package manager and project root detection.
|
|
147
|
+
* Useful for testing or when package.json changes.
|
|
148
|
+
*/
|
|
149
|
+
static clearCache() {
|
|
150
|
+
Command.cachedPackageManager = null;
|
|
151
|
+
Command.cachedRoot = null;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Check if a command is available in the system PATH.
|
|
155
|
+
*
|
|
156
|
+
* @param command - The command name to check
|
|
157
|
+
* @returns `true` if the command exists, `false` otherwise
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```typescript
|
|
161
|
+
* if (Command.isAvailable('yq')) {
|
|
162
|
+
* console.log('yq is installed');
|
|
163
|
+
* }
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
static isAvailable(command) {
|
|
167
|
+
validateCommandName(command);
|
|
168
|
+
try {
|
|
169
|
+
execSync(`command -v ${command}`, { stdio: "ignore" });
|
|
170
|
+
return true;
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Find a tool, checking global installation first, then the project's package manager.
|
|
177
|
+
*
|
|
178
|
+
* Search order:
|
|
179
|
+
* 1. Global command (in PATH)
|
|
180
|
+
* 2. Project's package manager (detected from package.json `packageManager` field)
|
|
181
|
+
*
|
|
182
|
+
* @param tool - The tool name to find
|
|
183
|
+
* @returns Search result with command string if found
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```typescript
|
|
187
|
+
* const biome = Command.findTool('biome');
|
|
188
|
+
* if (biome.available) {
|
|
189
|
+
* // biome.command is 'biome', 'pnpm exec biome', 'npx --no biome', etc.
|
|
190
|
+
* console.log(`Running: ${biome.command} check`);
|
|
191
|
+
* }
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
static findTool(tool) {
|
|
195
|
+
validateCommandName(tool);
|
|
196
|
+
if (Command.isAvailable(tool)) return {
|
|
197
|
+
available: true,
|
|
198
|
+
command: tool,
|
|
199
|
+
source: "global"
|
|
200
|
+
};
|
|
201
|
+
const pm = Command.detectPackageManager();
|
|
202
|
+
const execCmd = [...Command.getExecPrefix(pm), tool].join(" ");
|
|
203
|
+
try {
|
|
204
|
+
execSync(`${execCmd} --version`, { stdio: "ignore" });
|
|
205
|
+
return {
|
|
206
|
+
available: true,
|
|
207
|
+
command: execCmd,
|
|
208
|
+
source: pm
|
|
209
|
+
};
|
|
210
|
+
} catch {}
|
|
211
|
+
return {
|
|
212
|
+
available: false,
|
|
213
|
+
command: void 0,
|
|
214
|
+
source: void 0
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Find a tool or throw an error if not available.
|
|
219
|
+
*
|
|
220
|
+
* @param tool - The tool name to find
|
|
221
|
+
* @param errorMessage - Custom error message (optional)
|
|
222
|
+
* @returns The command string to use
|
|
223
|
+
* @throws Error if the tool is not available
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* ```typescript
|
|
227
|
+
* const biomeCmd = Command.requireTool('biome');
|
|
228
|
+
* // Throws if biome not found, otherwise returns command string
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
231
|
+
static requireTool(tool, errorMessage) {
|
|
232
|
+
const result = Command.findTool(tool);
|
|
233
|
+
if (!result.available || !result.command) throw new Error(errorMessage ?? `Required tool '${tool}' is not available. Install it globally or add it as a dev dependency.`);
|
|
234
|
+
return result.command;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Find the `savvy lint` CLI command.
|
|
238
|
+
*
|
|
239
|
+
* @remarks
|
|
240
|
+
* Resolves the unified `savvy` binary via the standard tool search and
|
|
241
|
+
* appends the `lint` subcommand, falling back to the cli dev build at
|
|
242
|
+
* `packages/cli/dist/dev/pkg/bin/savvy.js` for dogfooding scenarios where the
|
|
243
|
+
* bin isn't linked yet.
|
|
244
|
+
*
|
|
245
|
+
* @returns The command string to invoke `savvy lint`
|
|
246
|
+
*/
|
|
247
|
+
static findSavvyLint() {
|
|
248
|
+
const result = Command.findTool("savvy");
|
|
249
|
+
if (result.available && result.command) return `${result.command} lint`;
|
|
250
|
+
return `node ${Command.findRoot()}/packages/cli/dist/dev/pkg/bin/savvy.js lint`;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Execute a command and return its output.
|
|
254
|
+
*
|
|
255
|
+
* @param command - The command to execute
|
|
256
|
+
* @returns The command output as a string (trimmed)
|
|
257
|
+
* @throws If the command fails
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* ```typescript
|
|
261
|
+
* const version = Command.exec('node --version');
|
|
262
|
+
* console.log(version); // 'v20.10.0'
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
static exec(command) {
|
|
266
|
+
return execSync(command, { encoding: "utf-8" }).trim();
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Execute a command silently (ignore output).
|
|
270
|
+
*
|
|
271
|
+
* @param command - The command to execute
|
|
272
|
+
* @returns `true` if successful, `false` if failed
|
|
273
|
+
*/
|
|
274
|
+
static execSilent(command) {
|
|
275
|
+
try {
|
|
276
|
+
execSync(command, { stdio: "ignore" });
|
|
277
|
+
return true;
|
|
278
|
+
} catch {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
//#endregion
|
|
285
|
+
export { Command };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
//#region src/lint/utils/Filter.ts
|
|
2
|
+
/**
|
|
3
|
+
* Utilities for filtering staged file lists.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```typescript
|
|
7
|
+
* import { Filter } from '@savvy-web/lint-staged';
|
|
8
|
+
*
|
|
9
|
+
* const handler = (filenames: readonly string[]) => {
|
|
10
|
+
* const filtered = Filter.exclude(filenames, ['dist/', '__fixtures__']);
|
|
11
|
+
* return filtered.length > 0 ? `biome check ${Filter.shellEscape(filtered)}` : [];
|
|
12
|
+
* };
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Static utility class for filtering file lists.
|
|
17
|
+
*/
|
|
18
|
+
var Filter = class Filter {
|
|
19
|
+
/**
|
|
20
|
+
* Exclude files matching any of the given patterns.
|
|
21
|
+
*
|
|
22
|
+
* @param filenames - Array of file paths
|
|
23
|
+
* @param patterns - Patterns to exclude (uses `string.includes()`)
|
|
24
|
+
* @returns Filtered array of file paths
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const files = ['src/index.ts', 'dist/index.js', '__fixtures__/test.ts'];
|
|
29
|
+
* const filtered = Filter.exclude(files, ['dist/', '__fixtures__']);
|
|
30
|
+
* // Result: ['src/index.ts']
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
static exclude(filenames, patterns) {
|
|
34
|
+
if (patterns.length === 0) return [...filenames];
|
|
35
|
+
return filenames.filter((file) => !patterns.some((pattern) => file.includes(pattern)));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Include only files matching any of the given patterns.
|
|
39
|
+
*
|
|
40
|
+
* @param filenames - Array of file paths
|
|
41
|
+
* @param patterns - Patterns to include (uses `string.includes()`)
|
|
42
|
+
* @returns Filtered array of file paths
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* const files = ['src/index.ts', 'lib/utils.ts', 'test/foo.test.ts'];
|
|
47
|
+
* const filtered = Filter.include(files, ['src/', 'lib/']);
|
|
48
|
+
* // Result: ['src/index.ts', 'lib/utils.ts']
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
static include(filenames, patterns) {
|
|
52
|
+
if (patterns.length === 0) return [];
|
|
53
|
+
return filenames.filter((file) => patterns.some((pattern) => file.includes(pattern)));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Combine exclude and include filters.
|
|
57
|
+
*
|
|
58
|
+
* @param filenames - Array of file paths
|
|
59
|
+
* @param options - Filter options
|
|
60
|
+
* @returns Filtered array of file paths
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* const files = ['src/index.ts', 'src/index.test.ts', 'dist/index.js'];
|
|
65
|
+
* const filtered = Filter.apply(files, {
|
|
66
|
+
* include: ['src/'],
|
|
67
|
+
* exclude: ['.test.'],
|
|
68
|
+
* });
|
|
69
|
+
* // Result: ['src/index.ts']
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
static apply(filenames, options) {
|
|
73
|
+
let result = [...filenames];
|
|
74
|
+
if (options.include && options.include.length > 0) result = Filter.include(result, options.include);
|
|
75
|
+
if (options.exclude && options.exclude.length > 0) result = Filter.exclude(result, options.exclude);
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Escape file paths for safe shell command construction.
|
|
80
|
+
*
|
|
81
|
+
* Wraps each path in single quotes and escapes any embedded single quotes.
|
|
82
|
+
* This prevents issues with paths containing spaces or special characters.
|
|
83
|
+
*
|
|
84
|
+
* @param filenames - Array of file paths
|
|
85
|
+
* @returns Space-separated string of shell-escaped paths
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```typescript
|
|
89
|
+
* const files = ['/path/to/file.ts', '/path/with spaces/file.ts'];
|
|
90
|
+
* const escaped = Filter.shellEscape(files);
|
|
91
|
+
* // Result: "'/path/to/file.ts' '/path/with spaces/file.ts'"
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
static shellEscape(filenames) {
|
|
95
|
+
return filenames.map((f) => `'${f.replace(/'/g, "'\\''")}'`).join(" ");
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
//#endregion
|
|
100
|
+
export { Filter };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { dirname } from "node:path";
|
|
2
|
+
import { findWorkspaceRootSync, getWorkspacePackagesSync } from "workspaces-effect";
|
|
3
|
+
|
|
4
|
+
//#region src/lint/utils/Workspace.ts
|
|
5
|
+
/**
|
|
6
|
+
* Workspace-aware discovery utilities.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* Wraps the synchronous APIs from `workspaces-effect` with caching.
|
|
10
|
+
* Workspace layout does not change during a lint-staged run, so
|
|
11
|
+
* results are cached on first access. Use `resetWorkspaceCache()`
|
|
12
|
+
* in tests to clear state between runs.
|
|
13
|
+
*/
|
|
14
|
+
/** Sentinel indicating "not yet resolved". */
|
|
15
|
+
const UNRESOLVED = Symbol("unresolved");
|
|
16
|
+
let cachedRoot = UNRESOLVED;
|
|
17
|
+
let cachedPackages = UNRESOLVED;
|
|
18
|
+
let cachedPaths = UNRESOLVED;
|
|
19
|
+
/**
|
|
20
|
+
* Get the workspace root directory.
|
|
21
|
+
*
|
|
22
|
+
* @returns Absolute path to workspace root, or null if not in a workspace
|
|
23
|
+
*/
|
|
24
|
+
function getWorkspaceRoot() {
|
|
25
|
+
if (cachedRoot !== UNRESOLVED) return cachedRoot;
|
|
26
|
+
cachedRoot = findWorkspaceRootSync() ?? null;
|
|
27
|
+
return cachedRoot;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Get all leaf workspace packages (excludes root).
|
|
31
|
+
*
|
|
32
|
+
* @returns Array of workspace packages, or null if not in a workspace
|
|
33
|
+
*/
|
|
34
|
+
function getWorkspacePackages() {
|
|
35
|
+
if (cachedPackages !== UNRESOLVED) return cachedPackages;
|
|
36
|
+
const root = getWorkspaceRoot();
|
|
37
|
+
if (root === null) {
|
|
38
|
+
cachedPackages = null;
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
cachedPackages = (getWorkspacePackagesSync(root) ?? []).filter((pkg) => pkg.path !== root);
|
|
42
|
+
return cachedPackages;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get absolute paths of all leaf workspace package directories.
|
|
46
|
+
*
|
|
47
|
+
* @returns Array of absolute paths, empty if not in a workspace
|
|
48
|
+
*/
|
|
49
|
+
function getWorkspacePackagePaths() {
|
|
50
|
+
if (cachedPaths !== UNRESOLVED) return cachedPaths;
|
|
51
|
+
cachedPaths = getWorkspacePackages()?.map((pkg) => pkg.path) ?? [];
|
|
52
|
+
return cachedPaths;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Check if a file path is at a workspace root or leaf workspace root.
|
|
56
|
+
*
|
|
57
|
+
* @remarks
|
|
58
|
+
* Compares the file's parent directory against the workspace root
|
|
59
|
+
* and all leaf workspace roots. Returns true as a permissive
|
|
60
|
+
* fallback when not in a workspace, so single-package repos
|
|
61
|
+
* continue to work.
|
|
62
|
+
*
|
|
63
|
+
* @param filePath - Absolute path to the file
|
|
64
|
+
* @returns true if the file is at a workspace or leaf root
|
|
65
|
+
*/
|
|
66
|
+
function isWorkspacePackagePath(filePath) {
|
|
67
|
+
const root = getWorkspaceRoot();
|
|
68
|
+
if (root === null) return true;
|
|
69
|
+
const dir = dirname(filePath);
|
|
70
|
+
if (dir === root) return true;
|
|
71
|
+
return getWorkspacePackagePaths().includes(dir);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Clear all cached workspace data.
|
|
75
|
+
*
|
|
76
|
+
* @remarks
|
|
77
|
+
* Call this in test teardown to ensure clean state between tests.
|
|
78
|
+
*/
|
|
79
|
+
function resetWorkspaceCache() {
|
|
80
|
+
cachedRoot = UNRESOLVED;
|
|
81
|
+
cachedPackages = UNRESOLVED;
|
|
82
|
+
cachedPaths = UNRESOLVED;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
//#endregion
|
|
86
|
+
export { getWorkspacePackagePaths, getWorkspacePackages, getWorkspaceRoot, isWorkspacePackagePath, resetWorkspaceCache };
|