Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions docs/maintenance.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,11 @@ The skill documents compatible pinned dependencies, the TypeScript config loader

The shared configuration uses `@eslint/js` recommended and Unicorn `recommended`. Unicorn has no supported `strict` preset. Type-aware source uses TypeScript ESLint `strictTypeChecked` and `stylisticTypeChecked`. React files use React Hooks' modern flat `recommended-latest` preset.

The repository enables `strictNullChecks` and `allowJs`. The shared ESLint configuration applies these presets and project service to authored JavaScript and TypeScript. Covered extensions are `.js`, `.jsx`, `.mjs`, `.cjs`, `.ts`, `.tsx`, `.mts`, and `.cts`. The TypeScript program includes owned source across the repository with `**/*`. It excludes `node_modules/`, `dist/`, `.marketplace-build/`, and `.marketplace-verify/`.
The repository enables `strict`, `allowJs`, and `checkJs`. The compiler validates JavaScript as well as TypeScript. The shared ESLint configuration applies these presets and project service to authored JavaScript and TypeScript. Covered extensions are `.js`, `.jsx`, `.mjs`, `.cjs`, `.ts`, `.tsx`, `.mts`, and `.cts`. The TypeScript program includes owned source across the repository with `**/*`. It excludes `node_modules/`, `dist/`, `.marketplace-build/`, and `.marketplace-verify/`.

The compiler also validates indexed access, exact optional properties, overrides, index-signature access, switch fallthrough, return paths, and unused declarations and parameters. It rejects unreachable code, unused labels, and unresolved side-effect imports. It validates declaration files with `skipLibCheck: false`. Consistent filename casing, isolated modules, and verbatim module syntax match Bun execution. `bun run check-types` runs `tsc --noEmit` locally and in CI. Type-aware ESLint rules use type information but do not replace compiler diagnostics.

The shared policy permits `null` by disabling only `unicorn/no-null`. TypeScript `strictNullChecks` still requires safe handling of nullable values.

The shared configuration applies a 350-line lint limit and a 120-column Prettier formatting target. It requires braces for control flow, while concise arrow expressions remain valid. Unicorn rules without an explicit override use their recommended error severities and default options. Core `complexity` warns above its default maximum of 20. `unicorn/try-complexity` warns above its default maximum of 1.

Expand Down Expand Up @@ -122,7 +126,9 @@ Direct `void`, `undefined`, and `never` return annotations need no return descri

Preserve the inherited starless documentation-comment format.

`jdeurt/const-on-top` requires both `const` and `let` declarations before executable statements in their program or block scope. Imports, type declarations, function declarations, and variable declarations can share the leading declaration prefix. Loop-header declarations remain valid. Consecutive declarations of the same kind may stay together. Leave a blank line after the final `const` or `let` before another kind of declaration or other code. It offers no automatic hoist that could change evaluation order.
`jdeurt/const-on-top` requires both `const` and `let` declarations before executable statements in their program, block, or static-block scope. Imports, types, functions, and variables can share the declaration prefix. Runtime declarations may use either `const` or `let` first. After switching kinds, they cannot return to the earlier kind. Ambient `declare` bindings do not select a runtime group. Each nested scope tracks its own groups, and loop-header declarations remain valid.

The rule enforces declaration order only, regardless of whitespace. The separate `@stylistic/padding-line-between-statements` rule controls blank lines. The declaration rule has no automatic fix because reordering initializers can change evaluation order or dependencies.

The inherited `unicorn/consistent-function-scoping` error flags functions that can move to an outer scope because they do not capture local state. Closures that capture locals, inline callbacks, and class methods remain valid where the rule permits them. `no-inner-declarations` and `max-nested-callbacks` do not express a general restriction on nested functions. The shared configuration adds no blanket function-placement restriction.

