@aspruyt/json-config-sync 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -9
- package/dist/config-formatter.d.ts +3 -3
- package/dist/config-formatter.js +17 -4
- package/dist/config-normalizer.js +24 -8
- package/dist/config-validator.js +51 -10
- package/dist/config.d.ts +4 -3
- package/dist/env.d.ts +13 -0
- package/dist/env.js +28 -0
- package/dist/merge.d.ts +11 -0
- package/dist/merge.js +42 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@aspruyt/json-config-sync)
|
|
5
5
|
[](https://www.npmjs.com/package/@aspruyt/json-config-sync)
|
|
6
6
|
|
|
7
|
-
A CLI tool that syncs JSON or
|
|
7
|
+
A CLI tool that syncs JSON, YAML, or text configuration files across multiple GitHub and Azure DevOps repositories by creating pull requests. Output format is automatically detected from the target filename extension (`.json` → JSON, `.yaml`/`.yml` → YAML, other → text).
|
|
8
8
|
|
|
9
9
|
## Table of Contents
|
|
10
10
|
|
|
@@ -60,7 +60,8 @@ json-config-sync --config ./config.yaml
|
|
|
60
60
|
## Features
|
|
61
61
|
|
|
62
62
|
- **Multi-File Sync** - Sync multiple config files in a single run
|
|
63
|
-
- **
|
|
63
|
+
- **Multi-Format Output** - JSON, YAML, or plain text based on filename extension
|
|
64
|
+
- **Text Files** - Sync `.gitignore`, `.markdownlintignore`, etc. with string or lines array
|
|
64
65
|
- **Content Inheritance** - Define base config once, override per-repo as needed
|
|
65
66
|
- **Multi-Repo Targeting** - Apply same config to multiple repos with array syntax
|
|
66
67
|
- **Environment Variables** - Use `${VAR}` syntax for dynamic values
|
|
@@ -167,13 +168,13 @@ repos: # List of repositories
|
|
|
167
168
|
|
|
168
169
|
### Per-File Fields
|
|
169
170
|
|
|
170
|
-
| Field | Description
|
|
171
|
-
| --------------- |
|
|
172
|
-
| `content` | Base config
|
|
173
|
-
| `mergeStrategy` |
|
|
174
|
-
| `createOnly` | If `true`, only create file if it doesn't exist
|
|
175
|
-
| `header` | Comment line(s) at top of YAML files (string or array)
|
|
176
|
-
| `schemaUrl` | Adds `# yaml-language-server: $schema=<url>` to YAML files
|
|
171
|
+
| Field | Description | Required |
|
|
172
|
+
| --------------- | ------------------------------------------------------------------------------------------------ | -------- |
|
|
173
|
+
| `content` | Base config: object for JSON/YAML files, string or string[] for text files (omit for empty file) | No |
|
|
174
|
+
| `mergeStrategy` | Merge strategy: `replace`, `append`, `prepend` (for arrays and text lines) | No |
|
|
175
|
+
| `createOnly` | If `true`, only create file if it doesn't exist | No |
|
|
176
|
+
| `header` | Comment line(s) at top of YAML files (string or array) | No |
|
|
177
|
+
| `schemaUrl` | Adds `# yaml-language-server: $schema=<url>` to YAML files | No |
|
|
177
178
|
|
|
178
179
|
### Per-Repo Fields
|
|
179
180
|
|
|
@@ -450,6 +451,41 @@ scan:
|
|
|
450
451
|
|
|
451
452
|
**Note:** `header` and `schemaUrl` only apply to YAML output files (`.yaml`, `.yml`). They are ignored for JSON files.
|
|
452
453
|
|
|
454
|
+
### Text Files
|
|
455
|
+
|
|
456
|
+
Sync text files like `.gitignore`, `.markdownlintignore`, or `.env.example` using string or lines array content:
|
|
457
|
+
|
|
458
|
+
```yaml
|
|
459
|
+
files:
|
|
460
|
+
# String content (multiline text)
|
|
461
|
+
.markdownlintignore:
|
|
462
|
+
createOnly: true
|
|
463
|
+
content: |-
|
|
464
|
+
# Claude Code generated files
|
|
465
|
+
.claude/
|
|
466
|
+
|
|
467
|
+
# Lines array with merge strategy
|
|
468
|
+
.gitignore:
|
|
469
|
+
mergeStrategy: append
|
|
470
|
+
content:
|
|
471
|
+
- "node_modules/"
|
|
472
|
+
- "dist/"
|
|
473
|
+
|
|
474
|
+
repos:
|
|
475
|
+
- git: git@github.com:org/repo.git
|
|
476
|
+
files:
|
|
477
|
+
.gitignore:
|
|
478
|
+
content:
|
|
479
|
+
- "coverage/" # Appended to base lines
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
**Content Types:**
|
|
483
|
+
|
|
484
|
+
- **String content** (`content: |-`) - Direct text output with environment variable interpolation. Merging always replaces the base.
|
|
485
|
+
- **Lines array** (`content: ['line1', 'line2']`) - Each line joined with newlines. Supports merge strategies (`append`, `prepend`, `replace`).
|
|
486
|
+
|
|
487
|
+
**Validation:** JSON/YAML file extensions (`.json`, `.yaml`, `.yml`) require object content. Other extensions require string or string[] content.
|
|
488
|
+
|
|
453
489
|
## Supported Git URL Formats
|
|
454
490
|
|
|
455
491
|
### GitHub
|
|
@@ -11,7 +11,7 @@ export interface ConvertOptions {
|
|
|
11
11
|
*/
|
|
12
12
|
export declare function detectOutputFormat(fileName: string): OutputFormat;
|
|
13
13
|
/**
|
|
14
|
-
* Converts content
|
|
15
|
-
* Handles null content (empty files) and
|
|
14
|
+
* Converts content to string in the appropriate format.
|
|
15
|
+
* Handles null content (empty files), text content (string/string[]), and object content (JSON/YAML).
|
|
16
16
|
*/
|
|
17
|
-
export declare function convertContentToString(content: Record<string, unknown> | null, fileName: string, options?: ConvertOptions): string;
|
|
17
|
+
export declare function convertContentToString(content: Record<string, unknown> | string | string[] | null, fileName: string, options?: ConvertOptions): string;
|
package/dist/config-formatter.js
CHANGED
|
@@ -47,13 +47,13 @@ function buildCommentOnlyYaml(header, schemaUrl) {
|
|
|
47
47
|
return lines.join("\n") + "\n";
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
|
-
* Converts content
|
|
51
|
-
* Handles null content (empty files) and
|
|
50
|
+
* Converts content to string in the appropriate format.
|
|
51
|
+
* Handles null content (empty files), text content (string/string[]), and object content (JSON/YAML).
|
|
52
52
|
*/
|
|
53
53
|
export function convertContentToString(content, fileName, options) {
|
|
54
|
-
const format = detectOutputFormat(fileName);
|
|
55
54
|
// Handle empty file case
|
|
56
55
|
if (content === null) {
|
|
56
|
+
const format = detectOutputFormat(fileName);
|
|
57
57
|
if (format === "yaml" && options) {
|
|
58
58
|
const commentOnly = buildCommentOnlyYaml(options.header, options.schemaUrl);
|
|
59
59
|
if (commentOnly) {
|
|
@@ -62,6 +62,19 @@ export function convertContentToString(content, fileName, options) {
|
|
|
62
62
|
}
|
|
63
63
|
return "";
|
|
64
64
|
}
|
|
65
|
+
// Handle string content (text file)
|
|
66
|
+
if (typeof content === "string") {
|
|
67
|
+
// Ensure trailing newline for text files
|
|
68
|
+
return content.endsWith("\n") ? content : content + "\n";
|
|
69
|
+
}
|
|
70
|
+
// Handle string[] content (text file with lines)
|
|
71
|
+
if (Array.isArray(content)) {
|
|
72
|
+
// Join lines with newlines and ensure trailing newline
|
|
73
|
+
const text = content.join("\n");
|
|
74
|
+
return text.length > 0 ? text + "\n" : "";
|
|
75
|
+
}
|
|
76
|
+
// Handle object content (JSON/YAML)
|
|
77
|
+
const format = detectOutputFormat(fileName);
|
|
65
78
|
if (format === "yaml") {
|
|
66
79
|
// Use Document API for YAML to support comments
|
|
67
80
|
const doc = new Document(content);
|
|
@@ -75,5 +88,5 @@ export function convertContentToString(content, fileName, options) {
|
|
|
75
88
|
return stringify(doc, { indent: 2 });
|
|
76
89
|
}
|
|
77
90
|
// JSON format - comments not supported, ignore header/schemaUrl
|
|
78
|
-
return JSON.stringify(content, null, 2);
|
|
91
|
+
return JSON.stringify(content, null, 2) + "\n";
|
|
79
92
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { deepMerge, stripMergeDirectives, createMergeContext, } from "./merge.js";
|
|
2
|
-
import {
|
|
1
|
+
import { deepMerge, stripMergeDirectives, createMergeContext, isTextContent, mergeTextContent, } from "./merge.js";
|
|
2
|
+
import { interpolateContent } from "./env.js";
|
|
3
3
|
/**
|
|
4
4
|
* Normalizes header to array format.
|
|
5
5
|
*/
|
|
@@ -38,6 +38,10 @@ export function normalizeConfig(raw) {
|
|
|
38
38
|
if (repoOverride.content === undefined) {
|
|
39
39
|
mergedContent = null;
|
|
40
40
|
}
|
|
41
|
+
else if (isTextContent(repoOverride.content)) {
|
|
42
|
+
// Text content: use as-is (no merge directives to strip)
|
|
43
|
+
mergedContent = structuredClone(repoOverride.content);
|
|
44
|
+
}
|
|
41
45
|
else {
|
|
42
46
|
mergedContent = stripMergeDirectives(structuredClone(repoOverride.content));
|
|
43
47
|
}
|
|
@@ -45,7 +49,12 @@ export function normalizeConfig(raw) {
|
|
|
45
49
|
else if (fileConfig.content === undefined) {
|
|
46
50
|
// Root file has no content = empty file (unless repo provides content)
|
|
47
51
|
if (repoOverride?.content) {
|
|
48
|
-
|
|
52
|
+
if (isTextContent(repoOverride.content)) {
|
|
53
|
+
mergedContent = structuredClone(repoOverride.content);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
mergedContent = stripMergeDirectives(structuredClone(repoOverride.content));
|
|
57
|
+
}
|
|
49
58
|
}
|
|
50
59
|
else {
|
|
51
60
|
mergedContent = null;
|
|
@@ -56,14 +65,21 @@ export function normalizeConfig(raw) {
|
|
|
56
65
|
mergedContent = structuredClone(fileConfig.content);
|
|
57
66
|
}
|
|
58
67
|
else {
|
|
59
|
-
// Merge mode:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
// Merge mode: handle text vs object content
|
|
69
|
+
if (isTextContent(fileConfig.content)) {
|
|
70
|
+
// Text content merging
|
|
71
|
+
mergedContent = mergeTextContent(fileConfig.content, repoOverride.content, fileStrategy);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// Object content: deep merge file base + repo overlay
|
|
75
|
+
const ctx = createMergeContext(fileStrategy);
|
|
76
|
+
mergedContent = deepMerge(structuredClone(fileConfig.content), repoOverride.content, ctx);
|
|
77
|
+
mergedContent = stripMergeDirectives(mergedContent);
|
|
78
|
+
}
|
|
63
79
|
}
|
|
64
80
|
// Step 4: Interpolate env vars (only if content exists)
|
|
65
81
|
if (mergedContent !== null) {
|
|
66
|
-
mergedContent =
|
|
82
|
+
mergedContent = interpolateContent(mergedContent, { strict: true });
|
|
67
83
|
}
|
|
68
84
|
// Resolve fields: per-repo overrides root level
|
|
69
85
|
const createOnly = repoOverride?.createOnly ?? fileConfig.createOnly;
|
package/dist/config-validator.js
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import { isAbsolute } from "node:path";
|
|
2
2
|
const VALID_STRATEGIES = ["replace", "append", "prepend"];
|
|
3
|
+
/**
|
|
4
|
+
* Check if content is text type (string or string[]).
|
|
5
|
+
*/
|
|
6
|
+
function isTextContent(content) {
|
|
7
|
+
return (typeof content === "string" ||
|
|
8
|
+
(Array.isArray(content) &&
|
|
9
|
+
content.every((item) => typeof item === "string")));
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Check if content is object type (for JSON/YAML output).
|
|
13
|
+
*/
|
|
14
|
+
function isObjectContent(content) {
|
|
15
|
+
return (typeof content === "object" && content !== null && !Array.isArray(content));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Check if file extension is for structured output (JSON/YAML).
|
|
19
|
+
*/
|
|
20
|
+
function isStructuredFileExtension(fileName) {
|
|
21
|
+
const ext = fileName.toLowerCase().split(".").pop();
|
|
22
|
+
return ext === "json" || ext === "yaml" || ext === "yml";
|
|
23
|
+
}
|
|
3
24
|
/**
|
|
4
25
|
* Validates raw config structure before normalization.
|
|
5
26
|
* @throws Error if validation fails
|
|
@@ -19,11 +40,21 @@ export function validateRawConfig(config) {
|
|
|
19
40
|
if (!fileConfig || typeof fileConfig !== "object") {
|
|
20
41
|
throw new Error(`File '${fileName}' must have a configuration object`);
|
|
21
42
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
43
|
+
// Validate content type
|
|
44
|
+
if (fileConfig.content !== undefined) {
|
|
45
|
+
const hasText = isTextContent(fileConfig.content);
|
|
46
|
+
const hasObject = isObjectContent(fileConfig.content);
|
|
47
|
+
if (!hasText && !hasObject) {
|
|
48
|
+
throw new Error(`File '${fileName}' content must be an object, string, or array of strings`);
|
|
49
|
+
}
|
|
50
|
+
// Validate content type matches file extension
|
|
51
|
+
const isStructured = isStructuredFileExtension(fileName);
|
|
52
|
+
if (isStructured && hasText) {
|
|
53
|
+
throw new Error(`File '${fileName}' has JSON/YAML extension but string content. Use object content for structured files.`);
|
|
54
|
+
}
|
|
55
|
+
if (!isStructured && hasObject) {
|
|
56
|
+
throw new Error(`File '${fileName}' has text extension but object content. Use string or string[] for text files, or use .json/.yaml/.yml extension.`);
|
|
57
|
+
}
|
|
27
58
|
}
|
|
28
59
|
if (fileConfig.mergeStrategy !== undefined &&
|
|
29
60
|
!VALID_STRATEGIES.includes(fileConfig.mergeStrategy)) {
|
|
@@ -75,11 +106,21 @@ export function validateRawConfig(config) {
|
|
|
75
106
|
if (fileOverride.override && !fileOverride.content) {
|
|
76
107
|
throw new Error(`Repo ${getGitDisplayName(repo.git)} has override: true for file '${fileName}' but no content defined`);
|
|
77
108
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
109
|
+
// Validate content type
|
|
110
|
+
if (fileOverride.content !== undefined) {
|
|
111
|
+
const hasText = isTextContent(fileOverride.content);
|
|
112
|
+
const hasObject = isObjectContent(fileOverride.content);
|
|
113
|
+
if (!hasText && !hasObject) {
|
|
114
|
+
throw new Error(`Repo at index ${i}: file '${fileName}' content must be an object, string, or array of strings`);
|
|
115
|
+
}
|
|
116
|
+
// Validate content type matches file extension
|
|
117
|
+
const isStructured = isStructuredFileExtension(fileName);
|
|
118
|
+
if (isStructured && hasText) {
|
|
119
|
+
throw new Error(`Repo at index ${i}: file '${fileName}' has JSON/YAML extension but string content. Use object content for structured files.`);
|
|
120
|
+
}
|
|
121
|
+
if (!isStructured && hasObject) {
|
|
122
|
+
throw new Error(`Repo at index ${i}: file '${fileName}' has text extension but object content. Use string or string[] for text files, or use .json/.yaml/.yml extension.`);
|
|
123
|
+
}
|
|
83
124
|
}
|
|
84
125
|
if (fileOverride.createOnly !== undefined &&
|
|
85
126
|
typeof fileOverride.createOnly !== "boolean") {
|
package/dist/config.d.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import type { ArrayMergeStrategy } from "./merge.js";
|
|
2
2
|
export { convertContentToString } from "./config-formatter.js";
|
|
3
|
+
export type ContentValue = Record<string, unknown> | string | string[];
|
|
3
4
|
export interface RawFileConfig {
|
|
4
|
-
content?:
|
|
5
|
+
content?: ContentValue;
|
|
5
6
|
mergeStrategy?: ArrayMergeStrategy;
|
|
6
7
|
createOnly?: boolean;
|
|
7
8
|
header?: string | string[];
|
|
8
9
|
schemaUrl?: string;
|
|
9
10
|
}
|
|
10
11
|
export interface RawRepoFileOverride {
|
|
11
|
-
content?:
|
|
12
|
+
content?: ContentValue;
|
|
12
13
|
override?: boolean;
|
|
13
14
|
createOnly?: boolean;
|
|
14
15
|
header?: string | string[];
|
|
@@ -24,7 +25,7 @@ export interface RawConfig {
|
|
|
24
25
|
}
|
|
25
26
|
export interface FileContent {
|
|
26
27
|
fileName: string;
|
|
27
|
-
content:
|
|
28
|
+
content: ContentValue | null;
|
|
28
29
|
createOnly?: boolean;
|
|
29
30
|
header?: string[];
|
|
30
31
|
schemaUrl?: string;
|
package/dist/env.d.ts
CHANGED
|
@@ -22,3 +22,16 @@ export interface EnvInterpolationOptions {
|
|
|
22
22
|
* @returns A new object with interpolated values
|
|
23
23
|
*/
|
|
24
24
|
export declare function interpolateEnvVars(json: Record<string, unknown>, options?: EnvInterpolationOptions): Record<string, unknown>;
|
|
25
|
+
/**
|
|
26
|
+
* Interpolate environment variables in a string.
|
|
27
|
+
*/
|
|
28
|
+
export declare function interpolateEnvVarsInString(value: string, options?: EnvInterpolationOptions): string;
|
|
29
|
+
/**
|
|
30
|
+
* Interpolate environment variables in an array of strings.
|
|
31
|
+
*/
|
|
32
|
+
export declare function interpolateEnvVarsInLines(lines: string[], options?: EnvInterpolationOptions): string[];
|
|
33
|
+
/**
|
|
34
|
+
* Interpolate environment variables in content of any supported type.
|
|
35
|
+
* Handles objects, strings, and string arrays.
|
|
36
|
+
*/
|
|
37
|
+
export declare function interpolateContent(content: Record<string, unknown> | string | string[], options?: EnvInterpolationOptions): Record<string, unknown> | string | string[];
|
package/dist/env.js
CHANGED
|
@@ -86,3 +86,31 @@ function processValue(value, options) {
|
|
|
86
86
|
export function interpolateEnvVars(json, options = DEFAULT_OPTIONS) {
|
|
87
87
|
return processValue(json, options);
|
|
88
88
|
}
|
|
89
|
+
// =============================================================================
|
|
90
|
+
// Text Content Interpolation
|
|
91
|
+
// =============================================================================
|
|
92
|
+
/**
|
|
93
|
+
* Interpolate environment variables in a string.
|
|
94
|
+
*/
|
|
95
|
+
export function interpolateEnvVarsInString(value, options = DEFAULT_OPTIONS) {
|
|
96
|
+
return processString(value, options);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Interpolate environment variables in an array of strings.
|
|
100
|
+
*/
|
|
101
|
+
export function interpolateEnvVarsInLines(lines, options = DEFAULT_OPTIONS) {
|
|
102
|
+
return lines.map((line) => processString(line, options));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Interpolate environment variables in content of any supported type.
|
|
106
|
+
* Handles objects, strings, and string arrays.
|
|
107
|
+
*/
|
|
108
|
+
export function interpolateContent(content, options = DEFAULT_OPTIONS) {
|
|
109
|
+
if (typeof content === "string") {
|
|
110
|
+
return interpolateEnvVarsInString(content, options);
|
|
111
|
+
}
|
|
112
|
+
if (Array.isArray(content)) {
|
|
113
|
+
return interpolateEnvVarsInLines(content, options);
|
|
114
|
+
}
|
|
115
|
+
return interpolateEnvVars(content, options);
|
|
116
|
+
}
|
package/dist/merge.d.ts
CHANGED
|
@@ -34,3 +34,14 @@ export declare function stripMergeDirectives(obj: Record<string, unknown>): Reco
|
|
|
34
34
|
* Create a default merge context.
|
|
35
35
|
*/
|
|
36
36
|
export declare function createMergeContext(defaultStrategy?: ArrayMergeStrategy): MergeContext;
|
|
37
|
+
/**
|
|
38
|
+
* Check if content is text type (string or string[]).
|
|
39
|
+
*/
|
|
40
|
+
export declare function isTextContent(content: unknown): content is string | string[];
|
|
41
|
+
/**
|
|
42
|
+
* Merge two text content values.
|
|
43
|
+
* For strings: overlay replaces base entirely.
|
|
44
|
+
* For string arrays: applies merge strategy.
|
|
45
|
+
* For mixed types: overlay replaces base.
|
|
46
|
+
*/
|
|
47
|
+
export declare function mergeTextContent(base: string | string[], overlay: string | string[], strategy?: ArrayMergeStrategy): string | string[];
|
package/dist/merge.js
CHANGED
|
@@ -152,3 +152,45 @@ export function createMergeContext(defaultStrategy = "replace") {
|
|
|
152
152
|
defaultArrayStrategy: defaultStrategy,
|
|
153
153
|
};
|
|
154
154
|
}
|
|
155
|
+
// =============================================================================
|
|
156
|
+
// Text Content Utilities
|
|
157
|
+
// =============================================================================
|
|
158
|
+
/**
|
|
159
|
+
* Check if content is text type (string or string[]).
|
|
160
|
+
*/
|
|
161
|
+
export function isTextContent(content) {
|
|
162
|
+
return (typeof content === "string" ||
|
|
163
|
+
(Array.isArray(content) &&
|
|
164
|
+
content.every((item) => typeof item === "string")));
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Merge two text content values.
|
|
168
|
+
* For strings: overlay replaces base entirely.
|
|
169
|
+
* For string arrays: applies merge strategy.
|
|
170
|
+
* For mixed types: overlay replaces base.
|
|
171
|
+
*/
|
|
172
|
+
export function mergeTextContent(base, overlay, strategy = "replace") {
|
|
173
|
+
// If overlay is a string, it always replaces
|
|
174
|
+
if (typeof overlay === "string") {
|
|
175
|
+
return overlay;
|
|
176
|
+
}
|
|
177
|
+
// If overlay is an array
|
|
178
|
+
if (Array.isArray(overlay)) {
|
|
179
|
+
// If base is also an array, apply merge strategy
|
|
180
|
+
if (Array.isArray(base)) {
|
|
181
|
+
switch (strategy) {
|
|
182
|
+
case "append":
|
|
183
|
+
return [...base, ...overlay];
|
|
184
|
+
case "prepend":
|
|
185
|
+
return [...overlay, ...base];
|
|
186
|
+
case "replace":
|
|
187
|
+
default:
|
|
188
|
+
return overlay;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Base is string, overlay is array - overlay replaces
|
|
192
|
+
return overlay;
|
|
193
|
+
}
|
|
194
|
+
// Fallback (shouldn't reach here with proper types)
|
|
195
|
+
return overlay;
|
|
196
|
+
}
|
package/package.json
CHANGED