From 67f6b38a6646223321af0bf42512845d70139fca Mon Sep 17 00:00:00 2001 From: silou Date: Thu, 27 Mar 2025 13:24:49 +0100 Subject: [PATCH 1/6] add cleanup observable and use it in generate stories --- .../scripts/observables/cleanup.js | 74 +++++++++++++ .../scripts/observables/generate-stories.js | 104 +++++++++++++----- 2 files changed, 148 insertions(+), 30 deletions(-) create mode 100644 packages/@coorpacademy-components/scripts/observables/cleanup.js diff --git a/packages/@coorpacademy-components/scripts/observables/cleanup.js b/packages/@coorpacademy-components/scripts/observables/cleanup.js new file mode 100644 index 0000000000..b0994b33c7 --- /dev/null +++ b/packages/@coorpacademy-components/scripts/observables/cleanup.js @@ -0,0 +1,74 @@ +const {Observable} = require('rxjs'); +const {filter} = require('rxjs/operators'); +const {walkDirectory$} = require('./walk-directory'); // your existing directory walker +const {readdir,unlink, rmdir, stat} = require('fs'); +const {dirname} = require('path'); +const {rm} = require('fs/promises'); + + +const readAllStoryFiles$ = (cwd) => + walkDirectory$(cwd).pipe( + filter((filePath) => filePath.endsWith('.stories.tsx')) + ); + + + +function removeEmptyFolders$(folderPath, removeParents = false) { + return new Observable((observer) => { + stat(folderPath, (statErr, stats) => { + if (statErr || !stats.isDirectory()) { + observer.complete(); + return; + } + readdir(folderPath, (readErr, files) => { + if (readErr) { + observer.error(readErr); + return; + } + // Filter out system files if needed + files = files.filter((file) => file !== '.DS_Store'); + + if (files.length === 0) { + rmdir(folderPath, (rmdirErr) => { + if (rmdirErr) { + observer.error(rmdirErr); + return; + } + observer.next(folderPath); + if (removeParents && dirname(folderPath) !== folderPath) { + removeEmptyFolders$(dirname(folderPath), true).subscribe( + (val) => observer.next(val), + (err) => observer.error(err), + () => observer.complete() + ); + } else { + observer.complete(); + } + }); + } else { + // Not empty + observer.complete(); + } + }); + }); + }); +} + + +/** + * Recursively deletes a folder (rm -rf). + */ +function deleteFolder$(folderPath) { + return new Observable((observer) => { + rm(folderPath, {recursive: true, force: true}) + .then(() => { + observer.next(folderPath); + observer.complete(); + }) + .catch((err) => observer.error(err)); + }); +} + + + +module.exports = {readAllStoryFiles$, deleteFolder$, removeEmptyFolders$}; diff --git a/packages/@coorpacademy-components/scripts/observables/generate-stories.js b/packages/@coorpacademy-components/scripts/observables/generate-stories.js index b7a027182b..d001a4b506 100644 --- a/packages/@coorpacademy-components/scripts/observables/generate-stories.js +++ b/packages/@coorpacademy-components/scripts/observables/generate-stories.js @@ -1,43 +1,87 @@ -const {join, relative} = require('path'); -const {map} = require('rxjs/operators'); -const {concat, of} = require('rxjs'); +// generate-stories.js +const {join, dirname, relative} = require('path'); +const {of, from, concat} = require('rxjs'); +const {map, toArray, mergeMap} = require('rxjs/operators'); const {readComponentFixtures$} = require('./component-fixtures'); const {pascalCase} = require('./string'); const {readComponents$} = require('./components'); +const { + readAllStoryFiles$, + deleteFolder$, + removeEmptyFolders$ +} = require('./cleanup'); -const generateStories$ = cwd => - readComponents$(cwd).pipe( +/** + * Generate story files for all current components. + */ +const generateStories$ = (cwd) => { + // 1) Build [storiesPath, lines$] pairs for each component + const generation$ = readComponents$(cwd).pipe( map(({title, path, type, titleRaw, levels}) => { const testPath = join(path, 'test'); const storiesPath = join(testPath, 'index.stories.tsx'); - return [ - storiesPath, - concat( - of(`import React from 'react';`, `import ${title} from '..';`), - readComponentFixtures$({title, path, type}).pipe( - map( - ({fixture, fixturePath}) => - `import fixture${fixture} from './${relative(testPath, fixturePath)}';` - ) - ), - of( - ` -export default { - title: '${[...levels.map(pascalCase), titleRaw].join('/')}', - component: ${title} -};` - ), - readComponentFixtures$({title, path, type}).pipe( - map( - ({fixture}) => - ` + + // Observable that emits lines for fixture imports + const fixtureImports$ = readComponentFixtures$({title, path, type}).pipe( + map( + ({fixture, fixturePath}) => + `import fixture${fixture} from './${relative(testPath, fixturePath)}';` + ) + ); + + // Observable that emits lines for fixture exports + const fixtureExports$ = readComponentFixtures$({title, path, type}).pipe( + map( + ({fixture}) => ` export const ${pascalCase(fixture)} = (args: any) => <${title} {...args} />; ${pascalCase(fixture)}.args = fixture${fixture}.props;` - ) - ) ) - ]; + ); + + // Combine all lines in a single stream with concat() + const content$ = concat( + // 1) Basic imports + of(`import React from 'react';`, `import ${title} from '..';`), + // 2) Fixture imports + fixtureImports$, + // 3) Default export + of(` +export default { + title: '${[...levels.map(pascalCase), titleRaw].join('/')}', + component: ${title} +};`), + // 4) Fixture exports + fixtureExports$ + ); + + return [storiesPath, content$]; + }), + toArray() // gather all story definitions into an array + ); + + // 2) Remove stale story folders, then emit the new generation array + return generation$.pipe( + mergeMap((storyEntries) => { + const desiredStoryPaths = new Set(storyEntries.map(([p]) => p)); + + return readAllStoryFiles$(cwd).pipe( + mergeMap((existingStoryPath) => { + if (!desiredStoryPaths.has(existingStoryPath)) { + // This story is stale + const testFolder = dirname(existingStoryPath); + const parentFolder = dirname(testFolder); + return deleteFolder$(testFolder).pipe( + mergeMap(() => removeEmptyFolders$(parentFolder, true)) + ); + } + return of(null); + }), + toArray(), + // 3) Finally, emit the storyEntries array for the rest of the pipeline (writing files, etc.) + mergeMap(() => from(storyEntries)) + ); }) ); +}; -module.exports.generateStories$ = generateStories$; +module.exports = {generateStories$}; From cbe2602dd0160e7816f55e1d410b868983a349f8 Mon Sep 17 00:00:00 2001 From: silou Date: Thu, 27 Mar 2025 13:28:01 +0100 Subject: [PATCH 2/6] clean --- .../scripts/observables/cleanup.js | 12 +++--------- .../scripts/observables/generate-stories.js | 2 -- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/@coorpacademy-components/scripts/observables/cleanup.js b/packages/@coorpacademy-components/scripts/observables/cleanup.js index b0994b33c7..cee9d6f527 100644 --- a/packages/@coorpacademy-components/scripts/observables/cleanup.js +++ b/packages/@coorpacademy-components/scripts/observables/cleanup.js @@ -1,19 +1,16 @@ const {Observable} = require('rxjs'); const {filter} = require('rxjs/operators'); const {walkDirectory$} = require('./walk-directory'); // your existing directory walker -const {readdir,unlink, rmdir, stat} = require('fs'); +const {readdir, rmdir, stat} = require('fs'); const {dirname} = require('path'); const {rm} = require('fs/promises'); - const readAllStoryFiles$ = (cwd) => walkDirectory$(cwd).pipe( filter((filePath) => filePath.endsWith('.stories.tsx')) ); - - -function removeEmptyFolders$(folderPath, removeParents = false) { +const removeEmptyFolders$ = (folderPath, removeParents = false) => { return new Observable((observer) => { stat(folderPath, (statErr, stats) => { if (statErr || !stats.isDirectory()) { @@ -54,11 +51,10 @@ function removeEmptyFolders$(folderPath, removeParents = false) { }); } - /** * Recursively deletes a folder (rm -rf). */ -function deleteFolder$(folderPath) { +const deleteFolder$ = (folderPath) => { return new Observable((observer) => { rm(folderPath, {recursive: true, force: true}) .then(() => { @@ -69,6 +65,4 @@ function deleteFolder$(folderPath) { }); } - - module.exports = {readAllStoryFiles$, deleteFolder$, removeEmptyFolders$}; diff --git a/packages/@coorpacademy-components/scripts/observables/generate-stories.js b/packages/@coorpacademy-components/scripts/observables/generate-stories.js index d001a4b506..426c48efd9 100644 --- a/packages/@coorpacademy-components/scripts/observables/generate-stories.js +++ b/packages/@coorpacademy-components/scripts/observables/generate-stories.js @@ -1,4 +1,3 @@ -// generate-stories.js const {join, dirname, relative} = require('path'); const {of, from, concat} = require('rxjs'); const {map, toArray, mergeMap} = require('rxjs/operators'); @@ -29,7 +28,6 @@ const generateStories$ = (cwd) => { ) ); - // Observable that emits lines for fixture exports const fixtureExports$ = readComponentFixtures$({title, path, type}).pipe( map( ({fixture}) => ` From efe8b33cdff32688a267a24b3b1c71a3d49a3db9 Mon Sep 17 00:00:00 2001 From: silou Date: Thu, 27 Mar 2025 13:53:40 +0100 Subject: [PATCH 3/6] lint:fix --- .../scripts/observables/cleanup.js | 32 +++++++++---------- .../scripts/observables/generate-stories.js | 12 +++---- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/packages/@coorpacademy-components/scripts/observables/cleanup.js b/packages/@coorpacademy-components/scripts/observables/cleanup.js index cee9d6f527..ae89853f98 100644 --- a/packages/@coorpacademy-components/scripts/observables/cleanup.js +++ b/packages/@coorpacademy-components/scripts/observables/cleanup.js @@ -1,17 +1,15 @@ +const {readdir, rmdir, stat} = require('fs'); +const {dirname} = require('path'); +const {rm} = require('fs/promises'); const {Observable} = require('rxjs'); const {filter} = require('rxjs/operators'); const {walkDirectory$} = require('./walk-directory'); // your existing directory walker -const {readdir, rmdir, stat} = require('fs'); -const {dirname} = require('path'); -const {rm} = require('fs/promises'); -const readAllStoryFiles$ = (cwd) => - walkDirectory$(cwd).pipe( - filter((filePath) => filePath.endsWith('.stories.tsx')) - ); +const readAllStoryFiles$ = cwd => + walkDirectory$(cwd).pipe(filter(filePath => filePath.endsWith('.stories.tsx'))); const removeEmptyFolders$ = (folderPath, removeParents = false) => { - return new Observable((observer) => { + return new Observable(observer => { stat(folderPath, (statErr, stats) => { if (statErr || !stats.isDirectory()) { observer.complete(); @@ -23,10 +21,10 @@ const removeEmptyFolders$ = (folderPath, removeParents = false) => { return; } // Filter out system files if needed - files = files.filter((file) => file !== '.DS_Store'); + files = files.filter(file => file !== '.DS_Store'); if (files.length === 0) { - rmdir(folderPath, (rmdirErr) => { + rmdir(folderPath, rmdirErr => { if (rmdirErr) { observer.error(rmdirErr); return; @@ -34,8 +32,8 @@ const removeEmptyFolders$ = (folderPath, removeParents = false) => { observer.next(folderPath); if (removeParents && dirname(folderPath) !== folderPath) { removeEmptyFolders$(dirname(folderPath), true).subscribe( - (val) => observer.next(val), - (err) => observer.error(err), + val => observer.next(val), + err => observer.error(err), () => observer.complete() ); } else { @@ -49,20 +47,20 @@ const removeEmptyFolders$ = (folderPath, removeParents = false) => { }); }); }); -} +}; /** * Recursively deletes a folder (rm -rf). */ -const deleteFolder$ = (folderPath) => { - return new Observable((observer) => { +const deleteFolder$ = folderPath => { + return new Observable(observer => { rm(folderPath, {recursive: true, force: true}) .then(() => { observer.next(folderPath); observer.complete(); }) - .catch((err) => observer.error(err)); + .catch(err => observer.error(err)); }); -} +}; module.exports = {readAllStoryFiles$, deleteFolder$, removeEmptyFolders$}; diff --git a/packages/@coorpacademy-components/scripts/observables/generate-stories.js b/packages/@coorpacademy-components/scripts/observables/generate-stories.js index 426c48efd9..dc92fb9816 100644 --- a/packages/@coorpacademy-components/scripts/observables/generate-stories.js +++ b/packages/@coorpacademy-components/scripts/observables/generate-stories.js @@ -4,16 +4,12 @@ const {map, toArray, mergeMap} = require('rxjs/operators'); const {readComponentFixtures$} = require('./component-fixtures'); const {pascalCase} = require('./string'); const {readComponents$} = require('./components'); -const { - readAllStoryFiles$, - deleteFolder$, - removeEmptyFolders$ -} = require('./cleanup'); +const {readAllStoryFiles$, deleteFolder$, removeEmptyFolders$} = require('./cleanup'); /** * Generate story files for all current components. */ -const generateStories$ = (cwd) => { +const generateStories$ = cwd => { // 1) Build [storiesPath, lines$] pairs for each component const generation$ = readComponents$(cwd).pipe( map(({title, path, type, titleRaw, levels}) => { @@ -59,11 +55,11 @@ export default { // 2) Remove stale story folders, then emit the new generation array return generation$.pipe( - mergeMap((storyEntries) => { + mergeMap(storyEntries => { const desiredStoryPaths = new Set(storyEntries.map(([p]) => p)); return readAllStoryFiles$(cwd).pipe( - mergeMap((existingStoryPath) => { + mergeMap(existingStoryPath => { if (!desiredStoryPaths.has(existingStoryPath)) { // This story is stale const testFolder = dirname(existingStoryPath); From e903aa471a04bd445541396a32b927c21f0f2520 Mon Sep 17 00:00:00 2001 From: silou Date: Thu, 27 Mar 2025 15:53:33 +0100 Subject: [PATCH 4/6] lint:fix 2 --- .../@coorpacademy-components/scripts/observables/cleanup.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/@coorpacademy-components/scripts/observables/cleanup.js b/packages/@coorpacademy-components/scripts/observables/cleanup.js index ae89853f98..284a34342e 100644 --- a/packages/@coorpacademy-components/scripts/observables/cleanup.js +++ b/packages/@coorpacademy-components/scripts/observables/cleanup.js @@ -21,9 +21,9 @@ const removeEmptyFolders$ = (folderPath, removeParents = false) => { return; } // Filter out system files if needed - files = files.filter(file => file !== '.DS_Store'); + const files_ = files.filter(file => file !== '.DS_Store'); - if (files.length === 0) { + if (files_.length === 0) { rmdir(folderPath, rmdirErr => { if (rmdirErr) { observer.error(rmdirErr); @@ -58,6 +58,7 @@ const deleteFolder$ = folderPath => { .then(() => { observer.next(folderPath); observer.complete(); + return null; }) .catch(err => observer.error(err)); }); From 88e076609d0ef289183478fcaa3998d847306ac1 Mon Sep 17 00:00:00 2001 From: silou Date: Thu, 27 Mar 2025 23:26:53 +0100 Subject: [PATCH 5/6] use shareReplay for optmisation --- .../scripts/observables/generate-stories.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/@coorpacademy-components/scripts/observables/generate-stories.js b/packages/@coorpacademy-components/scripts/observables/generate-stories.js index dc92fb9816..868d5d9f46 100644 --- a/packages/@coorpacademy-components/scripts/observables/generate-stories.js +++ b/packages/@coorpacademy-components/scripts/observables/generate-stories.js @@ -1,6 +1,6 @@ const {join, dirname, relative} = require('path'); const {of, from, concat} = require('rxjs'); -const {map, toArray, mergeMap} = require('rxjs/operators'); +const {map, toArray, mergeMap, shareReplay} = require('rxjs/operators'); const {readComponentFixtures$} = require('./component-fixtures'); const {pascalCase} = require('./string'); const {readComponents$} = require('./components'); @@ -16,15 +16,19 @@ const generateStories$ = cwd => { const testPath = join(path, 'test'); const storiesPath = join(testPath, 'index.stories.tsx'); - // Observable that emits lines for fixture imports - const fixtureImports$ = readComponentFixtures$({title, path, type}).pipe( + // 1) Read the fixtures once, share the results + const fixtures$ = readComponentFixtures$({title, path, type}).pipe( + shareReplay(1) // caches and replays the emitted fixtures + ); + + const fixtureImports$ = fixtures$.pipe( map( ({fixture, fixturePath}) => `import fixture${fixture} from './${relative(testPath, fixturePath)}';` ) ); - const fixtureExports$ = readComponentFixtures$({title, path, type}).pipe( + const fixtureExports$ = fixtures$.pipe( map( ({fixture}) => ` export const ${pascalCase(fixture)} = (args: any) => <${title} {...args} />; @@ -32,7 +36,6 @@ ${pascalCase(fixture)}.args = fixture${fixture}.props;` ) ); - // Combine all lines in a single stream with concat() const content$ = concat( // 1) Basic imports of(`import React from 'react';`, `import ${title} from '..';`), @@ -50,7 +53,7 @@ export default { return [storiesPath, content$]; }), - toArray() // gather all story definitions into an array + toArray() ); // 2) Remove stale story folders, then emit the new generation array From 5494743667abd0ac8f1b4ad2fb55c79fd1f4b9ab Mon Sep 17 00:00:00 2001 From: silou Date: Mon, 31 Mar 2025 13:11:05 +0200 Subject: [PATCH 6/6] add correct chare replay params --- .../scripts/observables/generate-stories.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@coorpacademy-components/scripts/observables/generate-stories.js b/packages/@coorpacademy-components/scripts/observables/generate-stories.js index 868d5d9f46..f0cd6e5dfb 100644 --- a/packages/@coorpacademy-components/scripts/observables/generate-stories.js +++ b/packages/@coorpacademy-components/scripts/observables/generate-stories.js @@ -18,7 +18,8 @@ const generateStories$ = cwd => { // 1) Read the fixtures once, share the results const fixtures$ = readComponentFixtures$({title, path, type}).pipe( - shareReplay(1) // caches and replays the emitted fixtures + shareReplay({bufferSize: Infinity, refCount: true}) + // caches and replays the emitted fixtures ); const fixtureImports$ = fixtures$.pipe(