Skip to content

simply-project-setup-core

The backend engine behind a “standardize this Salesforce DX project” command: resolve which features are enabled from CLI flags, a preset, and a project-local config file’s overrides; copy each enabled feature’s template pack into the project; compose .gitignore; and merge each feature’s dependencies and package.json scripts. Full signatures and types are in the API reference.

This package ships no templates, presets, package.json defaults, or project-local config-file format — those are your command’s own opinions, supplied as plain data/callbacks.

Terminal window
npm install @simplysf/simply-project-setup-core

standardizeFiles/writeDependencies expect one subdirectory per feature id under a templatesPath you choose — the directory name is the feature id:

templates/
core/
.editorconfig
dependencies.json # optional — merged into package.json when "core" is included
eslint/
eslint.config.mjs
dependencies.json
gitignore/
base.gitignore # always composed into .gitignore
eslint.gitignore # appended when "eslint" is included

A file that should keep a project-local edit across re-runs contains a # -- START CUSTOMIZATION / # -- END CUSTOMIZATION block; re-running standardizeFiles re-copies everything outside that block and keeps whatever’s inside it from the existing file.

import { resolveSetupConfig, type SetupConfig } from '@simplysf/simply-project-setup-core';
const myConfig = loadMyConfigFile(); // however your command finds/reads/validates its own config file
const baseConfig: SetupConfig = {
include: ['core'],
exclude: [],
add: [],
banned: ['.prettierrc.mjs'],
};
const config = resolveSetupConfig({
flags, // your command's own parsed flags
localOverrides: myConfig?.setup, // e.g. { exclude: ['utam'] } — this package owns no config-file format
baseConfig,
presets: { hrm: ['core', 'eslint', 'prettier', 'jest'] },
booleanFeatures: ['eslint', 'prettier', 'jest'],
dependentFeatures: ['eslint', 'prettier', 'jest'], // any of these implies "package-json"
});

Precedence: baseConfiglocalOverrides.include/localOverrides.exclude → a named preset (if your preset flag matches one), otherwise each booleanFeatures flag toggling its feature on/off.

Copying template packs and composing .gitignore

Section titled “Copying template packs and composing .gitignore”
import { standardizeFiles } from '@simplysf/simply-project-setup-core';
const actions = standardizeFiles({
config,
templatesPath: path.join(import.meta.dirname, '..', 'templates'),
gitignoreHeader: "# Generated by 'myapp project setup'. Do not edit manually.\n\n",
renameFile: (dest) => (dest === '.prettier.config.mjs' ? 'prettier.config.mjs' : dest),
protectedFiles: ['.env'], // create once, never rewrite
jsonMergeFiles: ['.vscode/settings.json', '.myapprc.json'], // deep-merge, existing values win
regexCustomizations: [{ path: 'bin/deploy.sh', pattern: /^TARGET_ORG=(.*)$/m }], // keep one matched span
transformFile: ({ destRelativePath, content }) =>
destRelativePath === '.husky/pre-commit' ? content.replace('REPLACE_WITH_BRANCH_REGEX', myBranchRegex()) : content,
});
// actions: [{ file: '.editorconfig', action: 'CREATE' }, { file: '.vscode/settings.json', action: 'MERGE' }, ...]

A file that already exists is reconciled with its template by whichever of these matches it first: protectedFiles, then regexCustomizations, then jsonMergeFiles, then a # -- START/END CUSTOMIZATION block in the template, else plain overwrite. Pick one strategy per file.

Merging a JSON file instead of overwriting it

Section titled “Merging a JSON file instead of overwriting it”

jsonMergeFiles suits a JSON file the template should seed and extend but never reset — editor settings, a formatter rc file, a tool’s project config. Take a template pack vscode/ containing .vscode/settings.json:

{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"salesforcedx-vscode-core.push-or-deploy-on-save.enabled": false,
"files.exclude": { "**/.sfdx": true, "**/.sf": true }
}

and a project where a developer has already tuned that file:

{
"editor.formatOnSave": false,
"editor.tabSize": 2,
"files.exclude": { "**/.sfdx": true, "**/node_modules": true }
}
import { standardizeFiles } from '@simplysf/simply-project-setup-core';
const actions = standardizeFiles({
config: { include: ['vscode'], exclude: [], add: [] },
templatesPath,
jsonMergeFiles: ['.vscode/settings.json'],
});
// actions: [{ file: '.vscode/settings.json', action: 'MERGE' }]

The file on disk afterwards:

{
"editor.formatOnSave": false,
"editor.tabSize": 2,
"files.exclude": { "**/.sfdx": true, "**/node_modules": true, "**/.sf": true },
"editor.defaultFormatter": "esbenp.prettier-vscode",
"salesforcedx-vscode-core.push-or-deploy-on-save.enabled": false
}
  • The developer’s editor.formatOnSave and editor.tabSize survive: the target wins on any key both sides have, and keys only the target has are left alone.
  • The two settings only the template has are added, after the target’s own keys.
  • files.exclude is merged one level down because both sides hold an object there, so **/.sf is added without dropping **/node_modules.
  • Running the same call again reports no action for the file, since the merge result already matches what’s on disk.

Arrays are the one thing this merge never combines. With a template .vscode/extensions.json of { "recommendations": ["salesforce.salesforcedx-vscode", "esbenp.prettier-vscode"] } and a target of { "recommendations": ["salesforce.salesforcedx-vscode"] }, the target is left exactly as it is: the key exists on both sides, so the target’s array wins whole and the new recommendation is not appended. A file that is essentially one array belongs to the template (no strategy, plain overwrite) or to the project (protectedFiles), not to jsonMergeFiles.

Both files must be strict JSON. A target with // comments, which VS Code tolerates, is reported as an "ERROR" action and left untouched.

Merging dependencies and package.json scripts

Section titled “Merging dependencies and package.json scripts”
import { writeDependencies, standardizePackageJson } from '@simplysf/simply-project-setup-core';
if (config.include.includes('package-json')) {
await writeDependencies({ config, templatesPath });
standardizePackageJson({
config,
defaults: {
private: true,
type: 'module',
scripts: { format: 'prettier --write .', 'test:unit': 'vitest' },
featureScripts: { prettier: ['format'], jest: ['test:unit'] },
},
});
}

Nothing in this package special-cases a feature by name. A step like adding a package’s own name as a file: dependency for a UI-testing feature uses the same PackageJson class the engine itself uses:

import { PackageJson } from '@simplysf/simply-project-setup-core';
if (config.include.includes('utam')) {
const pjson = new PackageJson(projectPath);
const dependencies = pjson.get<Record<string, string>>('dependencies', {});
dependencies[pjson.contents.name] = 'file:';
pjson.write();
}