@dfosco/storyboard-react 2.2.0 → 2.4.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/package.json +2 -2
- package/src/vite/data-plugin.js +101 -24
- package/src/vite/data-plugin.test.js +174 -3
package/package.json
CHANGED
package/src/vite/data-plugin.js
CHANGED
|
@@ -7,39 +7,60 @@ import { parse as parseJsonc } from 'jsonc-parser'
|
|
|
7
7
|
const VIRTUAL_MODULE_ID = 'virtual:storyboard-data-index'
|
|
8
8
|
const RESOLVED_ID = '\0' + VIRTUAL_MODULE_ID
|
|
9
9
|
|
|
10
|
-
const GLOB_PATTERN = '**/*.{flow,scene,object,record,prototype}.{json,jsonc}'
|
|
10
|
+
const GLOB_PATTERN = '**/*.{flow,scene,object,record,prototype,folder}.{json,jsonc}'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Extract the data name and type suffix from a file path.
|
|
14
14
|
* Flows and records inside src/prototypes/{Name}/ get prefixed with the
|
|
15
15
|
* prototype name (e.g. "Dashboard/default"). Objects are never prefixed.
|
|
16
|
+
* Directories ending in .folder/ are skipped when extracting prototype scope.
|
|
16
17
|
*
|
|
17
18
|
* e.g. "src/data/default.flow.json" → { name: "default", suffix: "flow" }
|
|
18
19
|
* "src/prototypes/Dashboard/default.flow.json" → { name: "Dashboard/default", suffix: "flow" }
|
|
19
20
|
* "src/prototypes/Dashboard/helpers.object.json"→ { name: "helpers", suffix: "object" }
|
|
21
|
+
* "src/prototypes/X.folder/Dashboard/default.flow.json" → { name: "Dashboard/default", suffix: "flow", folder: "X" }
|
|
20
22
|
*/
|
|
21
23
|
function parseDataFile(filePath) {
|
|
22
24
|
const base = path.basename(filePath)
|
|
23
|
-
const match = base.match(/^(.+)\.(flow|scene|object|record|prototype)\.(jsonc?)$/)
|
|
25
|
+
const match = base.match(/^(.+)\.(flow|scene|object|record|prototype|folder)\.(jsonc?)$/)
|
|
24
26
|
if (!match) return null
|
|
27
|
+
|
|
28
|
+
// Skip _-prefixed files (drafts/internal)
|
|
29
|
+
if (match[1].startsWith('_')) return null
|
|
30
|
+
|
|
31
|
+
// Skip files inside _-prefixed directories
|
|
32
|
+
const normalized = filePath.replace(/\\/g, '/')
|
|
33
|
+
if (normalized.split('/').some(seg => seg.startsWith('_'))) return null
|
|
25
34
|
// Normalize .scene → .flow for backward compatibility
|
|
26
35
|
const suffix = match[2] === 'scene' ? 'flow' : match[2]
|
|
27
36
|
let name = match[1]
|
|
28
37
|
|
|
38
|
+
// Detect if this file is inside a .folder/ directory
|
|
39
|
+
const folderDirMatch = normalized.match(/(?:^|\/)src\/prototypes\/([^/]+)\.folder\//)
|
|
40
|
+
const folderName = folderDirMatch ? folderDirMatch[1] : null
|
|
41
|
+
|
|
42
|
+
// Folder metadata files are keyed by their folder directory name (sans .folder suffix)
|
|
43
|
+
if (suffix === 'folder') {
|
|
44
|
+
if (folderName) {
|
|
45
|
+
name = folderName
|
|
46
|
+
}
|
|
47
|
+
return { name, suffix, ext: match[3] }
|
|
48
|
+
}
|
|
49
|
+
|
|
29
50
|
// Prototype metadata files are keyed by their prototype directory name
|
|
51
|
+
// (skip .folder/ segments when determining prototype name)
|
|
30
52
|
if (suffix === 'prototype') {
|
|
31
|
-
const
|
|
32
|
-
const protoMatch = normalized.match(/(?:^|\/)src\/prototypes\/([^/]+)\//)
|
|
53
|
+
const protoMatch = normalized.match(/(?:^|\/)src\/prototypes\/(?:[^/]+\.folder\/)?([^/]+)\//)
|
|
33
54
|
if (protoMatch) {
|
|
34
55
|
name = protoMatch[1]
|
|
35
56
|
}
|
|
36
|
-
return { name, suffix, ext: match[3] }
|
|
57
|
+
return { name, suffix, ext: match[3], folder: folderName }
|
|
37
58
|
}
|
|
38
59
|
|
|
39
60
|
// Scope flows and records inside src/prototypes/{Name}/ with a prefix
|
|
61
|
+
// (skip .folder/ segments when determining prototype name)
|
|
40
62
|
if (suffix !== 'object') {
|
|
41
|
-
const
|
|
42
|
-
const protoMatch = normalized.match(/(?:^|\/)src\/prototypes\/([^/]+)\//)
|
|
63
|
+
const protoMatch = normalized.match(/(?:^|\/)src\/prototypes\/(?:[^/]+\.folder\/)?([^/]+)\//)
|
|
43
64
|
if (protoMatch) {
|
|
44
65
|
name = `${protoMatch[1]}/${name}`
|
|
45
66
|
}
|
|
@@ -65,6 +86,22 @@ function getGitAuthor(root, filePath) {
|
|
|
65
86
|
}
|
|
66
87
|
}
|
|
67
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Look up the most recent commit date for any file in a directory.
|
|
91
|
+
* Returns an ISO 8601 timestamp, or null if unavailable.
|
|
92
|
+
*/
|
|
93
|
+
function getLastModified(root, dirPath) {
|
|
94
|
+
try {
|
|
95
|
+
const result = execSync(
|
|
96
|
+
`git log -1 --format="%aI" -- "${dirPath}"`,
|
|
97
|
+
{ cwd: root, encoding: 'utf-8', timeout: 5000 },
|
|
98
|
+
).trim()
|
|
99
|
+
return result || null
|
|
100
|
+
} catch {
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
68
105
|
/**
|
|
69
106
|
* Scan the repo for all data files, validate uniqueness, return the index.
|
|
70
107
|
*/
|
|
@@ -72,8 +109,24 @@ function buildIndex(root) {
|
|
|
72
109
|
const ignore = ['node_modules/**', 'dist/**', '.git/**']
|
|
73
110
|
const files = globSync(GLOB_PATTERN, { cwd: root, ignore, absolute: false })
|
|
74
111
|
|
|
75
|
-
|
|
112
|
+
// Detect nested .folder/ directories (not supported)
|
|
113
|
+
// Scan directories directly since empty nested folders have no data files
|
|
114
|
+
const folderDirs = globSync('src/prototypes/**/*.folder', { cwd: root, ignore, absolute: false })
|
|
115
|
+
for (const dir of folderDirs) {
|
|
116
|
+
const normalized = dir.replace(/\\/g, '/')
|
|
117
|
+
const segments = normalized.split('/').filter(s => s.endsWith('.folder'))
|
|
118
|
+
if (segments.length > 1) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`[storyboard-data] Nested .folder directories are not supported.\n` +
|
|
121
|
+
` Found at: ${dir}\n` +
|
|
122
|
+
` Folders can only be one level deep inside src/prototypes/.`
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const index = { flow: {}, object: {}, record: {}, prototype: {}, folder: {} }
|
|
76
128
|
const seen = {} // "name.suffix" → absolute path (for duplicate detection)
|
|
129
|
+
const protoFolders = {} // prototype name → folder name (for injection)
|
|
77
130
|
|
|
78
131
|
for (const relPath of files) {
|
|
79
132
|
const parsed = parseDataFile(relPath)
|
|
@@ -86,8 +139,10 @@ function buildIndex(root) {
|
|
|
86
139
|
const hint = parsed.suffix === 'object'
|
|
87
140
|
? ' Objects are globally scoped — even inside src/prototypes/ they share a single namespace.\n' +
|
|
88
141
|
' Rename one of the files to avoid the collision.'
|
|
89
|
-
:
|
|
90
|
-
'
|
|
142
|
+
: parsed.suffix === 'folder'
|
|
143
|
+
? ' Folder names must be unique across the project.'
|
|
144
|
+
: ' Flows and records are scoped to their prototype directory.\n' +
|
|
145
|
+
' If both files are global (outside src/prototypes/), rename one to avoid the collision.'
|
|
91
146
|
|
|
92
147
|
throw new Error(
|
|
93
148
|
`[storyboard-data] Duplicate ${parsed.suffix} "${parsed.name}"\n` +
|
|
@@ -99,9 +154,14 @@ function buildIndex(root) {
|
|
|
99
154
|
|
|
100
155
|
seen[key] = absPath
|
|
101
156
|
index[parsed.suffix][parsed.name] = absPath
|
|
157
|
+
|
|
158
|
+
// Track which folder a prototype belongs to
|
|
159
|
+
if (parsed.suffix === 'prototype' && parsed.folder) {
|
|
160
|
+
protoFolders[parsed.name] = parsed.folder
|
|
161
|
+
}
|
|
102
162
|
}
|
|
103
163
|
|
|
104
|
-
return index
|
|
164
|
+
return { index, protoFolders }
|
|
105
165
|
}
|
|
106
166
|
|
|
107
167
|
/**
|
|
@@ -164,10 +224,10 @@ function readModesConfig(root) {
|
|
|
164
224
|
return fallback
|
|
165
225
|
}
|
|
166
226
|
|
|
167
|
-
function generateModule(index, root) {
|
|
227
|
+
function generateModule({ index, protoFolders }, root) {
|
|
168
228
|
const declarations = []
|
|
169
|
-
const INDEX_KEYS = ['flow', 'object', 'record', 'prototype']
|
|
170
|
-
const entries = { flow: [], object: [], record: [], prototype: [] }
|
|
229
|
+
const INDEX_KEYS = ['flow', 'object', 'record', 'prototype', 'folder']
|
|
230
|
+
const entries = { flow: [], object: [], record: [], prototype: [], folder: [] }
|
|
171
231
|
let i = 0
|
|
172
232
|
|
|
173
233
|
for (const suffix of INDEX_KEYS) {
|
|
@@ -184,13 +244,27 @@ function generateModule(index, root) {
|
|
|
184
244
|
}
|
|
185
245
|
}
|
|
186
246
|
|
|
247
|
+
// Auto-fill lastModified from git history for prototypes
|
|
248
|
+
if (suffix === 'prototype' && parsed) {
|
|
249
|
+
const protoDir = path.dirname(absPath)
|
|
250
|
+
const lastModified = getLastModified(root, protoDir)
|
|
251
|
+
if (lastModified) {
|
|
252
|
+
parsed = { ...parsed, lastModified }
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Inject folder association into prototype metadata
|
|
257
|
+
if (suffix === 'prototype' && protoFolders[name]) {
|
|
258
|
+
parsed = { ...parsed, folder: protoFolders[name] }
|
|
259
|
+
}
|
|
260
|
+
|
|
187
261
|
declarations.push(`const ${varName} = ${JSON.stringify(parsed)}`)
|
|
188
262
|
entries[suffix].push(` ${JSON.stringify(name)}: ${varName}`)
|
|
189
263
|
}
|
|
190
264
|
}
|
|
191
265
|
|
|
192
266
|
const imports = [`import { init } from '@dfosco/storyboard-core'`]
|
|
193
|
-
const initCalls = [`init({ flows, objects, records, prototypes })`]
|
|
267
|
+
const initCalls = [`init({ flows, objects, records, prototypes, folders })`]
|
|
194
268
|
|
|
195
269
|
// Feature flags from storyboard.config.json
|
|
196
270
|
const { config } = readConfig(root)
|
|
@@ -237,14 +311,15 @@ function generateModule(index, root) {
|
|
|
237
311
|
`const objects = {\n${entries.object.join(',\n')}\n}`,
|
|
238
312
|
`const records = {\n${entries.record.join(',\n')}\n}`,
|
|
239
313
|
`const prototypes = {\n${entries.prototype.join(',\n')}\n}`,
|
|
314
|
+
`const folders = {\n${entries.folder.join(',\n')}\n}`,
|
|
240
315
|
'',
|
|
241
316
|
'// Backward-compatible alias',
|
|
242
317
|
'const scenes = flows',
|
|
243
318
|
'',
|
|
244
319
|
initCalls.join('\n'),
|
|
245
320
|
'',
|
|
246
|
-
`export { flows, scenes, objects, records, prototypes }`,
|
|
247
|
-
`export const index = { flows, scenes, objects, records, prototypes }`,
|
|
321
|
+
`export { flows, scenes, objects, records, prototypes, folders }`,
|
|
322
|
+
`export const index = { flows, scenes, objects, records, prototypes, folders }`,
|
|
248
323
|
`export default index`,
|
|
249
324
|
].join('\n')
|
|
250
325
|
}
|
|
@@ -259,7 +334,7 @@ function generateModule(index, root) {
|
|
|
259
334
|
*/
|
|
260
335
|
export default function storyboardDataPlugin() {
|
|
261
336
|
let root = ''
|
|
262
|
-
let
|
|
337
|
+
let buildResult = null
|
|
263
338
|
|
|
264
339
|
return {
|
|
265
340
|
name: 'storyboard-data',
|
|
@@ -283,8 +358,8 @@ export default function storyboardDataPlugin() {
|
|
|
283
358
|
|
|
284
359
|
load(id) {
|
|
285
360
|
if (id !== RESOLVED_ID) return null
|
|
286
|
-
if (!
|
|
287
|
-
return generateModule(
|
|
361
|
+
if (!buildResult) buildResult = buildIndex(root)
|
|
362
|
+
return generateModule(buildResult, root)
|
|
288
363
|
},
|
|
289
364
|
|
|
290
365
|
configureServer(server) {
|
|
@@ -293,9 +368,11 @@ export default function storyboardDataPlugin() {
|
|
|
293
368
|
|
|
294
369
|
const invalidate = (filePath) => {
|
|
295
370
|
const parsed = parseDataFile(filePath)
|
|
296
|
-
|
|
371
|
+
// Also invalidate when files are added/removed inside .folder/ directories
|
|
372
|
+
const inFolder = filePath.replace(/\\/g, '/').includes('.folder/')
|
|
373
|
+
if (!parsed && !inFolder) return
|
|
297
374
|
// Rebuild index and invalidate virtual module
|
|
298
|
-
|
|
375
|
+
buildResult = null
|
|
299
376
|
const mod = server.moduleGraph.getModuleById(RESOLVED_ID)
|
|
300
377
|
if (mod) {
|
|
301
378
|
server.moduleGraph.invalidateModule(mod)
|
|
@@ -308,7 +385,7 @@ export default function storyboardDataPlugin() {
|
|
|
308
385
|
watcher.add(configPath)
|
|
309
386
|
const invalidateConfig = (filePath) => {
|
|
310
387
|
if (path.resolve(filePath) === configPath) {
|
|
311
|
-
|
|
388
|
+
buildResult = null
|
|
312
389
|
const mod = server.moduleGraph.getModuleById(RESOLVED_ID)
|
|
313
390
|
if (mod) {
|
|
314
391
|
server.moduleGraph.invalidateModule(mod)
|
|
@@ -327,7 +404,7 @@ export default function storyboardDataPlugin() {
|
|
|
327
404
|
|
|
328
405
|
// Rebuild index on each build start
|
|
329
406
|
buildStart() {
|
|
330
|
-
|
|
407
|
+
buildResult = null
|
|
331
408
|
},
|
|
332
409
|
}
|
|
333
410
|
}
|
|
@@ -69,13 +69,13 @@ describe('storyboardDataPlugin', () => {
|
|
|
69
69
|
const code = plugin.load(RESOLVED_ID)
|
|
70
70
|
|
|
71
71
|
expect(code).toContain("import { init } from '@dfosco/storyboard-core'")
|
|
72
|
-
expect(code).toContain('init({ flows, objects, records, prototypes })')
|
|
72
|
+
expect(code).toContain('init({ flows, objects, records, prototypes, folders })')
|
|
73
73
|
expect(code).toContain('"Test"')
|
|
74
74
|
expect(code).toContain('"Jane"')
|
|
75
75
|
expect(code).toContain('"First"')
|
|
76
76
|
// Backward-compat alias
|
|
77
77
|
expect(code).toContain('const scenes = flows')
|
|
78
|
-
expect(code).toContain('export { flows, scenes, objects, records, prototypes }')
|
|
78
|
+
expect(code).toContain('export { flows, scenes, objects, records, prototypes, folders }')
|
|
79
79
|
})
|
|
80
80
|
|
|
81
81
|
it('load returns null for other IDs', () => {
|
|
@@ -137,7 +137,7 @@ describe('storyboardDataPlugin', () => {
|
|
|
137
137
|
|
|
138
138
|
// .scene.json files should be normalized to the flows category
|
|
139
139
|
expect(code).toContain('"Legacy Scene"')
|
|
140
|
-
expect(code).toContain('init({ flows, objects, records, prototypes })')
|
|
140
|
+
expect(code).toContain('init({ flows, objects, records, prototypes, folders })')
|
|
141
141
|
})
|
|
142
142
|
|
|
143
143
|
it('buildStart resets the index cache', () => {
|
|
@@ -264,3 +264,174 @@ describe('prototype scoping', () => {
|
|
|
264
264
|
expect(code).toContain('flows')
|
|
265
265
|
})
|
|
266
266
|
})
|
|
267
|
+
|
|
268
|
+
describe('folder grouping', () => {
|
|
269
|
+
it('discovers .folder.json files and keys them by folder directory name', () => {
|
|
270
|
+
mkdirSync(path.join(tmpDir, 'src', 'prototypes', 'Getting Started.folder'), { recursive: true })
|
|
271
|
+
writeFileSync(
|
|
272
|
+
path.join(tmpDir, 'src', 'prototypes', 'Getting Started.folder', 'getting-started.folder.json'),
|
|
273
|
+
JSON.stringify({ meta: { title: 'Getting Started', description: 'Intro' } }),
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
const plugin = createPlugin()
|
|
277
|
+
const code = plugin.load(RESOLVED_ID)
|
|
278
|
+
|
|
279
|
+
expect(code).toContain('"Getting Started"')
|
|
280
|
+
expect(code).toContain('"Intro"')
|
|
281
|
+
expect(code).toContain('folders')
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('scopes prototypes inside .folder/ directories correctly', () => {
|
|
285
|
+
mkdirSync(path.join(tmpDir, 'src', 'prototypes', 'MyFolder.folder', 'Dashboard'), { recursive: true })
|
|
286
|
+
writeFileSync(
|
|
287
|
+
path.join(tmpDir, 'src', 'prototypes', 'MyFolder.folder', 'Dashboard', 'default.flow.json'),
|
|
288
|
+
JSON.stringify({ title: 'Dashboard Default' }),
|
|
289
|
+
)
|
|
290
|
+
writeFileSync(
|
|
291
|
+
path.join(tmpDir, 'src', 'prototypes', 'MyFolder.folder', 'Dashboard', 'dashboard.prototype.json'),
|
|
292
|
+
JSON.stringify({ meta: { title: 'Dashboard' } }),
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
const plugin = createPlugin()
|
|
296
|
+
const code = plugin.load(RESOLVED_ID)
|
|
297
|
+
|
|
298
|
+
// Flow should be scoped to prototype, not folder
|
|
299
|
+
expect(code).toContain('"Dashboard/default"')
|
|
300
|
+
expect(code).not.toContain('"MyFolder.folder/default"')
|
|
301
|
+
expect(code).not.toContain('"MyFolder/default"')
|
|
302
|
+
// Prototype should have folder field injected
|
|
303
|
+
expect(code).toContain('"folder":"MyFolder"')
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('does NOT prefix objects inside .folder/ directories', () => {
|
|
307
|
+
mkdirSync(path.join(tmpDir, 'src', 'prototypes', 'X.folder', 'Proto'), { recursive: true })
|
|
308
|
+
writeFileSync(
|
|
309
|
+
path.join(tmpDir, 'src', 'prototypes', 'X.folder', 'Proto', 'helpers.object.json'),
|
|
310
|
+
JSON.stringify({ util: true }),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
const plugin = createPlugin()
|
|
314
|
+
const code = plugin.load(RESOLVED_ID)
|
|
315
|
+
|
|
316
|
+
expect(code).toContain('"helpers"')
|
|
317
|
+
expect(code).not.toContain('"X/helpers"')
|
|
318
|
+
expect(code).not.toContain('"Proto/helpers"')
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
it('scopes records inside .folder/ directories to their prototype', () => {
|
|
322
|
+
mkdirSync(path.join(tmpDir, 'src', 'prototypes', 'X.folder', 'Blog'), { recursive: true })
|
|
323
|
+
writeFileSync(
|
|
324
|
+
path.join(tmpDir, 'src', 'prototypes', 'X.folder', 'Blog', 'posts.record.json'),
|
|
325
|
+
JSON.stringify([{ id: '1', title: 'Post' }]),
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
const plugin = createPlugin()
|
|
329
|
+
const code = plugin.load(RESOLVED_ID)
|
|
330
|
+
|
|
331
|
+
expect(code).toContain('"Blog/posts"')
|
|
332
|
+
expect(code).not.toContain('"X/posts"')
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it('allows prototypes with same name in different folders without clash', () => {
|
|
336
|
+
mkdirSync(path.join(tmpDir, 'src', 'prototypes', 'A.folder', 'Settings'), { recursive: true })
|
|
337
|
+
mkdirSync(path.join(tmpDir, 'src', 'prototypes', 'B.folder', 'Settings'), { recursive: true })
|
|
338
|
+
writeFileSync(
|
|
339
|
+
path.join(tmpDir, 'src', 'prototypes', 'A.folder', 'Settings', 'default.flow.json'),
|
|
340
|
+
JSON.stringify({ from: 'A' }),
|
|
341
|
+
)
|
|
342
|
+
writeFileSync(
|
|
343
|
+
path.join(tmpDir, 'src', 'prototypes', 'B.folder', 'Settings', 'default.flow.json'),
|
|
344
|
+
JSON.stringify({ from: 'B' }),
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
const plugin = createPlugin()
|
|
348
|
+
// Same flow name in same prototype name → duplicate collision
|
|
349
|
+
expect(() => plugin.load(RESOLVED_ID)).toThrow(/Duplicate flow "Settings\/default"/)
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it('throws on nested .folder/ directories', () => {
|
|
353
|
+
mkdirSync(path.join(tmpDir, 'src', 'prototypes', 'Outer.folder', 'Inner.folder', 'Proto'), { recursive: true })
|
|
354
|
+
writeFileSync(
|
|
355
|
+
path.join(tmpDir, 'src', 'prototypes', 'Outer.folder', 'Inner.folder', 'Proto', 'default.flow.json'),
|
|
356
|
+
JSON.stringify({ title: 'Nested' }),
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
const plugin = createPlugin()
|
|
360
|
+
expect(() => plugin.load(RESOLVED_ID)).toThrow(/Nested .folder directories are not supported/)
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it('throws on empty nested .folder/ directories', () => {
|
|
364
|
+
mkdirSync(path.join(tmpDir, 'src', 'prototypes', 'Outer.folder', 'Inner.folder'), { recursive: true })
|
|
365
|
+
|
|
366
|
+
const plugin = createPlugin()
|
|
367
|
+
expect(() => plugin.load(RESOLVED_ID)).toThrow(/Nested .folder directories are not supported/)
|
|
368
|
+
})
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
describe('underscore prefix ignoring', () => {
|
|
372
|
+
it('ignores _-prefixed data files', () => {
|
|
373
|
+
writeFileSync(
|
|
374
|
+
path.join(tmpDir, '_draft.flow.json'),
|
|
375
|
+
JSON.stringify({ title: 'Draft' }),
|
|
376
|
+
)
|
|
377
|
+
writeFileSync(
|
|
378
|
+
path.join(tmpDir, 'visible.flow.json'),
|
|
379
|
+
JSON.stringify({ title: 'Visible' }),
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
const plugin = createPlugin()
|
|
383
|
+
const code = plugin.load(RESOLVED_ID)
|
|
384
|
+
|
|
385
|
+
expect(code).toContain('"Visible"')
|
|
386
|
+
expect(code).not.toContain('"Draft"')
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
it('ignores data files inside _-prefixed directories', () => {
|
|
390
|
+
mkdirSync(path.join(tmpDir, '_archive'), { recursive: true })
|
|
391
|
+
writeFileSync(
|
|
392
|
+
path.join(tmpDir, '_archive', 'old.flow.json'),
|
|
393
|
+
JSON.stringify({ title: 'Archived' }),
|
|
394
|
+
)
|
|
395
|
+
writeFileSync(
|
|
396
|
+
path.join(tmpDir, 'current.flow.json'),
|
|
397
|
+
JSON.stringify({ title: 'Current' }),
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
const plugin = createPlugin()
|
|
401
|
+
const code = plugin.load(RESOLVED_ID)
|
|
402
|
+
|
|
403
|
+
expect(code).toContain('"Current"')
|
|
404
|
+
expect(code).not.toContain('"Archived"')
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
it('ignores prototype.json inside _-prefixed directories', () => {
|
|
408
|
+
mkdirSync(path.join(tmpDir, 'src', 'prototypes', '_WIP'), { recursive: true })
|
|
409
|
+
writeFileSync(
|
|
410
|
+
path.join(tmpDir, 'src', 'prototypes', '_WIP', 'wip.prototype.json'),
|
|
411
|
+
JSON.stringify({ meta: { title: 'Work in Progress' } }),
|
|
412
|
+
)
|
|
413
|
+
mkdirSync(path.join(tmpDir, 'src', 'prototypes', 'Live'), { recursive: true })
|
|
414
|
+
writeFileSync(
|
|
415
|
+
path.join(tmpDir, 'src', 'prototypes', 'Live', 'live.prototype.json'),
|
|
416
|
+
JSON.stringify({ meta: { title: 'Live' } }),
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
const plugin = createPlugin()
|
|
420
|
+
const code = plugin.load(RESOLVED_ID)
|
|
421
|
+
|
|
422
|
+
expect(code).toContain('"Live"')
|
|
423
|
+
expect(code).not.toContain('"Work in Progress"')
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
it('does not ignore files with _ in the middle of the name', () => {
|
|
427
|
+
writeFileSync(
|
|
428
|
+
path.join(tmpDir, 'my_flow.flow.json'),
|
|
429
|
+
JSON.stringify({ title: 'Has Underscore' }),
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
const plugin = createPlugin()
|
|
433
|
+
const code = plugin.load(RESOLVED_ID)
|
|
434
|
+
|
|
435
|
+
expect(code).toContain('"Has Underscore"')
|
|
436
|
+
})
|
|
437
|
+
})
|