chore: import upstream snapshot with attribution
i18n - Build Validation / Validate i18n Builds (24) (push) Has been cancelled
CI - Node.js / Lint (24) (push) Has been cancelled
CI - Node.js / Build (24) (push) Has been cancelled
CI - Node.js / Test (24) (push) Has been cancelled
CI - Node.js / Test - Upcoming Changes (24) (push) Has been cancelled
CI - Node.js / Test - i18n (italian, 24) (push) Has been cancelled
CI - Node.js / Test - i18n (portuguese, 24) (push) Has been cancelled
CD - Docker - GHCR Images / Build and Push Images (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 11:55:53 +08:00
commit dde272c4b8
19405 changed files with 2730632 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
dist
+4
View File
@@ -0,0 +1,4 @@
/* eslint-disable filenames-simple/naming-convention */
import { createLintStagedConfig } from '@freecodecamp/eslint-config/lintstaged';
export default createLintStagedConfig(import.meta.dirname);
+3
View File
@@ -0,0 +1,3 @@
import { configTypeChecked } from '@freecodecamp/eslint-config/base';
export default configTypeChecked;
+101
View File
@@ -0,0 +1,101 @@
{
"name": "@freecodecamp/shared",
"version": "0.0.1",
"author": "freeCodeCamp <team@freecodecamp.org>",
"license": "BSD-3-Clause",
"description": "Config and utils used in multiple workspaces",
"private": false,
"engines": {
"node": ">=24",
"pnpm": ">=10"
},
"scripts": {
"build": "tsdown --log-level warn --no-clean --format cjs --format esm",
"clean": "rm -rf dist",
"develop": "tsdown --log-level warn --no-clean --watch src --format cjs --format esm",
"lint": "eslint --max-warnings 0",
"test": "vitest run",
"test:watch": "vitest",
"test:ui": "vitest --ui",
"type-check": "tsc --noEmit"
},
"type": "module",
"exports": {
"./config/blocks": {
"import": "./dist/config/blocks.mjs",
"require": "./dist/config/blocks.cjs"
},
"./config/catalog": {
"import": "./dist/config/catalog.mjs",
"require": "./dist/config/catalog.cjs"
},
"./config/certification-settings": {
"import": "./dist/config/certification-settings.mjs",
"require": "./dist/config/certification-settings.cjs"
},
"./config/challenge-types": {
"import": "./dist/config/challenge-types.mjs",
"require": "./dist/config/challenge-types.cjs"
},
"./config/chapters": {
"import": "./dist/config/chapters.mjs",
"require": "./dist/config/chapters.cjs"
},
"./config/constants": {
"import": "./dist/config/constants.mjs",
"require": "./dist/config/constants.cjs"
},
"./config/curriculum": {
"import": "./dist/config/curriculum.mjs",
"require": "./dist/config/curriculum.cjs"
},
"./config/donation-settings": {
"import": "./dist/config/donation-settings.mjs",
"require": "./dist/config/donation-settings.cjs"
},
"./config/i18n": {
"import": "./dist/config/i18n.mjs",
"require": "./dist/config/i18n.cjs"
},
"./config/modules": {
"import": "./dist/config/modules.mjs",
"require": "./dist/config/modules.cjs"
},
"./utils/get-lines": {
"import": "./dist/utils/get-lines.mjs",
"require": "./dist/utils/get-lines.cjs"
},
"./utils/is-audited": {
"import": "./dist/utils/is-audited.mjs",
"require": "./dist/utils/is-audited.cjs"
},
"./utils/polyvinyl": {
"import": "./dist/utils/polyvinyl.mjs",
"require": "./dist/utils/polyvinyl.cjs"
},
"./utils/shuffle-array": {
"import": "./dist/utils/shuffle-array.mjs",
"require": "./dist/utils/shuffle-array.cjs"
},
"./utils/validate": {
"import": "./dist/utils/validate.mjs",
"require": "./dist/utils/validate.cjs"
},
"./package.json": "./package.json"
},
"repository": {
"type": "git",
"url": "git+https://github.com/freeCodeCamp/freeCodeCamp.git"
},
"bugs": {
"url": "https://github.com/freeCodeCamp/freeCodeCamp/issues"
},
"homepage": "https://github.com/freeCodeCamp/freeCodeCamp#readme",
"devDependencies": {
"@freecodecamp/eslint-config": "workspace:*",
"@vitest/ui": "^4.0.15",
"eslint": "^9.39.1",
"tsdown": "^0.21.0",
"vitest": "^4.0.15"
}
}
+53
View File
@@ -0,0 +1,53 @@
export enum BlockLabel {
lecture = 'lecture',
workshop = 'workshop',
lab = 'lab',
review = 'review',
quiz = 'quiz',
exam = 'exam',
certProject = 'cert-project',
/* The tags below refer to the Language Curricula chapter based certifications*/
warmup = 'warm-up',
learn = 'learn',
practice = 'practice'
}
export enum BlockLayouts {
/**
* These three are for the new Full Stack Developer Certification,
* so we can play with them without affecting the existing block layouts
*/
ChallengeList = 'challenge-list',
Link = 'link',
ChallengeGrid = 'challenge-grid',
DialogueGrid = 'dialogue-grid',
/**
* ChallengeList displays challenges in a list.
* This layout is used in backend blocks, The Odin Project blocks, and blocks in legacy certification.
* Example: https://www.freecodecamp.org/learn/javascript-algorithms-and-data-structures/#basic-javascript
*/
LegacyChallengeList = 'legacy-challenge-list',
/**
* LegacyChallengeGrid displays challenges in a grid.
* This layout is used for step-based blocks.
* Example: https://www.freecodecamp.org/learn/2022/responsive-web-design/#learn-html-by-building-a-cat-photo-app
*/
LegacyChallengeGrid = 'legacy-challenge-grid',
/**
* Link displays the block as a single link.
* This layout is used if the block has a single challenge.
* Example: https://www.freecodecamp.org/learn/2022/responsive-web-design/#build-a-survey-form-project
*/
LegacyLink = 'legacy-link',
/**
* ProjectList displays a list of certification projects.
* This layout is used in legacy certifications.
* Example: https://www.freecodecamp.org/learn/javascript-algorithms-and-data-structures/#javascript-algorithms-and-data-structures-projects
*/
ProjectList = 'project-list'
}
@@ -0,0 +1,13 @@
import { describe, it, expect } from 'vitest';
import { catalog } from './catalog';
import { superBlockStages, SuperBlockStage } from './curriculum';
describe('catalog', () => {
it('should have exactly one entry for each superblock in SuperBlockStage.Catalog', () => {
const catalogSuperBlocks = superBlockStages[SuperBlockStage.Catalog];
expect(catalog.map(course => course.superBlock.toString()).sort()).toEqual(
catalogSuperBlocks.map(sb => sb.toString()).sort()
);
});
});
+416
View File
@@ -0,0 +1,416 @@
import { SuperBlocks } from './curriculum';
enum Levels {
Beginner = 'beginner',
Intermediate = 'intermediate',
Advanced = 'advanced'
}
enum Topic {
Html = 'html',
CSS = 'css',
Js = 'js',
React = 'react',
Python = 'python',
DataAnalysis = 'data-analysis',
MachineLearning = 'machine-learning',
D3 = 'd3',
Api = 'api',
InformationSecurity = 'information-security',
ComputerFundamentals = 'computer-fundamentals',
ComputerScience = 'computer-science',
Math = 'math',
Databases = 'databases',
Bash = 'bash',
Git = 'git',
Editors = 'editors',
AI = 'ai'
}
interface Catalog {
superBlock: SuperBlocks;
level: Levels;
hours: number;
topic: Topic;
}
export const catalog: Catalog[] = [
{
superBlock: SuperBlocks.LearnPythonForBeginners,
level: Levels.Beginner,
hours: 5,
topic: Topic.Python
},
{
superBlock: SuperBlocks.IntroductionToAlgorithmsAndDataStructures,
level: Levels.Intermediate,
hours: 6,
topic: Topic.ComputerScience
},
{
superBlock: SuperBlocks.LearnRAGAndMCPFundamentals,
level: Levels.Intermediate,
hours: 2,
topic: Topic.AI
},
{
superBlock: SuperBlocks.SemanticHtml,
level: Levels.Beginner,
hours: 2,
topic: Topic.Html
},
{
superBlock: SuperBlocks.ComputerBasics,
level: Levels.Beginner,
hours: 2,
topic: Topic.ComputerFundamentals
},
{
superBlock: SuperBlocks.BasicCss,
level: Levels.Beginner,
hours: 3,
topic: Topic.CSS
},
{
superBlock: SuperBlocks.BasicHtml,
level: Levels.Beginner,
hours: 3,
topic: Topic.Html
},
{
superBlock: SuperBlocks.DesignForDevelopers,
level: Levels.Beginner,
hours: 2,
topic: Topic.CSS
},
{
superBlock: SuperBlocks.HtmlAndAccessibility,
level: Levels.Beginner,
hours: 2,
topic: Topic.Html
},
{
superBlock: SuperBlocks.CssFlexbox,
level: Levels.Intermediate,
hours: 2,
topic: Topic.CSS
},
{
superBlock: SuperBlocks.IntroductionToPrecalculus,
level: Levels.Intermediate,
hours: 6,
topic: Topic.Math
},
{
superBlock: SuperBlocks.IntroductionToGitAndGithub,
level: Levels.Intermediate,
hours: 20,
topic: Topic.Git
},
{
superBlock: SuperBlocks.IntroductionToPythonBasics,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Python
},
{
superBlock: SuperBlocks.AbsoluteAndRelativeUnits,
level: Levels.Intermediate,
hours: 2,
topic: Topic.CSS
},
{
superBlock: SuperBlocks.PseudoClassesAndElements,
level: Levels.Intermediate,
hours: 1,
topic: Topic.CSS
},
{
superBlock: SuperBlocks.HtmlFormsAndTables,
level: Levels.Beginner,
hours: 2,
topic: Topic.Html
},
{
superBlock: SuperBlocks.CssColors,
level: Levels.Intermediate,
hours: 2,
topic: Topic.CSS
},
{
superBlock: SuperBlocks.JavascriptFundamentalsReview,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Js
},
{
superBlock: SuperBlocks.StylingForms,
level: Levels.Intermediate,
hours: 1,
topic: Topic.CSS
},
{
superBlock: SuperBlocks.CssBoxModel,
level: Levels.Intermediate,
hours: 2,
topic: Topic.CSS
},
{
superBlock: SuperBlocks.IntroductionToBash,
level: Levels.Intermediate,
hours: 20,
topic: Topic.Bash
},
{
superBlock: SuperBlocks.IntroductionToLinearDataStructuresInPython,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Python
},
{
superBlock: SuperBlocks.LearnAlgorithmsInPython,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Python
},
{
superBlock: SuperBlocks.CssTypography,
level: Levels.Intermediate,
hours: 1,
topic: Topic.CSS
},
{
superBlock: SuperBlocks.CssAndAccessibility,
level: Levels.Intermediate,
hours: 2,
topic: Topic.CSS
},
{
superBlock: SuperBlocks.CssPositioning,
level: Levels.Intermediate,
hours: 2,
topic: Topic.CSS
},
{
superBlock: SuperBlocks.AttributeSelectors,
level: Levels.Intermediate,
hours: 2,
topic: Topic.CSS
},
{
superBlock: SuperBlocks.ResponsiveDesign,
level: Levels.Intermediate,
hours: 2,
topic: Topic.CSS
},
{
superBlock: SuperBlocks.CssVariables,
level: Levels.Intermediate,
hours: 2,
topic: Topic.CSS
},
{
superBlock: SuperBlocks.CssGrid,
level: Levels.Intermediate,
hours: 2,
topic: Topic.CSS
},
{
superBlock: SuperBlocks.CssAnimations,
level: Levels.Advanced,
hours: 2,
topic: Topic.CSS
},
{
superBlock: SuperBlocks.IntroductionToSQLAndPostgreSQL,
level: Levels.Intermediate,
hours: 30,
topic: Topic.Databases
},
{
superBlock: SuperBlocks.LearnBashScripting,
level: Levels.Intermediate,
hours: 20,
topic: Topic.Bash
},
{
superBlock: SuperBlocks.LearnSQLAndBash,
level: Levels.Intermediate,
hours: 30,
topic: Topic.Databases
},
{
superBlock: SuperBlocks.IntroductionToNano,
level: Levels.Intermediate,
hours: 10,
topic: Topic.Editors
},
{
superBlock: SuperBlocks.IntroductionToVariablesAndStringsInJS,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Js
},
{
superBlock: SuperBlocks.IntroductionToBooleansAndNumbersInJS,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Js
},
{
superBlock: SuperBlocks.IntroductionToFunctionsInJS,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Js
},
{
superBlock: SuperBlocks.IntroductionToArraysInJS,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Js
},
{
superBlock: SuperBlocks.IntroductionToObjectsInJS,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Js
},
{
superBlock: SuperBlocks.IntroductionToLoopsInJS,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Js
},
{
superBlock: SuperBlocks.IntroductionToHigherOrderFunctionsAndCallbacksInJS,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Js
},
{
superBlock: SuperBlocks.LearnDomManipulationAndEventsWithJS,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Js
},
{
superBlock: SuperBlocks.IntroductionToJavascriptAndAccessibility,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Js
},
{
superBlock: SuperBlocks.LearnJavascriptDebugging,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Js
},
{
superBlock: SuperBlocks.LearnBasicRegexWithJS,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Js
},
{
superBlock: SuperBlocks.IntroductionToDatesInJS,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Js
},
{
superBlock: SuperBlocks.LearnAudioAndVideoEventsWithJS,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Js
},
{
superBlock: SuperBlocks.IntroductionToMapsAndSetsInJS,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Js
},
{
superBlock: SuperBlocks.LearnLocalstorageAndCrudOperationsWithJS,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Js
},
{
superBlock: SuperBlocks.IntroductionToJavascriptClasses,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Js
},
{
superBlock: SuperBlocks.LearnRecursionWithJS,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Js
},
{
superBlock: SuperBlocks.IntroductionToFunctionalProgrammingWithJS,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Js
},
{
superBlock: SuperBlocks.IntroductionToAsynchronousJS,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Js
},
{
superBlock: SuperBlocks.LearnDataVisualizationWithD3,
level: Levels.Intermediate,
hours: 20,
topic: Topic.D3
},
{
superBlock: SuperBlocks.LearnOOPWithPython,
level: Levels.Intermediate,
hours: 3,
topic: Topic.Python
},
{
superBlock: SuperBlocks.LearnPythonLoopsAndSequences,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Python
},
{
superBlock: SuperBlocks.LearnPythonDictionariesAndSets,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Python
},
{
superBlock: SuperBlocks.LearnErrorHandlingInPython,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Python
},
{
superBlock: SuperBlocks.LearnPythonClassesAndObjects,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Python
},
{
superBlock: SuperBlocks.IntroductionToOOPInPython,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Python
},
{
superBlock: SuperBlocks.LearnGraphsAndTreesInPython,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Python
},
{
superBlock: SuperBlocks.LearnDynamicProgrammingInPython,
level: Levels.Intermediate,
hours: 40,
topic: Topic.Python
}
];
@@ -0,0 +1,81 @@
import { describe, it, expect } from 'vitest';
import {
Certification,
linkedInCredentialIds,
certToTitleMap,
certToIdMap,
certSlugTypeMap,
isCertified
} from './certification-settings';
describe('linkedInCredentialIds', () => {
it('should contain a value for all certifications', () => {
const allCertifications = Object.values(Certification).sort();
const linkedInCredentialIdsKeys = Object.keys(linkedInCredentialIds).sort();
expect(linkedInCredentialIdsKeys).toEqual(allCertifications);
});
});
describe('certToTitleMap', () => {
it('should not contain duplicate titles', () => {
const titles = Object.values(certToTitleMap);
const uniqueTitles = Array.from(new Set(titles));
expect(titles.length).toBe(uniqueTitles.length);
});
});
describe('certToIdMap', () => {
it('should have no duplicate values', () => {
const ids = Object.values(certToIdMap).sort();
const uniqueIds = Array.from(new Set(ids)).sort();
expect(uniqueIds).toEqual(ids);
});
});
describe('certSlugTypeMap', () => {
it('should contain a value for all certifications', () => {
const allCertifications = Object.values(Certification).sort();
const certSlugTypeMapKeys = Object.keys(certSlugTypeMap).sort();
expect(certSlugTypeMapKeys).toEqual(allCertifications);
});
it('should have no duplicate values', () => {
const types = Object.values(certSlugTypeMap).sort();
const uniqueTypes = Array.from(new Set(types)).sort();
expect(uniqueTypes).toEqual(types);
});
});
describe('isCertified', () => {
it('should return true if a user has the specified certification', () => {
const cert = Certification.RespWebDesignV9;
const user = {
isRespWebDesignCertV9: true
};
expect(isCertified(user, cert)).toBe(true);
});
it('should return false if a user does not have the specified certification', () => {
const cert = Certification.JsAlgoDataStruct;
const user = {
isRespWebDesignCertV9: true
};
expect(isCertified(user, cert)).toBe(false);
});
it('should return false if the certification does not exist', () => {
const cert = 'NonExistentCert' as Certification;
const user = {
isRespWebDesignCertV9: true
};
expect(isCertified(user, cert)).toBe(false);
});
});
@@ -0,0 +1,457 @@
import { SuperBlocks } from '../config/curriculum.js';
/**
* Certifications are not equivalent to superblocks. Each superblock corresponds
* to 0 or 1 certifications, but a certification may not correspond to a
* superblock.
*
* As an example of the former: the CodingInterviewPrep superblock does not
* correspond to a certification. As an example of the latter: the legacy
* front-end certification no longer has a corresponding superblock.
*
* The value of each enum member is the slug of the corresponding certification.
*/
export enum Certification {
RespWebDesign = 'responsive-web-design',
JsAlgoDataStructNew = 'javascript-algorithms-and-data-structures-v8',
FrontEndDevLibs = 'front-end-development-libraries',
DataVis = 'data-visualization',
RelationalDb = 'relational-database-v8',
BackEndDevApis = 'back-end-development-and-apis',
QualityAssurance = 'quality-assurance-v7',
SciCompPy = 'scientific-computing-with-python-v7',
DataAnalysisPy = 'data-analysis-with-python-v7',
InfoSec = 'information-security-v7',
MachineLearningPy = 'machine-learning-with-python-v7',
CollegeAlgebraPy = 'college-algebra-with-python-v8',
FoundationalCSharp = 'foundational-c-sharp-with-microsoft',
PythonV9 = 'python-v9',
RelationalDbV9 = 'relational-databases-v9',
// Upcoming certifications
RespWebDesignV9 = 'responsive-web-design-v9',
JsV9 = 'javascript-v9',
FrontEndDevLibsV9 = 'front-end-development-libraries-v9',
BackEndDevApisV9 = 'back-end-development-and-apis-v9',
A2English = 'a2-english-for-developers',
FullStackDeveloperV9 = 'full-stack-developer-v9',
B1English = 'b1-english-for-developers',
A2Spanish = 'a2-professional-spanish',
A2Chinese = 'a2-professional-chinese',
A1Chinese = 'a1-professional-chinese',
// Legacy certifications
LegacyFrontEnd = 'legacy-front-end',
JsAlgoDataStruct = 'javascript-algorithms-and-data-structures',
LegacyBackEnd = 'legacy-back-end',
LegacyDataVis = 'legacy-data-visualization',
LegacyInfoSecQa = 'information-security-and-quality-assurance',
LegacyFullStack = 'full-stack'
}
export function isCertification(x: string): x is Certification {
return Object.values(Certification).includes(x as Certification);
}
// "Current" certifications are the subset of standard certifications that are
// live and not legacy.
export const currentCertifications = [
Certification.A2English,
Certification.B1English,
Certification.FoundationalCSharp,
Certification.JsV9,
Certification.PythonV9,
Certification.RelationalDbV9,
Certification.RespWebDesignV9
] as const;
// "Legacy" certifications are another class of standard certifications. They're
// still live and claimable, but some parts of the UI handle them differently.
export const legacyCertifications = [
Certification.RespWebDesign,
Certification.JsAlgoDataStruct,
Certification.FrontEndDevLibs,
Certification.DataVis,
Certification.BackEndDevApis,
Certification.LegacyInfoSecQa,
Certification.LegacyFrontEnd,
Certification.JsAlgoDataStructNew,
Certification.LegacyBackEnd,
Certification.LegacyDataVis,
Certification.RelationalDb,
Certification.QualityAssurance,
Certification.SciCompPy,
Certification.DataAnalysisPy,
Certification.InfoSec,
Certification.MachineLearningPy,
Certification.CollegeAlgebraPy
] as const;
// The Legacy Full Stack certification can only be claimed when specific
// "current" and "legacy" certifications have been claimed.
export const legacyFullStackCertification = [
Certification.LegacyFullStack
] as const;
// "Upcoming" certifications are standard certifications that are not live unless
// showUpcomingChanges is true.
export const upcomingCertifications = [
Certification.FrontEndDevLibsV9,
Certification.BackEndDevApisV9,
Certification.FullStackDeveloperV9,
Certification.A2Spanish,
Certification.A2Chinese,
Certification.A1Chinese
] as const;
export const certToIdMap: Record<Certification, string> = {
// Legacy certifications
[Certification.LegacyFrontEnd]: '561add10cb82ac38a17513be',
[Certification.JsAlgoDataStruct]: '561abd10cb81ac38a17513bc',
[Certification.LegacyBackEnd]: '660add10cb82ac38a17513be',
[Certification.LegacyDataVis]: '561add10cb82ac39a17513bc',
[Certification.LegacyInfoSecQa]: '561add10cb82ac38a17213bc',
[Certification.LegacyFullStack]: '561add10cb82ac38a17213bd',
// Current certifications
[Certification.RespWebDesign]: '561add10cb82ac38a17513bc',
[Certification.JsAlgoDataStructNew]: '658180220947283cdc0689ce',
[Certification.FrontEndDevLibs]: '561acd10cb82ac38a17513bc',
[Certification.DataVis]: '5a553ca864b52e1d8bceea14',
[Certification.BackEndDevApis]: '561add10cb82ac38a17523bc',
[Certification.QualityAssurance]: '5e611829481575a52dc59c0e',
[Certification.InfoSec]: '5e6021435ac9d0ecd8b94b00',
[Certification.SciCompPy]: '5e44431b903586ffb414c951',
[Certification.DataAnalysisPy]: '5e46fc95ac417301a38fb934',
[Certification.MachineLearningPy]: '5e46fc95ac417301a38fb935',
[Certification.RelationalDb]: '606243f50267e718b1e755f4',
[Certification.CollegeAlgebraPy]: '61531b20cc9dfa2741a5b800',
[Certification.FoundationalCSharp]: '647f7da207d29547b3bee1ba',
[Certification.A2English]: '651dd7e01d697d0aab7833b7',
// Upcoming certifications
[Certification.RespWebDesignV9]: '68db314d3c11a8bff07c7535',
[Certification.JsV9]: '68c4069c1ef859270e17c495',
[Certification.FrontEndDevLibsV9]: '68e008aa5f80c6099d47b3a2',
[Certification.PythonV9]: '68e6bd5020effa1586e79855',
[Certification.RelationalDbV9]: '68e6bd5120effa1586e79856',
[Certification.BackEndDevApisV9]: '68e6bd5120effa1586e79857',
[Certification.FullStackDeveloperV9]: '64514fda6c245de4d11eb7bb',
[Certification.B1English]: '66607e53317411dd5e8aae21',
[Certification.A2Spanish]: '681a6b22e5a782fe3459984a',
[Certification.A2Chinese]: '682c3153086dd7cabe7f48bc',
[Certification.A1Chinese]: '68f1268149f045a650d4229e'
};
export const completionHours: Record<Certification, number> = {
[Certification.LegacyFrontEnd]: 300,
[Certification.JsAlgoDataStruct]: 300,
[Certification.LegacyBackEnd]: 300,
[Certification.LegacyDataVis]: 300,
[Certification.LegacyInfoSecQa]: 300,
[Certification.LegacyFullStack]: 1800,
[Certification.RespWebDesign]: 300,
[Certification.JsAlgoDataStructNew]: 300,
[Certification.FrontEndDevLibs]: 300,
[Certification.DataVis]: 300,
[Certification.BackEndDevApis]: 300,
[Certification.QualityAssurance]: 300,
[Certification.InfoSec]: 300,
[Certification.SciCompPy]: 300,
[Certification.DataAnalysisPy]: 300,
[Certification.MachineLearningPy]: 300,
[Certification.RelationalDb]: 300,
[Certification.CollegeAlgebraPy]: 300,
[Certification.FoundationalCSharp]: 300,
[Certification.A2English]: 300,
[Certification.RespWebDesignV9]: 300,
[Certification.JsV9]: 300,
[Certification.FrontEndDevLibsV9]: 300,
[Certification.PythonV9]: 300,
[Certification.RelationalDbV9]: 300,
[Certification.BackEndDevApisV9]: 300,
[Certification.FullStackDeveloperV9]: 1800,
[Certification.B1English]: 300,
[Certification.A2Spanish]: 300,
[Certification.A2Chinese]: 300,
[Certification.A1Chinese]: 300
};
type UserCertFlag =
| 'isFrontEndCert'
| 'isJsAlgoDataStructCert'
| 'isBackEndCert'
| 'isDataVisCert'
| 'isInfosecQaCert'
| 'isFullStackCert'
| 'isRespWebDesignCert'
| 'isJsAlgoDataStructCertV8'
| 'isFrontEndLibsCert'
| 'is2018DataVisCert'
| 'isApisMicroservicesCert'
| 'isQaCertV7'
| 'isInfosecCertV7'
| 'isSciCompPyCertV7'
| 'isDataAnalysisPyCertV7'
| 'isMachineLearningPyCertV7'
| 'isRelationalDatabaseCertV8'
| 'isCollegeAlgebraPyCertV8'
| 'isFoundationalCSharpCertV8'
| 'isA2EnglishCert'
| 'isRespWebDesignCertV9'
| 'isJavascriptCertV9'
| 'isFrontEndLibsCertV9'
| 'isPythonCertV9'
| 'isRelationalDatabaseCertV9'
| 'isBackEndDevApisCertV9'
| 'isFullStackDeveloperCertV9'
| 'isB1EnglishCert'
| 'isA2SpanishCert'
| 'isA2ChineseCert'
| 'isA1ChineseCert';
export const certSlugTypeMap: Record<Certification, UserCertFlag> = {
// legacy
[Certification.LegacyFrontEnd]: 'isFrontEndCert',
[Certification.JsAlgoDataStruct]: 'isJsAlgoDataStructCert',
[Certification.LegacyBackEnd]: 'isBackEndCert',
[Certification.LegacyDataVis]: 'isDataVisCert',
[Certification.LegacyInfoSecQa]: 'isInfosecQaCert',
[Certification.LegacyFullStack]: 'isFullStackCert',
// modern
[Certification.RespWebDesign]: 'isRespWebDesignCert',
[Certification.JsAlgoDataStructNew]: 'isJsAlgoDataStructCertV8',
[Certification.FrontEndDevLibs]: 'isFrontEndLibsCert',
[Certification.DataVis]: 'is2018DataVisCert',
[Certification.BackEndDevApis]: 'isApisMicroservicesCert',
[Certification.QualityAssurance]: 'isQaCertV7',
[Certification.InfoSec]: 'isInfosecCertV7',
[Certification.SciCompPy]: 'isSciCompPyCertV7',
[Certification.DataAnalysisPy]: 'isDataAnalysisPyCertV7',
[Certification.MachineLearningPy]: 'isMachineLearningPyCertV7',
[Certification.RelationalDb]: 'isRelationalDatabaseCertV8',
[Certification.CollegeAlgebraPy]: 'isCollegeAlgebraPyCertV8',
[Certification.FoundationalCSharp]: 'isFoundationalCSharpCertV8',
[Certification.A2English]: 'isA2EnglishCert',
[Certification.PythonV9]: 'isPythonCertV9',
[Certification.RelationalDbV9]: 'isRelationalDatabaseCertV9',
[Certification.RespWebDesignV9]: 'isRespWebDesignCertV9',
[Certification.JsV9]: 'isJavascriptCertV9',
// upcoming
[Certification.FrontEndDevLibsV9]: 'isFrontEndLibsCertV9',
[Certification.BackEndDevApisV9]: 'isBackEndDevApisCertV9',
[Certification.FullStackDeveloperV9]: 'isFullStackDeveloperCertV9',
[Certification.B1English]: 'isB1EnglishCert',
[Certification.A2Spanish]: 'isA2SpanishCert',
[Certification.A2Chinese]: 'isA2ChineseCert',
[Certification.A1Chinese]: 'isA1ChineseCert'
};
export type CertificationFlags = {
[key in UserCertFlag]: boolean;
};
export function isCertified(
user: Partial<CertificationFlags>,
cert: Certification
): boolean {
const certFlag = certSlugTypeMap[cert];
return Boolean(user[certFlag]);
}
// TODO: use i18n keys instead of hardcoded titles
export const certToTitleMap: Record<Certification, string> = {
// Legacy certifications
[Certification.LegacyFrontEnd]: 'Legacy Frontend',
[Certification.JsAlgoDataStruct]:
'Legacy JavaScript Algorithms and Data Structures V7',
[Certification.LegacyBackEnd]: 'Legacy Backend',
[Certification.LegacyDataVis]: 'Legacy Data Visualization',
[Certification.LegacyInfoSecQa]:
'Legacy Information Security and Quality Assurance',
[Certification.LegacyFullStack]: 'Legacy Full-Stack',
// Current certifications
[Certification.RespWebDesign]: 'Legacy Responsive Web Design V8',
[Certification.JsAlgoDataStructNew]:
'Legacy JavaScript Algorithms and Data Structures V8',
[Certification.FrontEndDevLibs]: 'Frontend Development Libraries V8',
[Certification.DataVis]: 'Data Visualization V8',
[Certification.BackEndDevApis]: 'Backend Development and APIs V8',
[Certification.QualityAssurance]: 'Quality Assurance',
[Certification.InfoSec]: 'Information Security',
[Certification.SciCompPy]: 'Scientific Computing with Python',
[Certification.DataAnalysisPy]: 'Data Analysis with Python',
[Certification.MachineLearningPy]: 'Machine Learning with Python',
[Certification.RelationalDb]: 'Relational Database V8',
[Certification.CollegeAlgebraPy]: 'College Algebra with Python',
[Certification.FoundationalCSharp]: 'Foundational C# with Microsoft',
[Certification.A2English]: 'A2 English for Developers',
// Upcoming certifications
[Certification.RespWebDesignV9]: 'Responsive Web Design',
[Certification.JsV9]: 'JavaScript',
[Certification.FrontEndDevLibsV9]: 'Frontend Development Libraries',
[Certification.PythonV9]: 'Python',
[Certification.RelationalDbV9]: 'Relational Database',
[Certification.BackEndDevApisV9]: 'Backend Development and APIs',
[Certification.FullStackDeveloperV9]: 'Full-Stack Developer',
[Certification.B1English]: 'B1 English for Developers',
[Certification.A2Spanish]: 'A2 Professional Spanish',
[Certification.A2Chinese]: 'A2 Professional Chinese',
[Certification.A1Chinese]: 'A1 Professional Chinese'
};
export const superBlockToCertMap: {
[key in SuperBlocks]: Certification | null;
} = {
[SuperBlocks.RespWebDesign]: Certification.RespWebDesign,
[SuperBlocks.JsAlgoDataStructNew]: Certification.JsAlgoDataStructNew,
[SuperBlocks.FrontEndDevLibs]: Certification.FrontEndDevLibs,
[SuperBlocks.DataVis]: Certification.DataVis,
[SuperBlocks.RelationalDb]: Certification.RelationalDb,
[SuperBlocks.BackEndDevApis]: Certification.BackEndDevApis,
[SuperBlocks.QualityAssurance]: Certification.QualityAssurance,
[SuperBlocks.SciCompPy]: Certification.SciCompPy,
[SuperBlocks.DataAnalysisPy]: Certification.DataAnalysisPy,
[SuperBlocks.InfoSec]: Certification.InfoSec,
[SuperBlocks.MachineLearningPy]: Certification.MachineLearningPy,
[SuperBlocks.CollegeAlgebraPy]: Certification.CollegeAlgebraPy,
[SuperBlocks.FoundationalCSharp]: Certification.FoundationalCSharp,
[SuperBlocks.RespWebDesignNew]: Certification.RespWebDesign,
[SuperBlocks.JsAlgoDataStruct]: Certification.JsAlgoDataStruct,
[SuperBlocks.RespWebDesignV9]: Certification.RespWebDesignV9,
[SuperBlocks.JsV9]: Certification.JsV9,
[SuperBlocks.FrontEndDevLibsV9]: Certification.FrontEndDevLibsV9,
[SuperBlocks.PythonV9]: Certification.PythonV9,
[SuperBlocks.RelationalDbV9]: Certification.RelationalDbV9,
[SuperBlocks.BackEndDevApisV9]: Certification.BackEndDevApisV9,
[SuperBlocks.FullStackDeveloperV9]: Certification.FullStackDeveloperV9,
[SuperBlocks.A2English]: Certification.A2English,
[SuperBlocks.B1English]: Certification.B1English,
[SuperBlocks.A1Spanish]: null,
[SuperBlocks.A2Spanish]: Certification.A2Spanish,
[SuperBlocks.A2Chinese]: Certification.A2Chinese,
[SuperBlocks.A1Chinese]: Certification.A1Chinese,
[SuperBlocks.PythonForEverybody]: null,
[SuperBlocks.CodingInterviewPrep]: null,
[SuperBlocks.ProjectEuler]: null,
[SuperBlocks.TheOdinProject]: null,
[SuperBlocks.RosettaCode]: null,
[SuperBlocks.BasicHtml]: null,
[SuperBlocks.SemanticHtml]: null,
[SuperBlocks.DevPlayground]: null,
[SuperBlocks.FullStackOpen]: null,
[SuperBlocks.HtmlFormsAndTables]: null,
[SuperBlocks.HtmlAndAccessibility]: null,
[SuperBlocks.ComputerBasics]: null,
[SuperBlocks.BasicCss]: null,
[SuperBlocks.DesignForDevelopers]: null,
[SuperBlocks.AbsoluteAndRelativeUnits]: null,
[SuperBlocks.PseudoClassesAndElements]: null,
[SuperBlocks.CssColors]: null,
[SuperBlocks.StylingForms]: null,
[SuperBlocks.CssBoxModel]: null,
[SuperBlocks.CssFlexbox]: null,
[SuperBlocks.CssTypography]: null,
[SuperBlocks.CssAndAccessibility]: null,
[SuperBlocks.CssPositioning]: null,
[SuperBlocks.AttributeSelectors]: null,
[SuperBlocks.ResponsiveDesign]: null,
[SuperBlocks.CssVariables]: null,
[SuperBlocks.CssGrid]: null,
[SuperBlocks.CssAnimations]: null,
[SuperBlocks.LearnPythonForBeginners]: null,
[SuperBlocks.IntroductionToAlgorithmsAndDataStructures]: null,
[SuperBlocks.LearnOOPWithPython]: null,
[SuperBlocks.LearnRAGAndMCPFundamentals]: null,
[SuperBlocks.IntroductionToPrecalculus]: null,
[SuperBlocks.IntroductionToBash]: null,
[SuperBlocks.IntroductionToSQLAndPostgreSQL]: null,
[SuperBlocks.LearnBashScripting]: null,
[SuperBlocks.LearnSQLAndBash]: null,
[SuperBlocks.IntroductionToNano]: null,
[SuperBlocks.IntroductionToGitAndGithub]: null,
[SuperBlocks.IntroductionToVariablesAndStringsInJS]: null,
[SuperBlocks.IntroductionToBooleansAndNumbersInJS]: null,
[SuperBlocks.IntroductionToFunctionsInJS]: null,
[SuperBlocks.IntroductionToArraysInJS]: null,
[SuperBlocks.IntroductionToObjectsInJS]: null,
[SuperBlocks.IntroductionToLoopsInJS]: null,
[SuperBlocks.JavascriptFundamentalsReview]: null,
[SuperBlocks.IntroductionToHigherOrderFunctionsAndCallbacksInJS]: null,
[SuperBlocks.LearnDomManipulationAndEventsWithJS]: null,
[SuperBlocks.IntroductionToJavascriptAndAccessibility]: null,
[SuperBlocks.LearnJavascriptDebugging]: null,
[SuperBlocks.LearnBasicRegexWithJS]: null,
[SuperBlocks.IntroductionToDatesInJS]: null,
[SuperBlocks.LearnAudioAndVideoEventsWithJS]: null,
[SuperBlocks.IntroductionToMapsAndSetsInJS]: null,
[SuperBlocks.LearnLocalstorageAndCrudOperationsWithJS]: null,
[SuperBlocks.IntroductionToJavascriptClasses]: null,
[SuperBlocks.LearnRecursionWithJS]: null,
[SuperBlocks.IntroductionToFunctionalProgrammingWithJS]: null,
[SuperBlocks.IntroductionToAsynchronousJS]: null,
[SuperBlocks.LearnDataVisualizationWithD3]: null,
[SuperBlocks.IntroductionToPythonBasics]: null,
[SuperBlocks.LearnPythonLoopsAndSequences]: null,
[SuperBlocks.LearnPythonDictionariesAndSets]: null,
[SuperBlocks.LearnErrorHandlingInPython]: null,
[SuperBlocks.LearnPythonClassesAndObjects]: null,
[SuperBlocks.IntroductionToOOPInPython]: null,
[SuperBlocks.IntroductionToLinearDataStructuresInPython]: null,
[SuperBlocks.LearnAlgorithmsInPython]: null,
[SuperBlocks.LearnGraphsAndTreesInPython]: null,
[SuperBlocks.LearnDynamicProgrammingInPython]: null
};
export const certificationRequirements: Partial<
Record<Certification, SuperBlocks[]>
> = {
[Certification.FullStackDeveloperV9]: [
SuperBlocks.RespWebDesignV9,
SuperBlocks.JsV9,
SuperBlocks.FrontEndDevLibsV9,
SuperBlocks.PythonV9,
SuperBlocks.RelationalDbV9,
SuperBlocks.BackEndDevApisV9
]
};
export type CertSlug = (typeof Certification)[keyof typeof Certification];
export const linkedInCredentialIds = {
[Certification.LegacyFrontEnd]: 'lfe',
[Certification.LegacyBackEnd]: 'lbe',
[Certification.LegacyDataVis]: 'ldv',
[Certification.LegacyInfoSecQa]: 'lisaqa',
[Certification.LegacyFullStack]: 'lfs',
[Certification.RespWebDesign]: 'rwd',
[Certification.FrontEndDevLibs]: 'fedl',
[Certification.JsAlgoDataStruct]: 'ljaads',
[Certification.DataVis]: 'dv',
[Certification.BackEndDevApis]: 'bedaa',
[Certification.QualityAssurance]: 'qa',
[Certification.InfoSec]: 'is',
[Certification.SciCompPy]: 'scwp',
[Certification.DataAnalysisPy]: 'dawp',
[Certification.MachineLearningPy]: 'mlwp',
[Certification.RelationalDb]: 'rd',
[Certification.CollegeAlgebraPy]: 'cawp',
[Certification.FoundationalCSharp]: 'fcswm',
[Certification.RespWebDesignV9]: 'rwdv9',
[Certification.JsV9]: 'jsv9',
[Certification.FrontEndDevLibsV9]: 'felv9',
[Certification.PythonV9]: 'pyv9',
[Certification.RelationalDbV9]: 'rdv9',
[Certification.BackEndDevApisV9]: 'bedv9',
[Certification.FullStackDeveloperV9]: 'fsdv9',
[Certification.JsAlgoDataStructNew]: 'jaads',
[Certification.A2English]: 'a2efd',
[Certification.B1English]: 'b1efd',
[Certification.A2Spanish]: 'a2ps',
[Certification.A2Chinese]: 'a2pc',
[Certification.A1Chinese]: 'a1pc'
};
export const oldDataVizId = '561add10cb82ac38a17513b3';
@@ -0,0 +1,197 @@
const html = 0;
const js = 1;
const backend = 2;
const zipline = 3;
const frontEndProject = 3;
const backEndProject = 4;
const jsProject = 5;
const modern = 6;
const step = 7;
const quiz = 8;
const invalid = 9;
const pythonProject = 10;
const video = 11;
const codeAllyPractice = 12;
const codeAllyCert = 13;
const multifileCertProject = 14;
const theOdinProject = 15;
const colab = 16;
const exam = 17;
const msTrophy = 18;
const multipleChoice = 19;
const python = 20;
const dialogue = 21;
const fillInTheBlank = 22;
const multifilePythonCertProject = 23;
const generic = 24;
const lab = 25;
const jsLab = 26;
const pyLab = 27;
const dailyChallengeJs = 28;
const dailyChallengePy = 29;
const examDownload = 30;
const review = 31;
export const challengeTypes = {
html,
js,
backend,
zipline,
frontEndProject,
backEndProject,
pythonProject,
jsProject,
modern,
step,
quiz,
invalid,
video,
codeAllyPractice,
codeAllyCert,
multifileCertProject,
theOdinProject,
colab,
exam,
msTrophy,
multipleChoice,
python,
dialogue,
fillInTheBlank,
multifilePythonCertProject,
generic,
lab,
jsLab,
pyLab,
dailyChallengeJs,
dailyChallengePy,
examDownload,
review
};
export const hasNoSolution = (challengeType: number): boolean => {
const noSolutions = [
backend,
zipline,
frontEndProject,
backEndProject,
step,
quiz,
invalid,
pythonProject,
video,
codeAllyPractice,
codeAllyCert,
theOdinProject,
colab,
exam,
msTrophy,
multipleChoice,
dialogue,
fillInTheBlank,
generic,
examDownload,
review
];
return noSolutions.includes(challengeType);
};
// determine the component view for each challenge
export const viewTypes = {
[html]: 'classic',
[js]: 'classic',
[jsProject]: 'classic',
[frontEndProject]: 'frontend',
[backEndProject]: 'backend',
[pythonProject]: 'frontend',
[modern]: 'modern',
[step]: 'step',
[quiz]: 'quiz',
[backend]: 'backend',
[video]: 'generic',
[codeAllyPractice]: 'codeAlly',
[codeAllyCert]: 'codeAlly',
[multifileCertProject]: 'classic',
[theOdinProject]: 'generic',
[colab]: 'frontend',
[exam]: 'exam',
[msTrophy]: 'msTrophy',
[multipleChoice]: 'generic',
[python]: 'modern',
[dialogue]: 'generic',
[fillInTheBlank]: 'fillInTheBlank',
[multifilePythonCertProject]: 'classic',
[generic]: 'generic',
[lab]: 'classic',
[jsLab]: 'classic',
[pyLab]: 'classic',
[dailyChallengeJs]: 'classic',
[dailyChallengePy]: 'classic',
[examDownload]: 'examDownload',
[review]: 'generic'
};
// determine the type of submit function to use for the challenge on completion
export const submitTypes = {
[html]: 'tests',
[js]: 'tests',
[jsProject]: 'tests',
// requires just a single url
// like codepen.com/my-project
[frontEndProject]: 'project.frontEnd',
// requires two urls
// a hosted URL where the app is running live
// project code url like GitHub
[backEndProject]: 'project.backEnd',
[pythonProject]: 'project.backEnd',
[step]: 'step',
[quiz]: 'tests',
[backend]: 'backend',
[modern]: 'tests',
[video]: 'tests',
[codeAllyCert]: 'project.frontEnd',
[multifileCertProject]: 'tests',
[theOdinProject]: 'tests',
[colab]: 'project.backEnd',
[exam]: 'exam',
[msTrophy]: 'msTrophy',
[multipleChoice]: 'tests',
[python]: 'tests',
[dialogue]: 'tests',
[fillInTheBlank]: 'tests',
[multifilePythonCertProject]: 'tests',
[generic]: 'tests',
[lab]: 'tests',
[jsLab]: 'tests',
[pyLab]: 'tests',
[dailyChallengeJs]: 'tests',
[dailyChallengePy]: 'tests',
[examDownload]: 'examDownload',
[review]: 'tests'
};
const dailyCodingChallengeTypes = [
challengeTypes.dailyChallengeJs,
challengeTypes.dailyChallengePy
];
export const getIsDailyCodingChallenge = (challengeType: number): boolean =>
dailyCodingChallengeTypes.includes(challengeType);
const labChallengeTypes = [
challengeTypes.lab,
challengeTypes.jsLab,
challengeTypes.pyLab
];
export const getIsLabChallenge = (challengeType: number): boolean =>
labChallengeTypes.includes(challengeType);
const dailyCodingChallengeLanguages = {
[challengeTypes.dailyChallengeJs]: 'javascript',
[challengeTypes.dailyChallengePy]: 'python'
};
export const getDailyCodingChallengeLanguage = (
challengeType: number
): string | undefined => dailyCodingChallengeLanguages[challengeType];
+53
View File
@@ -0,0 +1,53 @@
import type { Module } from './modules';
// TODO: Dynamically create these from intro.json or full-stack.json
export enum FsdChapters {
// original FSD
Welcome = 'freecodecamp',
Javascript = 'javascript',
FrontendLibraries = 'frontend-libraries',
BackendJavascript = 'backend-javascript',
Career = 'career',
// new FSD
RwdExam = 'responsive-web-design-certification-exam',
JsExam = 'javascript-certification-exam',
Fed = 'front-end-development-libraries',
FedExam = 'front-end-development-libraries-certification-exam',
PythonExam = 'python-certification-exam',
RdbExam = 'relational-databases-certification-exam',
Bed = 'back-end-development-and-apis',
BedExam = 'back-end-development-and-apis-certification-exam',
FsdExam = 'certified-full-stack-developer-exam',
// used in both
Html = 'html',
Css = 'css',
Python = 'python',
RelationalDatabases = 'relational-databases'
}
export enum A1ChineseChapters {
zhA1Welcome = 'zh-a1-chapter-welcome-to-a1-professional-chinese',
zhA1PinYin = 'zh-a1-chapter-pinyin',
zhA1Greetings = 'zh-a1-chapter-greetings-and-introductions',
zhA1GreetingsLegacy = 'zh-a1-chapter-greeting-and-self-introduction',
zhA1NumbersAndPersonalInformation = 'zh-a1-chapter-numbers-and-personal-information',
zhA1Family = 'zh-a1-chapter-introducing-colleagues-and-family',
zhA1Expressing = 'zh-a1-chapter-expressing-what-you-can-and-cant-do'
}
export enum A1SpanishChapters {
esA1Welcome = 'es-a1-chapter-welcome-to-a1-professional-spanish',
esA1Fundamentals = 'es-a1-chapter-spanish-fundamentals',
esA1Greetings = 'es-a1-chapter-greetings-and-introductions',
esA1Details = 'es-a1-chapter-basic-personal-details',
esA1Describing = 'es-a1-chapter-describing-company-and-people'
}
export interface Chapter {
dashedName: string;
comingSoon?: boolean;
modules: Module[];
chapterType?: string;
}
@@ -0,0 +1,27 @@
import { describe, it, expect } from 'vitest';
import { blocklistedUsernames } from './constants';
describe('constants', () => {
describe('blocklistedUsernames', () => {
it('should not contain duplicate values', () => {
const uniqueValues = new Set(blocklistedUsernames);
expect(blocklistedUsernames.length).toEqual(uniqueValues.size);
});
it('should contain all the letters in the latin alphabet', () => {
const alphabet = 'abcdefghijklmnopqrstuvwxyz'.split('');
expect(blocklistedUsernames).toEqual(expect.arrayContaining(alphabet));
});
});
});
// Type tests:
type BlocklistedUsernames = (typeof blocklistedUsernames)[number];
type HasString = string extends BlocklistedUsernames ? true : false;
type Expect<T extends true> = T;
// @ts-expect-error - This is intended to fail since we want to ensure that blocklistedUsernames is an array of literals
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type Test = Expect<HasString>;
+656
View File
@@ -0,0 +1,656 @@
const alphabet = [
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z'
] as const;
export const i18nConstants = [
// reserved paths for localizations
'afrikaans',
'arabic',
'bengali',
'catalan',
'chinese',
'czech',
'danish',
'dutch',
'espanol',
'finnish',
'french',
'german',
'greek',
'hebrew',
'hindi',
'hungarian',
'italian',
'japanese',
'korean',
'norwegian',
'polish',
'portuguese',
'romanian',
'russian',
'serbian',
'spanish',
'swahili',
'swedish',
'turkish',
'ukrainian',
'vietnamese'
] as const;
export const blocklistedUsernames = [
...alphabet,
...i18nConstants,
'about',
'academic-honesty',
'account',
'agile',
'all-stories',
'api',
'backend-challenge-completed',
'blocked',
'bonfire',
'catalog',
'cats.json',
'challenge-completed',
'challenge',
'challenges',
'chat',
'code-of-conduct',
'coding-bootcamp-cost-calculator',
'completed-bonfire',
'completed-challenge',
'completed-field-guide',
'completed-jsProject',
'completed-zipline-or-basejump',
'copyright-policy',
'copyright',
'deprecated-signin',
'donate',
'email-signin',
'events',
'exam',
'explorer',
'external',
'field-guide',
'forgot',
'forum',
'freecodecamp',
'get-help',
'get-pai',
'guide',
'how-nonprofit-projects-work',
'internal',
'jobs-form',
'jobs',
'jsProject',
'learn-to-code',
'learn',
'login',
'logout',
'map',
'modern-challenge-completed',
'news',
'nonprofit-project-instructions',
'nonprofits-form',
'nonprofits',
'open-api',
'passwordless-change',
'pmi-acp-agile-project-managers-form',
'pmi-acp-agile-project-managers',
'privacy-policy',
'privacy',
'profile',
'project-completed',
'reset',
'services',
'shop',
'signin',
'signout',
'signup',
'sitemap.xml',
'software-resources-for-nonprofits',
'sponsors',
'stories',
'support',
'terms-of-service',
'terms',
'the-fastest-web-page-on-the-internet',
'twitch',
'unsubscribe',
'unsubscribed',
'update-my-honesty',
'update-my-portfolio',
'update-my-profile-ui',
'update-my-quincy-email',
'update-my-socials',
'update-my-sound',
'update-my-theme',
'update-my-keyboard-shortcuts',
'update-my-username',
'user',
'username',
'wiki',
// some more names from https://github.com/marteinn/The-Big-Username-Blacklist-JS/blob/master/src/list.js
'.htaccess',
'.htpasswd',
'.well-known',
'400',
'401',
'403',
'404',
'405',
'406',
'407',
'408',
'409',
'410',
'411',
'412',
'413',
'414',
'415',
'416',
'417',
'421',
'422',
'423',
'424',
'426',
'428',
'429',
'431',
'500',
'501',
'502',
'503',
'504',
'505',
'506',
'507',
'508',
'509',
'510',
'511',
'about-us',
'abuse',
'access',
'accounts',
'ad',
'add',
'admin',
'administration',
'administrator',
'ads',
'advertise',
'advertising',
'aes128-ctr',
'aes128-gcm',
'aes192-ctr',
'aes256-ctr',
'aes256-gcm',
'affiliate',
'affiliates',
'ajax',
'alert',
'alerts',
'alpha',
'amp',
'analytics',
'app',
'apps',
'asc',
'assets',
'atom',
'auth',
'authentication',
'authorize',
'autoconfig',
'autodiscover',
'avatar',
'backup',
'banner',
'banners',
'beta',
'billing',
'billings',
'blog',
'blogs',
'board',
'bookmark',
'bookmarks',
'broadcasthost',
'business',
'buy',
'cache',
'calendar',
'campaign',
'captcha',
'careers',
'cart',
'cas',
'categories',
'category',
'cdn',
'cgi',
'cgi-bin',
'chacha20-poly1305',
'change',
'channel',
'channels',
'chart',
'checkout',
'clear',
'client',
'close',
'cms',
'com',
'comment',
'comments',
'community',
'compare',
'compose',
'config',
'connect',
'contact',
'contest',
'cookies',
'copy',
'count',
'create',
'crossdomain.xml',
'css',
'curve25519-sha256',
'customer',
'customers',
'customize',
'dashboard',
'db',
'deals',
'debug',
'delete',
'desc',
'destroy',
'dev',
'developer',
'developers',
'diffie-hellman-group-exchange-sha256',
'diffie-hellman-group14-sha1',
'disconnect',
'discuss',
'dns',
'dns0',
'dns1',
'dns2',
'dns3',
'dns4',
'docs',
'documentation',
'domain',
'download',
'downloads',
'downvote',
'draft',
'drop',
'ecdh-sha2-nistp256',
'ecdh-sha2-nistp384',
'ecdh-sha2-nistp521',
'edit',
'editor',
'email',
'enterprise',
'error',
'errors',
'event',
'example',
'exception',
'exit',
'explore',
'export',
'extensions',
'false',
'family',
'faq',
'faqs',
'favicon.ico',
'features',
'feed',
'feedback',
'feeds',
'file',
'files',
'filter',
'follow',
'follower',
'followers',
'following',
'fonts',
'forgot-password',
'forgotpassword',
'form',
'forms',
'forums',
'friend',
'friends',
'ftp',
'get',
'git',
'go',
'group',
'groups',
'guest',
'guidelines',
'guides',
'head',
'header',
'help',
'hide',
'hmac-sha',
'hmac-sha1',
'hmac-sha1-etm',
'hmac-sha2-256',
'hmac-sha2-256-etm',
'hmac-sha2-512',
'hmac-sha2-512-etm',
'home',
'host',
'hosting',
'hostmaster',
'htpasswd',
'http',
'httpd',
'https',
'humans.txt',
'icons',
'images',
'imap',
'img',
'import',
'index',
'info',
'insert',
'investors',
'invitations',
'invite',
'invites',
'invoice',
'is',
'isatap',
'issues',
'it',
'join',
'js',
'json',
'keybase.txt',
'legal',
'license',
'licensing',
'like',
'limit',
'live',
'load',
'local',
'localdomain',
'localhost',
'lock',
'lost-password',
'mail',
'mail0',
'mail1',
'mail2',
'mail3',
'mail4',
'mail5',
'mail6',
'mail7',
'mail8',
'mail9',
'mailer-daemon',
'mailerdaemon',
'marketing',
'marketplace',
'master',
'me',
'media',
'member',
'members',
'message',
'messages',
'metrics',
'mis',
'mobile',
'moderator',
'modify',
'more',
'mx',
'my',
'net',
'network',
'new',
'newsletter',
'newsletters',
'next',
'nil',
'no-reply',
'nobody',
'noc',
'none',
'noreply',
'notification',
'notifications',
'ns',
'ns0',
'ns1',
'ns2',
'ns3',
'ns4',
'ns5',
'ns6',
'ns7',
'ns8',
'ns9',
'null',
'oauth',
'oauth2',
'offer',
'offers',
'online',
'openid',
'order',
'orders',
'overview',
'owner',
'page',
'pages',
'partners',
'passwd',
'password',
'pay',
'payment',
'payments',
'photo',
'photos',
'pixel',
'plans',
'plugins',
'policies',
'policy',
'pop',
'pop3',
'popular',
'portfolio',
'post',
'postfix',
'postmaster',
'poweruser',
'preferences',
'premium',
'press',
'previous',
'pricing',
'print',
'private',
'prod',
'product',
'production',
'profiles',
'project',
'projects',
'public',
'purchase',
'put',
'quota',
'redirect',
'reduce',
'refund',
'refunds',
'register',
'registration',
'remove',
'replies',
'reply',
'report',
'request',
'request-password',
'reset-password',
'response',
'return',
'returns',
'review',
'reviews',
'robots.txt',
'root',
'rootuser',
'rsa-sha2-2',
'rsa-sha2-512',
'rss',
'rules',
'sales',
'save',
'script',
'sdk',
'search',
'secure',
'security',
'select',
'session',
'sessions',
'settings',
'setup',
'share',
'shift',
'site',
'sitemap',
'sites',
'smtp',
'sort',
'source',
'sql',
'ssh',
'ssh-rsa',
'ssl',
'ssladmin',
'ssladministrator',
'sslwebmaster',
'stage',
'staging',
'stat',
'static',
'statistics',
'stats',
'status',
'store',
'style',
'styles',
'stylesheet',
'stylesheets',
'subdomain',
'subscribe',
'sudo',
'super',
'superuser',
'survey',
'sync',
'sysadmin',
'system',
'tablet',
'tag',
'tags',
'team',
'telnet',
'terms-of-use',
'test',
'testimonials',
'theme',
'themes',
'today',
'tools',
'topic',
'topics',
'tour',
'training',
'translate',
'translations',
'trending',
'trial',
'true',
'umac-128',
'umac-128-etm',
'umac-64',
'umac-64-etm',
'undefined',
'unfollow',
'unlike',
'update',
'upgrade',
'usenet',
'users',
'uucp',
'var',
'verify',
'video',
'view',
'void',
'vote',
'webmail',
'webmaster',
'website',
'widget',
'widgets',
'wpad',
'write',
'www',
'www-data',
'www1',
'www2',
'www3',
'www4',
'you',
'yourname',
'yourusername',
'zlib'
] as const;
@@ -0,0 +1,75 @@
import { describe, it, expect } from 'vitest';
import { Languages } from './i18n';
import {
SuperBlocks,
SuperBlockStage,
superBlockStages,
notAuditedSuperBlocks,
generateSuperBlockList,
getAuditedSuperBlocks
} from './curriculum';
describe('superBlockOrder', () => {
it('should contain all SuperBlocks', () => {
const allSuperBlocks = Object.values(SuperBlocks);
const superBlockOrderValues = Object.values(superBlockStages).flat();
expect(superBlockOrderValues).toHaveLength(allSuperBlocks.length);
expect(superBlockOrderValues).toEqual(
expect.arrayContaining(allSuperBlocks)
);
});
});
describe('generateSuperBlockList', () => {
it('should return an array of SuperBlocks object with all elements when if all configs are true', () => {
const result = generateSuperBlockList({
showUpcomingChanges: true
});
expect(result).toHaveLength(Object.values(superBlockStages).flat().length);
});
it('should return an array of SuperBlocks without Upcoming when { showUpcomingChanges: false }', () => {
const result = generateSuperBlockList({
showUpcomingChanges: false
});
const tempSuperBlockMap = { ...superBlockStages };
tempSuperBlockMap[SuperBlockStage.Upcoming] = [];
expect(result).toHaveLength(Object.values(tempSuperBlockMap).flat().length);
});
});
describe('Immutability of superBlockOrder, notAuditedSuperBlocks, and flatSuperBlockMap', () => {
it('should not allow modification of superBlockOrder', () => {
expect(() => {
superBlockStages[SuperBlockStage.Core] = [];
}).toThrow(TypeError);
});
it('should not allow modification of notAuditedSuperBlocks', () => {
expect(() => {
notAuditedSuperBlocks[Languages.English] = [];
}).toThrow(TypeError);
});
it('should not allow modification of flatSuperBlockMap', () => {
expect(() => {
notAuditedSuperBlocks[Languages.English] = [];
}).toThrow(TypeError);
});
});
describe('getAuditedSuperBlocks', () => {
Object.keys(notAuditedSuperBlocks).forEach(language => {
it(`should return only audited SuperBlocks for ${language}`, () => {
const auditedSuperBlocks = getAuditedSuperBlocks({
language
});
auditedSuperBlocks.forEach(superblock => {
expect(notAuditedSuperBlocks[language as Languages]).not.toContain(
superblock
);
});
});
});
});
+370
View File
@@ -0,0 +1,370 @@
// TODO: eventually this should all flow from the curriculum service, since it
// defines the top-level structure of the curriculum.
import { Languages } from './i18n.js';
// all superblocks
export enum SuperBlocks {
RespWebDesignNew = '2022/responsive-web-design',
RespWebDesign = 'responsive-web-design',
JsAlgoDataStruct = 'javascript-algorithms-and-data-structures',
JsAlgoDataStructNew = 'javascript-algorithms-and-data-structures-v8',
FrontEndDevLibs = 'front-end-development-libraries',
DataVis = 'data-visualization',
RelationalDb = 'relational-database',
BackEndDevApis = 'back-end-development-and-apis',
QualityAssurance = 'quality-assurance',
SciCompPy = 'scientific-computing-with-python',
DataAnalysisPy = 'data-analysis-with-python',
InfoSec = 'information-security',
MachineLearningPy = 'machine-learning-with-python',
CodingInterviewPrep = 'coding-interview-prep',
TheOdinProject = 'the-odin-project',
ProjectEuler = 'project-euler',
CollegeAlgebraPy = 'college-algebra-with-python',
FoundationalCSharp = 'foundational-c-sharp-with-microsoft',
A2English = 'a2-english-for-developers',
B1English = 'b1-english-for-developers',
A1Spanish = 'a1-professional-spanish',
A2Spanish = 'a2-professional-spanish',
A2Chinese = 'a2-professional-chinese',
A1Chinese = 'a1-professional-chinese',
RosettaCode = 'rosetta-code',
PythonForEverybody = 'python-for-everybody',
BasicHtml = 'basic-html',
SemanticHtml = 'semantic-html',
DevPlayground = 'dev-playground',
FullStackOpen = 'full-stack-open',
RespWebDesignV9 = 'responsive-web-design-v9',
JsV9 = 'javascript-v9',
FrontEndDevLibsV9 = 'front-end-development-libraries-v9',
PythonV9 = 'python-v9',
RelationalDbV9 = 'relational-databases-v9',
BackEndDevApisV9 = 'back-end-development-and-apis-v9',
FullStackDeveloperV9 = 'full-stack-developer-v9',
HtmlFormsAndTables = 'html-forms-and-tables',
HtmlAndAccessibility = 'html-and-accessibility',
ComputerBasics = 'computer-basics',
BasicCss = 'basic-css',
DesignForDevelopers = 'design-for-developers',
AbsoluteAndRelativeUnits = 'absolute-and-relative-units',
PseudoClassesAndElements = 'pseudo-classes-and-elements',
CssColors = 'css-colors',
StylingForms = 'styling-forms',
CssBoxModel = 'css-box-model',
CssFlexbox = 'css-flexbox',
CssTypography = 'css-typography',
CssAndAccessibility = 'css-and-accessibility',
CssPositioning = 'css-positioning',
AttributeSelectors = 'attribute-selectors',
ResponsiveDesign = 'responsive-design',
CssVariables = 'css-variables',
CssGrid = 'css-grid',
CssAnimations = 'css-animations',
LearnPythonForBeginners = 'learn-python-for-beginners',
IntroductionToAlgorithmsAndDataStructures = 'introduction-to-algorithms-and-data-structures',
LearnOOPWithPython = 'learn-oop-with-python',
LearnRAGAndMCPFundamentals = 'learn-rag-mcp-fundamentals',
IntroductionToPrecalculus = 'introduction-to-precalculus',
IntroductionToBash = 'introduction-to-bash',
IntroductionToSQLAndPostgreSQL = 'introduction-to-sql-and-postgresql',
LearnBashScripting = 'learn-bash-scripting',
LearnSQLAndBash = 'learn-sql-and-bash',
IntroductionToNano = 'introduction-to-nano',
IntroductionToGitAndGithub = 'introduction-to-git-and-github',
IntroductionToVariablesAndStringsInJS = 'introduction-to-variables-and-strings-in-javascript',
IntroductionToBooleansAndNumbersInJS = 'introduction-to-booleans-and-numbers-in-javascript',
IntroductionToFunctionsInJS = 'introduction-functions-in-javascript',
IntroductionToArraysInJS = 'introduction-to-arrays-in-javascript',
IntroductionToObjectsInJS = 'introduction-to-objects-in-javascript',
IntroductionToLoopsInJS = 'introduction-to-loops-in-javascript',
JavascriptFundamentalsReview = 'javascript-fundamentals-review',
IntroductionToHigherOrderFunctionsAndCallbacksInJS = 'introduction-to-higher-order-functions-and-callbacks-in-javascript',
LearnDomManipulationAndEventsWithJS = 'learn-dom-manipulation-and-events-with-javascript',
IntroductionToJavascriptAndAccessibility = 'introduction-to-javascript-and-accessibility',
LearnJavascriptDebugging = 'learn-javascript-debugging',
LearnBasicRegexWithJS = 'learn-basic-regex-with-javascript',
IntroductionToDatesInJS = 'introduction-to-dates-in-javascript',
LearnAudioAndVideoEventsWithJS = 'learn-audio-and-video-events-with-javascript',
IntroductionToMapsAndSetsInJS = 'introduction-to-maps-and-sets-in-javascript',
LearnLocalstorageAndCrudOperationsWithJS = 'learn-localstorage-and-crud-operations-with-javascript',
IntroductionToJavascriptClasses = 'introduction-to-javascript-classes',
LearnRecursionWithJS = 'learn-recursion-with-javascript',
IntroductionToFunctionalProgrammingWithJS = 'introduction-to-functional-programming-with-javascript',
IntroductionToAsynchronousJS = 'introduction-to-asynchronous-javascript',
LearnDataVisualizationWithD3 = 'learn-data-visualization-with-d3',
IntroductionToPythonBasics = 'introduction-to-python-basics',
LearnPythonLoopsAndSequences = 'learn-python-loops-and-sequences',
LearnPythonDictionariesAndSets = 'learn-python-dictionaries-and-sets',
LearnErrorHandlingInPython = 'learn-error-handling-in-python',
LearnPythonClassesAndObjects = 'learn-python-classes-and-objects',
IntroductionToOOPInPython = 'introduction-to-oop-in-python',
IntroductionToLinearDataStructuresInPython = 'introduction-to-linear-data-structures-in-python',
LearnAlgorithmsInPython = 'learn-algorithms-in-python',
LearnGraphsAndTreesInPython = 'learn-graphs-and-trees-in-python',
LearnDynamicProgrammingInPython = 'learn-dynamic-programming-in-python'
}
export const languageSuperBlocks = [
SuperBlocks.A2English,
SuperBlocks.B1English,
SuperBlocks.A1Spanish,
SuperBlocks.A2Spanish,
SuperBlocks.A1Chinese,
SuperBlocks.A2Chinese
];
export enum ChallengeLang {
English = 'en-US',
Spanish = 'es',
Chinese = 'zh-CN'
}
// Mapping from superblock to a speech recognition language (BCP-47)
export const superBlockToSpeechLang: Partial<
Record<SuperBlocks, ChallengeLang>
> = {
[SuperBlocks.A1Chinese]: ChallengeLang.Chinese,
[SuperBlocks.A2Chinese]: ChallengeLang.Chinese,
[SuperBlocks.A2English]: ChallengeLang.English,
[SuperBlocks.B1English]: ChallengeLang.English,
[SuperBlocks.A1Spanish]: ChallengeLang.Spanish,
[SuperBlocks.A2Spanish]: ChallengeLang.Spanish
};
/*
* SuperBlockStages.Upcoming = SHOW_UPCOMING_CHANGES === 'true'
* 'Upcoming' is for development -> not shown on stag or prod anywhere
*
* SuperBlockStages.Next = deployed, but only shown if the Growthbook feature
* is enabled.
*
*/
export enum SuperBlockStage {
Core,
English,
Spanish,
Chinese,
Professional,
Extra,
Legacy,
Upcoming,
Next,
Catalog
}
const defaultStageOrder = [
SuperBlockStage.Core,
SuperBlockStage.English,
SuperBlockStage.Spanish,
SuperBlockStage.Chinese,
SuperBlockStage.Extra,
SuperBlockStage.Legacy,
SuperBlockStage.Professional,
SuperBlockStage.Next,
SuperBlockStage.Catalog
];
export function getStageOrder({
showUpcomingChanges
}: Config): SuperBlockStage[] {
const stageOrder = [...defaultStageOrder];
if (showUpcomingChanges) {
stageOrder.push(SuperBlockStage.Upcoming);
}
return stageOrder;
}
export type StageMap = {
[key in SuperBlockStage]: SuperBlocks[];
};
// Groups of superblocks in learn map. This should include all superblocks.
export const superBlockStages: StageMap = {
[SuperBlockStage.Core]: [
SuperBlocks.RespWebDesignV9,
SuperBlocks.JsV9,
SuperBlocks.FrontEndDevLibsV9,
SuperBlocks.PythonV9,
SuperBlocks.RelationalDbV9,
SuperBlocks.BackEndDevApisV9,
SuperBlocks.FullStackDeveloperV9
],
[SuperBlockStage.English]: [SuperBlocks.A2English, SuperBlocks.B1English],
[SuperBlockStage.Spanish]: [SuperBlocks.A1Spanish],
[SuperBlockStage.Chinese]: [SuperBlocks.A1Chinese],
[SuperBlockStage.Professional]: [SuperBlocks.FoundationalCSharp],
[SuperBlockStage.Extra]: [
SuperBlocks.TheOdinProject,
SuperBlocks.CodingInterviewPrep,
SuperBlocks.ProjectEuler,
SuperBlocks.RosettaCode
],
[SuperBlockStage.Legacy]: [
SuperBlocks.RespWebDesignNew,
SuperBlocks.JsAlgoDataStructNew,
SuperBlocks.FrontEndDevLibs,
SuperBlocks.DataVis,
SuperBlocks.RelationalDb,
SuperBlocks.BackEndDevApis,
SuperBlocks.QualityAssurance,
SuperBlocks.SciCompPy,
SuperBlocks.DataAnalysisPy,
SuperBlocks.InfoSec,
SuperBlocks.MachineLearningPy,
SuperBlocks.CollegeAlgebraPy,
SuperBlocks.RespWebDesign,
SuperBlocks.JsAlgoDataStruct,
SuperBlocks.PythonForEverybody
],
[SuperBlockStage.Next]: [],
[SuperBlockStage.Upcoming]: [
SuperBlocks.FullStackOpen,
SuperBlocks.A2Spanish,
SuperBlocks.A2Chinese,
SuperBlocks.DevPlayground
],
// Catalog is treated like upcoming for now
// Add catalog superBlocks to catalog.ts when adding new superBlocks
[SuperBlockStage.Catalog]: [
SuperBlocks.HtmlFormsAndTables,
SuperBlocks.BasicHtml,
SuperBlocks.SemanticHtml,
SuperBlocks.HtmlAndAccessibility,
SuperBlocks.ComputerBasics,
SuperBlocks.BasicCss,
SuperBlocks.DesignForDevelopers,
SuperBlocks.AbsoluteAndRelativeUnits,
SuperBlocks.PseudoClassesAndElements,
SuperBlocks.CssColors,
SuperBlocks.StylingForms,
SuperBlocks.CssBoxModel,
SuperBlocks.CssFlexbox,
SuperBlocks.CssTypography,
SuperBlocks.CssAndAccessibility,
SuperBlocks.CssPositioning,
SuperBlocks.AttributeSelectors,
SuperBlocks.ResponsiveDesign,
SuperBlocks.CssVariables,
SuperBlocks.CssGrid,
SuperBlocks.CssAnimations,
SuperBlocks.LearnPythonForBeginners,
SuperBlocks.IntroductionToAlgorithmsAndDataStructures,
SuperBlocks.LearnOOPWithPython,
SuperBlocks.LearnRAGAndMCPFundamentals,
SuperBlocks.IntroductionToPrecalculus,
SuperBlocks.IntroductionToBash,
SuperBlocks.IntroductionToSQLAndPostgreSQL,
SuperBlocks.LearnBashScripting,
SuperBlocks.LearnSQLAndBash,
SuperBlocks.IntroductionToNano,
SuperBlocks.IntroductionToGitAndGithub,
SuperBlocks.IntroductionToVariablesAndStringsInJS,
SuperBlocks.IntroductionToBooleansAndNumbersInJS,
SuperBlocks.IntroductionToFunctionsInJS,
SuperBlocks.IntroductionToArraysInJS,
SuperBlocks.IntroductionToObjectsInJS,
SuperBlocks.IntroductionToLoopsInJS,
SuperBlocks.JavascriptFundamentalsReview,
SuperBlocks.IntroductionToHigherOrderFunctionsAndCallbacksInJS,
SuperBlocks.LearnDomManipulationAndEventsWithJS,
SuperBlocks.IntroductionToJavascriptAndAccessibility,
SuperBlocks.LearnJavascriptDebugging,
SuperBlocks.LearnBasicRegexWithJS,
SuperBlocks.IntroductionToDatesInJS,
SuperBlocks.LearnAudioAndVideoEventsWithJS,
SuperBlocks.IntroductionToMapsAndSetsInJS,
SuperBlocks.LearnLocalstorageAndCrudOperationsWithJS,
SuperBlocks.IntroductionToJavascriptClasses,
SuperBlocks.LearnRecursionWithJS,
SuperBlocks.IntroductionToFunctionalProgrammingWithJS,
SuperBlocks.IntroductionToAsynchronousJS,
SuperBlocks.LearnDataVisualizationWithD3,
SuperBlocks.IntroductionToPythonBasics,
SuperBlocks.LearnPythonLoopsAndSequences,
SuperBlocks.LearnPythonDictionariesAndSets,
SuperBlocks.LearnErrorHandlingInPython,
SuperBlocks.LearnPythonClassesAndObjects,
SuperBlocks.IntroductionToOOPInPython,
SuperBlocks.IntroductionToLinearDataStructuresInPython,
SuperBlocks.LearnAlgorithmsInPython,
SuperBlocks.LearnGraphsAndTreesInPython,
SuperBlocks.LearnDynamicProgrammingInPython
]
};
Object.freeze(superBlockStages);
export const archivedSuperBlocks = superBlockStages[SuperBlockStage.Legacy];
type NotAuditedSuperBlocks = {
[key in Languages]: SuperBlocks[];
};
// when a superBlock is audited, remove it from its language below
// when adding a new language, add all (not audited) superblocks to the object
export const notAuditedSuperBlocks: NotAuditedSuperBlocks = {
[Languages.English]: [],
[Languages.Espanol]: [],
[Languages.Chinese]: [],
[Languages.ChineseTraditional]: [],
[Languages.Italian]: [],
[Languages.Portuguese]: [],
[Languages.Ukrainian]: [],
[Languages.Japanese]: [],
[Languages.German]: [],
[Languages.Swahili]: [],
[Languages.Korean]: []
};
Object.freeze(notAuditedSuperBlocks);
export const chapterBasedSuperBlocks = [
SuperBlocks.FullStackOpen,
SuperBlocks.A1Spanish,
SuperBlocks.RespWebDesignV9,
SuperBlocks.JsV9,
SuperBlocks.FrontEndDevLibsV9,
SuperBlocks.PythonV9,
SuperBlocks.RelationalDbV9,
SuperBlocks.BackEndDevApisV9,
SuperBlocks.FullStackDeveloperV9,
SuperBlocks.LearnDataVisualizationWithD3,
SuperBlocks.A1Chinese
];
Object.freeze(chapterBasedSuperBlocks);
export const certificationCollectionSuperBlocks = [
SuperBlocks.FullStackDeveloperV9
];
Object.freeze(certificationCollectionSuperBlocks);
type Config = {
showUpcomingChanges: boolean;
};
export function generateSuperBlockList(config: Config): SuperBlocks[] {
return getStageOrder(config)
.map(stage => superBlockStages[stage])
.flat();
}
export function getAuditedSuperBlocks({
language = 'english'
}: {
language: string;
}): SuperBlocks[] {
if (!Object.prototype.hasOwnProperty.call(notAuditedSuperBlocks, language)) {
throw Error(`'${language}' key not found in 'notAuditedSuperBlocks'`);
}
// To find the audited superblocks, we need to start with all superblocks.
const flatSuperBlockMap = generateSuperBlockList({
showUpcomingChanges: true
});
const auditedSuperBlocks = flatSuperBlockMap.filter(
superBlock =>
!notAuditedSuperBlocks[language as Languages].includes(superBlock)
);
return auditedSuperBlocks;
}
@@ -0,0 +1,156 @@
// Configuration for client side
export type DonationAmount = 500 | 1000 | 2000 | 2500 | 4000;
export type DonationDuration = 'one-time' | 'month';
export interface DonationConfig {
donationAmount: DonationAmount;
donationDuration: DonationDuration;
}
export const subscriptionAmounts: DonationAmount[] = [500, 1000, 2000, 4000];
export const subscriptionAmountsB: DonationAmount[] = [500, 1000, 2500, 4000];
export const defaultDonation: DonationConfig = {
donationAmount: 500,
donationDuration: 'month'
};
export const defaultTierAmount: DonationAmount = 2000;
export const defaultTierAmountB: DonationAmount = 2500;
export const onetimeSKUConfig = {
production: [
{ amount: '15000', id: 'sku_IElisJHup0nojP' },
{ amount: '10000', id: 'sku_IEliodY88lglPk' },
{ amount: '7500', id: 'sku_IEli9AXW8DwNtT' },
{ amount: '5000', id: 'sku_IElhJxkNh9UgDp' },
{ amount: '2500', id: 'sku_IElhQtqLgKZC8y' }
],
staging: [
{ amount: '15000', id: 'sku_IEPNpHACYJmUwz' },
{ amount: '10000', id: 'sku_IEPMY1OXxnY4WU' },
{ amount: '7500', id: 'sku_IEPLOotEqlMOWC' },
{ amount: '5000', id: 'sku_IEPKAxxAxfMnUI' },
{ amount: '2500', id: 'sku_IEPIgLRzViwq5z' }
]
};
// Configuration for server side
export const durationKeysConfig = ['month', 'one-time'];
export const donationOneTimeConfig = [100000, 25000, 6000];
export const donationSubscriptionConfig = {
duration: {
month: 'monthly'
},
plans: {
month: subscriptionAmounts
}
};
// Shared paypal configuration
// keep the 5 dollars for the modal
export const paypalConfigTypes = {
production: {
month: {
500: { planId: 'P-6B636789V3105190KMTJFH7A' },
1000: { planId: 'P-53P76823N8780520DMVTWF3I' },
2000: { planId: 'P-8HY47434FB9663500MVTWFOA' },
2500: { planId: 'P-1E758922LA293854BNC3SK3A' },
3000: { planId: 'P-1KY930839N8045117L6E4BKY' },
4000: { planId: 'P-0MH28916302828423MVTWEBI' },
5000: { planId: 'P-0WR49877YD949401BL6E4CTA' }
}
},
staging: {
month: {
500: { planId: 'P-37N14480BW163382FLZYPVMA' },
1000: { planId: 'P-28B62039J8092810UL6E3FXA' },
2000: { planId: 'P-7HR706961M9170433L6HI5VI' },
2500: { planId: 'P-2BK29709FB733490FNC3RPGQ' },
3000: { planId: 'P-35V33574BU596924JL6HI6XY' },
4000: { planId: 'P-45M45060289267734L6HJSXA' },
5000: { planId: 'P-0MD70861FY4172444L6HJTUQ' }
}
}
};
interface OneTimeConfig {
amount: DonationAmount;
duration: 'one-time';
planId: null;
}
interface SubscriptionConfig {
amount: DonationAmount;
duration: 'month';
planId: string;
}
export const paypalConfigurator = (
donationAmount: DonationAmount,
donationDuration: 'one-time' | 'month',
paypalConfig: {
month: {
500: { planId: string };
1000: { planId: string };
2000: { planId: string };
2500: { planId: string };
3000: { planId: string };
4000: { planId: string };
5000: { planId: string };
};
}
): OneTimeConfig | SubscriptionConfig => {
if (donationDuration === 'one-time') {
return { amount: donationAmount, duration: donationDuration, planId: null };
}
return {
amount: donationAmount,
duration: donationDuration,
planId: paypalConfig[donationDuration][donationAmount].planId
};
};
export const donationUrls = {
successUrl: 'https://www.freecodecamp.org/news/thank-you-for-donating/',
cancelUrl: 'https://freecodecamp.org/donate'
};
export enum PaymentContext {
Modal = 'modal',
DonatePage = 'donate page',
Certificate = 'certificate'
}
export enum PaymentProvider {
Paypal = 'paypal',
Patreon = 'patreon',
Stripe = 'stripe',
StripeCard = 'stripe card'
}
const stripeProductIds = {
production: {
month: {
500: 'prod_Cc9bIxB2NvjpLy',
1000: 'prod_BuiSxWk7jGSFlJ',
2000: 'prod_IElpZVK7kOn6Fe',
2500: 'prod_JCakZSxh12ZaDF',
4000: 'prod_IElq1foW39g3Cx'
}
},
staging: {
month: {
500: 'prod_GD1GGbJsqQaupl',
1000: 'prod_GD1IzNEXfSCGgy',
2000: 'prod_IEkNp8M03xvsuB',
2500: 'prod_T12UtcRPvzzVN1',
4000: 'prod_IEkPebxS63mVbs'
}
}
};
export const allStripeProductIdsArray = [
...Object.values(stripeProductIds['production']['month']),
...Object.values(stripeProductIds['staging']['month'])
];
+130
View File
@@ -0,0 +1,130 @@
export enum Languages {
English = 'english',
Espanol = 'espanol',
Chinese = 'chinese',
ChineseTraditional = 'chinese-traditional',
Italian = 'italian',
Portuguese = 'portuguese',
Ukrainian = 'ukrainian',
Japanese = 'japanese',
German = 'german',
Swahili = 'swahili',
Korean = 'korean'
}
/*
* List of languages with localizations enabled for builds.
*
* Client is the UI, and Curriculum is the Challenge Content.
*
* An error will be thrown if the CLIENT_LOCALE and CURRICULUM_LOCALE variables
* from the .env file aren't found in their respective arrays below
*/
export const availableLangs = {
client: [
Languages.English,
Languages.Espanol,
Languages.Chinese,
Languages.ChineseTraditional,
Languages.Italian,
Languages.Portuguese,
Languages.Ukrainian,
Languages.Japanese,
Languages.German,
Languages.Swahili,
Languages.Korean
],
curriculum: [
Languages.English,
Languages.Espanol,
Languages.Chinese,
Languages.ChineseTraditional,
Languages.Italian,
Languages.Portuguese,
Languages.Ukrainian,
Languages.Japanese,
Languages.German,
Languages.Swahili,
Languages.Korean
]
};
// ---------------------------------------------------------------------------
// Each client language needs an entry in the rest of the variables below
/* These strings set the i18next language. It needs to be the two character
* string for the language to take advantage of available functionality.
* Use a 639-1 code here https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
*/
export const i18nextCodes = {
[Languages.English]: 'en',
[Languages.Espanol]: 'es',
[Languages.Chinese]: 'zh',
[Languages.ChineseTraditional]: 'zh-Hant',
[Languages.Italian]: 'it',
[Languages.Portuguese]: 'pt-BR',
[Languages.Ukrainian]: 'uk',
[Languages.Japanese]: 'ja',
[Languages.German]: 'de',
[Languages.Swahili]: 'sw',
[Languages.Korean]: 'ko'
};
// These are for the language selector dropdown menu in the footer
export const LangNames: { [key: string]: string } = {
[Languages.English]: 'English',
[Languages.Espanol]: 'Español',
[Languages.Chinese]: '中文(简体字)',
[Languages.ChineseTraditional]: '中文(繁體字)',
[Languages.Italian]: 'Italiano',
[Languages.Portuguese]: 'Português',
[Languages.Ukrainian]: 'Українська',
[Languages.Japanese]: '日本語',
[Languages.German]: 'Deutsch',
[Languages.Swahili]: 'Swahili',
[Languages.Korean]: '한국어'
};
/* These are for formatting dates and numbers. Used with JS .toLocaleString().
* There's an example in profile/components/Camper.js
* List: https://github.com/unicode-cldr/cldr-dates-modern/tree/master/main
*/
export const LangCodes = {
[Languages.English]: 'en-US',
[Languages.Espanol]: 'es-419',
[Languages.Chinese]: 'zh',
[Languages.ChineseTraditional]: 'zh-Hant',
[Languages.Italian]: 'it',
[Languages.Portuguese]: 'pt-BR',
[Languages.Ukrainian]: 'uk',
[Languages.Japanese]: 'ja',
[Languages.German]: 'de',
[Languages.Swahili]: 'sw',
[Languages.Korean]: 'ko'
};
/**
* This array contains languages that should NOT appear in the language selector.
*/
export const hiddenLangs: Languages[] = [];
/**
* This array contains languages that use the RTL layouts.
*/
export const rtlLangs: Languages[] = [];
// locale is sourced from a JSON file, so we use getLangCode to
// find the associated enum values
export function getLangCode(locale: PropertyKey): string {
if (isPropertyOf(LangCodes, locale)) return LangCodes[locale];
throw new Error(`${String(locale)} is not a valid locale`);
}
function isPropertyOf<O>(
obj: Record<string, string>,
key: PropertyKey
): key is keyof O {
return Object.prototype.hasOwnProperty.call(obj, key);
}
+8
View File
@@ -0,0 +1,8 @@
import { BlockLabel } from './blocks';
export interface Module {
dashedName: string;
comingSoon?: boolean;
blocks: string[];
moduleType?: BlockLabel;
}
@@ -0,0 +1,34 @@
import { describe, it, expect } from 'vitest';
import { getLines } from './get-lines';
const content = 'one\ntwo\nthree';
describe('dasherize', () => {
it('returns a string', () => {
expect(getLines('')).toBe('');
});
it("returns '' when the second arg is empty", () => {
expect(getLines(content)).toBe('');
});
it("returns '' when the range is negative", () => {
expect(getLines(content, [1, -1])).toBe('');
});
it("returns '' when the range is [n,n]", () => {
expect(getLines(content, [0, 0])).toBe('');
expect(getLines(content, [1, 1])).toBe('');
expect(getLines(content, [2, 2])).toBe('');
});
it('returns the first line when the range is [0,2]', () => {
expect(getLines(content, [0, 2])).toBe('one');
});
it('returns the second line when the range is [1,3]', () => {
expect(getLines(content, [1, 3])).toBe('two');
});
it('returns the first and second lines when the range is [0,3]', () => {
expect(getLines(content, [0, 3])).toBe('one\ntwo');
});
it('returns the second and third lines when the range is [1,4]', () => {
expect(getLines(content, [1, 4])).toBe('two\nthree');
});
});
+10
View File
@@ -0,0 +1,10 @@
export function getLines(contents: string, range?: number[]): string {
if (!range) {
return '';
}
const lines = contents.split('\n');
const editableLines =
range[1] <= range[0] ? [] : lines.slice(range[0], range[1] - 1);
return editableLines.join('\n');
}
+8
View File
@@ -0,0 +1,8 @@
import { type SuperBlocks, getAuditedSuperBlocks } from '../config/curriculum';
export function isAuditedSuperBlock(language: string, superblock: SuperBlocks) {
const auditedSuperBlocks = getAuditedSuperBlocks({
language
});
return auditedSuperBlocks.includes(superblock);
}
@@ -0,0 +1,30 @@
import { describe, it, expect } from 'vitest';
import { createPoly, createSource } from './polyvinyl';
const polyData = {
name: 'test',
ext: 'js',
contents: 'var hello = world;',
history: ['test.js']
};
describe('createSource', () => {
it('should return a vinyl object with a source matching the contents', () => {
const original = createPoly(polyData);
const updated = createSource(original);
expect(original).not.toHaveProperty('source');
expect(updated).toHaveProperty('source', 'var hello = world;');
expect(updated).toMatchObject(original);
});
it('should not update the source if it already exists', () => {
const original = createPoly({
...polyData,
source: 'const hello = world;'
});
const updated = createSource(original);
expect(updated).toStrictEqual(original);
});
});
+135
View File
@@ -0,0 +1,135 @@
const exts = ['js', 'html', 'css', 'jsx', 'ts', 'tsx', 'py', 'json'] as const;
export type Ext = (typeof exts)[number];
export interface IncompleteChallengeFile {
fileKey: string;
ext: Ext;
name: string;
contents: string;
}
export interface ChallengeFile extends IncompleteChallengeFile {
editableRegionBoundaries?: number[];
editableContents?: string;
usesMultifileEditor?: boolean;
error?: unknown;
seed?: string;
source?: string;
path: string;
history: string[];
}
type PolyProps = {
name: string;
ext: string;
contents: string;
history?: string[];
};
// The types are a little awkward, but should suffice until we move the
// curriculum to TypeScript.
type AddedProperties = {
path: string;
fileKey: string;
error: null;
};
export function createPoly<Rest>({
name,
ext,
contents,
history,
...rest
}: PolyProps & Rest): PolyProps & AddedProperties & Rest {
if (typeof name !== 'string') throw new TypeError('name must be a string');
if (typeof ext !== 'string') throw new TypeError('ext must be a string');
if (typeof contents !== 'string')
throw new TypeError('contents must be a string');
return {
...rest,
history: Array.isArray(history) ? history : [name + '.' + ext],
name,
ext,
path: name + '.' + ext,
fileKey: name + ext,
contents,
error: null
} as PolyProps & AddedProperties & Rest;
}
export function isPoly(poly: unknown): poly is ChallengeFile {
function hasProperties(poly: unknown): poly is Record<string, unknown> {
return (
!!poly &&
typeof poly === 'object' &&
'contents' in poly &&
'name' in poly &&
'ext' in poly &&
'fileKey' in poly &&
'history' in poly
);
}
const hasCorrectTypes = (poly: Record<string, unknown>): boolean =>
typeof poly.contents === 'string' &&
typeof poly.name === 'string' &&
exts.includes(poly.ext as Ext) &&
typeof poly.fileKey === 'string' &&
Array.isArray(poly.history);
return hasProperties(poly) && hasCorrectTypes(poly);
}
function checkPoly(poly: ChallengeFile) {
if (!isPoly(poly)) throw Error('Not a PolyVinyl: ' + JSON.stringify(poly));
}
// setContent will lose source if not supplied
export function setContent(
contents: string,
poly: ChallengeFile,
source?: string
): ChallengeFile {
checkPoly(poly);
return {
...poly,
contents,
source
};
}
// This is currently only used to add back properties that are not stored in the
// database.
export function regenerateMissingProperties(file: IncompleteChallengeFile) {
const newPath = file.name + '.' + file.ext;
return {
...file,
path: newPath,
history: [newPath]
};
}
type Wrapper = (x: string) => Promise<string> | string;
// transformContents will keep a copy of the original
// code in the `source` property. If the original polyvinyl
// already contains a source, this version will continue as
// the source property
export async function transformContents(
wrap: Wrapper,
polyP: ChallengeFile | Promise<ChallengeFile>
) {
const poly = await polyP;
const newPoly = setContent(await wrap(poly.contents), poly, poly.source);
return newPoly;
}
// createSource(poly: PolyVinyl) => PolyVinyl
export function createSource<Rest>(
poly: Pick<ChallengeFile, 'contents' | 'source'> & Rest
): Rest & { contents: string; source: string } {
return {
...poly,
source: poly.source ?? poly.contents
};
}
@@ -0,0 +1,14 @@
/** Shuffle array using the FisherYates shuffle algorithm */
export const shuffleArray = <T>(arrToShuffle: Array<T>) => {
const arr = [...arrToShuffle];
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
// We know that i and j are within the bounds of the array, TS doesn't
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
[arr[i], arr[j]] = [arr[j]!, arr[i]!];
}
return arr;
};
+117
View File
@@ -0,0 +1,117 @@
import { describe, it, expect } from 'vitest';
import {
isValidUsername,
usernameTooShort,
validationSuccess,
usernameIsHttpStatusCode,
invalidCharError,
isMicrosoftTranscriptLink
} from './validate';
function inRange(num: number, range: number[]) {
return num >= range[0] && num <= range[1];
}
describe('isValidUsername', () => {
it('rejects strings with less than 3 characters', () => {
expect(isValidUsername('')).toStrictEqual(usernameTooShort);
expect(isValidUsername('12')).toStrictEqual(usernameTooShort);
expect(isValidUsername('a')).toStrictEqual(usernameTooShort);
expect(isValidUsername('12a')).toStrictEqual(validationSuccess);
});
it('rejects strings which are http response status codes 100-599', () => {
expect(isValidUsername('429')).toStrictEqual(usernameIsHttpStatusCode);
expect(isValidUsername('789')).toStrictEqual(validationSuccess);
});
it('rejects non-ASCII characters', () => {
expect(isValidUsername('👀👂👄')).toStrictEqual(invalidCharError);
});
it('rejects with invalidCharError even if the string is too short', () => {
expect(isValidUsername('.')).toStrictEqual(invalidCharError);
});
it('accepts alphanumeric characters', () => {
expect(
isValidUsername('abcdefghijklmnopqrstuvwxyz0123456789')
).toStrictEqual(validationSuccess);
});
it('accepts hyphens and underscores', () => {
expect(isValidUsername('a-b')).toStrictEqual(validationSuccess);
expect(isValidUsername('a_b')).toStrictEqual(validationSuccess);
});
it('rejects all other ASCII characters', () => {
const allowedCharactersList = ['-', '_', '+'];
const numbers = [48, 57];
const upperCase = [65, 90];
const lowerCase = [97, 122];
const base = 'user';
const finalCode = 127;
for (let code = 0; code <= finalCode; code++) {
const char = String.fromCharCode(code);
let expected: { valid: boolean; error: null | string } = invalidCharError;
if (allowedCharactersList.includes(char)) expected = validationSuccess;
if (inRange(code, numbers)) expected = validationSuccess;
if (inRange(code, upperCase)) expected = validationSuccess;
if (inRange(code, lowerCase)) expected = validationSuccess;
expect(isValidUsername(base + char)).toStrictEqual(expected);
}
});
});
const baseUrl = 'https://learn.microsoft.com/';
describe('isMicrosoftTranscriptLink', () => {
it('should reject links to domains other than learn.microsoft.com', () => {
{
expect(isMicrosoftTranscriptLink('https://lean.microsoft.com')).toBe(
false
);
expect(isMicrosoftTranscriptLink('https://learn.microsft.com')).toBe(
false
);
}
});
it('should reject links without a username', () => {
expect(isMicrosoftTranscriptLink(`${baseUrl}/en-us/users/`)).toBe(false);
});
it('should reject links without a unique id', () => {
expect(
isMicrosoftTranscriptLink(`${baseUrl}/en-us/users/moT01/transcript`)
).toBe(false);
});
it('should reject links with anything after the unique id', () => {
expect(
isMicrosoftTranscriptLink(
`${baseUrl}/en-us/users/moT01/transcript/any-id/more-stuff`
)
).toBe(false);
});
it('should reject the placeholder link', () => {
expect(
isMicrosoftTranscriptLink(
'https://learn.microsoft.com/LOCALE/users/USERNAME/transcript/ID'
)
).toBe(false);
expect(
isMicrosoftTranscriptLink(
'https://learn.microsoft.com/LOCALE/users/USERNAME/transcript/ID/'
)
).toBe(false);
});
it.each(['en-us', 'fr-fr', 'lang-country'])(
'should accept links with the %s locale',
locale => {
expect(
isMicrosoftTranscriptLink(
`https://learn.microsoft.com/${locale}/users/moT01/transcript/any-id`
)
).toBe(true);
}
);
});
+58
View File
@@ -0,0 +1,58 @@
type Valid = {
valid: true;
error: null;
};
type Invalid = {
valid: false;
error: string;
};
type Validated = Valid | Invalid;
export const invalidCharError: Invalid = {
valid: false,
error: 'contains invalid characters'
};
export const validationSuccess: Valid = { valid: true, error: null };
export const usernameTooShort: Invalid = {
valid: false,
error: 'is too short'
};
export const usernameIsHttpStatusCode: Invalid = {
valid: false,
error: 'is a reserved error code'
};
const validCharsRE = /^[a-zA-Z0-9\-_+]*$/;
export const isHttpStatusCode = (str: string): boolean => {
const output = parseInt(str, 10);
return !isNaN(output) && output >= 100 && output <= 599;
};
export const isValidUsername = (str: string): Validated => {
if (!validCharsRE.test(str)) return invalidCharError;
if (str.length < 3) return usernameTooShort;
if (isHttpStatusCode(str)) return usernameIsHttpStatusCode;
return validationSuccess;
};
// link template:
// https://learn.microsoft.com/LOCALE/users/USERNAME/transcript/ID
export const isMicrosoftTranscriptLink = (value: string): boolean => {
let url;
try {
url = new URL(value);
} catch {
return false;
}
const correctDomain = url.hostname === 'learn.microsoft.com';
const correctPath = !!url.pathname.match(
/^\/[^/]+\/users\/[^/]+\/transcript\/[^/]+$/
);
const notPlaceholder = !url.pathname.match(
'/LOCALE/users/USERNAME/transcript/ID'
);
return correctDomain && correctPath && notPlaceholder;
};
+10
View File
@@ -0,0 +1,10 @@
{
"include": ["src"],
"extends": "../../tsconfig-base.json",
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"outDir": "dist",
"noEmit": false
}
}
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from 'tsdown';
export default defineConfig({
entry: ['src/**/*.ts', '!src/**/*.test.ts'],
exports: true,
dts: true
});