Skip to content

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:

ts
import { 
populateFile
} from 'populate-files';
await
populateFile
({
filePath
: './src/version.ts',
content
: "export const version = '1.2.3';\n",
signed
: true,
});
ts
// @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 fileResult
Has a valid signatureOverwritten, keeping its manual sections.
Was edited outside its manual sectionsPopulateFilesOwnershipError, reason 'edited'.
Has no signaturePopulateFilesOwnershipError, reason 'not-owned': it may be a handwritten file.
Has a broken signature or section markersPopulateFilesOwnershipError, 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:

ts
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,
});
ts
// @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:

ts
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:

js
// 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.