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
8 changes: 4 additions & 4 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ The extension has no activation code. Changes to a snippet catalog take effect t

## Commands

There are no npm scripts, automated tests, or lint configuration. The PR workflow mirrors these local validation commands:
There is no lint configuration. The PR workflow mirrors these local validation commands:

```bash
# Install the locked packaging dependency
npm ci

# Validate all snippet catalogs parse as JSON
node -e "for (const f of ['snippets/snippets.json','snippets/jinja-snippets.json','snippets/django-snippets.json']) JSON.parse(require('fs').readFileSync(f, 'utf8'))"
# Validate snippet catalogs and snippet metadata rules
npm test

# Build a VSIX package (also validates the extension manifest)
npx vsce package
Expand All @@ -37,7 +37,7 @@ npx vsce package

## Pull request checks

PRs should keep the snippet catalogs parseable and the package manifest valid. The CI workflow runs `npm ci`, JSON parsing for all three snippet catalogs, and `npx vsce package` on every pull request.
PRs should keep snippet catalogs and metadata valid, and the package manifest valid. The CI workflow runs `npm ci`, `npm test`, and `npx vsce package` on every pull request.

## Releases

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
run: npm ci

- name: Validate snippet catalogs
run: node -e "for (const f of ['snippets/snippets.json','snippets/jinja-snippets.json','snippets/django-snippets.json']) JSON.parse(require('fs').readFileSync(f, 'utf8'))"
run: npm test

- name: Package extension
run: npx vsce package
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ Found a bug? Want a new snippet? Contributions are welcome!
Every pull request runs a small validation suite on Node.js 20:

1. `npm ci`
2. Parse all snippet catalogs as JSON
2. `npm test` (JSON + snippet metadata validation)
3. `npx vsce package`

If you want to mirror CI locally before opening a PR, run the same commands from the repository root.
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@
"url": "https://github.com/EndlessTrax/python-template-snippets"
},
"homepage": "https://github.com/EndlessTrax/python-template-snippets/blob/master/README.md",
"scripts": {
"test": "node scripts/validate-snippets.js"
},
"devDependencies": {
"@vscode/vsce": "^3.9.2"
}
Expand Down
115 changes: 115 additions & 0 deletions scripts/validate-snippets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
const fs = require("fs");
const path = require("path");

const catalogs = [
{
path: "snippets/snippets.json",
label: "shared",
expectedPrefix: "pt* (excluding ptj-*/ptd-*)",
prefixValidator: (prefix) =>
prefix.startsWith("pt") &&
!prefix.startsWith("ptj-") &&
!prefix.startsWith("ptd-"),
},
{
path: "snippets/jinja-snippets.json",
label: "jinja",
expectedPrefix: "ptj-*",
prefixValidator: (prefix) => prefix.startsWith("ptj-"),
},
{
path: "snippets/django-snippets.json",
label: "django",
expectedPrefix: "ptd-*",
prefixValidator: (prefix) => prefix.startsWith("ptd-"),
},
];

const errors = [];
const seenPrefixes = new Map();

function addError(message) {
errors.push(message);
}

function validateRequiredString(snippet, fieldName) {
return typeof snippet[fieldName] === "string" && snippet[fieldName].trim().length > 0;
}

for (const catalog of catalogs) {
const absolutePath = path.resolve(catalog.path);
let parsedCatalog;

try {
const rawCatalog = fs.readFileSync(absolutePath, "utf8");
parsedCatalog = JSON.parse(rawCatalog);
} catch (error) {
if (error && error.code) {
addError(`${catalog.path}: failed to read file (${error.message})`);
} else {
addError(`${catalog.path}: invalid JSON (${error.message})`);
}
continue;
}
Comment thread
Copilot marked this conversation as resolved.

if (
parsedCatalog === null ||
typeof parsedCatalog !== "object" ||
Array.isArray(parsedCatalog)
) {
addError(`${catalog.path}: root must be a JSON object keyed by snippet name`);
continue;
}

for (const [snippetName, snippet] of Object.entries(parsedCatalog)) {
if (snippet === null || typeof snippet !== "object" || Array.isArray(snippet)) {
addError(`${catalog.path} > "${snippetName}": snippet definition must be an object`);
continue;
}

if (!validateRequiredString(snippet, "prefix")) {
addError(`${catalog.path} > "${snippetName}": missing or invalid "prefix"`);
continue;
}

const prefix = snippet.prefix;

if (!Array.isArray(snippet.body) || snippet.body.length === 0) {
addError(`${catalog.path} > "${snippetName}": missing or invalid "body" (must be a non-empty array)`);
} else if (!snippet.body.every((line) => typeof line === "string")) {
addError(`${catalog.path} > "${snippetName}": invalid "body" (all entries must be strings)`);
}

if (!validateRequiredString(snippet, "description")) {
addError(`${catalog.path} > "${snippetName}": missing or invalid "description"`);
}

if (!catalog.prefixValidator(prefix)) {
addError(
`${catalog.path} > "${snippetName}": invalid prefix "${prefix}" for ${catalog.label} catalog (expected ${catalog.expectedPrefix})`
);
}

const existing = seenPrefixes.get(prefix);
if (existing) {
addError(
`duplicate prefix "${prefix}" found in ${catalog.path} > "${snippetName}" and ${existing.catalogPath} > "${existing.snippetName}"`
);
} else {
seenPrefixes.set(prefix, {
catalogPath: catalog.path,
snippetName,
});
}
}
}

if (errors.length > 0) {
console.error("Snippet validation failed:\n");
errors.forEach((error, index) => {
console.error(`${index + 1}. ${error}`);
});
process.exit(1);
}

console.log(`Snippet validation passed (${catalogs.length} catalogs, ${seenPrefixes.size} prefixes checked).`);