Signed files
A generated file is regenerated on every run, so a hand edit to it is lost silently, or, with check, fails CI without saying why. Set signed: true to sign the file instead, so populate-files can tell a stale file from an edited one:
import { populateFile } from 'populate-files';
await populateFile({
filePath: './src/version.ts',
content: "export const version = '1.2.3';\n",
signed: true,
});// @generated-signature v1:… Generated file. Do not edit outside of manual sections.
export const version = '1.2.3';The first line holds a generated-signature token: a hash of the rest of the file. It is written with the file's comment syntax, found from its extension (see supported files). Signed content must be a string.
What a run checks
Before overwriting a signed file that exists, populate-files checks it was written by populate-files and not changed since:
| The existing file | Result |
|---|---|
| Has a valid signature | Overwritten, keeping its manual sections. |
| Was edited outside its manual sections | PopulateFilesOwnershipError, reason 'edited'. |
| Has no signature | PopulateFilesOwnershipError, reason 'not-owned': it may be a handwritten file. |
| Has a broken signature or section markers | PopulateFilesOwnershipError, reason 'malformed'. |
These fail with or without check, so an edit fails locally and in CI instead of being lost. Move the change into the generator, or into a manual section, and regenerate. To discard the edit, pass force: true (--force) once.
force is also how to start signing a file that already exists unsigned: the first signed run would otherwise fail with 'not-owned'.
Manual sections
Mark a region that people may edit with manualSection(). Its content is left out of the signature, and kept when the file is regenerated:
import { manualSection } from 'generated-signature';
import { populateFile } from 'populate-files';
const filePath = './src/client.ts';
await populateFile({
filePath,
content: `export class Client {
readonly baseUrl = 'https://api.example.com';
${manualSection({ id: 'methods', indent: ' ', filePath, body: '// Add your own methods here' })}
}
`,
signed: true,
});// @generated-signature v1:… Generated file. Do not edit outside of manual sections.
export class Client {
readonly baseUrl = 'https://api.example.com';
// @manual-start methods
// Add your own methods here
// @manual-end methods
}body is the content until someone edits it. Add generated-signature as a dependency of the generator to import manualSection.
On each run, each section's content is taken from the existing file, matched by id, so the generator can change everything around it. A section the generator no longer writes fails the run with reason 'orphaned-section' if someone wrote anything in it, so their code isn't dropped. Rename it back, move the code, or pass force to drop it.
check compares the result after merging, so a file whose manual sections were edited is still up to date.
Your own header
A generator that already writes a header comment can put the token in it, instead of a separate first line. sign() fills in a token that is already there:
import { signatureToken } from 'generated-signature';
import { populateFile } from 'populate-files';
await populateFile({
filePath: './src/routes.ts',
content: `// Generated by my-router. ${signatureToken()}\nexport const routes = ['/'];\n`,
signed: true,
});Catch edits in the editor
populate-files only notices an edit on its next run. The ESLint rule reports it as soon as the file is saved:
// eslint.config.js
import generatedSignature from 'generated-signature/eslint';
export default [generatedSignature.configs.recommended];Files without comments
JSON can't hold a comment, so it can't be signed: signed: true on a new .json file fails with GeneratedSignatureCommentStyleError. Record it in a manifest instead, which keeps its hash in a separate file.