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.
npm install @simplysf/simply-project-setup-coreLaying out a templates directory
Section titled “Laying out a templates directory”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 includedA 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.
Resolving which features to apply
Section titled “Resolving which features to apply”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: baseConfig → localOverrides.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.formatOnSaveandeditor.tabSizesurvive: 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.excludeis merged one level down because both sides hold an object there, so**/.sfis 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'] }, }, });}Composing a feature-specific step
Section titled “Composing a feature-specific step”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();}