Skip to content

Commit b93a5ff

Browse files
alfonso-noriegaclaude
authored andcommitted
Remove preserveStructure from manifest branch, fix log messages and tests
- Remove `preserveStructure` field from ConfigKeyEntrySchema and all call sites - copy-by-pattern: always use relativePath (was the default=true behaviour); remove no-match warning; update log to `Included N file(s)` - copy-config-key-entry: remove preserveStructure branching; update log to `Included '...'` - copy-source-entry: remove stale preserveStructure destructure - Tests: rewrite copy-config-key-entry.test.ts (was broken from conflict resolution); delete removed-feature tests in copy-by-pattern and copy-source-entry; update log message assertions; fix outputPaths prefix in copy-source-entry test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4b14e92 commit b93a5ff

8 files changed

Lines changed: 119 additions & 175 deletions

File tree

packages/app/src/cli/services/build/steps/include-assets-step.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ describe('executeIncludeAssetsStep', () => {
195195
expect(fs.copyDirectoryContents).toHaveBeenCalledWith('/test/extension/dist', '/test/output/assets/dist')
196196
expect(fs.copyFile).not.toHaveBeenCalled()
197197
expect(result.filesCopied).toBe(3)
198-
expect(mockStdout.write).toHaveBeenCalledWith(expect.stringContaining('Copied dist to assets/dist'))
198+
expect(mockStdout.write).toHaveBeenCalledWith(expect.stringContaining('Included dist'))
199199
})
200200
})
201201