Expand Down
43 changes: 29 additions & 14 deletions eslint-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,30 +73,45 @@ function isDeclarationPrefix(statement: unknown): boolean {
const constOnTop: Rule.RuleModule = {
meta: {
type: "suggestion",
docs: { description: "Keep const and let declarations in the declaration prefix of their scope." },
docs: { description: "Keep const and let declarations in separate groups at the beginning of their scope." },
schema: [],
messages: { move: "Declare this variable at the top of its scope, before executable statements." },
messages: {
move: "Declare this variable at the top of its scope, before executable statements.",
group: "Keep {{kind}} declarations in one group. Do not interleave const and let declarations.",
},
},
create(context) {
function report(declaration: unknown, messageId: "move" | "group", kind: "const" | "let") {
const location = property(declaration, "loc");
const start = property(location, "start");
const line = property(start, "line");
const column = property(start, "column");

if (typeof line === "number" && typeof column === "number") {
context.report({ loc: { line, column }, messageId, data: { kind } });
}
}

function inspectBody(body: unknown[]) {
const seenKinds = new Set<string>();

let hasExecutableStatement = false;
let previousKind: string | undefined;

for (const statement of body) {
const declaration = declarationOf(statement);
const kind = property(declaration, "kind");

if (
hasExecutableStatement &&
typeOf(declaration) === "VariableDeclaration" &&
(kind === "const" || kind === "let")
) {
const location = property(declaration, "loc");
const start = property(location, "start");
const line = property(start, "line");
const column = property(start, "column");

if (typeof line === "number" && typeof column === "number") {
context.report({ loc: { line, column }, messageId: "move" });
if (typeOf(declaration) === "VariableDeclaration" && (kind === "const" || kind === "let")) {
if (hasExecutableStatement) {
report(declaration, "move", kind);
} else if (property(declaration, "declare") !== true) {
if (kind !== previousKind && seenKinds.has(kind)) {
report(declaration, "group", kind);
}

seenKinds.add(kind);
previousKind = kind;
}
}

Expand Down
1 change: 1 addition & 0 deletions eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export function createConfig(options: { typed?: boolean; tsconfigRootDir?: strin
"unicorn/no-repeated-call-wrappers": "warn",
"unicorn/try-complexity": "warn",
"unicorn/no-array-reduce": "off",
"unicorn/no-null": "off",
"unicorn/name-replacements": "off",
"unicorn/filename-case": ["error", { cases: { kebabCase: true, pascalCase: true } }],
},
Expand Down
9 changes: 7 additions & 2 deletions plugins/engineering/skills/formatting-linting/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ bun add --dev --exact eslint@10.13.0 @eslint/js@10.0.1 @stylistic/eslint-plugin@

These versions match the marketplace source's resolved dependencies. ESLint loads the TypeScript configuration through `jiti`. Keep `jiti` even for a JavaScript-only project. Install runtime type declarations needed by the project, such as Bun's types for Bun code. When updating versions, check the imported presets and APIs against the installed packages and run the complete project checks.

Provide a project `tsconfig.json` that includes every owned JavaScript and TypeScript source file. Enable strict null checks and `allowJs` so the typed ESLint presets can inspect JavaScript too. Match the project's runtime and module system. For a new Bun project, start with:
Provide a project `tsconfig.json` that includes every owned JavaScript and TypeScript source file. Enable `strict`, `allowJs`, and `checkJs` to check owned JavaScript and TypeScript. Type-aware ESLint rules supplement the compiler checks. Match the project's runtime and module system. For a new Bun project, start with:

```json
{
Expand All @@ -49,6 +49,7 @@ Provide a project `tsconfig.json` that includes every owned JavaScript and TypeS
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"allowJs": true,
"checkJs": true,
"strict": true,
"noEmit": true
},
Expand All @@ -73,6 +74,8 @@ Run automatic fixes only within the authorized source scope. Inspect the diff. R

The configuration uses recommended JavaScript and Unicorn rules with TypeScript ESLint strict and stylistic type-checked presets. React files also use React Hooks and React Doctor presets. It preserves inherited rule severities and defaults unless the source config explicitly overrides them.

The shared policy disables `unicorn/no-null` and permits `null`. Compiler strict null checks still require safe handling of nullable values.

Complexity warns above 20. Try complexity warns above 1. The zero-warning command makes both rules enforceable. TypeScript files use the TypeScript equivalents of `no-use-before-define`, `no-magic-numbers`, and `no-shadow`.

Prettier uses double quotes, semicolons, two-space indentation, 120 columns, ES5 trailing commas, and LF endings. Nonconflicting ESLint Stylistic recommended rules remain active. The config lists each omitted rule and its concrete Prettier conflict. Leave a blank line after the final import before a non-import statement. Leave blank lines between class members, including single-line methods, and around class declarations. Keep TypeScript overload signatures together with their implementation.
Expand All @@ -83,7 +86,9 @@ Use descriptions that explain the caller's contract. Keep type annotations in Ty

Direct void-returning routines and constructors do not require return descriptions. Declaration-only signatures require return descriptions for value annotations, including named aliases. Keep shared documentation with a stacked overload group. Preserve the inherited starless documentation-comment format.

`jdeurt/const-on-top` requires both `const` and `let` declarations before executable statements in their program or block scope. Leading imports, type declarations, function declarations, and variable declarations can share the declaration prefix. Consecutive declarations of the same kind may stay together. Leave a blank line after the final `const` or `let` before another kind of declaration or other code. Loop-header declarations remain valid.
`jdeurt/const-on-top` requires both `const` and `let` declarations before executable statements in their program, block, or static-block scope. Imports, types, functions, and variables can share the declaration prefix. Runtime declarations may use either `const` or `let` first. After switching kinds, they cannot return to the earlier kind. Ambient `declare` bindings do not select a runtime group. Each nested scope tracks its own groups, and loop-header declarations remain valid.

The rule enforces declaration order only, regardless of whitespace. The separate `@stylistic/padding-line-between-statements` rule controls blank lines.

Preserve evaluation order and dependencies when restructuring. The rule deliberately does not automatically hoist initializers.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,8 +388,10 @@ publishReport("reports/summary.json");
### 2B. Keep declarations in the right scope

Keep constant and mutable declarations in the declaration prefix of their scope.
Keep runtime declarations of each kind in one group. Either kind may come first, but do not return to that kind after switching.
Ambient declarations do not select a runtime group. Each nested scope has its own groups.
Initializers can execute work during declaration. Preserve their timing when you reorganize declarations.
Put a blank line between the final declaration in a group and the next executable statement.
Apply the separate spacing requirements in 1C. Declaration order does not depend on whitespace.
Imports, types, functions, and other variable declarations may precede these declarations.
Loop-header declarations may remain in the loop header.

Expand All @@ -406,6 +408,7 @@ Extract cohesive work when required to meet declaration placement while preservi
**Why:**

A declaration prefix exposes a scope's inputs and local state before its actions.
Separate declaration groups make transitions between constant and mutable bindings easy to find.
Blank lines distinguish initialization from later execution.
Other declarations can establish dependencies without starting that scope's main actions.
Loop-header declarations keep iteration state within its natural lifetime.
Expand Down
3 changes: 2 additions & 1 deletion scripts/build-marketplace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,8 @@ function parseOptions(args: string[]) {
if (args[i] === "--check") {
options.check = true;
} else if (next && args[i] === "--output" && !next.startsWith("--")) {
options.output = path.resolve(args[++i]);
options.output = path.resolve(next);
i++;
} else {
throw new Error("Usage: bun scripts/build-marketplace.ts [--check] [--output directory]");
}
Expand Down
3 changes: 2 additions & 1 deletion scripts/build-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ Validate marketplace metadata, plugin identities, versions, and supported harnes
function validateRegistryFields(registry: Registry) {
const marketplace = registry.marketplace;
const names = new Set<string>();
const requiredFields: (keyof Registry["marketplace"])[] = ["name", "owner", values.DESCRIPTION];

fields(registry.marketplace, ["name", "display_name", "owner", values.DESCRIPTION], values.MARKETPLACE);

for (const key of ["name", "owner", values.DESCRIPTION]) {
for (const key of requiredFields) {
requireText(marketplace[key], `marketplace.${key}`);
}

Expand Down
2 changes: 1 addition & 1 deletion scripts/compile-invocation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export function applyInvocationPolicies(
const files = new Map(output.files.map((file) => [file.path, file]));

for (const [name, metadata] of skills) {
const isExplicit = metadata.metadata?.invocation === "explicit";
const isExplicit = metadata.metadata?.["invocation"] === "explicit";

const skillPath = `skills/${name}/SKILL.md`;
const skill = requireSkill(files.get(skillPath), harness, name);
Expand Down
22 changes: 21 additions & 1 deletion scripts/compile-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,26 @@ function requireHarnessOutput(outputs: HarnessOutput[], harness: string, pluginN
return output;
}

/**
Restore a command server's working directory in compiled Codex output.
@param server - Compiled server record, if the compiler retained it.
@param directory - Source working directory with an optional plugin-root placeholder.
@param pluginName - Plugin name to identify missing output.
@param serverName - Server name to identify missing output.
*/
function restoreServerDirectory(
server: Record<string, unknown> | undefined,
directory: string,
pluginName: string,
serverName: string
) {
if (!server) {
throw new Error(`${pluginName}: missing compiled MCP server ${serverName}`);
}

server["cwd"] = directory.replaceAll(values.PLUGIN_ROOT_PLACEHOLDER, ".");
}

/**
Normalize compiled manifests, skill policies, and server paths for publication.
@param root - Source root used to validate compiler output paths.
Expand Down Expand Up @@ -117,7 +137,7 @@ function collectCompiledFiles(

for (const [name, server] of sourceServers) {
if ("command" in server && server.command && server.cwd) {
servers[name].cwd = server.cwd.replaceAll(values.PLUGIN_ROOT_PLACEHOLDER, ".");
restoreServerDirectory(servers[name], server.cwd, plugin.name, name);
}
}

Expand Down
7 changes: 4 additions & 3 deletions scripts/file-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function parseFrontmatterDocument(source: string) {
throw new Error("Skill frontmatter must contain one YAML mapping.");
}

return document;
return properties.parse(document);
}

/**
Expand All @@ -46,14 +46,15 @@ Read the leading YAML header and retain its end offset for document reconstructi
*/
export function parseSkillFrontmatter(source: string) {
const match = /^---\r?\n([\s\S]*?)\r?\n---(?=\r?\n|$)/.exec(source);
const header = match?.[1];

if (!match) {
if (!match || header === undefined) {
throw new Error("SKILL.md must start with YAML frontmatter.");
}

return {
header: { position: { end: { offset: match[0].length } } },
document: parseFrontmatterDocument(match[1]),
document: parseFrontmatterDocument(header),
};
}

Expand Down
4 changes: 2 additions & 2 deletions scripts/import-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,11 @@ export async function readImports(root: string): Promise<ExternalImport[]> {
const configSource = await readFile(path.join(root, IMPORTS_TOML), "utf8");
const config = jsonRecord.parse(Bun.TOML.parse(configSource));

if (Object.keys(config).some((key) => key !== "imports") || !Array.isArray(config.imports)) {
if (Object.keys(config).some((key) => key !== "imports") || !Array.isArray(config["imports"])) {
throw new Error("imports.toml must contain an imports array");
}

return parseImportEntries(root, config.imports);
return parseImportEntries(root, config["imports"]);
}

/**
Expand Down
20 changes: 10 additions & 10 deletions scripts/import-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ async function prepareAsdFixture(upstream: string) {
await writeFile(path.join(upstream, ASD_STE100_LINTER_PATH), ASD_STE100_LINTER_SOURCE);
await writeFile(path.join(upstream, "LICENSE"), FIXTURE_LICENSE);
initializeFixtureRepository(upstream);
process.env.GIT_CONFIG_COUNT = "4";
process.env.GIT_CONFIG_KEY_3 = gitRewrite;
process.env.GIT_CONFIG_VALUE_3 = ASD_STE100_REPOSITORY;
process.env["GIT_CONFIG_COUNT"] = "4";
process.env["GIT_CONFIG_KEY_3"] = gitRewrite;
process.env["GIT_CONFIG_VALUE_3"] = ASD_STE100_REPOSITORY;
}

/**
Expand Down Expand Up @@ -96,13 +96,13 @@ async function prepareFixture(upstream: string) {
}

initializeFixtureRepository(upstream);
process.env.GIT_CONFIG_COUNT = "3";
process.env.GIT_CONFIG_KEY_0 = gitRewrite;
process.env.GIT_CONFIG_VALUE_0 = "https://github.com/shadcn-ui/ui.git";
process.env.GIT_CONFIG_KEY_1 = gitRewrite;
process.env.GIT_CONFIG_VALUE_1 = "https://github.com/cloudflare/skills.git";
process.env.GIT_CONFIG_KEY_2 = gitRewrite;
process.env.GIT_CONFIG_VALUE_2 = "https://github.com/kucherenko/jscpd.git";
process.env["GIT_CONFIG_COUNT"] = "3";
process.env["GIT_CONFIG_KEY_0"] = gitRewrite;
process.env["GIT_CONFIG_VALUE_0"] = "https://github.com/shadcn-ui/ui.git";
process.env["GIT_CONFIG_KEY_1"] = gitRewrite;
process.env["GIT_CONFIG_VALUE_1"] = "https://github.com/cloudflare/skills.git";
process.env["GIT_CONFIG_KEY_2"] = gitRewrite;
process.env["GIT_CONFIG_VALUE_2"] = "https://github.com/kucherenko/jscpd.git";
}

/**
Expand Down
2 changes: 1 addition & 1 deletion scripts/import-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Move a top-level version into serialized skill metadata.
*/
function serializedImportedVersion(source: string, document: object, offset: number, version: string) {
const original = jsonRecord.parse(document);
const metadata = normalizedVersionMetadata(original.metadata, version);
const metadata = normalizedVersionMetadata(original["metadata"], version);
const normalized = { ...original, metadata };

Reflect.deleteProperty(normalized, "version");
Expand Down
Loading
Loading