Core Concepts
How GasPackᵐ works — packages, modules, namespaces, versioning, and module-level dependencies.
Packages & Modules
GasPackᵐ organizes code into packages that contain one or more modules. Each module is a self-contained unit with its own API, exported as a global namespace in Google Apps Script.
@company.com/utilities ← package
├── logger → LOGGER_V1 ← module → namespace
├── storage → STORAGE_V1 ← module → namespace
└── crypto → CRYPTO_V1 ← module → namespace | Concept | What it is | Example |
|---|---|---|
| Package | Container for related modules. Versioned with semver. Published as a unit. | @company.com/utilities |
| Module | Individual component with its own version and API. Exported as a namespace. | logger |
| Namespace | The global variable name in Apps Script. Includes a version suffix derived from the module's major version. | LOGGER_V1 |
Why namespaces?
Google Apps Script has no import system. All code shares a single global scope. GasPackᵐ
modules are compiled into IIFE-wrapped namespace globals so they don't collide:
function myScript() {
LOGGER_V1.info("Starting...");
const data = STORAGE_V1.get("key");
const hash = CRYPTO_V1.hash(data);
return hash;
}Versioning Model
GasPackᵐ has three levels of versioning. Understanding them is essential because in Apps Script, namespace references are hardcoded in your code — there's no abstraction layer to hide version changes.
The three levels
| Level | What it controls | Example |
|---|---|---|
| Module version | Individual module's semver. The major number drives the namespace suffix. | logger: 1.5.2 → LOGGER_V1logger: 2.5.2 → LOGGER_V2 |
| Namespace version | Derived from the module's major version. This is what you type in your Apps Script code. | _V1, _V2 |
| Package version | Additive rollup of all module changes. Each module bump adds to the package version: patch → +0.0.1, minor → +0.1.0 (patch resets), major → +1.0.0 (minor & patch reset). When multiple modules are bumped at once, the highest-impact type is applied. | @company.com/utilities@2.3.1 |
What changes require what bumps
| Module change | Namespace | Your code |
|---|---|---|
| 1.2.3 → 1.2.4 (patch) | _V1 (same) | No changes needed |
| 1.2.3 → 1.3.0 (minor) | _V1 (same) | No changes needed |
| 1.2.3 → 2.0.0 (major) | _V1 → _V2 | Find & replace all references |
Example: package evolution
v1.0.0 - Initial release
LOGGER 1.0.0 (LOGGER_V1)
STORAGE 1.0.0 (STORAGE_V1)
v1.1.0 - Logger enhancement
LOGGER 1.1.0 (LOGGER_V1) ← added log levels, namespace unchanged
STORAGE 1.0.0 (STORAGE_V1)
v2.0.0 - Storage breaking change
LOGGER 1.1.0 (LOGGER_V1) ← unchanged
STORAGE 2.0.0 (STORAGE_V2) ← API redesigned, namespace changed When the package bumps to v2.0.0, only STORAGE has a namespace change. Any Apps
Script code that references STORAGE_V1 needs to find-and-replace to STORAGE_V2. All LOGGER_V1 calls remain unchanged — no action required.
For package maintainers whose modules depend on STORAGE, adopting V2 is entirely optional. Their existing code continues to work on the V1
maintenance line. If a maintainer wants to take advantage of the new V2 API, they would update
their dependency declaration, refactor their source code to use STORAGE_V2, and
publish a new version of their own package. See Module Dependencies below for how to declare and manage these cross-package
relationships.
Bump CLI commands
All versioning targets modules — the package version is an additive rollup of module bumps over time. When bumping multiple modules at once, the highest-impact type determines the package bump.
# Bump a specific module
gpm bump logger patch # 1.5.2 → 1.5.3 (LOGGER_V1 stays)
gpm bump logger minor # 1.5.2 → 1.6.0 (LOGGER_V1 stays)
gpm bump logger major # 1.5.2 → 2.0.0 (LOGGER_V1 → LOGGER_V2)
# Bump multiple modules at once
gpm bump logger patch storage minor
# Show current module versions
gpm bumpModule-Level Dependencies
Modules can declare exactly which modules from other packages they depend on. This powers selective installation — when a developer installs your package, only the needed dependency modules are pulled in, not entire packages.
How it works
Imagine two packages in the registry, each with several modules:
@company.com/logger @company.com/storage
├── console (LOGGER_CONSOLE_V1) ├── script (STORAGE_SCRIPT_V1)
├── sheet (LOGGER_SHEET_V1) ├── user (STORAGE_USER_V1)
└── formatter (LOGGER_FORMATTER_V1) ├── cache (STORAGE_CACHE_V1)
└── memory (STORAGE_MEMORY_V1) Now you're building a package called @you.gmail.com/data-tools with a sync module. Your sync code logs results to a spreadsheet using LOGGER_SHEET_V1 and
saves state using STORAGE_SCRIPT_V1. It doesn't use console logging, the
formatter, user storage, cache, or memory storage.
Step 1: Declare what your module depends on
gpm dep add sync --dep @company.com/logger/sheet
gpm dep add sync --dep @company.com/storage/script This adds the dependencies to your gpm.json:
{
"modules": {
"sync": {
"namespace": "DATA_SYNC",
"entryPoint": "src/sync/index.js",
"dependencies": [
"@company.com/logger/sheet",
"@company.com/storage/script"
]
}
}
} Step 2: Build and publish
gpm build --strict-deps # validates deps match your code
gpm publish --access public The dependency declarations are included in the published package metadata and stored in the registry.
Step 3: A developer installs your package
gpm install @you.gmail.com/data-tools The registry sees that sync depends on logger/sheet and storage/script. It installs only those two modules — not all
3 logger modules and not all 4 storage modules. The developer's project stays lean.
CLI commands
# Add a dependency by canonical ref (@scope/package/module)
gpm dep add sync --dep @company.com/logger/sheet
# Add by namespace (resolved via registry lookup)
gpm dep add sync --dep-ns LOGGER_SHEET_V1
# List dependencies for a module
gpm dep list sync
# Scan compiled code and detect missing deps
gpm dep detect sync
# Remove a dependency
gpm dep remove sync --dep @company.com/logger/sheet Build validation
Use gpm build --strict-deps to turn dependency warnings into errors. The build scans
compiled code for namespace references and cross-checks against declared dependencies.
Domain Scoping
Every package is scoped to a domain: @domain/package-name. This prevents naming
conflicts and ties packages to verified identities.
Personal domains
Every user automatically gets a personal domain derived from their Google account email. Available immediately on login.
@jane.doe.gmail.com/my-utils
@joe.doe.company.com/data-tools Workspace domains Coming Soon
Teams and Enterprise accounts can register their Google Workspace domain as a package scope by adding a verification code as a DNS TXT entry. This enables team-wide publishing under a shared domain.
@acme-corp.com/billing-utils
@velocitydog.com/internal-tools Workspace domains unlock organizational capabilities:
- Teams — publish public and private packages under the shared workspace domain
- Enterprise — all Teams features plus self-hosted private registry for on-premise hosting
| Personal Domain | Workspace Domain Coming Soon | |
|---|---|---|
| Source | Google account email | Google Workspace + DNS verification |
| Setup | Automatic on login | Admin registers domain via DNS TXT record |
| Who can publish | Account owner only | Authorized workspace members |
| Public packages | Yes | Yes |
| Private packages | Pro account | Yes (Teams & Enterprise) |
| Self-hosted registry | No | Enterprise only |
Managing Major Version Changes
As a package maintainer, you have a real impact on the developers who depend on your work. A
module major bump changes the namespace — STORAGE_V1 becomes STORAGE_V2 — and every project and dependent package using that namespace needs
to update. With thoughtful API design, you can make these changes rare and well-managed.
Design for longevity
The best major bump is the one you never have to make. These patterns help you evolve your API without breaking existing code:
| Pattern | Why it works |
|---|---|
| Add new functions alongside existing ones | Existing callers are unaffected — this is a minor bump |
| Use optional parameters to extend signatures | Adding a trailing optional parameter never breaks existing calls |
| Accept option objects instead of positional args | New properties can always be added safely |
| Deprecate before removing with a logged warning | Gives developers time to migrate before the function disappears |
When a major change is the right call
Sometimes a breaking change is necessary — a fundamental redesign, a security fix that changes return types, or removing an API that was a mistake. When this happens, the best practice is to maintain both version lines:
@company.com/utilities
├── v1.x (STORAGE_V1) ← maintenance branch: security patches, critical fixes
└── v2.x (STORAGE_V2) ← active branch: new features land here Both remain available in the registry. Projects and packages on V1 continue receiving patches without being forced to refactor. Developers can migrate to V2 on their own timeline.
Keep a maintenance branch
After publishing a major bump, keep a Git branch for the previous major version (e.g., v1-maintenance). Apply security patches and critical bug fixes to both branches, and publish patch
releases for each. This ensures developers who haven't migrated yet still receive important
fixes.
Why this matters
In Apps Script, namespace references are hardcoded globals — there's no import system to
abstract them away. A project may depend on several packages that all use STORAGE_V1. Upgrading to _V2 requires every one of those package maintainers to release compatible
versions first. By maintaining both version lines and designing for backward compatibility, you
make the ecosystem more resilient for everyone.