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] }),
{},