diff --git a/package-lock.json b/package-lock.json index 58881cb03..e987084e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "devDependencies": { "@laboratoria/curriculum-parser": "^6.0.0-alpha.0", "@laboratoria/mdlint": "^1.2.3", + "@laboratoria/sdk-js": "8.0.0-beta.0", "@octokit/rest": "^20.0.2", "@sentry/vite-plugin": "^2.16.0", "@testing-library/jest-dom": "^6.1.5", @@ -1380,9 +1381,9 @@ "license": "Apache-2.0" }, "node_modules/@firebase/app": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.10.4.tgz", - "integrity": "sha512-oKd5cT+fDbQ22X8Am3tBOrSFdDp8n4NJDqld4uo+H/PL9F+D3ogtTeiPyDWw1lZK7FsMbmtRrPRozlmJFzSKAQ==", + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.10.5.tgz", + "integrity": "sha512-iY/fNot+hWPk9sTX8aHMqlcX9ynRvpGkskWAdUZ2eQQdLo8d1hSFYcYNwPv0Q/frGMasw8udKWMcFOEpC9fG8g==", "license": "Apache-2.0", "dependencies": { "@firebase/component": "0.6.7", @@ -1437,12 +1438,12 @@ "license": "Apache-2.0" }, "node_modules/@firebase/app-compat": { - "version": "0.2.34", - "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.34.tgz", - "integrity": "sha512-enteBla1gBYObauvsC9bRRoqHZnOW48ahYABZ+l+FEiWil1rw0gVihl8D4eLqtQp/ci8+fbOBf3ZL19uFq/OCw==", + "version": "0.2.35", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.35.tgz", + "integrity": "sha512-vgay/WRjeH0r97/Q6L6df2CMx7oyNFDsE5yPQ9oR1G+zx2eT0s8vNNh0WlKqQxUEWaOLRnXhQ8gy7uu0cBgTRg==", "license": "Apache-2.0", "dependencies": { - "@firebase/app": "0.10.4", + "@firebase/app": "0.10.5", "@firebase/component": "0.6.7", "@firebase/logger": "0.4.2", "@firebase/util": "1.9.6", @@ -1456,12 +1457,12 @@ "license": "Apache-2.0" }, "node_modules/@firebase/auth-compat": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.5.8.tgz", - "integrity": "sha512-qUgmv/mcth9wHPTOCKgAOeHe5c+BIOJVcbX2RfcjlXO3xnd8nRafwEkZKBNJUjy4oihYhqFMEMnTHLhwLJwLig==", + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.5.9.tgz", + "integrity": "sha512-RX8Zh/3zz2CsVbmYfgHkfUm4fAEPCl+KHVIImNygV5jTGDF6oKOhBIpf4Yigclyu8ESQKZ4elyN0MBYm9/7zGw==", "license": "Apache-2.0", "dependencies": { - "@firebase/auth": "1.7.3", + "@firebase/auth": "1.7.4", "@firebase/auth-types": "0.12.2", "@firebase/component": "0.6.7", "@firebase/util": "1.9.6", @@ -1473,9 +1474,9 @@ } }, "node_modules/@firebase/auth-compat/node_modules/@firebase/auth": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.7.3.tgz", - "integrity": "sha512-RiU1PjziOxLuyswtYtLK2qSjHIQJQGCk1h986SUFRbMQfzLXbQg8ZgXwxac1UAfDOzgzqPNCXhBuIlSK2UomoQ==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.7.4.tgz", + "integrity": "sha512-d2Fw17s5QesojwebrA903el20Li9/YGgkoOGJjagM4I1qAT36APa/FcZ+OX86KxbYKCtQKTMqraU8pxG7C2JWA==", "license": "Apache-2.0", "dependencies": { "@firebase/component": "0.6.7", @@ -1863,9 +1864,9 @@ } }, "node_modules/@firebase/vertexai-preview": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@firebase/vertexai-preview/-/vertexai-preview-0.0.1.tgz", - "integrity": "sha512-N8m9Xr0YZKy0t9SpQDuHrL2ppEAT/iqf88Y/O00QNA/Td/BMCL8sJ0c+Savh1TVrqh0rNp9n6HkZ39e/O5mwhA==", + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@firebase/vertexai-preview/-/vertexai-preview-0.0.2.tgz", + "integrity": "sha512-NOOL63kFQRq45ioi5P+hlqj/4LNmvn1URhGjQdvyV54c1Irvoq26aW861PRRLjrSMIeNeiLtCLD5pe+ediepAg==", "license": "Apache-2.0", "dependencies": { "@firebase/app-check-interop-types": "0.3.2", @@ -2009,9 +2010,9 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.9.14", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.14.tgz", - "integrity": "sha512-nOpuzZ2G3IuMFN+UPPpKrC6NsLmWsTqSsm66IRfnBt1D4pwTqE27lmbpcPM+l2Ua4gE7PfjRHI6uedAy7hoXUw==", + "version": "1.9.15", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", + "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==", "license": "Apache-2.0", "dependencies": { "@grpc/proto-loader": "^0.7.8", @@ -3124,12 +3125,14 @@ } }, "node_modules/@laboratoria/sdk-js": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@laboratoria/sdk-js/-/sdk-js-7.5.0.tgz", - "integrity": "sha512-OT4ACJUa/QTbmPqbTJXGyJ2jtL/2m4E26VP98aMDlPv7jEDYFImnmdjxE9nP1/zwzb4dPaTykTlK9EFvAEXNMw==", + "version": "8.0.0-beta.0", + "resolved": "https://registry.npmjs.org/@laboratoria/sdk-js/-/sdk-js-8.0.0-beta.0.tgz", + "integrity": "sha512-56QBgVjfbLxaIT/NWm+pN7RZqgN5TXWFLkA5X25NiIbpUBhyXZRm6JOv+j3bhg1kZ5/fPg/Bj+Ra0+AI3L+aPw==", + "dev": true, + "license": "MIT", "dependencies": { "blueimp-md5": "^2.19.0", - "firebase": "^10.9.0" + "firebase": "^10.12.2" }, "engines": { "node": ">=18.0.0" @@ -3162,6 +3165,19 @@ "react-router-dom": "^6.22.3" } }, + "node_modules/@laboratoria/sdk-react/node_modules/@laboratoria/sdk-js": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@laboratoria/sdk-js/-/sdk-js-7.6.0.tgz", + "integrity": "sha512-EzXlmqsKDu3GlQ/KfSX10c9E0tLowTDLl84eMErCZ5/q18KQVGqLPlFpVvixxba08+XACHKD1vqUNuwNtyTe/Q==", + "license": "MIT", + "dependencies": { + "blueimp-md5": "^2.19.0", + "firebase": "^10.11.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@mui/base": { "version": "5.0.0-beta.40", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", @@ -7620,20 +7636,20 @@ } }, "node_modules/firebase": { - "version": "10.12.1", - "resolved": "https://registry.npmjs.org/firebase/-/firebase-10.12.1.tgz", - "integrity": "sha512-B/R3BX26OAgreA64JN0lYspYRHMS36E19/Sv9WsyQu1RqPGBzWkBlt1RW6+38SdtMDlAnk3ibKL/SRSQHb1xRw==", + "version": "10.12.2", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-10.12.2.tgz", + "integrity": "sha512-ZxEdtSvP1I9su1yf32D8TIdgxtPgxwr6z3jYAR1TXS/t+fVfpoPc/N1/N2bxOco9mNjUoc+od34v5Fn4GeKs6Q==", "license": "Apache-2.0", "dependencies": { "@firebase/analytics": "0.10.4", "@firebase/analytics-compat": "0.2.10", - "@firebase/app": "0.10.4", + "@firebase/app": "0.10.5", "@firebase/app-check": "0.8.4", "@firebase/app-check-compat": "0.3.11", - "@firebase/app-compat": "0.2.34", + "@firebase/app-compat": "0.2.35", "@firebase/app-types": "0.9.2", - "@firebase/auth": "1.7.3", - "@firebase/auth-compat": "0.5.8", + "@firebase/auth": "1.7.4", + "@firebase/auth-compat": "0.5.9", "@firebase/database": "1.0.5", "@firebase/database-compat": "1.0.5", "@firebase/firestore": "4.6.3", @@ -7651,13 +7667,13 @@ "@firebase/storage": "0.12.5", "@firebase/storage-compat": "0.3.8", "@firebase/util": "1.9.6", - "@firebase/vertexai-preview": "0.0.1" + "@firebase/vertexai-preview": "0.0.2" } }, "node_modules/firebase/node_modules/@firebase/auth": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.7.3.tgz", - "integrity": "sha512-RiU1PjziOxLuyswtYtLK2qSjHIQJQGCk1h986SUFRbMQfzLXbQg8ZgXwxac1UAfDOzgzqPNCXhBuIlSK2UomoQ==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.7.4.tgz", + "integrity": "sha512-d2Fw17s5QesojwebrA903el20Li9/YGgkoOGJjagM4I1qAT36APa/FcZ+OX86KxbYKCtQKTMqraU8pxG7C2JWA==", "license": "Apache-2.0", "dependencies": { "@firebase/component": "0.6.7", diff --git a/package.json b/package.json index edc4a36a4..f8e4cb2f5 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "devDependencies": { "@laboratoria/curriculum-parser": "^6.0.0-alpha.0", "@laboratoria/mdlint": "^1.2.3", + "@laboratoria/sdk-js": "8.0.0-beta.0", "@octokit/rest": "^20.0.2", "@sentry/vite-plugin": "^2.16.0", "@testing-library/jest-dom": "^6.1.5", diff --git a/scripts/README.md b/scripts/README.md index 7f33f44b4..5af66301a 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -71,10 +71,25 @@ Argumentos: Opciones: * `--locale`: Puede ser `es` o `pt`. Por defecto es `es`. +* `--variant`: Variente de implementación (si el proyecto tiene variantes). * `--noop`: Si esta opción está presente el script nos dirá que es lo que haríá paso a paso pero sin realmente hacer nada. Es útil para familiarizarse con el script. +:warning: Para pasar opciones al script cuando invocamos a través de +`npm-scripts` (`npm run create-cohort-project`) hay que separar las opciones con +` -- ` para que `npm` las pase al script. + +```sh +npm run create-cohort-project projects/05-fleet-management-api /tmp XXX999 -- --variant java +``` + +Si invocamos directamente no es necesario. Por ejemplo: + +```sh +./scripts/create-cohort-project.mjs projects/05-fleet-management-api /tmp XXX999 --variant java +``` + *** ## Testing y linters diff --git a/scripts/create-cohort-project.mjs b/scripts/create-cohort-project.mjs index bb3cd571d..6396426b3 100755 --- a/scripts/create-cohort-project.mjs +++ b/scripts/create-cohort-project.mjs @@ -14,12 +14,15 @@ import { transformLearningObjectives, loadYaml, } from '@laboratoria/curriculum-parser/lib/project.js'; -import { getFilesWithLocales, +import { parseProject } from '@laboratoria/sdk-js'; +import { + getFilesWithLocales, defaultLocale, supportedLocales, getLearningObjectivesHeadings, getLearningObjectivesHierarchy, - createLearningObjectivesMarkdown } from './script-utils.mjs'; + createLearningObjectivesMarkdown +} from './script-utils.mjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const uiUrl = 'https://curriculum.laboratoria.la'; @@ -87,7 +90,7 @@ const copy = async (src, repoDir, opts) => { // rename / replace default files with localized content if (opts.locale && opts.locale !== defaultLocale) { - const files = getFilesWithLocales(repoDir, [ opts.locale ]); + const files = getFilesWithLocales(repoDir, [opts.locale]); await Promise.all(files.map(filepath => rename(`${filepath}`, `${filepath.replace(`.${opts.locale}`, '')}`))); } @@ -125,15 +128,21 @@ const addExplainDevConfigFile = async ({ project, cohort, track, repoDir }) => { }; const addLocalizedLearningObjectives = async (repoDir, opts, meta) => { - - const learningObjectives = await transformLearningObjectives(repoDir, { + const { learningObjectives, variants } = await transformLearningObjectives(repoDir, { lo: path.join(__dirname, '../learning-objectives'), }, meta); - // Note: learningObjectives returns list of specific oa's - // example: so for js/modules -> js/modules/esm, js/modules/common + if (variants?.length && !opts.variant) { + throw new Error('Project has variants, please specify one with --variant'); + } + + const parsedProject = parseProject({ ...meta, learningObjectives, variants }); + const combinedLearningObjectives = parsedProject.getCombinedLearningObjectives(opts.variant); - if (!learningObjectives) { + // Note: combinedLearningObjectives returns list of objects, each with a + // property `id` containing a string like: js/modules, s/modules/esm, etc + + if (!combinedLearningObjectives?.length) { return; } @@ -142,9 +151,14 @@ const addLocalizedLearningObjectives = async (repoDir, opts, meta) => { path.join(__dirname, '../learning-objectives', 'intl', `${lang}.yml`), ); - const categoryTree = getLearningObjectivesHierarchy(learningObjectives); + const categoryTree = getLearningObjectivesHierarchy(combinedLearningObjectives); const sectionTree = getLearningObjectivesHeadings(categoryTree, intl); - const text = createLearningObjectivesMarkdown(learningObjectives, sectionTree, intl, lang); + const text = createLearningObjectivesMarkdown( + combinedLearningObjectives, + sectionTree, + intl, + lang, + ); const readmePath = path.join(repoDir, 'README.md'); const contents = (await readFile(readmePath, 'utf8')).split('\n'); const startIndex = contents.findIndex( @@ -226,14 +240,13 @@ const main = async (args, opts) => { ensureSrc(src); const slug = path.basename(src).slice(3); - const repoName = prefix ? `${prefix}-${slug}` : slug; + const repoName = `${prefix ? `${prefix}-` : ''}${slug}${opts.variant ? `-${opts.variant}` : ''}`; const repoDir = dest ? `${dest}/${repoName}` : repoName; await ensureRepoDir(repoDir, opts); await copy(src, repoDir, opts); await addBootcampInfo(repoDir); const meta = await loadYaml(path.join(src, 'project.yml')); - // console.log('learning Objectives son', learningObjectives); await addExplainDevConfigFile({ project: slug, cohort: prefix, @@ -299,6 +312,9 @@ Este es un mensaje de ayuda para que puedas usarlo. # crea el proyecto Markdown Links en la ruta actual para DEV999 npm run create-cohort-project projects/04-md-links ./ DEV999 + # crea proyecto Fleet Management API en su variante de Java + npm run create-cohort-project projects/05-fleet-management-api / XXX999 -- --variant java + Acá puedes encontrar la documentación completa: https://github.com/Laboratoria/curriculum/tree/main/scripts#create-cohort-project `); diff --git a/scripts/script-utils.mjs b/scripts/script-utils.mjs index 422143865..4474e84ef 100644 --- a/scripts/script-utils.mjs +++ b/scripts/script-utils.mjs @@ -8,7 +8,7 @@ export const getFilesWithLocales = (dir, locales) => { const dirents = readdirSync(dir, { withFileTypes: true }); const filesWithExt = dirents.reduce((accumFiles, dirent) => { const fullPath = path.resolve(dir, dirent.name); - dirent.isDirectory() ? accumFiles.push(...getFilesWithLocales(fullPath, locales)) : accumFiles.push(fullPath); + dirent.isDirectory() ? accumFiles.push(...getFilesWithLocales(fullPath, locales)) : accumFiles.push(fullPath); return accumFiles; }, []); const regexLocaleMD = new RegExp(`\.(${locales.join('|')})\.md`); @@ -16,16 +16,16 @@ export const getFilesWithLocales = (dir, locales) => { } /** - * - * @param {array} learningObjectives + * + * @param {array} learningObjectives * @returns {object} an object with keys of top level categories and values of subcategory arrays ex. { 'js': ['js/testing', ...]} */ export const getLearningObjectivesHierarchy = (learningObjectives) => learningObjectives.reduce( - (memo, item) => { - const [ root, ...parts] = item.split('/'); + (memo, { id }) => { + const [root, ...parts] = id.split('/'); if (!memo[root]) { Object.assign(memo, { [root]: [] }) - } + } const subcatKeys = parts.slice(0, -1).reduce((acc, part, i, arr) => [...acc, `${root}/${arr.slice(0, i + 1).join('/')}`], []); subcatKeys.forEach(subcat => !memo[root].includes(subcat) && memo[root].push(subcat)); @@ -37,7 +37,7 @@ export const getLearningObjectivesHierarchy = (learningObjectives) => learningOb const getNearestSubcategoryParent = (objectiveKey) => objectiveKey.split('/').slice(0, -1).join('/'); /** - * + * * @param {object} categoryTree an object rep of hierarchy of categories and subcats, as returned in getLearningObjecttivesHierarchy * @param {object} intl localized objectives content * @returns {object} key is category/subcat, value is heading with intl string and heading level ex. { 'js/testing': '#### Testing' } @@ -49,7 +49,7 @@ export const getLearningObjectivesHeadings = (categoryTree, intl) => Object.keys return { ...memo, - [rootCategoryKey]: [`### ${localizedCat.title || intl[rootCategoryKey] || rootCategoryKey }\n`], + [rootCategoryKey]: [`### ${localizedCat.title || intl[rootCategoryKey] || rootCategoryKey}\n`], ...subcats.reduce((acc, subcatKey, i) => { const localized = intl[subcatKey]; const title = localized?.title || intl[subcatKey] || subcatKey; @@ -60,9 +60,10 @@ export const getLearningObjectivesHeadings = (categoryTree, intl) => Object.keys // Note: this suffix is to help understand the hierarchy of the deeper objectives in the readme const subcatSuffix = (depth > 2) ? ` _(${(intl[nearestParent]?.title || intl[nearestParent] || nearestParent)})_` : ''; const subheading = `${'#'.repeat(headingLevel)} ${title}${subcatSuffix}\n` - return ({...acc, [subcatKey]: [subheading]}) - } , {})} - }, + return ({ ...acc, [subcatKey]: [subheading] }) + }, {}) + } + }, {}, ); @@ -72,27 +73,27 @@ const linkToString = ({ title, url }, lang) => { }; export const createLearningObjectivesMarkdown = (learningObjectives, sectionTree, intl, lang) => { - learningObjectives.forEach((objectiveKey) => { - const item = intl[objectiveKey] || {}; - const title = item.title || objectiveKey.split('/').slice(1).join('/'); + learningObjectives.forEach(({ id }) => { + const item = intl[id] || {}; + const title = item.title || id.split('/').slice(1).join('/'); let markup; if (!item.links || !item.links.length) { markup = `\n- [ ] **${title}**\n`; } else { - // collapsible links + // collapsible links const detailsStart = '
Links

\n'; const detailsEnd = '\n

\n'; - markup = (item.links.reduce((prev, link) => `${prev}\n * ${linkToString(link, lang)}`, - `\n- [ ] **${title}**\n\n ${detailsStart}`) + markup = (item.links.reduce((prev, link) => `${prev}\n * ${linkToString(link, lang)}`, + `\n- [ ] **${title}**\n\n ${detailsStart}`) ) + detailsEnd; } - + // find out where to put the markup - const [ rootCategory, ...parts ] = objectiveKey.split('/'); + const [rootCategory, ...parts] = id.split('/'); if (parts.length === 1) { // we are just one level under the root cat sectionTree[rootCategory].push(markup); } else { - const nearestParent = getNearestSubcategoryParent(objectiveKey); + const nearestParent = getNearestSubcategoryParent(id); sectionTree[nearestParent].push(markup); } });