refactor: create shared workspace (#51454)

This commit is contained in:
Oliver Eyton-Williams
2023-09-07 19:36:01 +02:00
committed by GitHub
parent 3c0c14b427
commit 391fc2e34d
168 changed files with 278 additions and 263 deletions
+33
View File
@@ -0,0 +1,33 @@
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');
}
+25
View File
@@ -0,0 +1,25 @@
import {
type SuperBlocks,
getAuditedSuperBlocks
} from '../../shared/config/superblocks';
export function isAuditedSuperBlock(
language: string,
superblock: SuperBlocks,
{
showNewCurriculum,
showUpcomingChanges
}: { showNewCurriculum: boolean; showUpcomingChanges: boolean }
) {
// TODO: when all the consumers of this function use TypeScript we can remove
// this check
if (!language || !superblock)
throw Error('Both arguments must be provided for auditing');
const auditedSuperBlocks = getAuditedSuperBlocks({
showNewCurriculum,
showUpcomingChanges,
language
});
return auditedSuperBlocks.includes(superblock);
}
+170
View File
@@ -0,0 +1,170 @@
// originally based off of https://github.com/gulpjs/vinyl
const invariant = require('invariant');
// interface PolyVinyl {
// source: String,
// contents: String,
// name: String,
// ext: String,
// path: String,
// key: String,
// head: String,
// tail: String,
// history: [...String],
// error: Null|Object|Error
// }
// createPoly({
// name: String,
// ext: String,
// contents: String,
// history?: [...String],
// }) => PolyVinyl, throws
function createPoly({ name, ext, contents, history, ...rest } = {}) {
invariant(typeof name === 'string', 'name must be a string but got %s', name);
invariant(typeof ext === 'string', 'ext must be a string, but was %s', ext);
invariant(
typeof contents === 'string',
'contents must be a string but got %s',
contents
);
return {
...rest,
history: Array.isArray(history) ? history : [name + '.' + ext],
name,
ext,
path: name + '.' + ext,
fileKey: name + ext,
contents,
error: null
};
}
// isPoly(poly: Any) => Boolean
function isPoly(poly) {
return (
poly &&
typeof poly.contents === 'string' &&
typeof poly.name === 'string' &&
typeof poly.ext === 'string' &&
Array.isArray(poly.history)
);
}
// checkPoly(poly: Any) => Void, throws
function checkPoly(poly) {
invariant(
isPoly(poly),
'function should receive a PolyVinyl, but got %s',
poly
);
}
// setContent(contents: String, poly: PolyVinyl) => PolyVinyl
// setContent will loose source if set
function setContent(contents, poly) {
checkPoly(poly);
return {
...poly,
contents,
source: null
};
}
// setExt(ext: String, poly: PolyVinyl) => PolyVinyl
function setExt(ext, poly) {
checkPoly(poly);
const newPoly = {
...poly,
ext,
path: poly.name + '.' + ext,
fileKey: poly.name + ext
};
newPoly.history = [...poly.history, newPoly.path];
return newPoly;
}
// setImportedFiles(importedFiles: String[], poly: PolyVinyl) => PolyVinyl
function setImportedFiles(importedFiles, poly) {
checkPoly(poly);
const newPoly = {
...poly,
importedFiles: [...importedFiles]
};
return newPoly;
}
// This is currently only used to add back properties that are not stored in the
// database.
function regeneratePathAndHistory(poly) {
const newPath = poly.name + '.' + poly.ext;
const newPoly = {
...poly,
path: newPath,
history: [newPath]
};
checkPoly(newPoly);
return newPoly;
}
// clearHeadTail(poly: PolyVinyl) => PolyVinyl
function clearHeadTail(poly) {
checkPoly(poly);
return {
...poly,
head: '',
tail: ''
};
}
// compileHeadTail(padding: String, poly: PolyVinyl) => PolyVinyl
function compileHeadTail(padding = '', poly) {
return clearHeadTail(
transformContents(
() => [poly.head, poly.contents, poly.tail].join(padding),
poly
)
);
}
// transformContents(
// wrap: (contents: String) => String,
// poly: PolyVinyl
// ) => PolyVinyl
// 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
function transformContents(wrap, poly) {
const newPoly = setContent(wrap(poly.contents), poly);
// if no source exist, set the original contents as source
newPoly.source = poly.source || poly.contents;
return newPoly;
}
// transformHeadTailAndContents(
// wrap: (source: String) => String,
// poly: PolyVinyl
// ) => PolyVinyl
function transformHeadTailAndContents(wrap, poly) {
return {
...transformContents(wrap, poly),
head: wrap(poly.head),
tail: wrap(poly.tail)
};
}
module.exports = {
createPoly,
isPoly,
setContent,
setExt,
setImportedFiles,
compileHeadTail,
regeneratePathAndHistory,
transformContents,
transformHeadTailAndContents
};
+116
View File
@@ -0,0 +1,116 @@
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;
};