diff --git a/index.js b/index.js index 61dc0c7..d5882c2 100755 --- a/index.js +++ b/index.js @@ -54,7 +54,10 @@ program.command('project') .argument('', 'path to project directory') .option('--repo ', 'Repository') .option('--version ', 'Project version') - .option('--lo ', 'Path to yml file with reference learning objectives') + .option( + '--lo ', + 'Path to directory containing data.yml with reference learning objectives', + ) .option('--debug', 'Show error stack traces') .action(createHandler(parseProject)); diff --git a/lib/__tests__/__fixtures__/01-a-project-tags-not-array/README.md b/lib/__tests__/__fixtures__/01-a-project-tags-not-array/README.md new file mode 100644 index 0000000..841e5d5 --- /dev/null +++ b/lib/__tests__/__fixtures__/01-a-project-tags-not-array/README.md @@ -0,0 +1,15 @@ +# A project + +## Índice + +Blah blah blah + +*** + +## 1. Preámbulo + +Blah blah blah + +## 2. Resumen del proyecto + +Blah blah blah diff --git a/lib/__tests__/__fixtures__/01-a-project-tags-not-array/project.yml b/lib/__tests__/__fixtures__/01-a-project-tags-not-array/project.yml new file mode 100644 index 0000000..daec0ea --- /dev/null +++ b/lib/__tests__/__fixtures__/01-a-project-tags-not-array/project.yml @@ -0,0 +1,8 @@ +track: web-dev +tracks: + - web-dev +tags: omg +learningObjectives: + - html + - css + - dom diff --git a/lib/__tests__/__fixtures__/01-a-project-tags-not-strings/README.md b/lib/__tests__/__fixtures__/01-a-project-tags-not-strings/README.md new file mode 100644 index 0000000..841e5d5 --- /dev/null +++ b/lib/__tests__/__fixtures__/01-a-project-tags-not-strings/README.md @@ -0,0 +1,15 @@ +# A project + +## Índice + +Blah blah blah + +*** + +## 1. Preámbulo + +Blah blah blah + +## 2. Resumen del proyecto + +Blah blah blah diff --git a/lib/__tests__/__fixtures__/01-a-project-tags-not-strings/project.yml b/lib/__tests__/__fixtures__/01-a-project-tags-not-strings/project.yml new file mode 100644 index 0000000..17bbcb8 --- /dev/null +++ b/lib/__tests__/__fixtures__/01-a-project-tags-not-strings/project.yml @@ -0,0 +1,11 @@ +track: web-dev +tracks: + - web-dev +tags: + - featured + - foo: true + bar: 1 +learningObjectives: + - html + - css + - dom diff --git a/lib/__tests__/__fixtures__/01-a-project-with-exclude-outside-variant/README.md b/lib/__tests__/__fixtures__/01-a-project-with-exclude-outside-variant/README.md new file mode 100644 index 0000000..841e5d5 --- /dev/null +++ b/lib/__tests__/__fixtures__/01-a-project-with-exclude-outside-variant/README.md @@ -0,0 +1,15 @@ +# A project + +## Índice + +Blah blah blah + +*** + +## 1. Preámbulo + +Blah blah blah + +## 2. Resumen del proyecto + +Blah blah blah diff --git a/lib/__tests__/__fixtures__/01-a-project-with-exclude-outside-variant/project.yml b/lib/__tests__/__fixtures__/01-a-project-with-exclude-outside-variant/project.yml new file mode 100644 index 0000000..1fc4c8d --- /dev/null +++ b/lib/__tests__/__fixtures__/01-a-project-with-exclude-outside-variant/project.yml @@ -0,0 +1,19 @@ +track: web-dev +tracks: + - web-dev +learningObjectives: + - html/semantics + - css/selectors + - dom/selectors + - dom/events + - dom/manipulation + - js/data-types/primitive-vs-non-primitive + - js/data-types/strings + - js/variables + - js/conditionals + - js/functions + - js/semantics + - ux/user-understanding + - ux/prototyping + - id: css + exclude: true diff --git a/lib/__tests__/__fixtures__/01-a-project-with-invalid-learning-objectives/README.md b/lib/__tests__/__fixtures__/01-a-project-with-invalid-learning-objectives/README.md new file mode 100644 index 0000000..841e5d5 --- /dev/null +++ b/lib/__tests__/__fixtures__/01-a-project-with-invalid-learning-objectives/README.md @@ -0,0 +1,15 @@ +# A project + +## Índice + +Blah blah blah + +*** + +## 1. Preámbulo + +Blah blah blah + +## 2. Resumen del proyecto + +Blah blah blah diff --git a/lib/__tests__/__fixtures__/01-a-project-with-invalid-learning-objectives/project.yml b/lib/__tests__/__fixtures__/01-a-project-with-invalid-learning-objectives/project.yml new file mode 100644 index 0000000..6778209 --- /dev/null +++ b/lib/__tests__/__fixtures__/01-a-project-with-invalid-learning-objectives/project.yml @@ -0,0 +1,8 @@ +track: web-dev +tracks: + - web-dev +learningObjectives: + - html/semantics + - css/selectors + - name: react + optional: true diff --git a/lib/__tests__/__fixtures__/01-a-project-with-invalid-tags/README.md b/lib/__tests__/__fixtures__/01-a-project-with-invalid-tags/README.md new file mode 100644 index 0000000..841e5d5 --- /dev/null +++ b/lib/__tests__/__fixtures__/01-a-project-with-invalid-tags/README.md @@ -0,0 +1,15 @@ +# A project + +## Índice + +Blah blah blah + +*** + +## 1. Preámbulo + +Blah blah blah + +## 2. Resumen del proyecto + +Blah blah blah diff --git a/lib/__tests__/__fixtures__/01-a-project-with-invalid-tags/project.yml b/lib/__tests__/__fixtures__/01-a-project-with-invalid-tags/project.yml new file mode 100644 index 0000000..c961a5f --- /dev/null +++ b/lib/__tests__/__fixtures__/01-a-project-with-invalid-tags/project.yml @@ -0,0 +1,10 @@ +track: web-dev +tracks: + - web-dev +tags: + - featured + - foo +learningObjectives: + - html + - css + - dom diff --git a/lib/__tests__/__fixtures__/01-a-project-with-learning-objectives-turned-off/README.md b/lib/__tests__/__fixtures__/01-a-project-with-learning-objectives-turned-off/README.md new file mode 100644 index 0000000..841e5d5 --- /dev/null +++ b/lib/__tests__/__fixtures__/01-a-project-with-learning-objectives-turned-off/README.md @@ -0,0 +1,15 @@ +# A project + +## Índice + +Blah blah blah + +*** + +## 1. Preámbulo + +Blah blah blah + +## 2. Resumen del proyecto + +Blah blah blah diff --git a/lib/__tests__/__fixtures__/01-a-project-with-learning-objectives-turned-off/project.yml b/lib/__tests__/__fixtures__/01-a-project-with-learning-objectives-turned-off/project.yml new file mode 100644 index 0000000..7e5b9e4 --- /dev/null +++ b/lib/__tests__/__fixtures__/01-a-project-with-learning-objectives-turned-off/project.yml @@ -0,0 +1,22 @@ +track: web-dev +tracks: + - web-dev +learningObjectives: + - html/semantics + - css/selectors + - dom/selectors + - dom/events + - dom/manipulation + - js/data-types/primitive-vs-non-primitive + - js/data-types/strings + - js/variables + - js/conditionals + - js/functions + - js/semantics + - ux/user-understanding + - ux/prototyping +variants: + - name: foo + learningObjectives: + - id: css + exclude: true diff --git a/lib/__tests__/__fixtures__/01-a-project-with-learning-objectives-variants/README.md b/lib/__tests__/__fixtures__/01-a-project-with-learning-objectives-variants/README.md new file mode 100644 index 0000000..841e5d5 --- /dev/null +++ b/lib/__tests__/__fixtures__/01-a-project-with-learning-objectives-variants/README.md @@ -0,0 +1,15 @@ +# A project + +## Índice + +Blah blah blah + +*** + +## 1. Preámbulo + +Blah blah blah + +## 2. Resumen del proyecto + +Blah blah blah diff --git a/lib/__tests__/__fixtures__/01-a-project-with-learning-objectives-variants/project.yml b/lib/__tests__/__fixtures__/01-a-project-with-learning-objectives-variants/project.yml new file mode 100644 index 0000000..553b267 --- /dev/null +++ b/lib/__tests__/__fixtures__/01-a-project-with-learning-objectives-variants/project.yml @@ -0,0 +1,21 @@ +track: web-dev +tracks: + - web-dev +learningObjectives: + - html/semantics + - css/selectors + - dom/selectors + - dom/events + - dom/manipulation + - js/data-types/primitive-vs-non-primitive + - js/data-types/strings + - js/variables + - js/conditionals + - js/functions + - js/semantics + - ux/user-understanding + - ux/prototyping +variants: + - name: node + learningObjectives: + - node diff --git a/lib/__tests__/__fixtures__/01-a-project-with-optional-learning-objectives/README.md b/lib/__tests__/__fixtures__/01-a-project-with-optional-learning-objectives/README.md new file mode 100644 index 0000000..841e5d5 --- /dev/null +++ b/lib/__tests__/__fixtures__/01-a-project-with-optional-learning-objectives/README.md @@ -0,0 +1,15 @@ +# A project + +## Índice + +Blah blah blah + +*** + +## 1. Preámbulo + +Blah blah blah + +## 2. Resumen del proyecto + +Blah blah blah diff --git a/lib/__tests__/__fixtures__/01-a-project-with-optional-learning-objectives/project.yml b/lib/__tests__/__fixtures__/01-a-project-with-optional-learning-objectives/project.yml new file mode 100644 index 0000000..a04fbb8 --- /dev/null +++ b/lib/__tests__/__fixtures__/01-a-project-with-optional-learning-objectives/project.yml @@ -0,0 +1,19 @@ +track: web-dev +tracks: + - web-dev +learningObjectives: + - html/semantics + - css/selectors + - dom/selectors + - dom/events + - dom/manipulation + - js/data-types/primitive-vs-non-primitive + - js/data-types/strings + - js/variables + - js/conditionals + - js/functions + - js/semantics + - ux/user-understanding + - ux/prototyping + - id: react + optional: true diff --git a/lib/__tests__/__fixtures__/01-a-project-with-tags/README.md b/lib/__tests__/__fixtures__/01-a-project-with-tags/README.md new file mode 100644 index 0000000..841e5d5 --- /dev/null +++ b/lib/__tests__/__fixtures__/01-a-project-with-tags/README.md @@ -0,0 +1,15 @@ +# A project + +## Índice + +Blah blah blah + +*** + +## 1. Preámbulo + +Blah blah blah + +## 2. Resumen del proyecto + +Blah blah blah diff --git a/lib/__tests__/__fixtures__/01-a-project-with-tags/project.yml b/lib/__tests__/__fixtures__/01-a-project-with-tags/project.yml new file mode 100644 index 0000000..0b85acf --- /dev/null +++ b/lib/__tests__/__fixtures__/01-a-project-with-tags/project.yml @@ -0,0 +1,12 @@ +track: web-dev +tracks: + - web-dev +tags: + - featured + - beta + - deprecated + - hidden +learningObjectives: + - html + - css + - dom diff --git a/lib/__tests__/__fixtures__/01-a-project-with-variant-excluding-granular-objectives/README.md b/lib/__tests__/__fixtures__/01-a-project-with-variant-excluding-granular-objectives/README.md new file mode 100644 index 0000000..841e5d5 --- /dev/null +++ b/lib/__tests__/__fixtures__/01-a-project-with-variant-excluding-granular-objectives/README.md @@ -0,0 +1,15 @@ +# A project + +## Índice + +Blah blah blah + +*** + +## 1. Preámbulo + +Blah blah blah + +## 2. Resumen del proyecto + +Blah blah blah diff --git a/lib/__tests__/__fixtures__/01-a-project-with-variant-excluding-granular-objectives/project.yml b/lib/__tests__/__fixtures__/01-a-project-with-variant-excluding-granular-objectives/project.yml new file mode 100644 index 0000000..90b5a75 --- /dev/null +++ b/lib/__tests__/__fixtures__/01-a-project-with-variant-excluding-granular-objectives/project.yml @@ -0,0 +1,13 @@ +track: web-dev +tracks: + - web-dev +learningObjectives: + - dom/selectors + - dom/events + - dom/manipulation +variants: + - name: node + learningObjectives: + - node + - id: dom/manipulation + exclude: true diff --git a/lib/__tests__/__fixtures__/01-a-project-with-variant-learning-objectives-duplicated/README.md b/lib/__tests__/__fixtures__/01-a-project-with-variant-learning-objectives-duplicated/README.md new file mode 100644 index 0000000..841e5d5 --- /dev/null +++ b/lib/__tests__/__fixtures__/01-a-project-with-variant-learning-objectives-duplicated/README.md @@ -0,0 +1,15 @@ +# A project + +## Índice + +Blah blah blah + +*** + +## 1. Preámbulo + +Blah blah blah + +## 2. Resumen del proyecto + +Blah blah blah diff --git a/lib/__tests__/__fixtures__/01-a-project-with-variant-learning-objectives-duplicated/project.yml b/lib/__tests__/__fixtures__/01-a-project-with-variant-learning-objectives-duplicated/project.yml new file mode 100644 index 0000000..896dac3 --- /dev/null +++ b/lib/__tests__/__fixtures__/01-a-project-with-variant-learning-objectives-duplicated/project.yml @@ -0,0 +1,28 @@ +track: web-dev +tracks: + - web-dev +learningObjectives: + - html/semantics + - css/selectors + - dom/selectors + - dom/events + - dom/manipulation + - js/data-types/primitive-vs-non-primitive + - js/data-types/strings + - js/variables + - js/conditionals + - js/functions + - js/semantics + - ux/user-understanding + - ux/prototyping + - id: react + optional: true +variants: + - name: node + learningObjectives: + - html/semantics + - node + - id: react + optional: true + - id: angular + optional: true diff --git a/lib/__tests__/__fixtures__/01-a-project-with-variant-learning-objectives-turned-off/README.md b/lib/__tests__/__fixtures__/01-a-project-with-variant-learning-objectives-turned-off/README.md new file mode 100644 index 0000000..841e5d5 --- /dev/null +++ b/lib/__tests__/__fixtures__/01-a-project-with-variant-learning-objectives-turned-off/README.md @@ -0,0 +1,15 @@ +# A project + +## Índice + +Blah blah blah + +*** + +## 1. Preámbulo + +Blah blah blah + +## 2. Resumen del proyecto + +Blah blah blah diff --git a/lib/__tests__/__fixtures__/01-a-project-with-variant-learning-objectives-turned-off/project.yml b/lib/__tests__/__fixtures__/01-a-project-with-variant-learning-objectives-turned-off/project.yml new file mode 100644 index 0000000..cdbaca0 --- /dev/null +++ b/lib/__tests__/__fixtures__/01-a-project-with-variant-learning-objectives-turned-off/project.yml @@ -0,0 +1,22 @@ +track: web-dev +tracks: + - web-dev +learningObjectives: + - html/semantics + - css/selectors + - dom/selectors + - dom/events + - dom/manipulation + - js/data-types/primitive-vs-non-primitive + - js/data-types/strings + - js/variables + - js/conditionals + - js/functions + - js/semantics + - ux/user-understanding + - ux/prototyping +variants: + - name: cli + learningObjectives: + - id: css + exclude: true diff --git a/lib/__tests__/__snapshots__/project.spec.js.snap b/lib/__tests__/__snapshots__/project.spec.js.snap index 3a25a32..06065ad 100644 --- a/lib/__tests__/__snapshots__/project.spec.js.snap +++ b/lib/__tests__/__snapshots__/project.spec.js.snap @@ -1,24 +1,228 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`parseProject > does not duplicate optional lo if its present in both normal and variant los 1`] = ` +[ + { + "id": "html/semantics", + }, + { + "id": "css/selectors", + }, + { + "id": "dom/selectors", + }, + { + "id": "dom/events", + }, + { + "id": "dom/manipulation", + }, + { + "id": "js/data-types/primitive-vs-non-primitive", + }, + { + "id": "js/data-types/strings", + }, + { + "id": "js/variables/declaration", + }, + { + "id": "js/conditionals", + }, + { + "id": "js/functions", + }, + { + "id": "js/semantics", + }, + { + "id": "ux/user-understanding", + }, + { + "id": "ux/prototyping", + }, + { + "id": "react/jsx", + "optional": true, + }, + { + "id": "react/components", + "optional": true, + }, + { + "id": "react/events", + "optional": true, + }, + { + "id": "react/lists-and-keys", + "optional": true, + }, + { + "id": "react/conditional-rendering", + "optional": true, + }, + { + "id": "react/lifting-up-state", + "optional": true, + }, + { + "id": "react/hooks", + "optional": true, + }, + { + "id": "react/css-modules", + "optional": true, + }, + { + "id": "react/routing", + "optional": true, + }, +] +`; + +exports[`parseProject > does not duplicate optional lo if its present in both normal and variant los 2`] = ` +[ + { + "id": "node/npm-install", + }, + { + "id": "node/package.json", + }, + { + "id": "node/npm-scripts", + }, + { + "id": "node/process", + }, + { + "id": "node/filesystem", + }, + { + "id": "angular/components-and-templates", + "optional": true, + }, + { + "id": "angular/structural-directives", + "optional": true, + }, + { + "id": "angular/input-output", + "optional": true, + }, + { + "id": "angular/services", + "optional": true, + }, + { + "id": "angular/routing", + "optional": true, + }, + { + "id": "angular/observables", + "optional": true, + }, + { + "id": "angular/http-client", + "optional": true, + }, + { + "id": "angular/styles", + "optional": true, + }, +] +`; + +exports[`parseProject > excludes nested objectives in variant 1`] = ` +[ + { + "id": "dom/selectors", + }, + { + "id": "dom/events", + }, + { + "id": "dom/manipulation", + }, +] +`; + +exports[`parseProject > excludes nested objectives in variant 2`] = ` +[ + { + "id": "node/npm-install", + }, + { + "id": "node/package.json", + }, + { + "id": "node/npm-scripts", + }, + { + "id": "node/process", + }, + { + "id": "node/filesystem", + }, + { + "exclude": true, + "id": "dom/manipulation", + }, +] +`; + exports[`parseProject > expands learning objectives children when only parent is mentioned 1`] = ` [ - "html/semantics", - "css/selectors", - "css/flexbox", - "css/grid", - "css/media-queries", - "js/async/callbacks", - "js/async/promises", - "js/data-types/strings", - "scm/git/setup", - "scm/git/intro", - "scm/git/integration", - "scm/github/setup", - "scm/github/gh-pages", - "scm/github/collaboration", - "scm/github/project-management", - "scm/github/ci", - "http/headers", + { + "id": "html/semantics", + }, + { + "id": "css/selectors", + }, + { + "id": "css/flexbox", + }, + { + "id": "css/grid", + }, + { + "id": "css/media-queries", + }, + { + "id": "js/async/callbacks", + }, + { + "id": "js/async/promises", + }, + { + "id": "js/data-types/strings", + }, + { + "id": "scm/git/setup", + }, + { + "id": "scm/git/intro", + }, + { + "id": "scm/git/integration", + }, + { + "id": "scm/github/setup", + }, + { + "id": "scm/github/gh-pages", + }, + { + "id": "scm/github/collaboration", + }, + { + "id": "scm/github/project-management", + }, + { + "id": "scm/github/ci", + }, + { + "id": "http/headers", + }, ] `; @@ -36,6 +240,130 @@ implementar a funcionalidade para ocultar todos os dígitos de um cartão, excet os quatro últimos.