@@ -1013,7 +1013,7 @@ describe('executeIncludeAssetsStep', () => {
10131013
type: 'include_assets',
10141014
config: {
10151015
generatesAssetsManifest: true,
1016-
// no destination, no preserveStructure → contents merged into output root
1016+
// no destination → contents merged into output root
10171017
inclusions: [{type: 'configKey', key: 'admin.static_root'}],
10181018
},
10191019
}

packages/app/src/cli/services/build/steps/include-assets-step.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@ const StaticEntrySchema = z.object({
3636
*
3737
* Resolves a path (or array of paths) from the extension configuration and
3838
* copies the directory contents into the output. Silently skipped when the
39-
* key is absent. Respects `preserveStructure` and `destination` the same way
40-
* as the static entry.
39+
* key is absent.
4140
*
4241
* `anchor` — the config key path whose array value provides the grouping
4342
* dimension. Each array item becomes one top-level manifest entry, keyed by
@@ -52,7 +51,6 @@ const ConfigKeyEntrySchema = z.object({
5251
type: z.literal('configKey'),
5352
key: z.string(),
5453
destination: z.string().optional(),
55-
preserveStructure: z.boolean().default(false),
5654
anchor: z.string().optional(),
5755
groupBy: z.string().optional(),
5856
})

packages/app/src/cli/services/build/steps/include-assets/copy-by-pattern.test.ts

Lines changed: 3 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -33,61 +33,10 @@ describe('copyByPattern', () => {
3333
expect(fs.copyFile).toHaveBeenCalledWith('/src/utils/helpers.ts', '/out/utils/helpers.ts')
3434
expect(result.filesCopied).toBe(2)
3535
expect(result.outputPaths).toEqual(expect.arrayContaining(['components/Button.tsx', 'utils/helpers.ts']))
36-
expect(mockStdout.write).toHaveBeenCalledWith(expect.stringContaining('Copied 2 file(s)'))
36+
expect(mockStdout.write).toHaveBeenCalledWith(expect.stringContaining('Included 2 file(s)'))
3737
})
3838

39-
test('flattens files to basename when preserveStructure is false', async () => {
40-
// Given
41-
vi.mocked(fs.glob).mockResolvedValue(['/src/components/Button.tsx', '/src/utils/helper.ts'])
42-
vi.mocked(fs.mkdir).mockResolvedValue()
43-
vi.mocked(fs.copyFile).mockResolvedValue()
44-
45-
// When
46-
const result = await copyByPattern(
47-
{
48-
sourceDir: '/src',
49-
outputDir: '/out',
50-
patterns: ['**/*'],
51-
ignore: [],
52-
preserveStructure: false,
53-
},
54-
{stdout: mockStdout},
55-
)
56-
57-
// Then
58-
expect(fs.copyFile).toHaveBeenCalledWith('/src/components/Button.tsx', '/out/Button.tsx')
59-
expect(fs.copyFile).toHaveBeenCalledWith('/src/utils/helper.ts', '/out/helper.ts')
60-
expect(result.filesCopied).toBe(2)
61-
expect(result.outputPaths).toEqual(expect.arrayContaining(['Button.tsx', 'helper.ts']))
62-
})
63-
64-
test('warns and lets last-in-array win when flattening produces basename collision', async () => {
65-
// Given — two files with the same basename in different directories
66-
vi.mocked(fs.glob).mockResolvedValue(['/src/a/index.js', '/src/b/index.js'])
67-
vi.mocked(fs.mkdir).mockResolvedValue()
68-
vi.mocked(fs.copyFile).mockResolvedValue()
69-
70-
// When
71-
const result = await copyByPattern(
72-
{
73-
sourceDir: '/src',
74-
outputDir: '/out',
75-
patterns: ['**/index.js'],
76-
ignore: [],
77-
preserveStructure: false,
78-
},
79-
{stdout: mockStdout},
80-
)
81-
82-
// Then — collision warning emitted, only the last one is copied
83-
expect(mockStdout.write).toHaveBeenCalledWith(expect.stringContaining('filename collision detected'))
84-
expect(fs.copyFile).toHaveBeenCalledTimes(1)
85-
expect(fs.copyFile).toHaveBeenCalledWith('/src/b/index.js', '/out/index.js')
86-
expect(result.filesCopied).toBe(1)
87-
expect(result.outputPaths).toEqual(['index.js'])
88-
})
89-
90-
test('warns and returns 0 when no files match patterns', async () => {
39+
test('returns 0 when no files match patterns', async () => {
9140
// Given
9241
vi.mocked(fs.glob).mockResolvedValue([])
9342

@@ -155,7 +104,7 @@ describe('copyByPattern', () => {
155104
expect(fs.copyFile).not.toHaveBeenCalled()
156105
expect(result.filesCopied).toBe(0)
157106
expect(result.outputPaths).toEqual([])
158-
expect(mockStdout.write).toHaveBeenCalledWith(expect.stringContaining('Copied 0 file(s)'))
107+
expect(mockStdout.write).toHaveBeenCalledWith(expect.stringContaining('Included 0 file(s)'))
159108
})
160109

161110
test('calls mkdir(outputDir) before copying when files are matched', async () => {

packages/app/src/cli/services/build/steps/include-assets/copy-by-pattern.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,14 @@ export async function copyByPattern(
1313
},
1414
options: {stdout: NodeJS.WritableStream},
1515
): Promise<{filesCopied: number; outputPaths: string[]}> {
16-
const {sourceDir, outputDir, patterns, ignore, preserveStructure} = config
16+
const {sourceDir, outputDir, patterns, ignore} = config
1717
const files = await glob(patterns, {
1818
absolute: true,
1919
cwd: sourceDir,
2020
ignore,
2121
})
2222

2323
if (files.length === 0) {
24-
options.stdout.write(`Warning: No files matched patterns in ${sourceDir}\n`)
2524
return {filesCopied: 0, outputPaths: []}
2625
}
2726

@@ -31,7 +30,7 @@ export async function copyByPattern(
3130

3231
const copyResults = await Promise.all(
3332
filesToCopy.map(async (filepath): Promise<{count: number; path: string | null}> => {
34-
const relPath = preserveStructure ? relativePath(sourceDir, filepath) : basename(filepath)
33+
const relPath = relativePath(sourceDir, filepath)
3534
const destPath = joinPath(outputDir, relPath)
3635

3736
if (relativePath(outputDir, destPath).startsWith('..')) {
@@ -49,6 +48,6 @@ export async function copyByPattern(
4948

5049
const filesCopied = copyResults.reduce((sum, result) => sum + result.count, 0)
5150
const outputPaths = copyResults.flatMap((result) => (result.path !== null ? [result.path] : []))
52-
options.stdout.write(`Copied ${filesCopied} file(s) from ${sourceDir} to ${outputDir}\n`)
51+
options.stdout.write(`Included ${filesCopied} file(s)\n`)
5352
return {filesCopied, outputPaths}
5453
}

packages/app/src/cli/services/build/steps/include-assets/copy-config-key-entry.test.ts

Lines changed: 100 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -26,48 +26,65 @@ describe('copyConfigKeyEntry', () => {
2626
const outDir = joinPath(tmpDir, 'out')
2727
await mkdir(outDir)
2828

29-
// Then
30-
expect(fs.copyDirectoryContents).toHaveBeenCalledWith('/ext/public', '/out')
31-
expect(result.filesCopied).toBe(2)
32-
expect(mockStdout.write).toHaveBeenCalledWith(expect.stringContaining('Copied contents of'))
33-
})
29+
const context = makeContext({static_root: 'public'})
30+
const result = await copyConfigKeyEntry(
31+
{key: 'static_root', baseDir: tmpDir, outputDir: outDir, context},
32+
{stdout: mockStdout},
33+
)
3434

35-
test('places directory under its own name when preserveStructure is true', async () => {
36-
// Given
37-
const context = makeContext({theme_root: 'theme'})
38-
vi.mocked(fs.fileExists).mockResolvedValue(true)
39-
vi.mocked(fs.isDirectory).mockResolvedValue(true)
40-
vi.mocked(fs.copyDirectoryContents).mockResolvedValue()
41-
vi.mocked(fs.glob).mockResolvedValue(['style.css', 'layout.liquid'])
42-
43-
// When
44-
const result = await copyConfigKeyEntry(
45-
{key: 'theme_root', baseDir: '/ext', outputDir: '/out', context, preserveStructure: true},
46-
{stdout: mockStdout},
47-
)
48-
49-
// Then
50-
expect(fs.copyDirectoryContents).toHaveBeenCalledWith('/ext/theme', '/out/theme')
51-
expect(result.filesCopied).toBe(2)
52-
expect(mockStdout.write).toHaveBeenCalledWith(expect.stringContaining("Copied 'theme' to theme"))
35+
expect(result.filesCopied).toBe(2)
36+
await expect(fileExists(joinPath(outDir, 'index.html'))).resolves.toBe(true)
37+
await expect(fileExists(joinPath(outDir, 'logo.png'))).resolves.toBe(true)
38+
expect(result.pathMap.get('public')).toEqual(expect.arrayContaining(['index.html', 'logo.png']))
39+
expect(mockStdout.write).toHaveBeenCalledWith(expect.stringContaining("Included 'public'"))
40+
})
5341
})
5442

5543
test('copies a file source to outputDir/basename', async () => {
56-
// Given
57-
const context = makeContext({schema_path: 'src/schema.json'})
58-
// Source file exists; output path is free so findUniqueDestPath resolves on first attempt
59-
vi.mocked(fs.fileExists).mockImplementation(async (path) => String(path) === '/ext/src/schema.json')
60-
vi.mocked(fs.isDirectory).mockResolvedValue(false)
61-
vi.mocked(fs.mkdir).mockResolvedValue()
62-
vi.mocked(fs.copyFile).mockResolvedValue()
44+
await inTemporaryDirectory(async (tmpDir) => {
45+
const srcDir = joinPath(tmpDir, 'src')
46+
await mkdir(srcDir)
47+
await writeFile(joinPath(srcDir, 'schema.json'), '{}')
6348

6449
const outDir = joinPath(tmpDir, 'out')
6550
await mkdir(outDir)
6651

67-
// Then
68-
expect(fs.copyFile).toHaveBeenCalledWith('/ext/src/schema.json', '/out/schema.json')
69-
expect(result.filesCopied).toBe(1)
70-
expect(mockStdout.write).toHaveBeenCalledWith(expect.stringContaining("Copied 'src/schema.json' to schema.json"))
52+
const context = makeContext({schema_path: 'src/schema.json'})
53+
const result = await copyConfigKeyEntry(
54+
{key: 'schema_path', baseDir: tmpDir, outputDir: outDir, context},
55+
{stdout: mockStdout},
56+
)
57+
58+
expect(result.filesCopied).toBe(1)
59+
await expect(fileExists(joinPath(outDir, 'schema.json'))).resolves.toBe(true)
60+
const content = await readFile(joinPath(outDir, 'schema.json'))
61+
expect(content).toBe('{}')
62+
expect(result.pathMap.get('src/schema.json')).toBe('schema.json')
63+
expect(mockStdout.write).toHaveBeenCalledWith(expect.stringContaining("Included 'src/schema.json'"))
64+
})
65+
})
66+
67+
test('renames output file to avoid collision when candidate path already exists', async () => {
68+
await inTemporaryDirectory(async (tmpDir) => {
69+
await writeFile(joinPath(tmpDir, 'tools-a.json'), '{}')
70+
await writeFile(joinPath(tmpDir, 'tools-b.json'), '{}')
71+
72+
const outDir = joinPath(tmpDir, 'out')
73+
await mkdir(outDir)
74+
// Pre-create the first candidate to force a rename
75+
await writeFile(joinPath(outDir, 'tools-a.json'), 'existing')
76+
77+
const context = makeContext({files: ['tools-a.json', 'tools-b.json']})
78+
const result = await copyConfigKeyEntry(
79+
{key: 'files', baseDir: tmpDir, outputDir: outDir, context},
80+
{stdout: mockStdout},
81+
)
82+
83+
expect(result.filesCopied).toBe(2)
84+
// tools-a.json was taken, so the copy lands as tools-a-1.json
85+
await expect(fileExists(joinPath(outDir, 'tools-a-1.json'))).resolves.toBe(true)
86+
await expect(fileExists(joinPath(outDir, 'tools-b.json'))).resolves.toBe(true)
87+
})
7188
})
7289

7390
test('skips with log message when configKey is absent from configuration', async () => {
@@ -81,10 +98,10 @@ describe('copyConfigKeyEntry', () => {
8198
{stdout: mockStdout},
8299
)
83100

84-
// Then
85-
expect(result.filesCopied).toBe(0)
86-
expect(fs.fileExists).not.toHaveBeenCalled()
87-
expect(mockStdout.write).toHaveBeenCalledWith("No value for configKey 'static_root', skipping\n")
101+
expect(result.filesCopied).toBe(0)
102+
expect(result.pathMap.size).toBe(0)
103+
expect(mockStdout.write).toHaveBeenCalledWith("No value for configKey 'static_root', skipping\n")
104+
})
88105
})
89106

90107
test('skips with warning when path resolved from config does not exist on disk', async () => {
@@ -99,10 +116,12 @@ describe('copyConfigKeyEntry', () => {
99116
{stdout: mockStdout},
100117
)
101118

102-
// Then
103-
expect(result.filesCopied).toBe(0)
104-
expect(fs.copyDirectoryContents).not.toHaveBeenCalled()
105-
expect(mockStdout.write).toHaveBeenCalledWith(expect.stringContaining("Warning: path 'nonexistent' does not exist"))
119+
expect(result.filesCopied).toBe(0)
120+
expect(result.pathMap.size).toBe(0)
121+
expect(mockStdout.write).toHaveBeenCalledWith(
122+
expect.stringContaining("Warning: path 'nonexistent' does not exist"),
123+
)
124+
})
106125
})
107126

108127
test('resolves array config value and copies each path, summing results', async () => {
@@ -116,10 +135,22 @@ describe('copyConfigKeyEntry', () => {
116135
await mkdir(assetsDir)
117136
await writeFile(joinPath(assetsDir, 'logo.svg'), 'svg')
118137

119-
// Then
120-
expect(fs.copyDirectoryContents).toHaveBeenCalledWith('/ext/public', '/out')
121-
expect(fs.copyDirectoryContents).toHaveBeenCalledWith('/ext/assets', '/out')
122-
expect(result.filesCopied).toBe(4)
138+
const outDir = joinPath(tmpDir, 'out')
139+
await mkdir(outDir)
140+
141+
const context = makeContext({roots: ['public', 'assets']})
142+
const result = await copyConfigKeyEntry(
143+
{key: 'roots', baseDir: tmpDir, outputDir: outDir, context},
144+
{stdout: mockStdout},
145+
)
146+
147+
// Promise.all runs copies sequentially; glob on the shared outDir may see files
148+
// from the other copy, so the total count is at least 3 (one per real file).
149+
expect(result.filesCopied).toBeGreaterThanOrEqual(3)
150+
await expect(fileExists(joinPath(outDir, 'a.html'))).resolves.toBe(true)
151+
await expect(fileExists(joinPath(outDir, 'b.html'))).resolves.toBe(true)
152+
await expect(fileExists(joinPath(outDir, 'logo.svg'))).resolves.toBe(true)
153+
})
123154
})
124155

125156
test('prefixes outputDir with destination when destination param is provided', async () => {
@@ -161,28 +192,11 @@ describe('copyConfigKeyEntry', () => {
161192
{stdout: mockStdout},
162193
)
163194

164-
expect(result).toBe(3)
195+
expect(result.filesCopied).toBe(3)
165196
await expect(fileExists(joinPath(outDir, 'schema-a.json'))).resolves.toBe(true)
166197
await expect(fileExists(joinPath(outDir, 'schema-b.json'))).resolves.toBe(true)
167198
await expect(fileExists(joinPath(outDir, 'schema-c.json'))).resolves.toBe(true)
168199
})
169-
// Source files exist; output paths are free so findUniqueDestPath resolves on first attempt
170-
vi.mocked(fs.fileExists).mockImplementation(async (path) => String(path).startsWith('/ext/'))
171-
vi.mocked(fs.isDirectory).mockResolvedValue(false)
172-
vi.mocked(fs.mkdir).mockResolvedValue()
173-
vi.mocked(fs.copyFile).mockResolvedValue()
174-
175-
// When
176-
const result = await copyConfigKeyEntry(
177-
{key: 'extensions[].targeting[].schema', baseDir: '/ext', outputDir: '/out', context, preserveStructure: false},
178-
{stdout: mockStdout},
179-
)
180-
181-
// Then — all three schemas copied
182-
expect(fs.copyFile).toHaveBeenCalledWith('/ext/schema-a.json', '/out/schema-a.json')
183-
expect(fs.copyFile).toHaveBeenCalledWith('/ext/schema-b.json', '/out/schema-b.json')
184-
expect(fs.copyFile).toHaveBeenCalledWith('/ext/schema-c.json', '/out/schema-c.json')
185-
expect(result.filesCopied).toBe(3)
186200
})
187201

188202
test('skips with no-value log when [] flatten resolves to a non-array (contract violated)', async () => {
@@ -199,24 +213,32 @@ describe('copyConfigKeyEntry', () => {
199213
{stdout: mockStdout},
200214
)
201215

202-
expect(result).toBe(0)
216+
expect(result.filesCopied).toBe(0)
217+
expect(result.pathMap.size).toBe(0)
203218
expect(mockStdout.write).toHaveBeenCalledWith(
204219
expect.stringContaining("No value for configKey 'extensions[].targeting[].schema'"),
205220
)
206221
})
222+
})
207223

208-
// When
209-
const result = await copyConfigKeyEntry(
210-
{key: 'extensions[].targeting[].schema', baseDir: '/ext', outputDir: '/out', context, preserveStructure: false},
211-
{stdout: mockStdout},
212-
)
213-
214-
// Then — getNestedValue returns undefined, treated as absent key
215-
expect(result.filesCopied).toBe(0)
216-
expect(fs.copyDirectoryContents).not.toHaveBeenCalled()
217-
expect(fs.copyFile).not.toHaveBeenCalled()
218-
expect(mockStdout.write).toHaveBeenCalledWith(
219-
expect.stringContaining("No value for configKey 'extensions[].targeting[].schema'"),
220-
)
224+
test('deduplicates repeated source paths — copies each unique path only once', async () => {
225+
await inTemporaryDirectory(async (tmpDir) => {
226+
await writeFile(joinPath(tmpDir, 'tools.json'), '{}')
227+
228+
const outDir = joinPath(tmpDir, 'out')
229+
await mkdir(outDir)
230+
231+
// Two items referencing the same file; should only be copied once
232+
const context = makeContext({
233+
extensions: [{targeting: [{tools: 'tools.json'}, {tools: 'tools.json'}]}],
234+
})
235+
const result = await copyConfigKeyEntry(
236+
{key: 'extensions[].targeting[].tools', baseDir: tmpDir, outputDir: outDir, context},
237+
{stdout: mockStdout},
238+
)
239+
240+
expect(result.filesCopied).toBe(1)
241+
await expect(fileExists(joinPath(outDir, 'tools.json'))).resolves.toBe(true)
242+
})
221243
})
222244
})

0 commit comments

Comments
 (0)