Skip to content

Commit 532918b

Browse files
authored
Validate css folders (#68)
* add validation of css subfolder structure * add tests for validation of css folders
1 parent 115e4ad commit 532918b

File tree

5 files changed

+141
-42
lines changed

5 files changed

+141
-42
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ The following example would allow a folder with the name `js` that contains file
126126

127127
#### CSSDirectoryStructure
128128

129-
This option allows you to specify a custom CSS folder structure. This is used in the [package creation](#package-creation) step to generate a sub-folder structure within a specified folder, to assist in quickly spinning up a new package. It is not used in the [validation](#package-validation) or [publication](#package-publication) steps.
129+
This option allows you to specify a custom CSS folder structure. This is used in the [package creation](#package-creation) step to generate a sub-folder structure within a specified folder, to assist in quickly spinning up a new package. It is also used in the [validation](#package-validation) step to make sure that only valid CSS subdirectory naming is used.
130130

131131
The following shows an example folder structure, taken from the [Springer Nature Front-End Toolkits](https://github.com/springernature/frontend-toolkits) repository:
132132

__mocks__/fs.js

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,27 @@ const defaultPackageContents = {...{
3636
'.adotfile': 'file content'
3737
}, ...defaultFolders};
3838

39+
const brandPackageContents = {
40+
brandA: {
41+
'fileA.ext': 'file content here'
42+
},
43+
brandB: {
44+
'fileB.ext': 'file content here'
45+
}
46+
};
47+
48+
const cssFolderPackageContents = {
49+
'required.md': 'file content',
50+
folder1: {
51+
a: {
52+
'fileA.scss': 'file content here'
53+
},
54+
b: {
55+
'fileB.scss': 'file content here'
56+
}
57+
}
58+
};
59+
3960
const __fsMockFiles = () => {
4061
return {
4162
'packages/package/pass': defaultPackageContents,
@@ -52,41 +73,30 @@ const __fsMockFiles = () => {
5273
'packages/package/failIsFileType': defaultPackageContents,
5374
'packages/package/failIsTopLevelFile': defaultPackageContents,
5475
'path/to/global-package': defaultPackageContents,
55-
'path/to/global-package-b': {
56-
'some-file.txt': 'file content here',
57-
'empty-dir': {/** empty directory */}
58-
},
76+
'path/to/global-package-b': {'some-file.txt': 'file content here', 'empty-dir': {/** empty directory */}},
5977
'path/to/some.png': Buffer.from([8, 6, 7, 5, 3, 0, 9]),
6078
'some/other/path': {/** another empty directory */},
6179
'home/user/.npmrc': '//mock-registry.npmjs.org/:_authToken=xyz',
6280
'home/user-b/.npmrc': `//registry.npmjs.org/:_authToken=$\{NPM_TOKEN}`,
6381
'home/user-c/.npmrc': `//registry.npmjs.org/:_authToken=$\{OTHER_NPM_TOKEN}`,
64-
'context/brand-context': {
65-
brandA: {
66-
'fileA.ext': 'file content here'
67-
},
68-
brandB: {
69-
'fileB.ext': 'file content here'
70-
}
71-
},
72-
'context/brand-context-disallowed': {
73-
brandA: {
74-
'fileA.ext': 'file content here'
75-
},
76-
brandB: {
77-
'fileB.ext': 'file content here'
78-
},
79-
brandC: {
80-
'fileB.ext': 'file content here'
82+
'context/brand-context': brandPackageContents,
83+
'context/brand-context-disallowed': {...brandPackageContents, ...{brandC: {'fileB.ext': 'file content here'}}},
84+
'context/brand-context-empty': {/** empty directory */},
85+
'valid-context/brand-context': brandPackageContents,
86+
'packages/package/passWithCss': {
87+
...cssFolderPackageContents,
88+
...{
89+
folder1: {
90+
...cssFolderPackageContents.folder1, ...{c: {'fileC.css': 'file content here'}}
91+
}
8192
}
8293
},
83-
'context/brand-context-empty': {/** empty directory */},
84-
'valid-context/brand-context': {
85-
brandA: {
86-
'fileA.ext': 'file content here'
87-
},
88-
brandB: {
89-
'fileB.ext': 'file content here'
94+
'packages/package/failIsCssFolder': {
95+
...cssFolderPackageContents,
96+
...{
97+
folder1: {
98+
...cssFolderPackageContents.folder1, ...{d: {'fileD.css': 'file content here'}}
99+
}
90100
}
91101
}
92102
};

__mocks__/glob-results.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,26 @@ const packageFiles = {
168168
'packages/package/passContextWithReadme/brandA/folder2/file.json',
169169
'packages/package/passContextWithReadme/brandA/folder2/subfolder',
170170
'packages/package/passContextWithReadme/brandA/folder2/subfolder/file.js'
171+
],
172+
'packages/package/passWithCss/**/*': [
173+
'packages/package/passWithCss/required.md',
174+
'packages/package/passWithCss/folder1',
175+
'packages/package/passWithCss/folder1/a',
176+
'packages/package/passWithCss/folder1/b',
177+
'packages/package/passWithCss/folder1/c',
178+
'packages/package/passWithCss/folder1/a/fileA.scss',
179+
'packages/package/passWithCss/folder1/b/fileB.scss',
180+
'packages/package/passWithCss/folder1/c/fileC.css'
181+
],
182+
'packages/package/failIsCssFolder/**/*': [
183+
'packages/package/failIsCssFolder/required.md',
184+
'packages/package/failIsCssFolder/folder1',
185+
'packages/package/failIsCssFolder/folder1/a',
186+
'packages/package/failIsCssFolder/folder1/b',
187+
'packages/package/failIsCssFolder/folder1/d',
188+
'packages/package/failIsCssFolder/folder1/a/fileA.scss',
189+
'packages/package/failIsCssFolder/folder1/b/fileB.scss',
190+
'packages/package/failIsCssFolder/folder1/d/fileD.css'
171191
]
172192
};
173193

__tests__/unit/_validate/check-package-structure.test.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ const validationConfigNoFolders = {
3535
required: ['required.md']
3636
};
3737

38+
const validationConfigWithCss = {
39+
required: ['required.md'],
40+
folders: {
41+
folder1: ['scss', 'css']
42+
},
43+
CSSDirectoryStructure: {
44+
folder1: ['a', 'b', 'c']
45+
}
46+
};
47+
3848
describe('Check validation', () => {
3949
beforeEach(() => {
4050
mockfs(MOCK_PACKAGES);
@@ -142,4 +152,18 @@ describe('Check validation', () => {
142152
checkValidation(validationConfigWithChangelog, 'packages/package/passContextWithReadme', ['brandA'])
143153
).resolves.toEqual();
144154
});
155+
156+
test('Resolves when filesystem matches validationConfigWithCss', async () => {
157+
expect.assertions(1);
158+
await expect(
159+
checkValidation(validationConfigWithCss, 'packages/package/passWithCss')
160+
).resolves.toEqual();
161+
});
162+
163+
test('Rejects when invalid folder within CSS configuration', async () => {
164+
expect.assertions(1);
165+
await expect(
166+
checkValidation(validationConfigWithCss, 'packages/package/failIsCssFolder')
167+
).rejects.toThrowError(new Error('Invalid files or folders in failIsCssFolder'));
168+
});
145169
});

lib/js/_validate/_check-package-structure.js

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -84,18 +84,43 @@ function isRequired(relativeFilePath) {
8484
return required;
8585
}
8686

87+
/**
88+
* Check for valid paths within CSS folder structure
89+
* @private
90+
* @function isValidCssFolderPath
91+
* @param {String} relativeFilePath relative file/folder name
92+
* @param {String} topLevelFolder parent folder name
93+
* @param {Object} cssDirectoryStructure CSS directory structure
94+
* @return {Boolean}
95+
*/
96+
function isValidCssFolderPath(relativeFilePath, topLevelFolder, cssDirectoryStructure) {
97+
for (const folder of cssDirectoryStructure[topLevelFolder]) {
98+
const validCssPath = path.join(topLevelFolder, folder);
99+
100+
if (relativeFilePath.startsWith(validCssPath)) {
101+
return true;
102+
}
103+
}
104+
105+
return false;
106+
}
107+
87108
/**
88109
* Check if glob item is a valid folder name
89110
* If configFolders not set in config, all folders valid
90111
* @private
91112
* @function isFolder
92113
* @param {String} filePath full path to item
93114
* @param {String} relativeFilePath relative file/folder name
115+
* @param {Object} cssDirectoryStructure CSS directory structure
94116
* @param {String} brand optional name of the context brand
95117
* @return {Boolean}
96118
*/
97-
function isFolder(filePath, relativeFilePath, brand) {
119+
function isFolder(filePath, relativeFilePath, cssDirectoryStructure, brand) {
98120
const relativeFileName = (brand) ? `${brand}/${relativeFilePath}` : relativeFilePath;
121+
const splitGlob = relativeFilePath.split(path.sep);
122+
const topLevelFolder = splitGlob[0];
123+
let isValid = false;
99124

100125
// This is not a directory
101126
if (!fs.lstatSync(filePath).isDirectory()) {
@@ -107,8 +132,14 @@ function isFolder(filePath, relativeFilePath, brand) {
107132
return true;
108133
}
109134

110-
const splitGlob = relativeFilePath.split(path.sep);
111-
const isValid = configFolders.includes(splitGlob[0]);
135+
// Valid CSS subfolder or valid topLevelFolder
136+
isValid = (
137+
cssDirectoryStructure && // CSS folder structure is set
138+
topLevelFolder in cssDirectoryStructure && // This is a CSS folder
139+
splitGlob.length > 1 // This is not the topLevelFolder CSS folder
140+
) ?
141+
isValidCssFolderPath(relativeFilePath, topLevelFolder, cssDirectoryStructure) :
142+
configFolders.includes(topLevelFolder);
112143

113144
if (isValid) {
114145
reporter.success('validating', relativeFileName, 'is a valid folder');
@@ -126,12 +157,16 @@ function isFolder(filePath, relativeFilePath, brand) {
126157
* @private
127158
* @function isFileType
128159
* @param {String} relativeFilePath relative file/folder name
160+
* @param {Object} cssDirectoryStructure CSS directory structure
129161
* @param {String} brand optional name of the context brand
130162
* @return {Boolean}
131163
*/
132-
function isFileType(relativeFilePath, brand) {
164+
function isFileType(relativeFilePath, cssDirectoryStructure, brand) {
133165
const relativeFileName = (brand) ? `${brand}/${relativeFilePath}` : relativeFilePath;
134166
const splitGlob = relativeFilePath.split(path.sep);
167+
const topLevelFolder = splitGlob[0];
168+
const fileType = path.extname(relativeFilePath).slice(1);
169+
let isValid = false;
135170

136171
// This is a top level file
137172
if (splitGlob.length === 1) {
@@ -143,12 +178,18 @@ function isFileType(relativeFilePath, brand) {
143178
return true;
144179
}
145180

146-
const topLevelFolder = splitGlob[0];
147-
const fileType = path.extname(relativeFilePath).slice(1);
148-
149181
// Is a valid extension within a valid folder?
150182
if (configFolders.includes(topLevelFolder)) {
151-
const isValid = config.folders[topLevelFolder].includes(fileType);
183+
const isValidFileType = configFolders.includes(topLevelFolder) && config.folders[topLevelFolder].includes(fileType);
184+
185+
// Valid file in CSS subfolder or valid filetype within topLevelFolder
186+
isValid = (
187+
cssDirectoryStructure && // CSS folder structure is set
188+
topLevelFolder in cssDirectoryStructure && // This is a CSS folder
189+
splitGlob.length > 2 // not a file within top level CSS folder
190+
) ?
191+
isValidCssFolderPath(relativeFilePath, topLevelFolder, cssDirectoryStructure) && isValidFileType :
192+
isValidFileType;
152193

153194
if (!isValid) {
154195
reporter.fail('validating', relativeFileName, 'is not a valid file');
@@ -207,10 +248,11 @@ function removeNonValidatedPaths(filePaths) {
207248
* @function checkPackageStructure
208249
* @param {String} pathToPackage package path on filesystem
209250
* @param {Object} globSettings configuration for glob search
251+
* @param {Object} cssDirectoryStructure CSS directory structure
210252
* @param {String} brand optional name of the context brand
211253
* @return {Promise}
212254
*/
213-
async function checkPackageStructure(pathToPackage, globSettings, brand) {
255+
async function checkPackageStructure(pathToPackage, globSettings, cssDirectoryStructure, brand) {
214256
try {
215257
const filePaths = await globby(globSettings.pattern, globSettings.options);
216258
const pathsToValidate = removeNonValidatedPaths(filePaths);
@@ -224,8 +266,8 @@ async function checkPackageStructure(pathToPackage, globSettings, brand) {
224266
if (
225267
!isRequired(relativeFilePath) &&
226268
!isBrandReadme(brand, relativeFileName) &&
227-
!isFolder(filePath, relativeFilePath, brand) &&
228-
!isFileType(relativeFilePath, brand)
269+
!isFolder(filePath, relativeFilePath, cssDirectoryStructure, brand) &&
270+
!isFileType(relativeFilePath, cssDirectoryStructure, brand)
229271
) {
230272
// If not recongnised at any other step then invalid top level file
231273
reporter.fail('validating', relativeFileName, 'is not a valid top level file');
@@ -271,7 +313,8 @@ async function init(validationConfig, pathToPackage, configuredBrands) {
271313
{
272314
pattern: `${pathToPackage}/*`,
273315
options: {onlyFiles: true}
274-
}
316+
},
317+
config.CSSDirectoryStructure
275318
);
276319

277320
// Validate files within each brand
@@ -282,6 +325,7 @@ async function init(validationConfig, pathToPackage, configuredBrands) {
282325
pattern: `${pathToPackage}/${brand}/**/*`,
283326
options: {onlyFiles: false}
284327
},
328+
config.CSSDirectoryStructure,
285329
brand
286330
);
287331
}
@@ -292,7 +336,8 @@ async function init(validationConfig, pathToPackage, configuredBrands) {
292336
{
293337
pattern: `${pathToPackage}/**/*`,
294338
options: {onlyFiles: false}
295-
}
339+
},
340+
config.CSSDirectoryStructure
296341
);
297342
}
298343

0 commit comments

Comments
 (0)