" `; +exports[`parseProject > includes "exclude" prop when learning objective has it in variant 1`] = ` +[ + { + "id": "html/semantics", + }, + { + "id": "css/selectors", + }, + { + "id": "dom/selectors", + }, + { + "id": "dom/events", + }, + { + "id": "dom/manipulation", + }, + { + "id": "js/data-types/primitive-vs-non-primitive", + }, + { + "id": "js/data-types/strings", + }, + { + "id": "js/variables/declaration", + }, + { + "id": "js/conditionals", + }, + { + "id": "js/functions", + }, + { + "id": "js/semantics", + }, + { + "id": "ux/user-understanding", + }, + { + "id": "ux/prototyping", + }, +] +`; + +exports[`parseProject > includes "optional" prop learning objectives when present in yml 1`] = ` +[ + { + "id": "html/semantics", + }, + { + "id": "css/selectors", + }, + { + "id": "dom/selectors", + }, + { + "id": "dom/events", + }, + { + "id": "dom/manipulation", + }, + { + "id": "js/data-types/primitive-vs-non-primitive", + }, + { + "id": "js/data-types/strings", + }, + { + "id": "js/variables/declaration", + }, + { + "id": "js/conditionals", + }, + { + "id": "js/functions", + }, + { + "id": "js/semantics", + }, + { + "id": "ux/user-understanding", + }, + { + "id": "ux/prototyping", + }, + { + "id": "react/jsx", + "optional": true, + }, + { + "id": "react/components", + "optional": true, + }, + { + "id": "react/events", + "optional": true, + }, + { + "id": "react/lists-and-keys", + "optional": true, + }, + { + "id": "react/conditional-rendering", + "optional": true, + }, + { + "id": "react/lifting-up-state", + "optional": true, + }, + { + "id": "react/hooks", + "optional": true, + }, + { + "id": "react/css-modules", + "optional": true, + }, + { + "id": "react/routing", + "optional": true, + }, +] +`; + exports[`parseProject > parses a project with learning objectives validating against known list 1`] = ` { "cover": null, @@ -46,19 +374,45 @@ exports[`parseProject > parses a project with learning objectives validating aga }, }, "learningObjectives": [ - "html/semantics", - "css/selectors", - "dom/selectors", - "dom/events", - "dom/manipulation", - "js/data-types/primitive-vs-non-primitive", - "js/data-types/strings", - "js/variables/declaration", - "js/conditionals", - "js/functions", - "js/semantics", - "ux/user-understanding", - "ux/prototyping", + { + "id": "html/semantics", + }, + { + "id": "css/selectors", + }, + { + "id": "dom/selectors", + }, + { + "id": "dom/events", + }, + { + "id": "dom/manipulation", + }, + { + "id": "js/data-types/primitive-vs-non-primitive", + }, + { + "id": "js/data-types/strings", + }, + { + "id": "js/variables/declaration", + }, + { + "id": "js/conditionals", + }, + { + "id": "js/functions", + }, + { + "id": "js/semantics", + }, + { + "id": "ux/user-understanding", + }, + { + "id": "ux/prototyping", + }, ], "path": "lib/__tests__/__fixtures__/01-a-project-with-learning-objectives", "prefix": 1, @@ -83,19 +437,45 @@ exports[`parseProject > parses a project with learning objectives without valida }, }, "learningObjectives": [ - "html/semantics", - "css/selectors", - "dom/selectors", - "dom/events", - "dom/manipulation", - "js/data-types/primitive-vs-non-primitive", - "js/data-types/strings", - "js/variables", - "js/conditionals", - "js/functions", - "js/semantics", - "ux/user-understanding", - "ux/prototyping", + { + "id": "html/semantics", + }, + { + "id": "css/selectors", + }, + { + "id": "dom/selectors", + }, + { + "id": "dom/events", + }, + { + "id": "dom/manipulation", + }, + { + "id": "js/data-types/primitive-vs-non-primitive", + }, + { + "id": "js/data-types/strings", + }, + { + "id": "js/variables", + }, + { + "id": "js/conditionals", + }, + { + "id": "js/functions", + }, + { + "id": "js/semantics", + }, + { + "id": "ux/user-understanding", + }, + { + "id": "ux/prototyping", + }, ], "path": "lib/__tests__/__fixtures__/01-a-project-with-learning-objectives", "prefix": 1, @@ -121,19 +501,45 @@ poderá cifrar e decifrar um texto indicando a chave de deslocamento (offset }, }, "learningObjectives": [ - "html/semantics", - "css/selectors", - "browser/dom/selectors", - "browser/dom/events", - "browser/dom/manipulation", - "js/data-types/primitive", - "js/data-types/strings", - "js/variables", - "js/conditionals", - "js/functions", - "js/semantics", - "user-centricity/centricity", - "product-design/interactivity", + { + "id": "html/semantics", + }, + { + "id": "css/selectors", + }, + { + "id": "browser/dom/selectors", + }, + { + "id": "browser/dom/events", + }, + { + "id": "browser/dom/manipulation", + }, + { + "id": "js/data-types/primitive", + }, + { + "id": "js/data-types/strings", + }, + { + "id": "js/variables", + }, + { + "id": "js/conditionals", + }, + { + "id": "js/functions", + }, + { + "id": "js/semantics", + }, + { + "id": "user-centricity/centricity", + }, + { + "id": "product-design/interactivity", + }, ], "path": "lib/__tests__/__fixtures__/01-a-project-with-pt-translation", "prefix": 1, diff --git a/lib/__tests__/project.spec.js b/lib/__tests__/project.spec.js index 150149a..34d8780 100644 --- a/lib/__tests__/project.spec.js +++ b/lib/__tests__/project.spec.js @@ -159,6 +159,144 @@ describe('parseProject', () => { }); }); + it('includes "optional" prop learning objectives when present in yml', () => { + const p = resolveFixturePath('01-a-project-with-optional-learning-objectives'); + return parseProject(p, { + repo: 'Laboratoria/bootcamp', + version: '1.0.0', + lo: path.join(__dirname, '__fixtures__', 'learning-objectives'), + }, pkg) + .then((result) => { + const reactLearningObjectives = result.learningObjectives + .filter(lo => lo.id.startsWith('react')); + + expect(reactLearningObjectives.length).toBe(9); + reactLearningObjectives.forEach((lo) => { + expect(lo.optional).toBe(true); + }); + expect(result.learningObjectives).toMatchSnapshot(); + }); + }); + + it('throws when exclude used outside of a variant', () => { + const p = resolveFixturePath('01-a-project-with-exclude-outside-variant'); + expect.assertions(1); + return parseProject(p, { + repo: 'Laboratoria/bootcamp', + version: '1.0.0', + lo: path.join(__dirname, '__fixtures__', 'learning-objectives'), + }, pkg) + .catch((err) => { + expect(err.message).toBe('Only variants can have excluded learning objectives'); + }); + }); + + it('includes "exclude" prop when learning objective has it in variant', () => { + const p = resolveFixturePath('01-a-project-with-learning-objectives-turned-off'); + return parseProject(p, { + repo: 'Laboratoria/bootcamp', + version: '1.0.0', + lo: path.join(__dirname, '__fixtures__', 'learning-objectives'), + }, pkg) + .then((result) => { + const [variant] = result.variants; + expect(variant.learningObjectives.length).toBe(4); + variant.learningObjectives.forEach((lo) => { + expect(lo.exclude).toBe(true); + }); + + expect(result.learningObjectives).toMatchSnapshot(); + }); + }); + + it('does not duplicate optional lo if its present in both normal and variant los', () => { + const p = resolveFixturePath('01-a-project-with-variant-learning-objectives-duplicated'); + return parseProject(p, { + repo: 'Laboratoria/bootcamp', + version: '1.0.0', + lo: path.join(__dirname, '__fixtures__', 'learning-objectives'), + }, pkg) + .then((result) => { + const [variant] = result.variants; + expect(result.learningObjectives).toMatchSnapshot(); + expect(variant.learningObjectives).toMatchSnapshot(); + }); + }); + + it('excludes nested objectives in variant', () => { + const p = resolveFixturePath('01-a-project-with-variant-excluding-granular-objectives'); + return parseProject(p, { + repo: 'Laboratoria/bootcamp', + version: '1.0.0', + lo: path.join(__dirname, '__fixtures__', 'learning-objectives'), + }, pkg) + .then((result) => { + const [variant] = result.variants; + expect(result.learningObjectives).toMatchSnapshot(); + expect(variant.learningObjectives).toMatchSnapshot(); + }); + }); + + it('throws when LO does not have an id', () => { + const p = resolveFixturePath('01-a-project-with-invalid-learning-objectives'); + expect.assertions(1); + return parseProject(p, { + repo: 'Laboratoria/bootcamp', + version: '1.0.0', + lo: path.join(__dirname, '__fixtures__', 'learning-objectives'), + }, pkg) + .catch((err) => { + expect(err.message).toBe('Invalid learning objective: { name: \'react\', optional: true }'); + }); + }); + + it('includes allowed tags when present in yml', () => { + const p = resolveFixturePath('01-a-project-with-tags'); + return parseProject(p, { + repo: 'Laboratoria/bootcamp', + version: '1.0.0', + }, pkg) + .then((result) => { + expect(result.tags).toEqual(['featured', 'beta', 'deprecated', 'hidden']); + }); + }); + + it('throws when unknown tags when present in yml', () => { + const p = resolveFixturePath('01-a-project-with-invalid-tags'); + expect.assertions(1); + return parseProject(p, { + repo: 'Laboratoria/bootcamp', + version: '1.0.0', + }, pkg) + .catch((err) => { + expect(err.message).toBe('Invalid tag: foo'); + }); + }); + + it('throws when tags not array', () => { + const p = resolveFixturePath('01-a-project-tags-not-array'); + expect.assertions(1); + return parseProject(p, { + repo: 'Laboratoria/bootcamp', + version: '1.0.0', + }, pkg) + .catch((err) => { + expect(err.message).toBe('Invalid tags'); + }); + }); + + it('throws when tags not array of strings', () => { + const p = resolveFixturePath('01-a-project-tags-not-strings'); + expect.assertions(1); + return parseProject(p, { + repo: 'Laboratoria/bootcamp', + version: '1.0.0', + }, pkg) + .catch((err) => { + expect(err.message).toBe('Invalid tag'); + }); + }); + it('extracts first paragraph of _resumen del proyecto_ as summary', () => { const p = resolveFixturePath('01-a-project-with-summary'); expect.assertions(2); diff --git a/lib/project.js b/lib/project.js index 66c0cda..af053ec 100644 --- a/lib/project.js +++ b/lib/project.js @@ -1,3 +1,4 @@ +import util from 'node:util'; import { existsSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; @@ -100,45 +101,108 @@ const findFlattenedChildren = (known, learningObjective) => { }; // @description este función valida y "aplana" los learning objetives -export const transformLearningObjectives = async (dir, opts, meta) => { - if (!meta || !meta.learningObjectives) { - return undefined; +export const transformLearningObjectives = async (dir, opts, meta = {}) => { + const { learningObjectives, variants } = meta; + + if (!learningObjectives) { + // FIXME: Deberíamos ignorar variantes si no hay learning objectives?? + return {}; } - const { learningObjectives } = meta; + const shouldValidate = opts.lo && existsSync(`${opts.lo}/data.yml`); + const known = !shouldValidate ? null : await loadYaml(`${opts.lo}/data.yml`); + const knownFlattened = !shouldValidate ? null : flattenLearningObjectives(known); - if (!opts.lo || !existsSync(`${opts.lo}/data.yml`)) { - return learningObjectives; - } + const parseLearningObjectives = (arr, isVariant = false) => { + const parsed = arr.map((strOrObj) => { + const obj = ( + typeof strOrObj === 'string' + ? { id: strOrObj } + : strOrObj + ); - const known = await loadYaml(`${opts.lo}/data.yml`); - const knownFlattened = flattenLearningObjectives(known); + if (typeof obj?.id !== 'string') { + throw new Error( + `Invalid learning objective: ${util.inspect(strOrObj)}`, + ); + } - const unknown = learningObjectives.filter( - item => !knownFlattened.includes(item), - ); + if (shouldValidate && !knownFlattened.includes(obj.id)) { + throw Object.assign( + new Error(`Unknown learning objectives: ${obj.id}.`), + { path: meta.__source || dir }, + ); + } + + if (!isVariant && obj.exclude) { + throw new Error('Only variants can have excluded learning objectives'); + } + + return obj; + }); + + if (!shouldValidate) { + return parsed; + } + + // Expand children when only parent is mentioned? + return parsed.reduce( + ({ expanded, ids }, learningObjective) => { + const { id, ...rest } = learningObjective; + const flattenedChildren = findFlattenedChildren(known, id); + const x = ( + !flattenedChildren + ? [learningObjective] + : parseLearningObjectives(flattenedChildren).map(obj => ({ ...obj, ...rest })) + ); + const unique = x.filter(obj => !ids.includes(obj.id)); + return { + expanded: expanded.concat(unique), + ids: ids.concat(unique.map(obj => obj.id)), + }; + }, + { expanded: [], ids: [] }, + ).expanded; + }; + + const parsedLearningObjectives = parseLearningObjectives(learningObjectives); + + return { + learningObjectives: parsedLearningObjectives, + variants: variants?.map(variant => ({ + ...variant, + learningObjectives: parseLearningObjectives(variant.learningObjectives, true) + .filter(({ id, optional, exclude }) => !parsedLearningObjectives.some( + lo => ( + lo.id === id + && !!optional === !!lo.optional + && !!exclude === !!lo.exclude + ), + )), + })), + }; +}; - if (unknown.length) { - throw Object.assign( - new Error(`Unknown learning objectives: ${unknown.join(', ')}.`), - { path: meta.__source || dir }, - ); +const allowedTags = ['featured', 'beta', 'deprecated', 'hidden']; +const parseTags = (tags) => { + if (!tags) { + return null; } - // Expand children when only parent is mentioned? - return [...new Set(learningObjectives.reduce( - (memo, learningObjective) => { - const flattenedChildren = findFlattenedChildren( - known, - learningObjective, - ); - if (flattenedChildren) { - return memo.concat(flattenedChildren); - } - return memo.concat(learningObjective); - }, - [], - ))]; + if (!Array.isArray(tags)) { + throw new Error('Invalid tags'); + } + + tags.forEach((tag) => { + if (typeof tag !== 'string') { + throw new Error('Invalid tag'); + } + if (!allowedTags.includes(tag)) { + throw new Error(`Invalid tag: ${tag}`); + } + }); + + return tags; }; export const parseProject = async (dir, opts, pkg) => { @@ -155,11 +219,16 @@ export const parseProject = async (dir, opts, pkg) => { }), ); - const { cover, thumb } = meta; + const { cover, thumb, tags } = meta; const { track, tracks } = parseTracks(meta); - const learningObjectives = await transformLearningObjectives(dir, opts, meta); + const { + learningObjectives, + variants, + } = await transformLearningObjectives(dir, opts, meta) || {}; + + const parsedTags = parseTags(tags); return { slug, @@ -171,7 +240,9 @@ export const parseProject = async (dir, opts, pkg) => { prefix: parseInt(prefix, 10), track, tracks, + ...(!!parsedTags?.length && { tags: parsedTags }), ...(!!learningObjectives && { learningObjectives }), + ...(!!variants && { variants }), intl: langs.reduce( (memo, lang, idx) => ({ ...memo, [lang]: parsedLocales[idx] }), {},