1
1
const fs = require ( 'fs' ) ;
2
2
const path = require ( 'path' ) ;
3
3
const util = require ( 'util' ) ;
4
+ const assert = require ( 'assert' ) ;
4
5
const yaml = require ( 'js-yaml' ) ;
5
- const { findIndex } = require ( 'lodash' ) ;
6
+ const { findIndex, isEmpty } = require ( 'lodash' ) ;
6
7
const readDirP = require ( 'readdirp' ) ;
7
8
const { helpCategoryMap } = require ( '../client/utils/challenge-types' ) ;
8
9
const { showUpcomingChanges } = require ( '../config/env.json' ) ;
@@ -22,13 +23,13 @@ const { getSuperOrder, getSuperBlockFromDir } = require('./utils');
22
23
23
24
const access = util . promisify ( fs . access ) ;
24
25
25
- const challengesDir = path . resolve ( __dirname , './ challenges' ) ;
26
- const metaDir = path . resolve ( challengesDir , '_meta' ) ;
27
- exports . challengesDir = challengesDir ;
28
- exports . metaDir = metaDir ;
26
+ const CHALLENGES_DIR = path . resolve ( __dirname , 'challenges' ) ;
27
+ const META_DIR = path . resolve ( CHALLENGES_DIR , '_meta' ) ;
28
+ exports . CHALLENGES_DIR = CHALLENGES_DIR ;
29
+ exports . META_DIR = META_DIR ;
29
30
30
31
const COMMENT_TRANSLATIONS = createCommentMap (
31
- path . resolve ( __dirname , './ dictionaries' )
32
+ path . resolve ( __dirname , 'dictionaries' )
32
33
) ;
33
34
34
35
function getTranslatableComments ( dictionariesDir ) {
@@ -109,20 +110,19 @@ function getTranslationEntry(dicts, { engId, text }) {
109
110
if ( entry ) {
110
111
return { ...acc , [ lang ] : entry } ;
111
112
} else {
112
- throw Error ( `Missing translation for comment
113
- '${ text } '
114
- with id of ${ engId } ` ) ;
113
+ // default to english
114
+ return { ...acc , [ lang ] : text } ;
115
115
}
116
116
} , { } ) ;
117
117
}
118
118
119
119
function getChallengesDirForLang ( lang ) {
120
- return path . resolve ( challengesDir , `./ ${ lang } ` ) ;
120
+ return path . resolve ( CHALLENGES_DIR , `${ lang } ` ) ;
121
121
}
122
122
123
123
function getMetaForBlock ( block ) {
124
124
return JSON . parse (
125
- fs . readFileSync ( path . resolve ( metaDir , `./ ${ block } /meta.json` ) , 'utf8' )
125
+ fs . readFileSync ( path . resolve ( META_DIR , `${ block } /meta.json` ) , 'utf8' )
126
126
) ;
127
127
}
128
128
@@ -153,7 +153,9 @@ const walk = (root, target, options, cb) => {
153
153
} ;
154
154
155
155
exports . getChallengesForLang = async function getChallengesForLang ( lang ) {
156
- const root = getChallengesDirForLang ( lang ) ;
156
+ // english determines the shape of the curriculum, all other languages mirror
157
+ // it.
158
+ const root = getChallengesDirForLang ( 'english' ) ;
157
159
// scaffold the curriculum, first set up the superblocks, then recurse into
158
160
// the blocks
159
161
const curriculum = await walk (
@@ -162,6 +164,9 @@ exports.getChallengesForLang = async function getChallengesForLang(lang) {
162
164
{ type : 'directories' , depth : 0 } ,
163
165
buildSuperBlocks
164
166
) ;
167
+ Object . entries ( curriculum ) . forEach ( ( [ name , superBlock ] ) => {
168
+ assert ( ! isEmpty ( superBlock . blocks ) , `superblock ${ name } has no blocks` ) ;
169
+ } ) ;
165
170
const cb = ( file , curriculum ) => buildChallenges ( file , curriculum , lang ) ;
166
171
// fill the scaffold with the challenges
167
172
return walk (
@@ -173,10 +178,7 @@ exports.getChallengesForLang = async function getChallengesForLang(lang) {
173
178
} ;
174
179
175
180
async function buildBlocks ( { basename : blockName } , curriculum , superBlock ) {
176
- const metaPath = path . resolve (
177
- __dirname ,
178
- `./challenges/_meta/${ blockName } /meta.json`
179
- ) ;
181
+ const metaPath = path . resolve ( META_DIR , `${ blockName } /meta.json` ) ;
180
182
181
183
if ( fs . existsSync ( metaPath ) ) {
182
184
// try to read the file, if the meta path does not exist it should be a certification.
@@ -240,9 +242,10 @@ async function buildChallenges({ path: filePath }, curriculum, lang) {
240
242
) {
241
243
return ;
242
244
}
245
+ const createChallenge = generateChallengeCreator ( CHALLENGES_DIR , lang ) ;
243
246
const challenge = isCert
244
- ? await createCertification ( challengesDir , filePath , lang )
245
- : await createChallenge ( challengesDir , filePath , lang , meta ) ;
247
+ ? await createCertification ( CHALLENGES_DIR , filePath , lang )
248
+ : await createChallenge ( filePath , meta ) ;
246
249
247
250
challengeBlock . challenges = [ ...challengeBlock . challenges , challenge ] ;
248
251
}
@@ -258,8 +261,7 @@ async function parseTranslation(transPath, dict, lang, parse = parseMD) {
258
261
: translatedChal ;
259
262
}
260
263
261
- // eslint-disable-next-line no-unused-vars
262
- async function createCertification ( basePath , filePath , lang ) {
264
+ async function createCertification ( basePath , filePath ) {
263
265
function getFullPath ( pathLang ) {
264
266
return path . resolve ( __dirname , basePath , pathLang , filePath ) ;
265
267
}
@@ -270,90 +272,111 @@ async function createCertification(basePath, filePath, lang) {
270
272
return parseCert ( getFullPath ( 'english' ) ) ;
271
273
}
272
274
273
- async function createChallenge ( basePath , filePath , lang , maybeMeta ) {
274
- function getFullPath ( pathLang ) {
275
+ // This is a slightly weird abstraction, but it lets us define helper functions
276
+ // without passing around a ton of arguments.
277
+ function generateChallengeCreator ( basePath , lang ) {
278
+ function getFullPath ( pathLang , filePath ) {
275
279
return path . resolve ( __dirname , basePath , pathLang , filePath ) ;
276
280
}
277
- let meta ;
278
- if ( maybeMeta ) {
279
- meta = maybeMeta ;
280
- } else {
281
- const metaPath = path . resolve (
282
- metaDir ,
283
- `./${ getBlockNameFromPath ( filePath ) } /meta.json`
284
- ) ;
285
- meta = require ( metaPath ) ;
286
- }
287
- const { superBlock } = meta ;
288
- if ( ! curriculumLangs . includes ( lang ) )
289
- throw Error ( `${ lang } is not a accepted language.
290
- Trying to parse ${ filePath } ` ) ;
291
- if ( lang !== 'english' && ! ( await hasEnglishSource ( basePath , filePath ) ) )
292
- throw Error ( `Missing English challenge for
281
+
282
+ async function validate ( filePath , superBlock ) {
283
+ const invalidLang = ! curriculumLangs . includes ( lang ) ;
284
+ if ( invalidLang )
285
+ throw Error ( `${ lang } is not a accepted language.
286
+ Trying to parse ${ filePath } ` ) ;
287
+
288
+ const missingEnglish =
289
+ lang !== 'english' && ! ( await hasEnglishSource ( basePath , filePath ) ) ;
290
+ if ( missingEnglish )
291
+ throw Error ( `Missing English challenge for
293
292
${ filePath }
294
293
It should be in
295
- ${ getFullPath ( 'english' ) }
294
+ ${ getFullPath ( 'english' , filePath ) }
296
295
` ) ;
297
- // assumes superblock names are unique
298
- // while the auditing is ongoing, we default to English for un-audited certs
299
- // once that's complete, we can revert to using isEnglishChallenge(fullPath)
300
- const useEnglish = lang === 'english' || ! isAuditedCert ( lang , superBlock ) ;
301
-
302
- const challenge = await ( useEnglish
303
- ? parseMD ( getFullPath ( 'english' ) )
304
- : parseTranslation ( getFullPath ( lang ) , COMMENT_TRANSLATIONS , lang ) ) ;
305
-
306
- const challengeOrder = findIndex (
307
- meta . challengeOrder ,
308
- ( [ id ] ) => id === challenge . id
309
- ) ;
310
- const {
311
- name : blockName ,
312
- hasEditableBoundaries,
313
- order,
314
- isPrivate,
315
- required = [ ] ,
316
- template,
317
- time,
318
- usesMultifileEditor
319
- } = meta ;
320
- challenge . block = dasherize ( blockName ) ;
321
- challenge . hasEditableBoundaries = ! ! hasEditableBoundaries ;
322
- challenge . order = order ;
323
- const superOrder = getSuperOrder ( superBlock , {
324
- showNewCurriculum : process . env . SHOW_NEW_CURRICULUM === 'true'
325
- } ) ;
326
- if ( superOrder !== null ) challenge . superOrder = superOrder ;
327
- /* Since there can be more than one way to complete a certification (using the
296
+
297
+ const missingAuditedChallenge =
298
+ isAuditedCert ( lang , superBlock ) &&
299
+ ! fs . existsSync ( getFullPath ( lang , filePath ) ) ;
300
+ if ( missingAuditedChallenge )
301
+ throw Error ( `Missing ${ lang } audited challenge for
302
+ ${ filePath }
303
+ No audited challenges should fallback to English.
304
+ ` ) ;
305
+ }
306
+
307
+ function addMetaToChallenge ( challenge , meta ) {
308
+ const challengeOrder = findIndex (
309
+ meta . challengeOrder ,
310
+ ( [ id ] ) => id === challenge . id
311
+ ) ;
312
+
313
+ challenge . block = meta . name ? dasherize ( meta . name ) : null ;
314
+ challenge . hasEditableBoundaries = ! ! meta . hasEditableBoundaries ;
315
+ challenge . order = meta . order ;
316
+ const superOrder = getSuperOrder ( meta . superBlock , {
317
+ showNewCurriculum : process . env . SHOW_NEW_CURRICULUM === 'true'
318
+ } ) ;
319
+ if ( superOrder !== null ) challenge . superOrder = superOrder ;
320
+ /* Since there can be more than one way to complete a certification (using the
328
321
legacy curriculum or the new one, for instance), we need a certification
329
322
field to track which certification this belongs to. */
330
- // TODO: generalize this to all superBlocks
331
- challenge . certification =
332
- superBlock === '2022/responsive-web-design'
333
- ? 'responsive-web-design'
334
- : superBlock ;
335
- challenge . superBlock = superBlock ;
336
- challenge . challengeOrder = challengeOrder ;
337
- challenge . isPrivate = challenge . isPrivate || isPrivate ;
338
- challenge . required = required . concat ( challenge . required || [ ] ) ;
339
- challenge . template = template ;
340
- challenge . time = time ;
341
- challenge . helpCategory =
342
- challenge . helpCategory || helpCategoryMap [ challenge . block ] ;
343
- challenge . translationPending =
344
- lang !== 'english' && ! isAuditedCert ( lang , superBlock ) ;
345
- challenge . usesMultifileEditor = ! ! usesMultifileEditor ;
346
- if ( challenge . challengeFiles ) {
347
- // The client expects the challengeFiles to be an array of polyvinyls
348
- challenge . challengeFiles = challengeFilesToPolys ( challenge . challengeFiles ) ;
349
- }
350
- if ( challenge . solutions ?. length ) {
351
- // The test runner needs the solutions to be arrays of polyvinyls so it
352
- // can sort them correctly.
353
- challenge . solutions = challenge . solutions . map ( challengeFilesToPolys ) ;
323
+ // TODO: generalize this to all superBlocks
324
+ challenge . certification =
325
+ meta . superBlock === '2022/responsive-web-design'
326
+ ? 'responsive-web-design'
327
+ : meta . superBlock ;
328
+ challenge . superBlock = meta . superBlock ;
329
+ challenge . challengeOrder = challengeOrder ;
330
+ challenge . isPrivate = challenge . isPrivate || meta . isPrivate ;
331
+ challenge . required = ( meta . required || [ ] ) . concat ( challenge . required || [ ] ) ;
332
+ challenge . template = meta . template ;
333
+ challenge . time = meta . time ;
334
+ challenge . helpCategory =
335
+ challenge . helpCategory || helpCategoryMap [ challenge . block ] ;
336
+ challenge . translationPending =
337
+ lang !== 'english' && ! isAuditedCert ( lang , meta . superBlock ) ;
338
+ challenge . usesMultifileEditor = ! ! meta . usesMultifileEditor ;
339
+ if ( challenge . challengeFiles ) {
340
+ // The client expects the challengeFiles to be an array of polyvinyls
341
+ challenge . challengeFiles = challengeFilesToPolys (
342
+ challenge . challengeFiles
343
+ ) ;
344
+ }
345
+ if ( challenge . solutions ?. length ) {
346
+ // The test runner needs the solutions to be arrays of polyvinyls so it
347
+ // can sort them correctly.
348
+ challenge . solutions = challenge . solutions . map ( challengeFilesToPolys ) ;
349
+ }
354
350
}
355
351
356
- return challenge ;
352
+ async function createChallenge ( filePath , maybeMeta ) {
353
+ const meta = maybeMeta
354
+ ? maybeMeta
355
+ : require ( path . resolve (
356
+ META_DIR ,
357
+ `${ getBlockNameFromPath ( filePath ) } /meta.json`
358
+ ) ) ;
359
+
360
+ await validate ( filePath , meta . superBlock ) ;
361
+
362
+ const useEnglish =
363
+ lang === 'english' ||
364
+ ! isAuditedCert ( lang , meta . superBlock ) ||
365
+ ! fs . existsSync ( getFullPath ( lang , filePath ) ) ;
366
+
367
+ const challenge = await ( useEnglish
368
+ ? parseMD ( getFullPath ( 'english' , filePath ) )
369
+ : parseTranslation (
370
+ getFullPath ( lang , filePath ) ,
371
+ COMMENT_TRANSLATIONS ,
372
+ lang
373
+ ) ) ;
374
+
375
+ addMetaToChallenge ( challenge , meta ) ;
376
+
377
+ return challenge ;
378
+ }
379
+ return createChallenge ;
357
380
}
358
381
359
382
function challengeFilesToPolys ( files ) {
@@ -390,4 +413,4 @@ function getBlockNameFromPath(filePath) {
390
413
391
414
exports . hasEnglishSource = hasEnglishSource ;
392
415
exports . parseTranslation = parseTranslation ;
393
- exports . createChallenge = createChallenge ;
416
+ exports . generateChallengeCreator = generateChallengeCreator ;
0 commit comments