Introduce TomlFile abstraction for TOML file I/O#6943
Conversation
31f057f to
80bf13d
Compare
Coverage report
Test suite run success3812 tests passing in 1456 suites. Report generated by 🧪jest coverage report action from a4d144a |
|
We detected some changes at Caution DO NOT create changesets for features which you do not wish to be included in the public changelog of the next CLI release. |
|
🤖 Code Review · #projects-dev-ai for questions ✅ Complete - No issues 📋 History✅ 1 findings → ✅ No issues → ✅ 1 findings → ✅ No issues → ✅ No issues |
|
Tophatted, everything looked good with no errors or weird behaviour Scenarios tested (in cli repo: pnpm shopify app xxxxx --path ):
Result:
|
| "./node/toml": { | ||
| "node": "./dist/public/node/toml/index.js", | ||
| "types": "./dist/public/node/toml/index.d.ts" | ||
| }, |
There was a problem hiding this comment.
you shouldn't need this, is already covered by the wildcard below :thinking_face:
There was a problem hiding this comment.
hmm, IIRC imports from the new toml dir didn't work without this
There was a problem hiding this comment.
yea this is necessary to import from `@shopify/cli-kit/node/toml which the next pr does
There was a problem hiding this comment.
all other modules work with the wildcard export, for instance /node/crypto or /node/path
I guess the issue is having an index file, can we do this without an index file and expose a toml.ts directly?
There was a problem hiding this comment.
node/crypto and node/path both point to files in the public/node root, not subfolders. i put the toml files in their own subdir since it's a domain with multiple file instances and i didn't want to keep expanding the root, which is already very large.
There was a problem hiding this comment.
i guess we could do re-exports from the root, but i don't see much value in it
There was a problem hiding this comment.
Just worried of having to manually export things here, can grow a lot very quickly, let's leave it for now and we can revisit if this grows too much.
There was a problem hiding this comment.
Just curious -- why? The cost of change is basically zero to update an import alias/etc. I personally prefer more structured directories, as it's easier to visually navigate.
There was a problem hiding this comment.
just a preference of having automatic exports (or maybe an index file) and not needing to update the package.json for every folder/file we add to the project
| const raw = await readFile(this.path) | ||
| const updated = updateTomlValues(raw, [[keys, undefined]]) | ||
| await writeFile(this.path, updated) | ||
| this.content = this.decode(updated) |
There was a problem hiding this comment.
Should we decode before writing to disk to ensure validity?
There was a problem hiding this comment.
I thought about this and didn't because we're effectively testing @shopify/toml-patch for that. but on reflection I think the hardening is better in any case. updating
Adds a general-purpose TomlFile class in cli-kit with read/patch/remove/ replace/transformRaw methods. Restructures toml utilities into a toml/ directory with codec (internal encode/decode) and index (public JsonMapType export) modules. Only TomlFile and JsonMapType are publicly exported. No callsite migrations in this commit — purely additive. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
e13bc32 to
a4d144a
Compare
Differences in type declarationsWe detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:
New type declarationspackages/cli-kit/dist/public/node/toml/codec.d.tsimport { JsonMap } from '../../../private/common/json.js';
export type JsonMapType = JsonMap;
/**
* Given a TOML string, it returns a JSON object.
*
* @param input - TOML string.
* @returns JSON object.
*/
export declare function decodeToml(input: string): JsonMapType;
/**
* Given a JSON object, it returns a TOML string.
*
* @param content - JSON object.
* @returns TOML string.
*/
export declare function encodeToml(content: JsonMap | object): string;
packages/cli-kit/dist/public/node/toml/index.d.tsexport { decodeToml, encodeToml } from './codec.js';
export type { JsonMapType } from './codec.js';
packages/cli-kit/dist/public/node/toml/toml-file.d.tsimport { JsonMapType } from './codec.js';
/**
* Thrown when a TOML file cannot be parsed. Includes the file path for context.
*/
export declare class TomlParseError extends Error {
readonly path: string;
constructor(path: string, cause: Error);
}
/**
* General-purpose TOML file abstraction.
*
* Provides a unified interface for reading, patching, removing keys from, and replacing
* the content of TOML files on disk.
*
* - `read` populates content from disk
* - `patch` does surgical WASM-based edits (preserves comments and formatting)
* - `remove` deletes a key by dotted path (preserves comments and formatting)
* - `replace` does a full re-serialization (comments and formatting are NOT preserved).
* - `transformRaw` applies a function to the raw TOML string on disk.
*/
export declare class TomlFile {
/**
* Read and parse a TOML file from disk. Throws if the file doesn't exist or contains invalid TOML.
* Parse errors are wrapped in {@link TomlParseError} with the file path for context.
*
* @param path - Absolute path to the TOML file.
* @returns A TomlFile instance with parsed content.
*/
static read(path: string): Promise<TomlFile>;
readonly path: string;
content: JsonMapType;
constructor(path: string, content: JsonMapType);
/**
* Surgically patch values in the TOML file, preserving comments and formatting.
*
* Accepts a nested object whose leaf values are set in the TOML. Intermediate tables are
* created automatically. Setting a leaf to `undefined` removes it (use `remove()` for a
* clearer API when deleting keys).
*
* @example
* ```ts
* await file.patch({build: {dev_store_url: 'my-store.myshopify.com'}})
* await file.patch({application_url: 'https://example.com', auth: {redirect_urls: ['...']}})
* ```
*/
patch(changes: {
[key: string]: unknown;
}): Promise<void>;
/**
* Remove a key from the TOML file by dotted path, preserving comments and formatting.
*
* @param keyPath - Dotted key path to remove (e.g. 'build.include_config_on_deploy').
* @example
* ```ts
* await file.remove('build.include_config_on_deploy')
* ```
*/
remove(keyPath: string): Promise<void>;
/**
* Replace the entire file content. The file is fully re-serialized — comments and formatting
* are NOT preserved.
*
* @param content - The new content to write.
* @example
* ```ts
* await file.replace({client_id: 'abc', name: 'My App'})
* ```
*/
replace(content: JsonMapType): Promise<void>;
/**
* Transform the raw TOML string on disk. Reads the file, applies the transform function
* to the raw text, writes back, and re-parses to keep `content` in sync.
*
* Use this for text-level operations that can't be expressed as structured edits —
* e.g. Injecting comments or positional insertion of keys in arrays-of-tables.
* Subsequent `patch()` calls will preserve any comments added this way.
*
* @param transform - A function that receives the raw TOML string and returns the modified string.
* @example
* ```ts
* await file.transformRaw((raw) => `# Header comment\n${raw}`)
* ```
*/
transformRaw(transform: (raw: string) => string): Promise<void>;
private decode;
}
Existing type declarationsWe found no diffs with existing type declarations |

Summary
Adds a general-purpose
TomlFileclass incli-kitwithread/patch/remove/replace/transformRawmethods. Restructures TOML utilities into atoml/directory with internal codec module (encodeToml/decodeToml).Why this change exists
Today, the concerns of "parse a file", "validate a file", "validate Shopify domain config", and "transform file contents to domain config" are interwoven and dispersed across callsites. This makes it very difficult to instrument changes or drive consistency in the codebase. This pr introduces a TOML file abstraction that owns a specific slice of jobs: read/write TOML, and check whether that TOML is valid syntax. It does not care about Shopify domain knowledge or validation. This is the first step in providing a clearer lifecycle of config read/writes, and separation between transform/validate/IO jobs.
Current problems
The responsibility for "write a TOML file" is split between the function that knew the content and the function that knew the path.
With this PR:
every file in the app package imports.
Test plan