Run scripts and keep data

Your app's command-line scripts, the data they import and your design files go in the custom/ folder at the root. A script that talks to Firebase gets the project, the install's database and the credentials from runAdminScript, so it never handles credentials itself.

The custom folder

custom/ at the root of the repository belongs to your app. Arc CMS ships it with only a README and never changes it, and npm run check:core counts everything in it as custom. Nothing in it is bundled into the site. A suggested layout, none of it required:

FolderFor
custom/scripts/Scripts you run from the command line, such as data imports and one-off fixes
custom/data/Source files those scripts read, such as CSV or JSON exports
custom/design/Design notes, mockups and other working files

Scripts that need packages of their own can have a custom/package.json: run npm install in custom/. Git ignores node_modules at any depth, so its packages are never committed.

The app's developer documentation goes in docs/custom/, next to these docs. Code that runs in the site or in Cloud Functions goes in src/custom/ and functions/src/custom/ (Use the custom space).

Write a script

  1. Create the script, such as custom/scripts/import-words.mjs, and hand your code to runAdminScript from scripts/arc-admin-script.mjs:

    import { readFileSync } from 'node:fs';
    import { runAdminScript } from '../../scripts/arc-admin-script.mjs';
    
    runAdminScript(async ({ db, args }) => {
      const words = JSON.parse(readFileSync(args[0] ?? 'custom/data/words.json', 'utf8'));
      const batch = db.batch();
      for (const word of words) batch.set(db.collection('words').doc(word.id), word);
      await batch.commit();
      console.log(`Imported ${words.length} words`);
    });
  2. Sign in to Firebase once, if you have not:

    firebase login
  3. Run it from the root of the repository. Before your code runs, it prints the project, the database and the credentials it uses:

    node custom/scripts/import-words.mjs custom/data/words.json

Which project

Run withProject
No flagThe default alias in .firebaserc (the dev project), or GCLOUD_PROJECT when it is set
--prodThe production alias
--project=ALIAS_OR_IDAny alias in .firebaserc, or a project id

These flags are taken out; every other argument reaches your script in args, in order.

What your script gets

NameWhat it is
dbFirestore for the install's own database: the one in ARC_DATABASE_ID, read from functions/.env and functions/.env.PROJECT_ID the way a deploy reads them
authFirebase Authentication, to read or change users
storageCloud Storage. storage.bucket() is the bucket your environment file names for this project
appThe Admin SDK app behind them
projectId, databaseIdWhere the script runs
prodWhether it runs against the production alias, for a script that should ask before changing live data
argsThe command-line arguments left after the flags above

Use db rather than calling getFirestore() yourself. A project can hold more than one Arc CMS database (Share a Firebase project), and getFirestore() always opens (default), which may belong to another install.

Credentials

When GOOGLE_APPLICATION_CREDENTIALS is set, such as to a service account key, the script signs in with it. Otherwise it uses your Firebase CLI login through a temporary credentials file, which it deletes when the script ends, fails or is stopped with Ctrl+C. With neither, it stops and says to run firebase login.

When your code throws, the error is printed and the script exits with 1. npm run seed:dev and npm run forms:remove-referrals run the same way.

Test a script

Files named custom/**/*.spec.ts run with the rest of the suite in npm run test. Keep the logic of a script in a function you can import, and test that function with sample data rather than a real project.